五子棋
2021-5-15 ~ 2021-5-23
(0)
感谢慢羊羊的数字拼图游戏,我用那篇文章初步学了 EasyX 后简单地完成了五子棋游戏的初始的骨架。
参考了陈可佳的博弈五子棋,看懂了代码,可是 AI 算法的架子骨摆在那里,我也没做实际上的修改,就只能做些锦上添花的工作,比如
- 一些关键的变量不采用魔数,而封装成变量——增强可读性、可拓展性
- 不使用全局变量,而是全部采用类来封装
- 是封装了单选框类、按钮类、棋盘类
- 将一些复杂的小任务封装成函数——增强可读性
- 将架构设计得方便将来拓展更多的 AI——增强可拓展性
画面如下:
图 1 玩家对玩家
图 2 AI 对 AI
可以看到,有彩色的提示框,分别可以提示上一次落子的位置以及目前鼠标的位置。红黄绿蓝是来自谷歌图标颜色的灵感。
你可以自己下棋,也可以下到一半选择 AI 和你一起下,甚至可以让 AI 自己下棋!
虽然有点慢,陈可佳说用了 4 层,我没改。不过其实 3 层或者 2 层应该也足够用,你可以在
if (layer < 3)
这句修改层数。
最后感谢慢羊羊对我文章格式的指导。
源码如下,有问题加我 QQ: 782429852。
/////////////////////////////////////////////////////////
// 程序名称:五子棋
// 编译环境:VS 2019,EasyX_20210224
// 作 者:安生晓
// 最后修改:2021-5-15
//
#include <easyx.h>
#include <time.h>
#include <conio.h>
#include <iostream>
#include <vector>
// 位置
struct Position
{
int x; // 0 <= x <= 14
int y; // 0 <= y <= 14
int score; // 分值,用于 AI 估分
bool operator==(const Position& b)
{
return x == b.x && y == b.y;
}
};
// 带文字的按钮类
class TextButton
{
public:
TextButton(int x, int y, LPCTSTR str, COLORREF background_color = RGB(255, 205, 150)) :x(x), y(y), str(str), background_color(background_color) {};
static const int left = 5;
static const int top = 5;
static const int roundRadius = 5;
int x;
int y;
LPCTSTR str;
COLORREF background_color;
bool isIn(int point_x, int point_y); // 判断点是否在按钮范围内
void draw(); // 绘制按钮
};
bool TextButton::isIn(int point_x, int point_y)
{
if (point_x >= x && point_x <= x + textwidth(str) + 2 * left && point_y >= y && point_y <= y + textheight(str) + 2 * top)
return true;
else
return false;
}
void TextButton::draw()
{
auto pstyle = std::make_shared<LINESTYLE>();
getlinestyle(pstyle.get());
setlinestyle(PS_SOLID, 1);
COLORREF tempColor = getfillcolor();
setfillcolor(background_color); // 填充颜色设置
fillroundrect(x, y, x + textwidth(str) + 2 * left, y + textheight(str) + 2 * top, roundRadius, roundRadius);
setfillcolor(tempColor);
setlinestyle(pstyle.get());
outtextxy(x + left, y + top, str);
}
// 单选框类
class Radio
{
public:
Radio(int x, int y, int value) :x(x), y(y), value(value) {};
static const int radius = 6;
int x;
int y;
int value;
bool isIn(int point_x, int point_y); // 判断点是否在单选框范围内
void draw(int groupValue); // 绘制单选框
};
bool Radio::isIn(int point_x, int point_y)
{
if (point_x >= x - radius && point_x <= x + radius && point_y >= y - radius && point_y <= y + radius)
return true;
else
return false;
}
void Radio::draw(int groupValue)
{
auto pstyle = std::make_shared<LINESTYLE>();
getlinestyle(pstyle.get());
setlinestyle(PS_SOLID, 1);
circle(x, y, radius);
setlinestyle(pstyle.get());
if (value == groupValue) {
COLORREF tempColor = getfillcolor();
setfillcolor(GREEN);
fillcircle(x, y, radius / 3);
setfillcolor(tempColor);
}
}
// 棋盘类
class ChessBoard
{
public:
bool is_black = true; // 轮到黑子下棋还是白子
int who_win = 0; // 1 为黑子获胜,2 为白子获胜,0 为平局
std::vector<Position> black_chesses = std::vector<Position>(0); // 存储已经下过的黑子坐标,方便检查判定
std::vector<Position> white_chesses = std::vector<Position>(0); // 存储已经下过的白子坐标,方便检查判定
int g_Map[15][15] = {}; // 棋盘,1 为黑子,2 为白子,0 为无子
COLORREF background_color = RGB(255, 205, 150); // 棋盘背景色
int left_padding = 30; // 棋盘左边距
int top_padding = 33; // 棋盘上边距
int spacing = 40; // 棋盘格子间距
Position mouse_position = Position{ -1,-1 }; // 鼠标位置
// 两个单选框组的选择变量
int blackRadioGroupValue = 0;
int whiteRadioGroupValue = 0;
// 0-玩家 2-人机(决策树算法)
Radio player_black = Radio(left_padding + 15 * spacing + 20, top_padding + 5 * spacing, 0);
Radio DT_black = Radio(left_padding + 15 * spacing + 20, top_padding + 5 * spacing + 50, 2);
Radio player_white = Radio(left_padding + 18 * spacing + 20, top_padding + 5 * spacing, 0);
Radio DT_white = Radio(left_padding + 18 * spacing + 20, top_padding + 5 * spacing + 50, 2);
TextButton initButton = TextButton(left_padding + 16 * spacing + 20, top_padding + 5 * spacing + 75, L"重新开始");
void init(); // 初始化
void draw(); // 绘制游戏界面
void drawPromptBox(const Position& p); // 绘制提示框
bool checkchess(Position& p, bool is_black, int xdiff, int ydiff); // 检查是否满足获胜条件
bool oneWin(); // 判断是否有一方胜利
void play(); // 开始游戏
// 决策树
void DT();
Position findBestPosition(bool is_black, int layer);
bool hasNeighbour(int x, int y);
bool firstRandSet();
int dx[4]{ 1,0,1,1 }; // - | \ / 四个方向
int dy[4]{ 0,1,1,-1 };
int Score[3][5] = //评分表
{
{ 0, 80, 250, 500, 500 }, // 防守0子
{ 0, 0, 80, 250, 500 }, // 防守1子
{ 0, 0, 0, 80, 500 } // 防守2子
};
int MAXxs[361] = {}; //最优x坐标
int MAXys[361] = {}; //最优y坐标
int mylength = 0; //最优解数
};
void ChessBoard::init()
{
// 变量初始化
is_black = true;
who_win = 0;
black_chesses = std::vector<Position>(0);
white_chesses = std::vector<Position>(0);
for (int i = 0; i < 15; i++)
{
for (int j = 0; j < 15; j++)
{
g_Map[i][j] = 0;
}
}
// 清空鼠标缓冲区
FlushMouseMsgBuffer();
}
void ChessBoard::drawPromptBox(const Position& p)
{
// 绘制有四种颜色的提示框
COLORREF linecolor = getlinecolor();
int half_spacing = spacing / 2;
int length = spacing / 3;
int x = p.x * spacing + left_padding;
int y = p.y * spacing + top_padding;
setlinestyle(PS_SOLID, 2);
setlinecolor(RED);
// 左上角
line(x - half_spacing, y - half_spacing, x - half_spacing, y - half_spacing + length); // 横
line(x - half_spacing, y - half_spacing, x - half_spacing + length, y - half_spacing); // 竖
setlinecolor(YELLOW);
// 右上角
line(x + half_spacing, y - half_spacing, x + half_spacing, y - half_spacing + length); // 横
line(x + half_spacing, y - half_spacing, x + half_spacing - length, y - half_spacing); // 竖
setlinecolor(GREEN);
// 右上角
line(x - half_spacing, y + half_spacing, x - half_spacing, y + half_spacing - length); // 横
line(x - half_spacing, y + half_spacing, x - half_spacing + length, y + half_spacing); // 竖
setlinecolor(BLUE);
// 右下角
line(x + half_spacing, y + half_spacing, x + half_spacing, y + half_spacing - length); // 横
line(x + half_spacing, y + half_spacing, x + half_spacing - length, y + half_spacing); // 竖
setlinecolor(linecolor);
}
void ChessBoard::draw()
{
BeginBatchDraw(); // 开始批量绘制,防止闪烁用的
setlinestyle(PS_SOLID, 2);
cleardevice(); // 清屏
// 画背景
setfillcolor(background_color); // 填充颜色设置
solidrectangle(0, 0, 14 * spacing + left_padding * 2, 14 * spacing + top_padding * 2);
settextcolor(BLACK);
int number = 0;
// 坐标(数值)
TCHAR strnum[15][3] = { _T("1"),_T("2") ,_T("3") ,_T("4"),_T("5") ,_T("6") ,_T("7"),_T("8"),_T("9"),_T("10"), _T("11"),_T("12") ,_T("13") ,_T("14"),_T("15") };
// 坐标(字母)
TCHAR strabc[15][3] = { _T("A"),_T("B") ,_T("C") ,_T("D"),_T("E") ,_T("F") ,_T("G"),_T("H"),_T("I"),_T("J"), _T("K"),_T("L") ,_T("M") ,_T("N"),_T("O") };
// 画坐标
for (int i = 0; i < 15; i++)
{
outtextxy(left_padding + number - 6, top_padding - 23, strnum[i]);
outtextxy(left_padding - 14, top_padding + number - 6, strabc[i]);
number += spacing;
}
setlinecolor(BLACK);
for (int x = 0; x < 15; x++) // 画横线
line(left_padding, x * spacing + top_padding, 14 * spacing + left_padding, x * spacing + top_padding);
for (int y = 0; y < 15; y++) // 画竖线
line(y * spacing + left_padding, top_padding, y * spacing + left_padding, 14 * spacing + top_padding);
// 画五颗星位
for (int i : {3, 7, 11})
{
for (int j : {3, 7, 11})
{
setfillcolor(BLACK);
fillcircle(i * spacing + left_padding, j * spacing + top_padding, 4);
}
}
for (int x = 0; x < 15; x++) // 画棋子
{
for (int y = 0; y < 15; y++) {
if (g_Map[x][y] == 1) {
setfillcolor(BLACK);
fillcircle(x * spacing + left_padding, y * spacing + top_padding, 15);
}
else if (g_Map[x][y] == 2) {
setfillcolor(WHITE);
fillcircle(x * spacing + left_padding, y * spacing + top_padding, 15);
}
}
}
// 最后一颗棋子绘制提示边框
if (black_chesses.size() == white_chesses.size())
{
if (!white_chesses.empty())
{
drawPromptBox(white_chesses.back());
}
}
else
{
if (!black_chesses.empty())
{
drawPromptBox(black_chesses.back());
}
}
if (mouse_position.x != -1)
{
drawPromptBox(mouse_position);
}
LOGFONT* font = new LOGFONT();
gettextstyle(font);
settextstyle(30, 0, L"楷体", 0, 0, 4, false, false, false, 0, OUT_TT_ONLY_PRECIS, CLIP_DEFAULT_PRECIS, ANTIALIASED_QUALITY, DEFAULT_PITCH);
settextcolor(BLACK);
outtextxy(15 * spacing + 2 * left_padding, top_padding, L"五子棋");
settextstyle(font);
outtextxy(player_black.x + Radio::radius + 5, player_black.y - Radio::radius - 3, L"玩家A");
outtextxy(player_black.x + Radio::radius + 10 + textwidth(L"五子棋"), player_black.y - Radio::radius - 3, L"玩家B");
outtextxy(DT_black.x + Radio::radius + 30, DT_black.y - Radio::radius - 3, L"决策树");
// 绘制黑方单选框
player_black.draw(blackRadioGroupValue);
DT_black.draw(blackRadioGroupValue);
// 绘制白方单选框
player_white.draw(whiteRadioGroupValue);
DT_white.draw(whiteRadioGroupValue);
// 绘制重新开始按钮
initButton.draw();
EndBatchDraw(); // 结束批量绘制
}
// xdiff 和 ydiff 只在-1,0,1中取。可以用来表示横、竖、左斜、右斜方向
bool ChessBoard::checkchess(Position& p, bool is_black, int xdiff, int ydiff)
{
Position temp = Position{};
// std::cout << "p.x = " << p.x << ", p.y = " << p.y << std::endl;
for (int i = 1; i < 5; i++) // 我这是是反方向判断的,如果你觉得不爽可以通过修改+-号修改判断的方向
{
temp.x = p.x - xdiff * i;
temp.y = p.y - ydiff * i;
if (is_black)
{
if (find(black_chesses.begin(), black_chesses.end(), temp) == black_chesses.end())
{
return false;
}
}
else
{
if (find(white_chesses.begin(), white_chesses.end(), temp) == white_chesses.end())
{
return false;
}
}
}
return true;
}
bool ChessBoard::oneWin()
{
for (Position& p : black_chesses)
{
if (checkchess(p, true, 0, 1) || checkchess(p, true, 1, 0) || checkchess(p, true, 1, 1) || checkchess(p, true, -1, 1))
{
who_win = 1; return true;
}
}
for (Position& p : white_chesses)
{
if (checkchess(p, false, 0, 1) || checkchess(p, false, 1, 0) || checkchess(p, false, 1, 1) || checkchess(p, false, -1, 1))
{
who_win = 2; return true;
}
}
// std::cout << "没有胜利" << std::endl;
return false;
}
void ChessBoard::play()
{
MOUSEMSG msg;
while (black_chesses.size() < 113) // 游戏主循环
{
msg = GetMouseMsg(); // 获取鼠标信息
if (msg.mkLButton) { // 检测是否点击单选框或重新开始按钮
if (player_black.isIn(msg.x, msg.y))
{
blackRadioGroupValue = player_black.value; continue;
}
else if (DT_black.isIn(msg.x, msg.y))
{
blackRadioGroupValue = DT_black.value; continue;
}
else if (player_white.isIn(msg.x, msg.y))
{
whiteRadioGroupValue = player_white.value; continue;
}
else if (DT_white.isIn(msg.x, msg.y))
{
whiteRadioGroupValue = DT_white.value; continue;
}
else if (initButton.isIn(msg.x, msg.y))
{
init(); continue;
}
}
if (is_black)
{
switch (blackRadioGroupValue)
{
case 0:
if (msg.x >= left_padding && msg.x < 14 * spacing + left_padding && msg.y >= top_padding && msg.y < 14 * spacing + top_padding)
{
int x = (msg.x - left_padding + spacing / 2) / spacing;
int y = (msg.y - top_padding + spacing / 2) / spacing;
mouse_position.x = x;
mouse_position.y = y;
if (msg.mkLButton)
{
if (g_Map[x][y] == 0)
{
g_Map[x][y] = 1;
black_chesses.emplace_back(Position{ x,y });
is_black = !is_black;
if (!oneWin());
else { draw(); return; }
}
}
}
else
{
mouse_position.x = -1;
mouse_position.y = -1;
}
break;
case 2:
if (!firstRandSet())
{
DT();
}
if (!oneWin());
else { draw(); return; }
break;
default:
is_black = !is_black;
break;
}
}
else {
switch (whiteRadioGroupValue)
{
case 0:
if (msg.x >= left_padding && msg.x < 14 * spacing + left_padding && msg.y >= top_padding && msg.y < 14 * spacing + top_padding)
{
int x = (msg.x - left_padding + spacing / 2) / spacing;
int y = (msg.y - top_padding + spacing / 2) / spacing;
mouse_position.x = x;
mouse_position.y = y;
if (msg.mkLButton)
{
if (g_Map[x][y] == 0)
{
g_Map[x][y] = 2;
white_chesses.emplace_back(Position{ x,y });
is_black = !is_black;
if (!oneWin());
else { draw(); return; }
}
}
}
else
{
mouse_position.x = -1;
mouse_position.y = -1;
}
break;
case 2:
if (!firstRandSet())
{
DT();
}
if (!oneWin());
else { draw(); return; }
break;
default:
is_black = !is_black;
break;
}
}
draw();
// Sleep(10);
}
}
bool ChessBoard::firstRandSet()
{ // 第一步随便下
if (black_chesses.empty() && white_chesses.empty())
{
srand(0);
int x = rand() % 15;
int y = rand() % 15;
g_Map[x][y] = is_black ? 1 : 2;
if (is_black)
{
black_chesses.emplace_back(Position{ x,y });
}
else
{
white_chesses.emplace_back(Position{ x,y });
}
is_black = !is_black;
return true;
}
return false;
}
// DT
void ChessBoard::DT()
{
Position best = findBestPosition(is_black, 0); // 寻找最佳位置
if (best.x == -1) { return; }
g_Map[best.x][best.y] = is_black ? 1 : 2; // 下在最佳位置
if (is_black)
{
black_chesses.emplace_back(best);
}
else
{
white_chesses.emplace_back(best);
}
is_black = !is_black;
}
bool ChessBoard::hasNeighbour(int x, int y)
{
for (int i = 0; i < 4; i++)
{
if (g_Map[x + dx[i]][y + dy[i]] != 0 || g_Map[x - dx[i]][y - dy[i]] != 0)
return true;
}
return false;
}
Position ChessBoard::findBestPosition(bool is_black, int layer) // layer 层数,0/1/2/3一共四层
{
if (layer == 0) mylength = 0;
int MAXnumber = INT_MIN; //最佳分数
for (int i = 0; i < 15; i++)
{
for (int j = 0; j < 15; j++)
{
if (g_Map[i][j] == 0)
{
//遍历每一个空位置
int length; //当前方向长度
int emeny; //当前方向敌子
int nowi = 0; //现在遍历到的y坐标
int nowj = 0; //现在遍历到的x坐标
int thescore = 0; //这个位置的初始分数
if (!hasNeighbour(i, j))
continue; //如果周围没有棋子,就不用递归了
// 自己
g_Map[i][j] = is_black ? 1 : 2;// 尝试下在这里
// 4条直线,每条直线有上下两个方向,所以一共是八个方向
for (int k = 0; k < 4; k++)
{
length = 0;
emeny = 0;
// 直线的一个方向
nowi = i;
nowj = j;
while (nowi <= 14 && nowj <= 14 && nowi >= 0 && nowj >= 0 && g_Map[nowi][nowj] == (is_black ? 1 : 2))
{
length++;
nowj += dy[k];
nowi += dx[k];
}
if (nowi < 0 || nowj < 0 || nowi > 14 || nowj > 14 || g_Map[nowi][nowj] == (is_black ? 2 : 1))
{
emeny++;
}
// 直线的另一个方向
nowi = i;
nowj = j;
while (nowi <= 14 && nowj <= 14 && nowi >= 0 && nowj >= 0 && g_Map[nowi][nowj] == (is_black ? 1 : 2))
{
length++;
nowj -= dy[k];
nowi -= dx[k];
}
if (nowi < 0 || nowj < 0 || nowi > 14 || nowj > 14 || g_Map[nowi][nowj] == (is_black ? 2 : 1))
{
emeny++;
}
length -= 2;//判断长度
if (length > 4)
{
length = 4;
}
if (Score[emeny][length] == 500)
{
//己方胜利,结束递归
g_Map[i][j] = 0;
return{ i,j,Score[emeny][length] };
}
thescore += Score[emeny][length];
length = 0;
emeny = 0;
}
//敌人(原理同上)
g_Map[i][j] = is_black ? 2 : 1;
for (int k = 0; k < 4; k++)
{
length = 0;
emeny = 0;
nowi = i;
nowj = j;
while (nowi <= 14 && nowj <= 14 && nowi >= 0 && nowj >= 0 && g_Map[nowi][nowj] == (is_black ? 2 : 1))
{
length++;
nowj += dy[k];
nowi += dx[k];
}
if (nowi < 0 || nowj < 0 || nowi > 14 || nowj > 14 || g_Map[nowi][nowj] == (is_black ? 1 : 2))
{
emeny++;
}
nowi = i;
nowj = j;
while (nowi <= 14 && nowj <= 14 && nowi >= 0 && nowj >= 0 && g_Map[nowi][nowj] == (is_black ? 2 : 1))
{
length++;
nowj -= dy[k];
nowi -= dx[k];
}
if (nowi < 0 || nowj < 0 || nowi > 14 || nowj > 14 || g_Map[nowi][nowj] == (is_black ? 1 : 2))
{
emeny++;
}
length -= 2;
if (length > 4)
{
length = 4;
}
if (Score[emeny][length] == 500)
{
g_Map[i][j] = 0;
return{ i,j,Score[emeny][length] };
}
thescore += Score[emeny][length];
length = 0;
emeny = 0;
}
g_Map[i][j] = 0;
// 如果已经比最高分数小,就没必要递归了
if (thescore >= MAXnumber)
{
if (layer < 3)
{
// 只能找4层,否则时间太长
g_Map[i][j] = is_black ? 1 : 2;
// 递归寻找对方分数
int nowScore = thescore - findBestPosition(is_black, layer + 1).score;// 递归求出这个位置的分值
g_Map[i][j] = 0;
if (nowScore > MAXnumber)
{
// 比最高分值大
MAXnumber = nowScore;
if (layer == 0)
{
// 第一层
mylength = 0; // 清空数组
}
}
if (layer == 0)
{
// 第一层
if (nowScore >= MAXnumber)
{
// 把当前位置加入数组
MAXxs[mylength] = i;
MAXys[mylength] = j;
mylength++;
}
}
}
else
{
// 如果递归到了最后一层
if (thescore > MAXnumber)
{
// 直接更新
MAXnumber = thescore;
}
}
}
}
}
}
if (layer == 0)
{
if (mylength != 0)
{
// 第一层,在最优解列表里随机找一个落子位置
int mynum = rand() % mylength;
return { MAXxs[mynum],MAXys[mynum],MAXnumber };
}
else
{
// 输了,找不到最优解,搜索能否随机落子
for (int i = 0; i < 15; i++)
{
for (int j = 0; j < 15; j++)
{
if (g_Map[i][j] == 0) {
MAXxs[mylength] = i;
MAXxs[mylength] = j;
mylength++;
}
}
}
int mynum = rand() % mylength; // 不会出现 mylength = 0 的情况,因为只要棋盘满了就会自动退出。
return { MAXxs[mynum],MAXys[mynum],MAXnumber };
}
}
// 其他层(之所以返回 x = -2, y = -2 是因为其他层不需要返回位置)
return { -2,-2,MAXnumber };
}
int main()
{
HWND wnd = initgraph(800, 620); // 创建绘图窗口
SetWindowText(wnd, L"五子棋"); //设置窗口的标题
setbkcolor(WHITE);
setbkmode(TRANSPARENT); // 设置透明文字输出背景
ChessBoard chessBoard;
do
{
chessBoard.init(); // 初始化
chessBoard.play(); // 开始游戏
} while (MessageBox(
wnd,
chessBoard.who_win == 1 ? L"恭喜黑方胜利!\n重来一局吗?" : (chessBoard.who_win == 2 ? L"恭喜白方胜利!\n重来一局吗?" : L"居然是平局,太妙了\n重来一局吗?"),
chessBoard.who_win == 1 ? L"黑子胜利" : (chessBoard.who_win == 2 ? L"白子胜利" : L"平局"),
MB_YESNO | MB_ICONQUESTION) == IDYES);
closegraph(); // 关闭绘图窗口(但不会关闭控制台,也就是说你如果用cout输出,这一步之后是可以看得到的控制台)
return 0;
}
添加评论
取消回复