五子棋AI
这里仅仅是搬运了一下实验报告,最下方附有全部代码,图片在相册里。
背景介绍
利用估价函数并基于博弈论中的\(min-max\)搜索及\(Alpha-Beta\)剪枝,\(hash\)算法来实现电脑最优局面的选择,实现一定意义下的电脑\(AI\)。该程序使用\(C++11\)实现。
实验目的
利用估价函数对局面打分并使用搜索得到一个可以与人进行对弈五子棋的\(AI\)。
期望使用一些算法能让电脑走子在时间和对弈强度上相对平衡,从而获得一个既不弱于普通人的棋弈水平,又使得每一步步时不会过长。
实验环境
开发环境:操作系统 \(win11\),编辑器 \(visual \ studio\ 2022\)。
运行环境:\(c++11\)。
工具:\(typora\)。
库:\(EasyX,map,algorithm\)。
实验内容
1.利用EasyX库得到图片窗口获得棋盘并实现棋子由鼠标生成功能,以及悔棋功能。
使用EasyX图形库来绘制棋子,棋盘和悔棋菜单,同时支持鼠标点击位置进行落子,注意控制参数坐标。
悔棋选项,通过记录上一手己方和对方的走棋位置,对局面进行复原。但是若是终局了则无法悔棋。
2.估价函数的实现
考虑对一个局面进行打分,分别把所有的行列两条斜线分别取出来。数出其中的不同类型的棋型,并记录得分。
比如\(01110\)(\(0\)表示空位\(1\)表示黑子\(2\)表示白子)定义此局面为\(100\)分\(211110\)定义为\(80\)分。
需要注意统计方式,注意不重不漏。
3.\(min-max\)搜索和\(Alpha-Beta\)剪枝
定义过估计函数,该对弈变为零和游戏,设每一个局面的得分为\(AI\)得分-人类得分,那么\(AI\)显然想让当前局面的得分尽可能高并一直高下去,即对应\(max\)的搜索过程,而仅仅考虑当前局面的最优解还不够还需要知道人类做出应对后怎么让自己最优,所以\(AI\)要作为人类再次走棋期望得到一个使局面分数尽可能低的分数,为\(min\)搜索。该搜索使用\(dfs\)进行,但是我们不可能一直搜索下去,这里为了平衡时间和对弈强度考虑深度搜索\(6\)步。
但是这样开销仍然很大每一步的可能性很多,考虑使用\(Alpha-Beta\)剪枝即从其他局面获取的信息有上界\(b\)和下界\(a\),若当前正在搜的局面为最小值搜索且\(b>=\)当前局面的值\(c\),那么当前局面的所有儿子节点就不需要再搜了因为向上反的值一定是\(<=c\)的而从其他儿子可以向上返\(b\)。同理最大值的局面,为了让剪枝效果更明显我们需要把每一步的决策进行排序,来增加剪枝的可能性。
4.启发式搜索
虽然经过了上述剪枝,但是程序开销仍然很大,若降低搜索层数,则\(AI\)的棋力会较弱,为了降低时间复杂度,需要将每一步后的决策数量降低。可以知道每次落子不会离棋盘的其他棋子距离过远,即当前落子和棋盘上其他的的契比雪夫距离不会很大。考虑设置契比雪夫距离为1,每次将决策进行搜集及排序来降低搜索决策个数和更好进行剪枝。
5.算杀
注意到当轮到自己的时候若已经有\(4\)子连在一起并有机会形成五连,那么此时不需要再进行搜索。
若自己形不成五连反观对方是否是局点,检测整个局面进行对四连的优先围堵。
6.局面hash
由于需要对局面打分,而不同的落子顺序可能得到相同的局面,对整个棋盘进行hash,并使用map以hash值为键值建立与分数的映射,保证每个局面的局面分数只求一次。
实验结果
开局情况,鼠标所在位置会有一个蓝色的放子提示框。
最右侧悔棋菜单在己方回合点击可以进行上一步的悔棋。
\(AI\)获胜则会有爱拼才会赢字样出现。
己方获胜则会出现天外飞仙字样。
因为复盘意义不大故选择用清晰的图片提示并可查看终局的结果。
\(AI\)和多人都进行了对弈胜多输少,每次走子会选择未来几步内最优的结果使得其对弈强度较高,完成了预期目标。
但局内每步均时在5s左右,在局面极为复杂情况下会达到8s。
总结
经过多次测试发现\(AI\)已经远远比普通的人对弈水平高出一档提前算出\(6\)部情况使大多初等陷阱完全失效,需要有长远的布局和谋划才能胜出。但对于五子棋来说布局往往很困难,所以本次\(AI\)强度的实现大大超出了预期。
但同时中盘的棋子较多使得决策很多,步时较慢,尽管使用了很多优化仍然达不到目前五子棋人机对弈的机器速度。
经过本次实验,掌握了\(EasyX\)图形库的使用,学习了\(min-max\)搜索和\(Alpha-Beta\)剪枝,以及对局面进行hash处理等方法技巧, 并且利用估价函数构造了一个在一定程度上可以称作\(AI\)的程序,同时代码能力得到了长足的进步。
参考文献
[2] 知乎:C++超详细五子棋游戏
[3] CSDN:五子棋AI算法
code
#include<graphics.h>
#include<easyx.h>
#include<stdio.h>
#include<conio.h>
//#include<bits/stdc++.h>
#include<iostream>
#include<cstdio>
#include<ctime>
#include<cctype>
#include<queue>
#include<deque>
#include<stack>
#include<iostream>
#include<iomanip>
#include<cstdio>
#include<cstring>
#include<string>
#include<ctime>
#include<cmath>
#include<cctype>
#include<cstdlib>
#include<queue>
#include<deque>
#include<stack>
#include<vector>
#include<algorithm>
#include<utility>
#include<bitset>
#include<set>
#include<map>
using namespace std;
#define ll long long
#define db double
#define INF 10000000
#define inf 100000000000000000ll
#define ldb long double
#define pb push_back
#define put_(x) printf("%d ",x);
#define putl_(x) printf("%lld ",x);
#define get(x) x=read()
#define putl(x) printf("%lld\n",x)
#define rep(p,n,i,k) for(int i=p;i<=n;i+=k)
#define go(x) for(int i=lin[x],tn=ver[i];i;tn=ver[i=nex[i]])
#define pii pair<int,int>
#define mk make_pair
#define gf(x) scanf("%lf",&x)
#define uint unsigned long long
#define ui unsigned
#define sq sqrt
#define l(w) t[w].l
#define r(w) t[w].r
#define id(w) t[w].id
#define R(w) s[w].r
#define sum(w) t[w].sum
#define mod 998244353
#define sc(A) scanf("%d",&A)
#define scs(A) scanf("%s",A);
#define put(A) printf("%d\n",A)
#define sub(x,y) (x-y<0?x-y+mod:x-y)
#define ull unsigned long long
#define S second
#define F first
int a[500][500];
ull s1[16][16], s2[16][16], cur;
int flag = 0, top;
IMAGE img, img1, img2;
struct wy { int F, S; }w;
struct jl { int x, y, v; }q[505];
inline int cmp1(jl a, jl b) { return a.v > b.v; }
inline int cmp2(jl a, jl b) { return a.v < b.v; }
MOUSEMSG ms;
//对当前棋局进行打分估价 ai-玩家得分进行max-min搜索并进行α-β剪枝。
//注意到对局面打分这里采用当前局面对对方的防守压力判断
int l[5] = { 0,0,10,100,1000 };//定义活1 1分 活2 10分 活3 100分 活4 1000 活5 INF
int d[5] = { 0,0,1,8,80 };//定义 死2 1分 死3 8分 死4 80分 死5 INF
int z[6] = { 0,0,50,500 };//定义 中2 5 中3 50 中>=4 500分
int dd[5] = { 0,0,1,5,50 };//死中2 1 分 死中3 5分 死中>=4 50分
int aa[50], s[50], ss;
map <ull, int> H;
int deepth = 6, dis = 1;
int wx, wy, hq = 0, l1, l2, l3, l4;
inline int maxdfs(int dep, int a, int b, ull s);
inline int mindfs(int dep, int a, int b, ull s);
inline void Hash()
{
srand(time(0));
rep(0, 14, i, 1)rep(0, 14, j, 1)
{
s1[i][j] = (ll)rand() * rand() * rand() * rand();
s2[i][j] = (ll)rand() * rand() * rand() * rand();
}
}
inline void placeh(int x, int y)//hei
{
setfillcolor(BLACK);
a[x][y] = 1;
l1 = x; l2 = y;
solidcircle(y * 30 + 20, x * 30 + 20, 12);
cur ^= s1[x][y];
}
inline void placeb(int x, int y)//bai
{
setfillcolor(WHITE);
a[x][y] = 2;
l3 = x; l4 = y;
solidcircle(y * 30 + 20, x * 30 + 20, 12);
cur ^= s2[x][y];
}
inline void render()
{
for (int i = 20; i <= 440; i += 30)
{
setlinecolor(BLACK);
line(20, i, 440, i);
line(i, 20, i, 440);
}
w.F = -1; hq = 0;
rep(20, 440, i, 30)rep(20, 440, j, 30)
{
int x = j / 30;
int y = i / 30;
if (a[x][y] == 1)
{
setfillcolor(BLACK);
solidcircle(i, j, 12);
}
if (a[x][y] == 2)
{
setfillcolor(WHITE);
solidcircle(i, j, 12);
}
if (ms.y > i - 15 && ms.y < i + 15 && ms.x > j - 15 && ms.x < j + 15)
w.F = j, w.S = i;
}
//绘制当前鼠标所在的提示框
if (w.F != -1)
{
setlinecolor(BLUE);
circle(w.F, w.S, 15);
}
if (ms.x >= 480 && ms.x <= 580 && ms.y >= 140 && ms.y <= 190)hq = 1;
//悔棋提示框
setlinecolor(BROWN);
rectangle(480, 140, 580, 190);
setbkmode(TRANSPARENT);
settextcolor(RED);
settextstyle(50, 0, _T("宋体"));
outtextxy(480, 140, _T("悔棋"));
settextcolor(BLACK);
settextstyle(25, 0, _T("宋体"));
outtextxy(480, 210, _T("观棋不语真君子"));
outtextxy(480, 250, _T("落子无悔大丈夫"));
}
inline void update()
{
if (hq)
{
cur ^= s1[l1][l2];
cur ^= s2[l3][l4];
a[l1][l2] = 0;
a[l3][l4] = 0;
return;
}
if (w.F == -1)return;
if (!a[w.S / 30][w.F / 30])
{
placeh(w.S / 30, w.F / 30);
flag = 1;
}
}
inline int judge()
{
rep(0, 14, i, 1)
rep(0, 14, j, 1)
{
if (a[i][j] == 0)continue;
int q = a[i][j];
if (i >= 2 && i <= 12)
if (a[i - 1][j] == a[i - 2][j] && a[i + 1][j] == a[i + 2][j] && q == a[i + 1][j] && q == a[i - 1][j])
return q;
if (j >= 2 && j <= 12)
if (a[i][j - 1] == a[i][j - 2] && a[i][j + 1] == a[i][j + 2] && q == a[i][j + 1] && q == a[i][j - 1])
return q;
if (i >= 2 && i <= 13 && j >= 2 && j <= 13)
{
if (a[i - 1][j - 1] == a[i - 2][j - 2] && a[i + 1][j + 1] == a[i + 2][j + 2] && q == a[i - 1][j - 1] && q == a[i + 1][j + 1])
return q;
if (a[i - 1][j + 1] == a[i - 2][j + 2] && a[i + 1][j - 1] == a[i + 2][j - 2] && q == a[i - 1][j + 1] && q == a[i + 1][j - 1])
return q;
}
}
return 0;
}
inline struct wy find(int col)
{
//成四禁手
struct wy ans; ans.F = -1; ans.S = -1;
rep(0, 14, i, 1)
rep(0, 14, j, 1)
{
if (a[i][j])continue;
//横着找
int cnt = 0;
int x = i, y = j;
while (y)
{
if (a[i][y - 1] == col)++cnt, --y;
else break;
}
if (cnt >= 4) { ans.F = i; ans.S = j; return ans; }
y = j;
while (y <= 13)
{
if (a[i][y + 1] == col)++cnt, ++y;
else break;
}
if (cnt >= 4) { ans.F = i; ans.S = j; return ans; }
//竖着找
cnt = 0;
while (x)
{
if (a[x - 1][j] == col)++cnt, --x;
else break;
}
if (cnt >= 4) { ans.F = i; ans.S = j; return ans; }
x = i;
while (x <= 13)
{
if (a[x + 1][j] == col)++cnt, ++x;
else break;
}
if (cnt >= 4) { ans.F = i; ans.S = j; return ans; }
//左斜着找
cnt = 0;
x = i; y = j;
while (x && y)
{
if (a[x - 1][y - 1] == col)++cnt, --x, --y;
else break;
}
if (cnt >= 4) { ans.F = i; ans.S = j; return ans; }
x = i; y = j;
while (x <= 13 && y <= 13)
{
if (a[x + 1][y + 1] == col)++cnt, ++x, ++y;
else break;
}
if (cnt >= 4) { ans.F = i; ans.S = j; return ans; }
//右斜着找
cnt = 0; x = i; y = j;
while (x && y <= 13)
{
if (a[x - 1][y + 1] == col)++cnt, --x, ++y;
else break;
}
if (cnt >= 4) { ans.F = i; ans.S = j; return ans; }
x = i; y = j;
while (x <= 13 && y)
{
if (a[x + 1][y - 1] == col)++cnt, ++x, --y;
else break;
}
if (cnt >= 4) { ans.F = i; ans.S = j; return ans; }
}
return ans;
}
inline int pf(int p)//对当前的行列斜线抽出来打分
{
aa[0] = 3 - p; aa[16] = 3 - p;
int ans = 0;
rep(1, 15, i, 1)
{
s[i] = 0;
if (aa[i] == p)
{
s[i] = s[i - 1] == 0 ? i : s[i - 1];
int cnt = i - s[i] + 1;
if (aa[i + 1] == 0)
{
if (aa[s[i] - 1])ans += cnt >= 5 ? INF : d[cnt];
else ans += cnt >= 5 ? INF : l[cnt];
}
if (aa[i + 1] == 3 - p)
{
if (aa[s[i] - 1] == 0)ans += cnt >= 5 ? INF : d[cnt];
}
}
if (aa[i] == 0)
{
int l = i - 1;
while (l && aa[l] == p)--l;
int r = i + 1;
while (r <= 15 && aa[r] == p)++r;
int w1 = i - 1 - l, w2 = r - i - 1;
if (w1 && w2)
{
if (aa[l] && aa[r]);
if (!aa[l] && aa[r])ans += w1 + w2 >= 4 ? 50 : dd[w1 + w2];
if (!aa[r] && aa[l])ans += w1 + w2 >= 4 ? 50 : dd[w1 + w2];
if (!aa[r] && !aa[l])ans += w1 + w2 >= 4 ? 500 : z[w1 + w2];
}
}
}
return ans;
}
inline int pf_dian(int x, int y)
{
int cnt = 0;
ss = 0;
rep(0, 14, i, 1)
aa[++ss] = a[i][y];
cnt += pf(2) - pf(1);
ss = 0;
rep(0, 14, j, 1)
aa[++ss] = a[x][j];
cnt += pf(2) - pf(1);
ss = 0;
int xx = x; int yy = y;
while (xx >= 0 && yy >= 0)--xx, --yy;
while (xx <= 14 && yy <= 15)aa[++ss] = a[xx][yy], ++xx, ++yy;
xx = x; yy = y;
cnt += pf(2) - pf(1);
ss = 0;
while (xx <= 14 && yy >= 0)++xx, --yy;
while (xx >= 0 && yy <= 14)aa[++ss] = a[xx][yy], --xx, ++yy;
cnt += pf(2) - pf(1);
return cnt;
}
inline int gj()
{
int ans = 0;
rep(0, 14, i, 1)
{
ss = 0;
rep(0, 14, j, 1)
{
aa[++ss] = a[i][j];
}
ans += pf(2) - pf(1);
}
rep(0, 14, j, 1)
{
ss = 0;
rep(0, 14, i, 1)
{
aa[++ss] = a[i][j];
}
ans += pf(2) - pf(1);
}
rep(0, 14, i, 1)
{
ss = 0;
int x = i, y = 0;
while (x >= 0 && y <= 14)
{
aa[++ss] = a[x][y];
--x; ++y;
}
ans += pf(2) - pf(1);
}
rep(1, 14, j, 1)
{
ss = 0;
int x = 14, y = j;
while (x >= 0 && y <= 14)
{
aa[++ss] = a[x][y];
--x; ++y;
}
ans += pf(2) - pf(1);
}
rep(0, 14, i, 1)
{
ss = 0;
int x = i, y = 0;
while (x <= 14 && y <= 14)
{
aa[++ss] = a[x][y];
++x; ++y;
}
ans += pf(2) - pf(1);
}
rep(1, 14, j, 1)
{
ss = 0;
int x = 0, y = j;
while (x >= 0 && y <= 14)
{
aa[++ss] = a[x][y];
++x; ++y;
}
ans += pf(2) - pf(1);
}
return ans;
}
inline void getstate(int ww)
{
rep(0, 14, i, 1)rep(0, 14, j, 1)
{
if (a[i][j])continue;
rep(max(0, i - 1), min(15, i + 1), k, 1)
rep(max(0, j - 1), min(15, j + 1), w, 1)
if (a[k][w]) {
q[++top].x = i;
q[top].y = j;
goto ff;
}
ff:;
}
}
inline int mindfs(int dep, int A, int b, ull s)
{
top = 0;
vector<jl>qq;
int top1;
getstate(dis);
top1 = top;
rep(1, top1, i, 1)qq.push_back(q[i]);
for (int i = 0; i < top1; ++i)
{
int res = pf_dian(qq[i].x, qq[i].y);
a[qq[i].x][qq[i].y] = 1;
qq[i].v = pf_dian(qq[i].x, qq[i].y) - res;
a[qq[i].x][qq[i].y] = 0;
if (qq[i].v <= -1000000)return -INF;
}
sort(qq.begin(), qq.end(), cmp2);
int bb = INF;
rep(0, top1 - 1, i, 1)
{
a[qq[i].x][qq[i].y] = 1;
s ^= s1[qq[i].x][qq[i].y];
int res = maxdfs(dep - 1, min(bb, A), b, s);
a[qq[i].x][qq[i].y] = 0;
s ^= s1[qq[i].x][qq[i].y];
if (res < bb)bb = res;
if (bb < b)break;
}
return bb;
}
inline int maxdfs(int dep, int A, int b, ull s)
{
if (dep <= 0)
{
if (H.find(s) != H.end())return H[s];
return H[s] = gj();
}
top = 0;
vector<jl>qq;
int top1;
getstate(dis);
top1 = top;
rep(1, top1, i, 1)qq.push_back(q[i]);
for (int i = 1; i <= top1; ++i)
{
int res = pf_dian(qq[i - 1].x, qq[i - 1].y);
a[qq[i - 1].x][qq[i - 1].y] = 2;
qq[i - 1].v = pf_dian(qq[i - 1].x, qq[i - 1].y) - res;
a[qq[i - 1].x][qq[i - 1].y] = 0;
if (qq[i-1].v >= 1000000)
{
if (dep == deepth)wx = qq[i-1].x, wy = qq[i-1].y;
return INF;
}
}
sort(qq.begin(), qq.end(), cmp1);
int bb = -INF, cx = 0, cy = 0;
rep(1, top1, i, 1)
{
a[qq[i - 1].x][qq[i - 1].y] = 2;
s ^= s2[qq[i - 1].x][qq[i - 1].y];
int res = mindfs(dep - 1, A, max(bb, b), s);
a[qq[i - 1].x][qq[i - 1].y] = 0;
s ^= s2[qq[i - 1].x][qq[i - 1].y];
if (res > bb)bb = res, cx = qq[i - 1].x, cy = qq[i - 1].y;
if (bb > A)break;
}
if (dep == deepth)
{
wx = cx; wy = cy;
}
return bb;
}
inline void play()
{
int win = 0;
Hash();
while (win == 0)
{
ms = GetMouseMsg();
//开始批量绘图
BeginBatchDraw();
//清屏
cleardevice();
putimage(0, 0, &img);
render();
//结束双缓冲绘图
EndBatchDraw();
if (ms.uMsg == WM_LBUTTONDOWN)update();
if (flag == 1)
{
win = judge();
if (win == 2)goto flag1;
if (win == 1)goto flag2;
// 电脑走
struct wy cc = find(2);//找到禁手位置
if (cc.F != -1)
{
placeb(cc.F, cc.S);
flag = 0;
}
else
{
struct wy cc = find(1);
if (cc.F == -1)
{
maxdfs(deepth, INF, -INF, cur);
placeb(wx, wy);
flag = 0;
}
else
{
placeb(cc.F, cc.S);
flag = 0;
}
}
win = judge();
if (win == 2)goto flag1;
if (win == 1)goto flag2;
}
}
while (1)
{
flag1:;
BeginBatchDraw();
cleardevice();
putimage(0, 0, &img);
putimage(33, 100, &img1);
render();
EndBatchDraw();
}
while (1)
{
flag2:;
BeginBatchDraw();
cleardevice();
putimage(0, 0, &img);
putimage(33, 100, &img2);
render();
EndBatchDraw();
}
}
int main()
{
//注意运行此程序需要将这图片放在对应位置之下或者自己重新设置地址
//注意斜杠为双斜杠而不是单斜杠
initgraph(665, 490, SHOWCONSOLE);
loadimage(&img, "C:\\Users\\49025\\Desktop\\弈棋悟道\\cc.jpg", 660, 660, 1);
loadimage(&img1, "C:\\Users\\49025\\Desktop\\弈棋悟道\\aa.png", 550, 275, 0.1);
loadimage(&img2, "C:\\Users\\49025\\Desktop\\弈棋悟道\\bb.jpg", 556, 308, 0.1);
play();
//while(1)putimage(33,33, &img2);
return 0;
}