前言
最近一直在写博文。虽然没有人看,但是我写博文,并不是为了提高自己的人气,而是为了锻炼自己的表达能力和写代码的能力。
正文
演示
项目简介
好啦,我先来介绍一下本次的项目吧。
EasyX是c++的一个图像渲染框架,通过easyX可以轻松的实现各种简易的gui页面。
五子棋的玩法非常简单,就是两人交替下棋,谁先连出五子谁赢。
我这次的目标是实现图形页面的人机对战。
思路部分
我们先实现相对简单的,UI绘制吧!
首先我们新建一个头文件Chess.h , 并在里面定义一些基本值
#define SIZE 15//定义棋盘内行列棋子的个数
#define debug false//是否为 debug 模式
#define UNIT 20//定义 UNIT 单位,1 UNIT 约等于 20 px
#ifndef NANI_BEAN_H
#define NANI_BEAN_H
class Chess {
public:
int x;//得益于 c++ 的数组问题,数组内的元素无法感知到自己的 x 和 y
//所以需要自己记录
int y;
int color = 0;//黑1 白2 0没有 人是1 AI是2
};
#endif //NANI_BEAN_H
再在main.cpp中定义一些基本值
#define ToolHeight UNIT* 3// 标题栏所占的高度
static int HW = 16; // 窗口大小,单位 UNIT( UNIT 声明在 Bean.h 中)
struct Chess data[SIZE][SIZE]; // 所有棋子的集合
static int mouseX = UNIT, mouseY = UNIT; // 鼠标的 x 和 y,把他们拿到外面,是为了在 debug 模式下输出鼠标信息
static int centerX, length, centerY; // 棋盘中心的 x , y 以及棋盘长度
static int mouseI, mouseJ; // 鼠标所在区域的 x, y
然后 我们开始UI绘制
我们通过while进行帧驱动
然后把绘制区域分为两个部分,即:标题栏和棋盘
while (true) {
BeginBatchDraw();// 收集绘制
drawToolBar();// 绘制标题栏
drawBoard();// 绘制棋盘
EndBatchDraw();// 开始绘制
}
绘制标题栏drawToolBar
setfillcolor(WHITE);
solidrectangle(0, 0, HW * UNIT, ToolHeight);//绘制标题背景
setbkmode(TRANSPARENT);
settextcolor(BLUE);
settextstyle(UNIT, 0, nullptr);
char *title = "Simon 的 AI 五子棋";
char version[30] = "EasyX 版本";
strcat(version, GetEasyXVer());
outtextxy(UNIT, (ToolHeight - UNIT) / 2, title);
settextcolor(BLACK);
outtextxy(UNIT, (ToolHeight - UNIT) / 2 - UNIT, version);// 绘制标题和 EasyX 版本
绘制棋盘drawBoard
setfillcolor(RGB(0xe9, 0xe7, 0xef));
solidrectangle(0, ToolHeight, HW * UNIT, HW * UNIT + ToolHeight);//绘制标题以下的区域颜色
centerX = HW * UNIT / 2;// 算出棋盘中心的 X,Y
centerY = centerX + ToolHeight;
length = SIZE * UNIT;// 计算出棋盘的边长
POINT pts[] = {// 棋盘的四个点
{centerX - length / 2, centerY - length / 2},
{centerX + length / 2, centerY - length / 2},
{centerX + length / 2, centerY + length / 2},
{centerX - length / 2, centerY + length / 2}
};
setfillcolor(RGB(0x41, 0x55, 0x5d));
solidpolygon(pts, 4);// 绘制棋盘底色
setlinecolor(BLACK);
// 绘制棋盘线条
for (int i = 0; i < (SIZE + 1); ++i) {// 竖着
line(centerX - length / 2 + UNIT * i, centerY - length / 2, centerX - length / 2 + UNIT * i,
centerY + length / 2);
}
for (int i = 0; i < (SIZE + 1); ++i) {// 横着
line(centerX - length / 2, centerY - length / 2 + UNIT * i, centerX + length / 2,
centerY - length / 2 + UNIT * i);
}
for (int i = 0; i < SIZE; ++i) {// 棋子
for (int j = 0; j < SIZE; ++j) {
if (data[i][j].color == 0)continue;
setfillcolor(data[i][j].color == 1 ? BLACK : WHITE);
solidcircle(centerX - length / 2 + UNIT * i + UNIT / 2, centerY - length / 2 + UNIT * j + UNIT / 2,
UNIT / 2 - 3);
}
}
这里的坐标计算其实很简单,可以参考我下面的图片
UI绘制完成后,我们还需要做与用户的鼠标交互
新建一个函数ObserveMouse()
,并在drawBoard
后调用
void ObserveMouse() {
if (MouseHit()) {//如果鼠标没有点击,则不交互
MOUSEMSG msg = GetMouseMsg();
mouseX = msg.x;
mouseY = msg.y;
// 下面寻找 X 和 Y 是一个优雅做法,如果两层 for 嵌套,处理不好很容易浪费性能
for (int i = 0; i < SIZE; ++i)// 获取当前鼠标在棋盘上对应位置的 X
if (centerX - length / 2 + UNIT * i < mouseX)
if (mouseX < centerX - length / 2 + UNIT * i + UNIT) {
mouseI = i;
break;
}
for (int j = 0; j < SIZE; ++j)// 获取当前鼠标在棋盘上对应位置的 Y
if (centerY - length / 2 + UNIT * j < mouseY)
if (mouseY < centerY - length / 2 + UNIT * j + UNIT) {
mouseJ = j;
break;
}
if (data[mouseI][mouseJ].color != 0)return;
if (msg.mkLButton && msg.uMsg != WM_MOUSEMOVE) {// 如果只监听点击事件的话,会出现按住左键移动鼠标,下了一排棋
if (turn == HUMAN) {
turn = AI;
data[mouseI][mouseJ].color = 1;// 人下完了,换 AI
if (isFive())return;
}
if (turn == AI) {
预留代码部分
turn = HUMAN;
if (isFive())return;
}
}
FlushMouseMsgBuffer();
}
if (data[mouseI][mouseJ].color != 0)return;
setfillcolor(RGB(0XEE, 0XDE, 0XB0));// #eedeb0
solidcircle(centerX - length / 2 + UNIT * mouseI + UNIT / 2, centerY - length / 2 + UNIT * mouseJ + UNIT / 2,
UNIT / 2 - 3);// 绘制鼠标选中但还未落子的区域
}
在交互后,我们需要判断棋盘中是否存在五子连线,我们要写一个简单的算法。
int sameLine(int x, int y, int mLastx, int mLasty) // 判断一条线有多少个同色子
{
int num = 0; // 同一直线同色棋子数
int DefaultColor = data[mLastx][mLasty].color;
if (DefaultColor == 0)return 0; // 遇到 0,直接跳
int xx;
int yy;
// 1,0 横着 0,1 竖着
if (x == 1 && y == 0) {
for (xx = mLastx; xx >= 0; xx--) {// 向左判断
if (data[xx][mLasty].color == DefaultColor) {
num++;
} else break;
}
for (xx = mLastx; xx < (SIZE - 1); xx++) {// 向右判断
if (data[xx][mLasty].color == DefaultColor) {
num++;
} else break;
}
} else if (x == 0 && y == 1) {
for (yy = mLasty; yy >= 0; yy--) {// 向上判断
if (data[mLastx][yy].color == DefaultColor) {
num++;
} else break;
}
for (yy = mLasty; yy < (SIZE - 1); yy++) {// 向下判断
if (data[mLastx][yy].color == DefaultColor) {
num++;
} else break;
}
} else if (x == 1 && y == 1) {
for (xx = mLastx, yy = mLasty; xx < (SIZE - 1) && yy >= 0; xx++, yy--) {// 右上判断
if (data[xx][yy].color == DefaultColor) {
num++;
} else break;
}
for (xx = mLastx, yy = mLasty; xx >= 0 && yy < (SIZE - 1); yy++, xx--) {// 左下判断
if (data[xx][yy].color == DefaultColor) {
num++;
} else break;
}
} else if (x == -1 && y == 1) {
for (xx = mLastx, yy = mLasty; xx >= 0 && yy >= 0; yy--, xx--) {// 左上判断
if (data[xx][yy].color == DefaultColor) {
num++;
} else break;
}
for (xx = mLastx, yy = mLasty; xx < (SIZE - 1) && yy < (SIZE - 1); yy++, xx++) {// 右下判断
if (data[xx][yy].color == DefaultColor) {
num++;
} else break;
}
}
return --num;
}
bool isFive() {// 在每一个点判断是否存在 5 子连线
for (int i = 0; i < SIZE; ++i) {
for (int j = 0; j < SIZE; ++j) {
if (sameLine(1, 0, i, j) >= 5 || sameLine(0, 1, i, j) >= 5 || sameLine(1, 1, i, j) >= 5 ||
sameLine(-1, 1, i, j) >= 5)
return true;
}
}
return false;
}
接下来就是我们程序的重头戏,AI算法了
首先我们要了解五子棋中的一些术语
活X:一方的X个连在一起的子 【两边都没有被堵住】
冲X:一方的X个连在一起的字【一边被堵住】
其余可参考五子棋常用术语
首先我们创建一个类,叫做FiveChessAI_simon
类中变量及作用如下
private:
const int STEP_KILL = 99999;//活 5
const int STEP_DANGER = 99998;//冲 5 或跳 5
const int STEP_FOUR = 88888;//出现有两个点可以成五的四
const int STEP_SLAY = 77777;//双线成杀
const int STEP_AT_FOUR = 44444;// 冲四或跳四
Chess chess[SIZE][SIZE]; // 接受棋盘的所有棋子
int ownWeight[SIZE][SIZE] = {0,}; // 己方每个点权重
int oppositeWeight[SIZE][SIZE] = {0,}; // 对方每个点权重
int computerColor; // 电脑要走的棋子颜色
首先我们实现一个算法,判断某个棋子两边的生存空间
private:
//x,y为要判断的位置坐标,color是当前颜色,px,py是判断方向的
//mode为0时,返回一侧空位个数
//mode为1时,返回连线同色个数
//mode为2时,返回不相邻( 1 个空位)连续同色个数
int oneSide(int x, int y, int color, int px, int py, int mode) {
int num = 0, space = 0;
while (!(x + px < 0 || x + px > (SIZE - 1) ||
y + py < 0 || y + py > (SIZE - 1))) {
x += px;
y += py;
if (chess[x][y].color == 3 - color) break;
if (mode == 1 && chess[x][y].color != color) break;
if (mode == 2 && chess[x][y].color == 0) {
space++;
if (space > 1) break;
continue;
}
num++;
}
return num;
}
我再来实现一个算法,让他根据左右生产空间,连续同色,以及不相邻同色个数来判断跳几
private:
//参数 左生存空间 左连续同色个数 左不相邻同色个数 同理r开头的参数是右
int isJump(int ll, int ls, int lns, int rl, int rs, int rns) {
int sum = ls + rs + 1;
int lj = sum + lns - ls;
int rj = sum + rns - rs;
lj = lj == sum ? 0 : lj; //分开活,冲和跳
rj = rj == sum ? 0 : rj;
int num = (lj > rj ? lj : rj);
if (num >= 4) return num; //跳四以上不考虑冲
if (num < 2) return 0; //跳 2 以下无意义
int l_side = 0, r_side = 0;
if ((lns + 1 >= ll) ^ (rl == rs))
l_side = 1;//左冲
if ((rns + 1 >= rl) ^ (ll == ls))
r_side = 1; //右冲
if (lj == num && l_side == 0)
return num;//活跳
if (rj == num && r_side == 0)
return num;//活跳
return 10 + num;//冲跳,十位标识有障碍形成冲
}
我们再实现一个算法,判断冲几和活几
private:
//左生存空间 左连续同色个数 同理右
int isSide(int ll, int ls, int rl, int rs) {
if (ll == ls ^ rl == rs)
return ls + rs + 1;
return 0;
}
private:
//参数同上
int isLife(int ll, int ls, int rl, int rs) {
int num = ls + rs + 1;
if (num > 4 || (ll > ls && rl > rs))
return num;
return 0;
}
最后我们根据冲,跳,活可以生成某个点所在的线的权重
private:
//后两个参数是代表过点的哪一条线
int singleLine(int x, int y, int color, int px, int py) {
int leftLive = oneSide(x, y, color, px, py, 0); //左边生存空间
int rightLive = oneSide(x, y, color, -px, -py, 0); //右边生存空间
if (leftLive + rightLive < 4) return 1;//左右生存空间少于 4,此线无意义;
int leftSame = oneSide(x, y, color, px, py, 1); //左边相邻连续同色
int rightSame = oneSide(x, y, color, -px, -py, 1); //右边相邻连续同色
int leftNSame = oneSide(x, y, color, px, py, 2); //左边不相邻( 1 个空位)连续同色
int rightNSame = oneSide(x, y, color, -px, -py, 2); //右边不相邻( 1 个空位)连续同色
int life = isLife(leftLive, leftSame, rightLive, rightSame);
int side = isSide(leftLive, leftSame, rightLive, rightSame);
int jump = isJump(leftLive, leftSame, leftNSame, rightLive, rightSame, rightNSame);
return life * 1000 + side * 100 + jump; //千位为活子数,百位冲子数,十为标识个位的跳是否一边被拦
}
有了求点的4线权重的函数后,我们通过算法,把四线的权重进一步计算,生成每个点的权重
private:
int weightSum(int x, int y, int color, bool ignoreFour) {
int weight = 0; // 总权重
// 如果坐标处有子,则没有权重,如果是对面权重计算需要判断已有局面
if (chess[x][y].color > 0)
if (color == computerColor ||
chess[x][y].color == computerColor) {
return -1;//如果当前的点是自己的棋,则设置权重为 -1
}
获取 横竖 阳线 左斜线 右斜线 阴线 四线权重
int line[4];
line[0] = singleLine(x, y, color, 1, 0);
line[1] = singleLine(x, y, color, 0, 1);
line[2] = singleLine(x, y, color, 1, 1);
line[3] = singleLine(x, y, color, -1, 1);
int doubleLine = 0; //成双线杀的条件,大于等于 2 表示成杀
int four = 0;//活四及以上或跳四及以上个数
int op = chess[x][y].color > 0 ? -1 : 1;//已有子的坐标系数为 -1
for (int i = 0; i < 4; i++) {
int life = line[i] / 1000;
int side = line[i] / 100 % 10;
int jump = line[i] % 100;
//活 5
if (life > 4) {
weight = STEP_KILL;
//冲 5 或跳 5
if (side > 4 || jump % 10 > 4)
weight = STEP_DANGER;
return weight * op;
}
//活 4
if (life == 4) {
weight = STEP_FOUR;
return weight * op;
}
//活三,冲四 ,跳四及以上,活跳三
if (life == 3 || side == 4 || jump % 10 >= 4 ||
(jump / 10 == 0 && jump % 10 == 3)) {
doubleLine++; //满足双杀条件
weight += ((jump % 10 >= 3) ? 8000 : 10000); //跳权重少点
//冲四跳四
if (side >= 4 || jump % 10 >= 4)
four++;
//活二,冲三,冲跳三
} else if (life == 2 || side == 3 || jump % 10 == 3)
weight += 1000;
//余下权重计算
else {
weight = weight + life * 100 + side * 100 +
(jump % 10) * 10 - (jump / 10);
//中心点权重加 100 ,AI 开局就不会乱走
if (x == 7 && y == 7)
weight += 100;
}
}
if (doubleLine > 1)
weight = STEP_SLAY;//双线成杀
else if (doubleLine == 1 && four > 0 && ignoreFour)
weight = STEP_AT_FOUR;// 冲四或跳四
return weight * op;
}
我们已经可以计算某个点的权重,那我们把所有权重记录下了,并和坐标对应
private:
void calculateWeight() {
for (int i = 0; i < SIZE; i++)
for (int j = 0; j < SIZE; j++) {
ownWeight[i][j] = weightSum(i, j, computerColor, false);
oppositeWeight[i][j] = weightSum(i, j, 3 - computerColor, false);
}
}
最后我们取出权重最大的点,并加入一些额外的判断,让其选出最适合的点
public:
Chess *AIGo(Chess chess[SIZE][SIZE], int color) { // 返回计算机落子
for (int i = 0; i < SIZE; ++i)
for (int j = 0; j < SIZE; ++j)
this->chess[i][j] = chess[i][j];//复制棋盘
this->computerColor = color;
calculateWeight(); // 计算权重
Chess *point = new Chess(); // 定义返回的对象
point->color = computerColor; //返回的颜色必然是自己要走的颜色
point->x = -1;
point->y = -1;
int x1 = -1, y1 = -1; //己方最大权重记录坐标
int x2 = -1, y2 = -1; //对方最大权重记录坐标
int x3 = -1, y3 = -1; //双方权重合值最大记录坐标
int ownMax = 0; // 己方最大权重
int oppositeMax = 0; // 对方最大权重
int oppositeMin = 0; // 对方最小权重
int sumMax = 0;//双方合起来的权重
开始分析所有权重
for (int i = 0; i < SIZE; i++)
for (int j = 0; j < SIZE; j++) {
处理己方权重
if (ownMax < ownWeight[i][j]) {
ownMax = ownWeight[i][j]; // 获取己方最大权重
x1 = i; // 获取坐标
y1 = j;
if (debug)
printf("owner AIGo:weight=%4d,X=%2d,Y=%2d\n", ownMax, i, j);
} else if (ownMax == ownWeight[i][j]) {//权重相同,则随机选中一个点
if (rand() % 100 > 50) {
ownMax = ownWeight[i][j]; // 获取己方最大权重
x1 = i; // 获取坐标
y1 = j;
if (debug)
printf("owner AIGo:weight=%4d,X=%2d,Y=%2d\n", ownMax, i, j);
}
}
//己方已经四连,直接落子
if (ownMax == STEP_KILL || ownMax == STEP_DANGER) {
point->x = x1;
point->y = y1;
return point;
}
///处理对方权重
if (oppositeMax < oppositeWeight[i][j]) {
oppositeMax = oppositeWeight[i][j]; // 获取对方最大权重
x2 = i; // 获取坐标
y2 = j;
if (debug)
printf("opposite AIGo:weight=%4d,X=%2d,Y=%2d\n", ownMax, i, j);
} else if (oppositeMax == oppositeWeight[i][j])
if (rand() % 100 > 50) { //权重相同,则随机选中一个点
oppositeMax = oppositeWeight[i][j]; // 获取对方最大权重
x2 = i; // 获取坐标
y2 = j;
if (debug)
printf("opposite AIGo:weight=%4d,X=%2d,Y=%2d\n", ownMax, i, j);
}
//最小权重,负的所以是大于
if (oppositeMin > oppositeWeight[i][j])
oppositeMin = oppositeWeight[i][j];
///处理双方权重相加
if (sumMax < ownWeight[i][j] + oppositeWeight[i][j]) { //两边总权重
sumMax = ownWeight[i][j] + oppositeWeight[i][j];
x3 = i;
y3 = j;
} else if (sumMax == ownWeight[i][j] + oppositeWeight[i][j]) {
if (rand() % 100 > 50) { //权重相同,则随机选中一个点
sumMax = ownWeight[i][j] + oppositeWeight[i][j];
x3 = i;
y3 = j;
}
}
}
if (ownMax < 10 && oppositeMax < 10) return point;//和棋
///开始根据权重分析形势
//对方将要 5 连但是可以拦截
if (oppositeMax == STEP_DANGER) {
if (debug)
printf("对面即将五连,X=%2d,Y=%2d\n", x2, y2);
point->x = x2;
point->y = y2;
return point;
}
//己方将活四
if (ownMax == STEP_FOUR) {
point->x = x1;
point->y = y1;
return point;
}
//对方已经双线成杀,不拦截,全力冲四跳四
if (oppositeMin == -STEP_SLAY) {
if (debug)
printf("对面双线成杀,X=%2d,Y=%2d\n", x2, y2);
for (int i = 0; i < SIZE; i++)
for (int j = 0; j < SIZE; j++) {
ownWeight[i][j] = weightSum(i, j, computerColor, true);
if (ownWeight[i][j] == STEP_AT_FOUR) {
point->x = i;
point->y = j;
return point;
}
}
}
//己方将要双线成杀
if (ownMax == STEP_SLAY) {
point->x = x1;
point->y = y1;
return point;
}
//对面将活四 或 将双线成杀
if (oppositeMax == STEP_FOUR ||
oppositeMax == STEP_SLAY) {
point->x = x2;
point->y = y2;
return point;
}
//剩下走双方权重相合最大的点
point->x = x3;
point->y = y3;
return point;
}
至此,整个程序的功能均已完成
完整代码
Bean.h
//
// Created by Simon on 2022/3/5.
//
#define SIZE 15//定义棋盘内行列棋子的个数
#define debug false//是否为 debug 模式
#define UNIT 20//定义 UNIT 单位,1 UNIT 约等于 20 px
#ifndef NANI_BEAN_H
#define NANI_BEAN_H
class Chess {
public:
int x;//得益于 c++ 的数组问题,数组内的元素无法感知到自己的 x 和 y ,所以需要自己记录
int y;
int color = 0;//黑 1 白 2 0 没有 人是 1 AI 是 2
};
#endif //NANI_BEAN_H
SimonAi.h
//
// Created by Simon on 2022/3/5.
//
#include <cstdlib>
#include <valarray>
#include "Bean.h"
#ifndef NANI_SIMONAI_H
#define NANI_SIMONAI_H
class FiveChessAI_simon {
// AI 算法,本算法防守为主,可攻则攻
private:
const int STEP_KILL = 99999;//活 5
const int STEP_DANGER = 99998;//冲 5 或跳 5
const int STEP_FOUR = 88888;//出现有两个点可以成五的四
const int STEP_SLAY = 77777;//双线成杀
const int STEP_AT_FOUR = 44444;
Chess chess[SIZE][SIZE]; // 接受棋盘的所有棋子
int ownWeight[SIZE][SIZE] = {0,}; // 己方每个点权重
int oppositeWeight[SIZE][SIZE] = {0,}; // 对方每个点权重
int computerColor; // 电脑要走的棋子颜色 黑 1 白 2 ,0 为空 默认人类 1 AI 2
/**
* 五子棋 AI
*
* @author Simon
*/
public:
Chess *AIGo(Chess chess[SIZE][SIZE], int color) { // 返回计算机落子
for (int i = 0; i < SIZE; ++i)
for (int j = 0; j < SIZE; ++j)
this->chess[i][j] = chess[i][j];//复制棋盘
this->computerColor = color;
calculateWeight(); // 计算权重
Chess *point = new Chess(); // 定义返回的对象
point->color = computerColor; //返回的颜色必然是自己要走的颜色
point->x = -1;
point->y = -1;
int x1 = -1, y1 = -1; //己方最大权重记录坐标
int x2 = -1, y2 = -1; //对方最大权重记录坐标
int x3 = -1, y3 = -1; //双方权重合值最大记录坐标
int ownMax = 0; // 己方最大权重
int oppositeMax = 0; // 对方最大权重
int oppositeMin = 0; // 对方最小权重
int sumMax = 0;//双方合起来的权重
开始分析所有权重
for (int i = 0; i < SIZE; i++)
for (int j = 0; j < SIZE; j++) {
处理己方权重
if (ownMax < ownWeight[i][j]) {
ownMax = ownWeight[i][j]; // 获取己方最大权重
x1 = i; // 获取坐标
y1 = j;
if (debug)
printf("owner AIGo:weight=%4d,X=%2d,Y=%2d\n", ownMax, i, j);
} else if (ownMax == ownWeight[i][j]) {//权重相同,则随机选中一个点
if (rand() % 100 > 50) {
ownMax = ownWeight[i][j]; // 获取己方最大权重
x1 = i; // 获取坐标
y1 = j;
if (debug)
printf("owner AIGo:weight=%4d,X=%2d,Y=%2d\n", ownMax, i, j);
}
}
//己方已经四连,直接落子
if (ownMax == STEP_KILL || ownMax == STEP_DANGER) {
point->x = x1;
point->y = y1;
return point;
}
///处理对方权重
if (oppositeMax < oppositeWeight[i][j]) {
oppositeMax = oppositeWeight[i][j]; // 获取对方最大权重
x2 = i; // 获取坐标
y2 = j;
if (debug)
printf("opposite AIGo:weight=%4d,X=%2d,Y=%2d\n", ownMax, i, j);
} else if (oppositeMax == oppositeWeight[i][j])
if (rand() % 100 > 50) { //权重相同,则随机选中一个点
oppositeMax = oppositeWeight[i][j]; // 获取对方最大权重
x2 = i; // 获取坐标
y2 = j;
if (debug)
printf("opposite AIGo:weight=%4d,X=%2d,Y=%2d\n", ownMax, i, j);
}
//最小权重,负的所以是大于
if (oppositeMin > oppositeWeight[i][j])
oppositeMin = oppositeWeight[i][j];
///处理双方权重相加
if (sumMax < ownWeight[i][j] + oppositeWeight[i][j]) { //两边总权重
sumMax = ownWeight[i][j] + oppositeWeight[i][j];
x3 = i;
y3 = j;
} else if (sumMax == ownWeight[i][j] + oppositeWeight[i][j]) {
if (rand() % 100 > 50) { //权重相同,则随机选中一个点
sumMax = ownWeight[i][j] + oppositeWeight[i][j];
x3 = i;
y3 = j;
}
}
}
if (ownMax < 10 && oppositeMax < 10) return point;//和棋
///开始根据权重分析形势
//对方将要 5 连但是可以拦截
if (oppositeMax == STEP_DANGER) {
if (debug)
printf("对面即将五连,X=%2d,Y=%2d\n", x2, y2);
point->x = x2;
point->y = y2;
return point;
}
//己方将活四
if (ownMax == STEP_FOUR) {
point->x = x1;
point->y = y1;
return point;
}
//对方已经双线成杀,不拦截,全力冲四跳四
if (oppositeMin == -STEP_SLAY) {
if (debug)
printf("对面双线成杀,X=%2d,Y=%2d\n", x2, y2);
for (int i = 0; i < SIZE; i++)
for (int j = 0; j < SIZE; j++) {
ownWeight[i][j] = weightSum(i, j, computerColor, true);
if (ownWeight[i][j] == STEP_AT_FOUR) {
point->x = i;
point->y = j;
return point;
}
}
}
//己方将要双线成杀
if (ownMax == STEP_SLAY) {
point->x = x1;
point->y = y1;
return point;
}
//对面将活四 或 将双线成杀
if (oppositeMax == STEP_FOUR ||
oppositeMax == STEP_SLAY) {
point->x = x2;
point->y = y2;
return point;
}
//剩下走双方权重相合最大的点
point->x = x3;
point->y = y3;
return point;
}
获取双方所有坐标的权重
private:
void calculateWeight() {
for (int i = 0; i < SIZE; i++)
for (int j = 0; j < SIZE; j++) {
ownWeight[i][j] = weightSum(i, j, computerColor, false);
oppositeWeight[i][j] = weightSum(i, j, 3 - computerColor, false);
}
}
分析一个点所能形成的局势,用权重表示
private:
int weightSum(int x, int y, int color, bool ignoreFour) {
int weight = 0; // 总权重
// 如果坐标处有子,则没有权重,如果是对面权重计算需要判断已有局面
if (chess[x][y].color > 0)
if (color == computerColor ||
chess[x][y].color == computerColor) {
return -1;//如果当前的点是自己的棋,则设置权重为 -1
}
获取 横竖 阳线 左斜线 右斜线 阴线 四线权重
int line[4];
line[0] = singleLine(x, y, color, 1, 0);
line[1] = singleLine(x, y, color, 0, 1);
line[2] = singleLine(x, y, color, 1, 1);
line[3] = singleLine(x, y, color, -1, 1);
int doubleLine = 0; //成双线杀的条件,大于等于 2 表示成杀
int four = 0;//活四及以上或跳四及以上个数
int op = chess[x][y].color > 0 ? -1 : 1;//已有子的坐标系数为 -1
for (int i = 0; i < 4; i++) {
int life = line[i] / 1000;
int side = line[i] / 100 % 10;
int jump = line[i] % 100;
//活 5
if (life > 4) {
weight = STEP_KILL;
//冲 5 或跳 5
if (side > 4 || jump % 10 > 4)
weight = STEP_DANGER;
return weight * op;
}
//活 4
if (life == 4) {
weight = STEP_FOUR;
return weight * op;
}
//活三,冲四 ,跳四及以上,活跳三
if (life == 3 || side == 4 || jump % 10 >= 4 ||
(jump / 10 == 0 && jump % 10 == 3)) {
doubleLine++; //满足双杀条件
weight += ((jump % 10 >= 3) ? 8000 : 10000); //跳权重少点
//冲四跳四
if (side >= 4 || jump % 10 >= 4)
four++;
//活二,冲三,冲跳三
} else if (life == 2 || side == 3 || jump % 10 == 3)
weight += 1000;
//余下权重计算
else {
weight = weight + life * 100 + side * 100 +
(jump % 10) * 10 - (jump / 10);
//中心点权重加 100 ,AI 开局就不会乱走
if (x == 7 && y == 7)
weight += 100;
}
}
if (doubleLine > 1)
weight = STEP_SLAY;//双线成杀
else if (doubleLine == 1 && four > 0 && ignoreFour)
weight = STEP_AT_FOUR;// 冲四或跳四
return weight * op;
}
单线权重计算
private:
int singleLine(int x, int y, int color, int px, int py) {
int leftLive = oneSide(x, y, color, px, py, 0); //左边生存空间
int rightLive = oneSide(x, y, color, -px, -py, 0); //右边生存空间
if (leftLive + rightLive < 4) return 1;//左右生存空间少于 4,此线无意义;
int leftSame = oneSide(x, y, color, px, py, 1); //左边相邻连续同色
int rightSame = oneSide(x, y, color, -px, -py, 1); //右边相邻连续同色
int leftNSame = oneSide(x, y, color, px, py, 2); //左边不相邻( 1 个空位)连续同色
int rightNSame = oneSide(x, y, color, -px, -py, 2); //右边不相邻( 1 个空位)连续同色
int life = isLife(leftLive, leftSame, rightLive, rightSame);
int side = isSide(leftLive, leftSame, rightLive, rightSame);
int jump = isJump(leftLive, leftSame, leftNSame, rightLive, rightSame, rightNSame);
return life * 1000 + side * 100 + jump; //千位为活子数,百位冲子数,十为标识个位的跳是否一边被拦
}
判断活几
private:
int isLife(int ll, int ls, int rl, int rs) {
int num = ls + rs + 1;
if (num > 4 || (ll > ls && rl > rs))
return num;
return 0;
}
判断冲几
private:
int isSide(int ll, int ls, int rl, int rs) {
if (ll == ls ^ rl == rs)
return ls + rs + 1;
return 0;
}
判断跳几
private:
int isJump(int ll, int ls, int lns, int rl, int rs, int rns) {
int sum = ls + rs + 1;
int lj = sum + lns - ls;
int rj = sum + rns - rs;
lj = lj == sum ? 0 : lj; //分开活,冲和跳
rj = rj == sum ? 0 : rj;
int num = (lj > rj ? lj : rj);
if (num >= 4) return num; //跳四以上不考虑冲
if (num < 2) return 0; //跳 2 以下无意义
int l_side = 0, r_side = 0;
if ((lns + 1 >= ll) ^ (rl == rs))
l_side = 1;//左冲
if ((rns + 1 >= rl) ^ (ll == ls))
r_side = 1; //右冲
if (lj == num && l_side == 0)
return num;//活跳
if (rj == num && r_side == 0)
return num;//活跳
return 10 + num;//冲跳,十位标识有障碍形成冲
}
单线参数获取
private:
int oneSide(int x, int y, int color, int px, int py, int mode) {
int num = 0, space = 0;
while (!(x + px < 0 || x + px > (SIZE - 1) ||
y + py < 0 || y + py > (SIZE - 1))) {
x += px;
y += py;
if (chess[x][y].color == 3 - color) break;
if (mode == 1 && chess[x][y].color != color) break;
if (mode == 2 && chess[x][y].color == 0) {
space++;
if (space > 1) break;
continue;
}
num++;
}
return num;
}
};
#endif //NANI_SIMONAI_H
main.cpp
#include <string>
#include <conio.h>
#include <graphics.h>
#include "Bean.h"
#include "SimonAi.h"
#define ToolHeight UNIT* 3// 标题栏所占的高度
// 0 没下棋 1 黑 2 白
static int HW = 16; // 窗口大小,单位 UNIT( UNIT 声明在 Bean.h 中)
static time_t start; // 程序开始时的时间
struct Chess data[SIZE][SIZE]; // 所有棋子的集合
static int mouseX = UNIT, mouseY = UNIT; // 鼠标的 x 和 y,把他们拿到外面,是为了在 debug 模式下输出鼠标信息
static int centerX, length, centerY; // 棋盘中心的 x , y 以及棋盘长度
static int mouseI, mouseJ; // 鼠标所在区域的 x, y
enum Turn // 这里其实可以删掉,没有必要做枚举
{
HUMAN, AI
} turn = HUMAN;
int sameLine(int x, int y, int mLastx, int mLasty) // 判断一条线有多少个同色子
{
int num = 0; // 同一直线同色棋子数
int DefaultColor = data[mLastx][mLasty].color;
if (DefaultColor == 0)return 0; // 遇到 0,直接跳
int xx;
int yy;
// 1,0 横着 0,1 竖着
if (x == 1 && y == 0) {
for (xx = mLastx; xx >= 0; xx--) {// 向左判断
if (data[xx][mLasty].color == DefaultColor) {
num++;
} else break;
}
for (xx = mLastx; xx < (SIZE - 1); xx++) {// 向右判断
if (data[xx][mLasty].color == DefaultColor) {
num++;
} else break;
}
} else if (x == 0 && y == 1) {
for (yy = mLasty; yy >= 0; yy--) {// 向上判断
if (data[mLastx][yy].color == DefaultColor) {
num++;
} else break;
}
for (yy = mLasty; yy < (SIZE - 1); yy++) {// 向下判断
if (data[mLastx][yy].color == DefaultColor) {
num++;
} else break;
}
} else if (x == 1 && y == 1) {
for (xx = mLastx, yy = mLasty; xx < (SIZE - 1) && yy >= 0; xx++, yy--) {// 右上判断
if (data[xx][yy].color == DefaultColor) {
num++;
} else break;
}
for (xx = mLastx, yy = mLasty; xx >= 0 && yy < (SIZE - 1); yy++, xx--) {// 左下判断
if (data[xx][yy].color == DefaultColor) {
num++;
} else break;
}
} else if (x == -1 && y == 1) {
for (xx = mLastx, yy = mLasty; xx >= 0 && yy >= 0; yy--, xx--) {// 左上判断
if (data[xx][yy].color == DefaultColor) {
num++;
} else break;
}
for (xx = mLastx, yy = mLasty; xx < (SIZE - 1) && yy < (SIZE - 1); yy++, xx++) {// 右下判断
if (data[xx][yy].color == DefaultColor) {
num++;
} else break;
}
}
return --num;
}
bool isFive() {// 在每一个点判断是否存在 5 子连线
for (int i = 0; i < SIZE; ++i) {
for (int j = 0; j < SIZE; ++j) {
if (sameLine(1, 0, i, j) >= 5 || sameLine(0, 1, i, j) >= 5 || sameLine(1, 1, i, j) >= 5 ||
sameLine(-1, 1, i, j) >= 5)
return true;
}
}
return false;
}
void ObserveMouse() {
if (MouseHit()) {
MOUSEMSG msg = GetMouseMsg();
mouseX = msg.x;
mouseY = msg.y;
// 下面寻找 X 和 Y 是一个优雅做法,如果两层 for 嵌套,处理不好很容易浪费性能
for (int i = 0; i < SIZE; ++i)// 获取当前鼠标在棋盘上对应位置的 X
if (centerX - length / 2 + UNIT * i < mouseX)
if (mouseX < centerX - length / 2 + UNIT * i + UNIT) {
mouseI = i;
break;
}
for (int j = 0; j < SIZE; ++j)// 获取当前鼠标在棋盘上对应位置的 Y
if (centerY - length / 2 + UNIT * j < mouseY)
if (mouseY < centerY - length / 2 + UNIT * j + UNIT) {
mouseJ = j;
break;
}
if (data[mouseI][mouseJ].color != 0)return;
if (msg.mkLButton && msg.uMsg != WM_MOUSEMOVE) {// 如果只监听点击事件的话,会出现按住左键移动鼠标,下了一排棋
if (turn == HUMAN) {
turn = AI;
data[mouseI][mouseJ].color = 1;// 人下完了,换 AI
if (isFive())return;
}
if (turn == AI) {
FiveChessAI_simon AI;
Chess *chess = AI.AIGo(data, 2);// 2 代表的是 AI 的颜色
data[chess->x][chess->y].x = chess->x;
data[chess->x][chess->y].y = chess->y;
data[chess->x][chess->y].color = chess->color;
turn = HUMAN;
if (isFive())return;
}
}
FlushMouseMsgBuffer();
}
if (data[mouseI][mouseJ].color != 0)return;
setfillcolor(RGB(0XEE, 0XDE, 0XB0));// #eedeb0
solidcircle(centerX - length / 2 + UNIT * mouseI + UNIT / 2, centerY - length / 2 + UNIT * mouseJ + UNIT / 2,
UNIT / 2 - 3);// 绘制鼠标选中但还未落子的区域
}
void drawToolBar() {
static int R = 10;// 这里的 RGB 是实现标题流光的效果
static int G = 10;
static int B = 10;
R = R + 2;
G = R * 3;
B = B + 4;
if (++R > 240)
R = 10;
if (++G > 240)
G = 10;
if (++B > 240)
B = 10;
setfillcolor(WHITE);
solidrectangle(0, 0, HW * UNIT, ToolHeight);// 画出标题背景色
setbkmode(TRANSPARENT);
settextcolor(RGB(R, G, B));
settextstyle(UNIT, 0, nullptr);
char *title = "Simon 的 AI 五子棋";
char version[30] = "EasyX 版本";
strcat(version, GetEasyXVer());
outtextxy(UNIT, (ToolHeight - UNIT) / 2, title);
settextcolor(BLACK);
outtextxy(UNIT, (ToolHeight - UNIT) / 2 - UNIT, version);// 绘制标题和 EasyX 版本
if (debug) {// 判断是否开启 debug 模式,如果开启,则输出鼠标的 X 和 Y 的信息,不开启则输出游戏时长
char mouse[30];
sprintf(mouse, "x:%d y:%d", mouseX, mouseY);
outtextxy(UNIT, (ToolHeight - UNIT) / 2 + UNIT, mouse);
} else {
char time[30];
sprintf(time, "运行时长:%d 秒", (clock() - start) / CLOCKS_PER_SEC);
outtextxy(UNIT, (ToolHeight - UNIT) / 2 + UNIT, time);
}
}
void drawBoard() {
setfillcolor(RGB(0xe9, 0xe7, 0xef));
solidrectangle(0, ToolHeight, HW * UNIT, HW * UNIT + ToolHeight);//绘制标题以下的区域颜色
centerX = HW * UNIT / 2;// 算出棋盘中心的 X,Y
centerY = centerX + ToolHeight;
length = SIZE * UNIT;// 计算出棋盘的边长
POINT pts[] = {// 棋盘的四个点
{centerX - length / 2, centerY - length / 2},
{centerX + length / 2, centerY - length / 2},
{centerX + length / 2, centerY + length / 2},
{centerX - length / 2, centerY + length / 2}
};
setfillcolor(RGB(0x41, 0x55, 0x5d));
solidpolygon(pts, 4);// 绘制棋盘底色
setlinecolor(BLACK);
// 绘制棋盘线条
for (int i = 0; i < (SIZE + 1); ++i) {// 竖着
line(centerX - length / 2 + UNIT * i, centerY - length / 2, centerX - length / 2 + UNIT * i,
centerY + length / 2);
}
for (int i = 0; i < (SIZE + 1); ++i) {// 横着
line(centerX - length / 2, centerY - length / 2 + UNIT * i, centerX + length / 2,
centerY - length / 2 + UNIT * i);
}
for (int i = 0; i < SIZE; ++i) {// 棋子
for (int j = 0; j < SIZE; ++j) {
if (data[i][j].color == 0)continue;
setfillcolor(data[i][j].color == 1 ? BLACK : WHITE);
solidcircle(centerX - length / 2 + UNIT * i + UNIT / 2, centerY - length / 2 + UNIT * j + UNIT / 2,
UNIT / 2 - 3);
}
}
}
int main() {
start = clock();
srand(time(0));// 初始化随机数种子,为了在 AI 下棋时具有随机加权
initgraph(HW * UNIT, HW * UNIT + ToolHeight, debug ? SHOWCONSOLE : NULL);// 如果是 debug 模式,则开启控制台
while (true) {// 开启帧驱动
BeginBatchDraw();// 收集绘制
drawToolBar();// 绘制标题栏
drawBoard();// 绘制棋盘
ObserveMouse();// 监听鼠标
if (isFive()) // 是否有 5
goto end;
EndBatchDraw();// 开始绘制
}
end:// 游戏结束后的绘制
{
setfillcolor(WHITE);
solidrectangle(0, 0, HW * UNIT, ToolHeight);
outtextxy(UNIT, (ToolHeight - UNIT) / 2, "高下立判啦~~");
drawBoard();// 绘制出绝杀的那步棋
EndBatchDraw();// 很关键的一行
}
getchar();
return 0;
}
结语
如果存在有和我雷同的代码,那一定是他抄袭的我的(笑)
本来想把这个代码传到codebus(easyX社区)的,但是社区发布代码,必须严格遵守他们设定的代码规范,我这种规范的JAVA式风格,在代码检查中出现1500多个错误。
本人目前的精力实在有限,还是待我改日再试一番吧!