大一上完一学期,接到老师的下学期专周作业开始写五子棋。最终效果不错。记录下我的整个开发过程和一下代码。五子棋算法不难,就是细节多。
最初,模仿着学长dos五子棋的风格参考无数博客开始写。首先main.c调用其他文件,一个run();函数作为选择界面,调用其他单独的函数。首先写玩家对战pvp,第一个成品。用一个二维数组表示棋盘,一个结构的x,y表示光标位置,
wasd控制光标移动,光标就是图中极丑的符号,j表示下棋,一个无线循环,一个变量player代表玩家,一次填1,一次填2,来模拟下棋。每次光标改变和改变数组后,用system("cls");这个windows API清除屏幕,然后再次根据数组情况重新打印棋盘来模拟下棋的过程。图形组成用的是输入法自带的制表符。
关键算法:判断胜负,每次落子后判断当前位置是否构成五子连珠。每次下棋后分横竖撇捺四个方向判断,
比如竖向,一个循环向上找与当前位置相同的棋子计数,然后循环向下找。四个反向有一个反向计数达 到5即可,判断胜负。
2,打印棋盘,遍历数组,对于每一个位置特判是否为1,2,或光标。打印,然后组织好if,else特判边界,最后边界
其他提示界面按需求随便加就好了。
胜负判断代码一部分:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
int i,num=1; int flag=arr[x][y]; ///横,OK num=1; for(i=1;i<5;i++) { if(x-i>=0&&arr[x-i][y]==flag) { num++; } else break; } for(i=1;i<5;i++) { if(x+i<SIZE&&arr[x+i][y]==flag) { num++; } else break; } if(num>=5)return flag;
第一个版本初步模仿了五子棋的游戏过程,当还是有弊端,比如屏幕闪烁,在连续按方向键的时候屏幕不断闪动,不好看。查找相关资料,我们使用了局部刷新技术。手写gotoxy函数。函数如下
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
void gotoxy(int x,int y) { COORD pos; pos.X=x; pos.Y=y; SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE),pos); }
COORD的定义在windows.h里面的一个结构,成员变量x,y.明显就是拿来放坐标的,我们定义一个结构然后这一句函数字面意思获取光标位置SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE),pos);
同样定义在windows.h里面的一个,作用是光标移动到pos上,STD_OUTPUT_HANDLE是显示器的意思,windows是编程中有句柄这个概念,暂时理解成指针。这样一个函数gotoxy(1,2);就代表这光标达到1,2这个坐标的位置,如果在这里printf那么输出也在这个位置。整个DOS就是一个坐标轴,以DOS左上角为原点,向右为X轴,向下为Y轴。这样我们就在下棋的位置刷新就可以了,不断闪烁的光标成了天然的光标,这是我们理解的二维数组arr[][]如果直接理解为arr[x][y]那么打印在DOS上将呈现关于Y=-X对称。有人觉得反正棋盘是正方形,没什么关系。但实际上却关系到五子棋AI的算法,开发AI的过程中就出现过,左下角落子,电脑下右上角的情况。
然而这样又遇到了新的问题,坐标与数组的对于关系。举个例子。对于上图中的十字,如果一下W,Y坐标-1,如果D,X坐标+2,重点是这个2,而不是想象中的1,在这里掉坑里了一次。加上上面空出的一个\t,我的坐标对于的判断胜负函数由坐标对应的变为了winner((x-8)/2,y);
另外一个问题,一直想WASD+J改方向键+空格。写了多次老不成功。后来发现原因。windows中的功能键getch();读取ASCII码和普通字符不一样,只读取一次,发现方向键都是一个数字-32。第二次才是真的ASCII码值。于是我们多加一个if.c=getch();if(c==32){c=getch();},空格键可以直接读取。然后我们就写出了界面相对完美的五子棋。
玩家对战写完了,DOS画面也相当完美,就开始加AI。在这里我直接复制PVP();函数改变player变量,使其不变,直接写上AI();这个函数完成的是在数组中下棋,并在屏幕正确的位置打印棋子。都是坐标运算。除了AI的实现都简单。对于五子棋AI这个小人工智能的实现是个大问题,我们最后再来谈。直接进入图形库部分。
这里我使用的是EGE图形库。先来欣赏下成品。表示不适合画图,专周交作业时画风贴图可能会变。图一为开始界面,图二选择两个版本的AI,图三为和AI对战图,图四是胜利图。全程实现了鼠标操作。
首先是codeblocks安装EGE图形库,虽然DEVC++也用过图形库,配置起来也没codeblocks麻烦,但是我们大学用的是codeblocks,机房的IDE是codeblocks和vs,这个版的五子棋也是全程codeblocks编写的。下载EGE图形库,文件内的include和lib里面的内容复制到codeblocks目录下MinGW中的include和lib里面。建立c++工程,图形库要求c++,但是仍然可以在c++工程里面写c. (为了老系统可以用,c++本身就继承了c的很多东西)。建立好工程后,点击project下的build-option.选择linker-setting,点击add添加libgraphics.a,libgdi32.a,libimm32.a ,libmsimg32.a ,libole32.a ,liboleaut32.a ,libwinmm.a ,libuuid.a 这八个文件。然后在旁边的other linker option 上写上-mwindows
然后就可以在main.cpp里面测试了。比如在main里面写
int main()
{
initgraph(640,480)//初始化一个640*480的图形窗口
getch();//等待按键,这里getch()是定义在graphcsi.h图形库中的,如果有conio.h会冲突
closegraph()//然后关闭画面
}
这样我们就完成了创建一个什么都没有的图形窗口,等待按键,按键后消失。绘图画面和DOS的坐标轴是一样的。
开发中用到的一些高频函数;
绘制简单图形,文字。//大写字母都是已经定义的宏;
circle(200,200,100);
//在坐标(200,200)的地方画一个空心圆
bar(50,100,300,200);
//画一个矩形,矩形左上角坐标(50,100)右下角(300,200).
line(100,100,500,200);
//从(100,100)到(500,200)画一条线。
setcolor(GREEN);
//设置画笔颜色
setfillcolor(EGERGB(0xff,0x0,0x80));
//设置填充色
setfontbkcolor(RED);
//文字背景色
setbkmode(TRANSPARENT);
//文字背景透明
setfont(12,0,"华文琥珀");
//12,0的位置对于字体长宽,宽0代表默认,后面字符串代表字体。
outtextxy(100,100," ");
//在100,100,的位置输出字符串,不读取'\t','\n',下面一个函数读
outtextrect(100,120,200,100," ");
//前四个坐标和矩形一样划定写字范围,最后一个是字符串。
插入图片:
PIMAGE img=newimage(800,800);
//PIMAGE是图形库中存放图片的一个结构,或者理解为c++的类,创建对象
//img用newimage并初始化.img可以理解为指针
getimage(img,"begin.jpg",800,800);
//这样就让指针直接同一目录的图片
putimage(0,0,img);
//在0,0的位置吧图片放出来;
delimage(img);
//用完要释放空间。
文字部分:
setbkmode(TRANSPARENT);
setcolor(LIGHTBLUE);
setfont(70,0,"华文行楷");
outtextxy(300,200,"五子棋");
//写下背景透明的五子棋几个字,程序首页的三个字就这么实现。
然后颜色的表示:
计算机可以表示256*256*256种颜色,三个参数分别代表红黄蓝。EGERGB();中三个参数就是三种颜色。
也有用色调,饱和度,亮度的其他函数,或者直接用宏。
读取鼠标,键盘信息;
mouse_msg msg={0};//定义一个结构储存鼠标信息。
for(;is_run();delay_fps(60))
{
while(mousemsg())
msg=getmouse();
...
}
is_run();判断图形画面还在,delay_fps(60);是延时函数。单位毫秒。当有鼠标信息的时候,读取鼠标信息给结构msg.然后用msg的成员变量表示鼠标状态,如msg.x,msg.y代表鼠标坐标。msg.is_left();msg.is_right();
等代表左键是否按下。程序中,直接条件语句判断坐标在某范围且按下鼠标来判断,进入那一游戏模式(函数);比如首页选择按钮的写法,获取鼠标信息后
if(msg.y<=500&&msg.y>=400&&msg.is_left){pvp();}
clearviewport();函数清除界面,绘制新的界面。在此界面中依然可以c=getch()获取键盘ASCII值,所以没有用图形库的键盘函数。其他详情见图形库文档(一本书);
整个游戏的开始由一个无限循环中套一个鼠标消息循环
while(1)
{
/*鼠标消息*/
for(;is_run();delay_fps(60))
{
//获取鼠标信息进入对于函数,然后break;重新进入大循环代表着
//下一局游戏,设定结束游戏按钮直接定一个flag=1,然后跳出大循环
}
}
进入pvp();函数。打印棋盘,这里我用PIMAGE获取了一块木板图片。然后用循环line函数画线构造棋盘,在坐标50,50开始划线,每格大小50*50,那么鼠标坐标减25(开头空出来的部分)除以50,直接对应上了二维数组的位置。pvp();模式中,数组一次填1,一次填2,屏幕上一次填黑棋,一次填白棋。每次落子后加入一个判断胜负函数。出现五子连珠。跳出循环。打印结束界面。进入开始菜单。
pve();模式先把pvp();改成一个自会写1,只会下黑子的函数。然后写上AI();后面再来实现这个函数,这个函数功能是在二维数组中填数字和在屏幕上打印电脑的棋子。这两个功能只要AI算出下棋的位置都好办,接下来就是这么找下棋的位置。
我的文件结构main.cpp调用run.h,run.h调用run.cpp,run.cpp调用ai.h,ai.h调用ai.cpp;
AI算法部分:
先推荐一本书《pc游戏编程---人机博弈》,五子棋就是一种人机博弈的模型,电脑根据局势分析,选择最优的位置落子。在每个可落子的位置,在电脑中都有一个分值。电脑根据分值落子。
我的第一个AI仅仅是产生随机数,产生的随机数x,y必须符合表示位置无棋子,根据猴子排序的原理。无数的猴子在无数个打字机上打字在某个时候能打出莎士比亚的著作。详情搜莎士比亚和猴子的故事。完成了第一个无脑的AI.
第二个AI是一篇博客上的一个简单的算法:五子棋评分表算法,我的第一个AI,效果还行,和之后的AI对比被舍去。一个15*15的棋盘上,构成的连续五个格子的情况是可数的(横竖撇捺),这样的五个格子我们称之为五元组。我们根据每一个五元组中黑棋白棋的个数为其打分,然后将五元组中每个格子的分数加上五元组的得分,那么一个格子的得分就是多个包含这一位置的五元组得分的和。遍历棋盘所有五元组,将五元组的黑棋白棋个数交给估值函数。然后再次遍历相同五元组,加分,用一个新的二维数组goal[][]储存。根据五子棋的思路,找连续棋子个数多的地方下棋,那么这个位置打高分。如果前方被堵,就给低分。相同棋子个数时,比如同是三连子,玩家的三连子给稍微高一点的分数,代表防守优先。用a,b分别表示棋子个数,那么估值函数。
int table(int a,int b)///a computer vs b player
{
int ans=0;
if(a&&b)ans=0;
else if(b==1)ans=35;
else if(b==2)ans=800;
else if(b==3)ans=15000;
else if(b==4)ans=800000;
else if(b==5)ans=1600000;
else if(a==1)ans=15;
else if(a==2)ans=400;
else if(a==3)ans=1800;
else if(a==4)ans=100000;
else if(a==5)ans=2000000;
else ans=0;
return ans;
}
第二个AI,查寻相关资料,对局势细分,直接对落子后落子位置造成的局势变化打分,我们将两头无敌方棋子的情况认定为活棋。堵了认为是死棋。
例如 ●●●○死三 ●●●活三 。活三和死四的威力相等,因为活三可以轻易变为死四。对局势分析,我们根据计算位置向横竖撇捺四个方向搜索,查寻连续棋子个数,并判断是活棋还是死棋。用一个二维数组ret[][]表示状态。第二维表示活棋还是死棋,然后根据对于情况打分。估值函数如下,这里请忽略omg变量。player变量记录,用来假设黑棋落子和白棋落子形成的局势。位置得分为黑白棋估分中较大的一个。
第三个AI,向着博弈树的方向写,我们让计算机模拟下棋。每一个情况对于这之后多种情况,于是我们得到一颗树。被称为博弈树,然后我们对每一种走法评分。下发是指数阶的。先不考虑算多久,我们可以用递归搜索来写这棵树,模拟,一次白棋,一次黑棋模拟。最多模拟一定步数。这里直接用两层深搜写的,第二步位置得分比第一部低。这里的level直接对应着上面的omg,每搜一次level/100,后面的步数得分就没有第一步的高。挺简单的深搜。player=1或2,那么异或3就等于另一个。不过效果没感觉跟好