本文介绍本人的大一课程设计作业项目:五子棋,本来准备挑战更有难度的机器视觉目标检测系统(对于我来说困难),但是在经历各种不可预测的事情(包括变成小人)之后,还是放弃了,然后写了这个项目(之前就有写过一点,是在疯狂改进)。项目主要使用easyx库,具体知识大家可以自行搜索学习,在文章内就不大量解释了。写这篇文章主要是记录自己的人生第一个课设。
目录
前言
一、程序包含的库和全局变量
二、功能函数介绍
1.初始化界面、结束界面与棋盘绘制
2.棋子绘制和落子
3.模式展示
4.游戏数据初始化与悔棋
5.简单AI算法
6.复盘功能
7.背景音乐与音效设置
三、函数封装
1.开始函数
2.人人对战
3.人机对战
四、总结
我使用的是vs2022,项目主要使用c++的库easyx,具体库的安装大家可以自行搜索(安装在vs上很简单),项目还引用了mmsystem.h库,主要用来播放音乐,注意还需包括一个语句#pragma comment(lib,"winmm.lib"),是为了告诉编译器,加载winmm. lib库文件,也可以在vs里链接器直接添加。
具体的全局变量设置和作用,在下面的代码段中都有注释,大家可以直接看。接下来对函数的介绍就简单介绍一些easyx库函数的使用,具体大家自行学习,然后介绍一些注意事项。
#ifndef _STA_H//防止报错重复定义
#define _STA_H
#include//引用图形库头文件
#include
#include
#include
#include//播放音乐头文件
#pragma comment(lib,"winmm.lib")//告诉编译器,加载winmm. lib库文件
int chess[15][15] = { 0 };//定义棋盘数组,0表示没有棋子1表示白棋2表示黑棋
int Change_Chess = -1;//用于改变棋子颜色
int Current_color = 2;//代表棋子颜色,2表示黑色1表示白色(黑棋先走)
int AIx;//AIx_y在AI函数中获取人机判断出的优势位置然后下
int AIy;
int xx;//悔棋标志,在下棋过程中记录每一步的棋来实现悔棋函数
int yy;
int AIxx;
int AIyy;
int position[3];//AI算法中用来保存优势位置的行列坐标和分数
int win;//用来获取胜利一方的棋子颜色
int music;//背景音乐是否播放的标志 奇数时表示音乐在播放\偶数表示音乐关闭
int piece_num;//记录每一局的下棋总步数为了实现复盘
struct chess {
int f_x;
int f_y;
}fupan[225];//记录每一步棋的坐标,225表示棋盘可以下的最大步数
IMAGE Img;//存放开始界面图片
IMAGE MusicImg1;//存放正在播放音乐的图片
IMAGE MusicImg2;//存放音乐暂停的图片
MOUSEMSG Mouse_message = { 0 }; //定义一个与鼠标相关的全局结构体变量
......
#endif
首先是各类函数的声明
void Page_initialization1();//初始化(正在播放音乐菜单显示)
void Page_initialization2();//初始化(音乐暂停菜单显示)
void Checkerboard_display_AI();//绘制人机对战棋盘
void Checkerboard_display_Player();//绘制人人对战棋盘
void f_Checkerboard_display();//绘制复盘时的棋盘
void Drawpiece(int x, int y, int color);//绘制棋子
void Drawpiece_regret(int x, int y, int color);//悔棋时绘制棋子(去除了落子声音防止出现延迟)
/*easyx库内xy轴是原点在左上角,x轴横向且向右为正,y轴竖向且向下为正。
与二维数组行和列的对应关系是:x对应列,y对应行
所以在下面落子以及之后的AI判断等等函数需要考虑这个关系
*/
bool PieceSet(int y, int x, int color);//判断是否有棋子,给数组赋值然后落子
void Pieceset_fupan(int y, int x, int color);//复盘时给数组赋值
//模式展示:当鼠标移动到固定位置时字体变色
void Modelshow();//开始界面模式展示
void Modelshow_AI(MOUSEMSG m);//人机对战模式展示
void Modelshow_Player(MOUSEMSG m);//人人对战模式展示
//游戏数据初始化
void Game_Initialization();//每次对局开始前的初始化
void f_Game_Initialization();//复盘时的初始化(清空棋盘,恢复棋子最开始颜色)
int winner();//胜负判断(暴力判断)
void regret_AI();
void regret_Player();
void music_open();//播放背景音乐
void music_close();//关闭背景音乐
void music_set();//落子声音
//简单AI算法
void judge(int color);//寻找优势位置
int Data(int num, int count);//算分数来辅助judge函数
void AI();//分别判断黑子和白子的最大优势位置,然后判断是堵截还是进攻
//结束界面
void over_AI(int x);
void over_Player(int x);
void _fupan();
//对战界面
void playerVSplayer();//人人对战
void playerVSAI();//人机对战
void begin();//开始
初始化界面包括背景音乐播放和背景音乐暂停两个,差别不大,只是一个图片不一样。
void Page_initialization1() {//初始化界面(音乐 播放状态)
initgraph(950, 800); //创建窗口
loadimage(&Img, _T("D:\\开始界面.jpg"), 950, 800);//加载图像
loadimage(&MusicImg1, _T("D:\\播放.jpg"), 40, 40);//加载图像
putimage(0, 0, &Img);//在窗口中打印图像
putimage(900, 750, &MusicImg1);//在窗口中打印图像(音乐播放)
setbkmode(TRANSPARENT); //设置背景的风格 tranparent(透明的)
}
void Page_initialization2() {//初始化界面(音乐 暂停状态)
initgraph(950, 800); //创建窗口
loadimage(&Img, _T("D:\\开始界面.jpg"), 950, 800);//加载图像
loadimage(&MusicImg2, _T("D:\\暂停.jpg"), 40, 40);//加载图像
putimage(0, 0, &Img);//在窗口中打印图像
putimage(900, 750, &MusicImg1);//在窗口中打印图像(音乐暂停)
setbkmode(TRANSPARENT); //设置背景的风格 tranparent(透明的)
}
initgraph():创建初始化图形窗口。
oadimage():从文件中读取图像。
putimage():在窗口上绘制指定的图像。
setbkmode():设置背景。
注意最后一句设置背景风格为透明是为了之后在上面输出文字的时候,文字的背景不会是黑框而是图片。容易遇到的问题就是创建窗口后输出图片还是一片黑,有可能是因为你图片的像素和窗口大小差距过大,建议改成和创建的窗口相同。
运行效果:
这是没有包括音乐播放和暂停的图片,并且那些字是p上去的。
结束界面也包括两种,一种是人机对战的结束界面,另外一种是人人对战的结束界面。
void over_AI(int x)
{
initgraph(400, 400);//;创建窗口
settextstyle(50, 24, _T("宋体"));//设置字体格式
settextcolor(RED); //字体颜色
if (x == 1)//判断胜利方
{
outtextxy(100, 50, _T("电脑获胜"));
}
else if (x == 2)
{
outtextxy(100, 50, _T("玩家获胜"));
}
settextcolor(WHITE);
outtextxy(160, 200, _T("复盘"));//展示选项
outtextxy(60, 300, _T("返回开始界面"));
while (true)
{
MOUSEMSG n;//鼠标信息
n = GetMouseMsg();//获取鼠标消息
switch (n.uMsg) {
case WM_LBUTTONDOWN://左键按下
if (n.x <= 260 && n.x >= 160 && n.y <= 250 && n.y >= 200)
_fupan(); //复盘游戏
if (n.x <= 280 && n.x >= 60 && n.y <= 350 && n.y >= 300)
{
begin(); // 返回开始界面
}
}
}
}
void over_Player(int x) {
initgraph(400, 400);//创建窗口
settextstyle(50, 24, _T("宋体"));//设置字体格式
settextcolor(RED); //字体颜色
if (x == 2)//判断胜利方
{
outtextxy(100, 50, _T("黑棋获胜"));
}
else if (x == 1)
{
outtextxy(100, 50, _T("白棋获胜"));
}
settextcolor(WHITE);
outtextxy(160, 200, _T("复盘"));//展示选项
outtextxy(60, 300, _T("返回开始界面"));
while (true)
{
MOUSEMSG n;//鼠标信息
n = GetMouseMsg();//获取鼠标消息
switch (n.uMsg) {
case WM_LBUTTONDOWN://左键按下
if (n.x <= 260 && n.x >= 160 && n.y <= 250 && n.y >= 200)
_fupan(); //复盘游戏
if (n.x <= 280 && n.x >= 60 && n.y <= 350 && n.y >= 300)
{
begin(); // 返回开始界面
}
}
}
}
settextstyle():设置字体格式
settextcolor():设置字体颜色
outtextxy():在指定位置输出文字
MOUSEMSG鼠标信息变量(结构体)
struct MOUSEMSG
{
UINT uMsg; // 当前鼠标消息
bool mkCtrl; // Ctrl 键是否按下
bool mkShift; // Shift 键是否按下
bool mkLButton; // 鼠标左键是否按下
bool mkMButton; // 鼠标中键是否按下
bool mkRButton; // 鼠标右键是否按下
short x; // 当前鼠标 x 坐标
short y; // 当前鼠标 y 坐标
short wheel; // 鼠标滚轮滚动值 (120 的倍数)
};
需要注意的就是设置输出文字的位置和之后设置的点击跳转的位置要相同。
绘制棋盘包括三种情况,除了人人对战、人机对战之外,还有复盘时的棋盘,但是三者差距不大,就是文字输出有差距。
void Checkerboard_display_AI() {//绘制棋盘
//创建窗口
initgraph(950, 800);//棋盘设置大小700*700
//加载图片
loadimage(NULL, _T("D:\\棋盘.jpg"), 950, 800);
//设置线为黑色
setlinecolor(BLACK);
//绘制棋盘
for (int i = 50; i <= 750; i += 50)//画棋盘
{
line(i, 50, i, 750);
line(50, i, 750, i);
}
setbkmode(TRANSPARENT); //设置背景的风格 tranparent(透明的)
settextcolor(RGB(0, 0, 0)); //设置字体的颜色
settextstyle(40, 0, _T("宋体"));
outtextxy(800, 200, _T("悔 棋"));//展示功能
outtextxy(800, 300, _T("重 来"));
outtextxy(800, 400, _T("后 手"));
outtextxy(800, 500, _T("退 出"));
}
void Checkerboard_display_Player() {//绘制棋盘
//创建窗口
initgraph(950, 800);
//加载图片
loadimage(NULL, _T("D:\\棋盘.jpg"), 950, 800);
//设置线为黑色
setlinecolor(BLACK);
//绘制棋盘
for (int i = 50; i <= 750; i += 50)//画棋盘
{
line(i, 50, i, 750);
line(50, i, 750, i);
}
setbkmode(TRANSPARENT); //设置背景的风格 tranparent(透明的)
settextcolor(RGB(0, 0, 0)); //设置字体的颜色
settextstyle(40, 0, _T("宋体"));
outtextxy(800, 200, _T("悔 棋"));//展示功能
outtextxy(800, 300, _T("重 来"));
outtextxy(800, 400, _T("退 出"));
}
void f_Checkerboard_display() {//绘制棋盘
//创建窗口
initgraph(950, 800);
//加载图片
loadimage(NULL, _T("D:\\棋盘.jpg"), 950, 800);
//设置线为黑色
setlinecolor(BLACK);
//绘制棋盘
for (int i = 50; i <= 750; i += 50)//画棋盘
{
line(i, 50, i, 750);
line(50, i, 750, i);
}
setbkmode(TRANSPARENT); //设置背景的风格 tranparent(透明的)
settextcolor(RGB(0, 0, 0)); //设置字体的颜色
settextstyle(40, 0, _T("宋体"));
outtextxy(800, 500, _T("退 出"));
}
setlinecolor():设置线的颜色
line():画线,两点之间画线,可以通过控制变量来实现棋盘的绘制
之前提到的设置背景为透明就是在这里使用,如果不设置就会使有字的那一块背景变成黑色)。
棋子绘制,当鼠标点击时,在指定位置绘制棋子。
void Drawpiece(int x, int y, int color) {//绘制棋子
if (color == 1) {//填充白色
setfillcolor(WHITE);
}
else if (color == 2) {//填充黑色
setfillcolor(BLACK);
}
solidcircle(x * 50 + 50, y * 50 + 50, 25);//在指定位置画出棋子
music_set();//播放落子声音
}
setfillcoolor():设置填充颜色
solidcircle():在指定位置绘制圆
棋子绘制函数比较简单,注意绘制的位置,要与鼠标获取坐标后与数组坐标之间的转化对应,然后就是在绘制圆下面加了一个自定义函数music_set,是播放棋子落子声音的。在悔棋时的绘制就不能有落子声音(会出现延迟,变成一个个棋子重新一步步的下,具体为什么可以看悔棋函数)
void Drawpiece_regret(int x, int y, int color) {//绘制棋子
if (color == 1) {//填充白色
setfillcolor(WHITE);
}
else if (color == 2) {//填充黑色
setfillcolor(BLACK);
}
solidcircle(x * 50 + 50, y * 50 + 50, 25);//在指定位置画出棋子
}
落子,就是给数组的指定位置上赋值,比较简单
bool PieceSet(int y, int x, int color) //放置棋子,返回true表示放置成功,false 表示放置失败
{
if (chess[x][y] != 0)//当前位置有棋子
{
return false;
}
chess[x][y] = color;//无棋子则给数组赋值
fupan[piece_num].f_x = x;//为复盘记录每一步位置
fupan[piece_num].f_y = y;
piece_num += 1;//棋子数+1
return true;
}
void Pieceset_fupan(int y, int x, int color) {//复盘时只需要给数组赋值即可
chess[x][y] = color;//给数组赋值
}
要注意的是在正常落子函数中(上面一个),有几步操作是用来记录每一步棋的具体位置的,用在复盘功能中。但是在复盘过程中不需要再记录一次,所以有了下面一个函数,只需要给数组赋值。
特别注意:参数顺序是int y,int x,而在传参时是x,y,这是因为easyx库内xy轴是原点在左上角,x轴横向且向右为正,y轴竖向且向下为正。与二维数组行和列的对应关系是:x对应列,y对应行
所以在落子以及之后的AI判断等等函数需要考虑这个关系
模式展示主要指,在鼠标移向这些表示模式的字时字体变红(告知玩家鼠标在这),移开时又显示黑色。主要包括三种,一是在开始界面的,然后两种是人机对战和人人对战的过程中。
void Modelshow() {//开始界面展示模式:使得鼠标移动到字体的时候字体变色
Mouse_message = GetMouseMsg(); //获取鼠标的消息,使用的全局鼠标信息变量
settextstyle(70, 60, _T("楷体"));//设置字体大小格式
if (Mouse_message.x >= 230 && Mouse_message.x <= 690 && Mouse_message.y >= 350 && Mouse_message.y <= 420)
{
settextcolor(RGB(250, 0, 0)); //设置字体的颜色
outtextxy(230, 350, _T("人机对战")); //输出文字
}
else if (Mouse_message.x >= 230 && Mouse_message.x <= 690 && Mouse_message.y >= 430 && Mouse_message.y <= 500)
{
settextcolor(RGB(250, 0, 0)); //设置字体的颜色
outtextxy(230, 430, _T("人人对战"));//输出文字
}
else
{
settextcolor(RGB(0, 0, 0)); //设置字体的颜色
outtextxy(230, 350, _T("人机对战")); //输出文字
outtextxy(230, 430, _T("人人对战"));
}
}
void Modelshow_AI(MOUSEMSG m) {//人机对战模式展示(形参接受一个鼠标信息变量,获取鼠标位置)
//传入与之后相同的鼠标变量,防止两个函数鼠标变量不同,同时获取信息会有延迟(特别是出现连续落子的颜色相同)
settextstyle(40, 0, _T("宋体"));//设置字体结构
if (m.x <= 900 && m.y < 250 && m.x >= 800 && m.y > 200) //悔棋
{
settextcolor(RGB(250, 0, 0)); //设置字体的颜色
outtextxy(800, 200, _T("悔 棋"));//展示功能
}
else if (m.x <= 900 && m.y < 350 && m.x >= 800 && m.y > 300) //重开
{
settextcolor(RGB(250, 0, 0)); //设置字体的颜色
outtextxy(800, 300, _T("重 来"));
}
else if (m.x <= 900 && m.y < 450 && m.x >= 800 && m.y > 400) //退出
{
settextcolor(RGB(250, 0, 0)); //设置字体的颜色
outtextxy(800, 400, _T("后 手"));
}
else if (m.x <= 900 && m.y < 550 && m.x >= 800 && m.y > 500) //退出
{
settextcolor(RGB(250, 0, 0)); //设置字体的颜色
outtextxy(800, 500, _T("退 出"));
}
else {
settextcolor(RGB(0, 0, 0));
outtextxy(800, 200, _T("悔 棋"));//展示功能
outtextxy(800, 300, _T("重 来"));
outtextxy(800, 400, _T("后 手"));
outtextxy(800, 500, _T("退 出"));
}
}
void Modelshow_Player(MOUSEMSG m) {//人人对战模式展示
//传入与之后相同的鼠标变量,防止两个函数鼠标变量不同,同时获取信息会有延迟(特别是出现连续落子的颜色相同)
settextstyle(40, 0, _T("宋体"));
if (m.x <= 900 && m.y < 250 && m.x >= 800 && m.y > 200) //悔棋
{
settextcolor(RGB(250, 0, 0)); //设置字体的颜色
outtextxy(800, 200, _T("悔 棋"));//展示功能
}
else if (m.x <= 900 && m.y < 350 && m.x >= 800 && m.y > 300) //重开
{
settextcolor(RGB(250, 0, 0)); //设置字体的颜色
outtextxy(800, 300, _T("重 来"));
}
else if (m.x <= 900 && m.y < 450 && m.x >= 800 && m.y > 400) //退出
{
settextcolor(RGB(250, 0, 0)); //设置字体的颜色
outtextxy(800, 400, _T("退 出"));
}
else {
settextcolor(RGB(0, 0, 0));
outtextxy(800, 200, _T("悔 棋"));//展示功能
outtextxy(800, 300, _T("重 来"));
outtextxy(800, 400, _T("退 出"));
}
}
settextcolor():除了可以直接在括号里输入颜色,也可以通过RGB(0,0,0)的方式来设置颜色。
特别注意:这里的初始界面的模式展示里是使用的全局鼠标信息变量和开始界面函数里使用的是相同的。然后在后面两个人人对战和人机对战的模式展示中,设置了形参来接受人机对战人人对战函数中设置的鼠标信息变量,这是为了防止使用不同的鼠标变量同时获取信息会导致鼠标不灵敏。
游戏数据初始化主要包括初始化棋盘、棋子颜色、下棋步数以及记录每步棋具体位置的结构体。
void Game_Initialization() {//游戏数据初始化
Change_Chess = -1;//用于改变棋子
Current_color = 2;//改变棋子的变量
for (int i = 0; i < 15; i++)//初始化棋盘(清空棋子)
{
for (int j = 0; j < 15; j++)
{
chess[i][j] = 0;
}
}
piece_num = 0;//初始化步数
for (int i = 0; i < 100; i++) {
fupan[i].f_x = 0;
fupan[i].f_y = 0;
}
}
void f_Game_Initialization() {//与原本初始化分开,防止在过程中使用了重开功能后记录棋子步数等出现问题
Change_Chess = -1;//用于改变棋子
Current_color = 2;//改变棋子的变量
for (int i = 0; i < 15; i++)//初始化棋盘(清空棋子)
{
for (int j = 0; j < 15; j++)
{
chess[i][j] = 0;
}
}
}
不同的就是复盘时的初始化只需要初始化棋盘和棋子颜色即可,所以单独写一个函数。
悔棋包括两种,也就是人机对战和人人对战,因为人人对战悔的是当前玩家的这一步,而人机对战是悔的人与人机。
void regret_AI() {//悔棋重新绘制棋盘,且将记录的上一步棋的位置改为0,然后遍历棋盘重新绘制有棋的地方
Checkerboard_display_AI();//重新绘制棋盘
chess[xx][yy] = 0;//将上一步棋子数据清空
chess[AIxx][AIyy] = 0;
for (int i = 0; i < 15; i++) {//遍历数组把有棋子的位置画上棋子
for (int j = 0; j < 15; j++) {
if (chess[i][j] == 1) {
Drawpiece_regret(j, i, 1);//注意行列与xy轴的对应关系
}
if (chess[i][j] == 2) {
Drawpiece_regret(j, i, 2);
}
}
}
piece_num--;//使下棋的步数-1
}
void regret_Player() {//悔棋重新绘制棋盘,且将记录的上一步棋的位置改为0,然后遍历棋盘重新绘制有棋的地方
Checkerboard_display_Player();//重新绘制棋盘
chess[xx][yy] = 0;//将上一步棋子数据清空
for (int i = 0; i < 15; i++) {//遍历数组把有棋子的位置画上棋子
for (int j = 0; j < 15; j++) {
if (chess[i][j] == 1) {
Drawpiece_regret(j, i, 1);//注意行列与xy轴的对应关系
}
if (chess[i][j] == 2) {
Drawpiece_regret(j, i, 2);
}
}
}
piece_num--;//使下棋的步数-1
}
悔棋的思路:首先有变量记录每一步棋的位置,然后在悔棋函数中,把这个位置在棋盘数组中的数值改为0,之后重新绘制棋盘,再遍历数组把有棋的位置上绘制出来就可以。
要注意的就是画棋子的函数与正常的函数不同,去除了落子声音,要不然会有延迟,会出现从头到尾的棋一步一步下出来(就像我的复盘函数一样)
这算法是我借鉴几篇文章之后,写的一个算法,主要思路就是遍历数组,在没棋的位置进行判断,朝四个方向判断(—、|、\、/),判断在该位置下棋,会形成怎么样的棋形(就是如果在该位置落子会有几个相同的棋子相连),且在这个基础上判断连棋的同方向的最后一个(结束连棋的那个位置)是没有棋子还是边界或对方的棋子,然后基于五种不同的连棋数(1-5)中的两种情况(是空或被挡住)设置不同的分数,在遍历数组的时候记录每个位置在四个方向上的得分和,然后记录每次分数与下一次进行比较来得出最大值,再接着分别判断和记录黑棋和白棋的最大优势位置,最后进行比较判断是堵截还是进攻。
void judge(int color) {//寻找优势位置
int num = 1, right = 0, left = 0;//记录棋子数,记录左右位置的情况(无棋子或边界或为对方棋子)
/*若right或left为0表示左右没有棋子优势较大,对应data中的活x
若为1,则表示左右为边界或对方棋子,对应data中的冲x
*/
int n, m, max = 0, score = 0;//记录分数以及最大值
int addx, addy;//用来使行列改变
for (int i = 0; i < 15; i++) {
for (int j = 0; j < 15; j++) {
score = 0;//每一次初始化分数为0
if (chess[i][j] != 0)
{
continue;//如果当前位置有棋子,不判断
}
else {
// --方向
addx = 0; addy = 1; n = i; m = j;//保存这次的行列来进行变化
//向右
while (1) {
n += addx; m += addy;//列变化而不是行,因为行列与xy轴的对应关系(下面变化相同的道理)
if (chess[n][m] == 0) { right = 0; break; }
else if (chess[n][m] != color || m >= 15) { right++; break; }
num++;
}
addx = 0; addy = -1; n = i; m = j;
//向左
while (1) {
n += addx; m += addy;
if (chess[n][m] == 0) { left = 0; break; }
else if (chess[n][m] != color || m < 0) { left++; break; }
num++;
}
score += Data(num, right + left);//用data来算分数,并用score来记录
// |方向
num = 1; right = 0, left = 0;//每一次改变方向要重置这些变量
addx = -1; addy = 0; n = i; m = j;
//向上
while (1) {
n += addx; m += addy;
if (chess[n][m] == 0) { right = 0; break; }
else if (chess[n][m] != color || n < 15) { right++; break; }
num++;
}
//向下
addx = 1; addy = 0; n = i; m = j;
while (1) {
n += addx; m += addy;
if (chess[n][m] == 0) { left = 0; break; }
else if (chess[n][m] != color || n >= 15) { left++; break; }
num++;
}
score += Data(num, right + left);
// \方向
num = 1; right = 0, left = 0;
addx = 1; addy = 1; n = i; m = j;
//向右下
while (1) {
n += addx; m += addy;
if (chess[n][m] == 0) { right = 0; break; }
else if (chess[n][m] != color || m >= 15 || n >= 15) { right++; break; }
num++;
}
//向左上
addx = -1; addy = -1; n = i; m = j;
while (1) {
n += addx; m += addy;
if (chess[n][m] == 0) { left = 0; break; }
else if (chess[n][m] != color || m < 0 || n < 0) { left++; break; }
num++;
}
score += Data(num, right + left);
// /方向
num = 1; right = 0, left = 0;
addx = -1; addy = 1; n = i; m = j;
//向右上
while (1) {
n += addx; m += addy;
if (chess[n][m] == 0) { right = 0; break; }
else if (chess[n][m] != color || n < 0 || m >= 15) { right++; break; }
num++;
}
//向左下
addx = 1; addy = -1; n = i; m = j;
while (1) {
n += addx; m += addy;
if (chess[n][m] == 0) { left = 0; break; }
else if (chess[n][m] != color || n >= 15 || m < 0) { left++; break; }
num++;
}
score += Data(num, right + left);
if (score > max) {//每一次用max保存分数,下一次比较,最后找出最大值
max = score;
position[0] = i;//用来保存每一次的位置和分数
position[1] = j;
position[2] = score;
}
}
}
}
}
int Data(int num, int count)//用来计算分数辅助判断优势位置
{
switch (num)//棋子数
{
case 1:
if (count == 0)//活一:表示在该处落子,就只有这一个棋子,两边都没有阻挡(边界或对方的棋子)优势较大
return 2;
else if (count == 1)//冲一:表示在该处落子,就只有这一个棋子,两边有一种阻挡(边界或对方的棋子)优势较小
return 1;
else return 0;
break;
case 2:
if (count == 0)//活二;接下来都同理活一和冲一
return 20;
else if (count == 1)//冲二
return 10;
else return 0;
break;
case 3:
if (count == 0)//活三
return 300;
else if (count == 1)//冲三
return 50;
else return 0;
break;
case 4:
if (count == 0)//活四
return 4000;
else if (count == 1)//冲四
return 1000;
else return 0;
break;
case 5://五
return 5000;
break;
default:
return 0;
break;
}
}
void AI() {
int old_max = 0, new_max = 0, n = 0, m = 0;
judge(2);//判断黑子的优势位置
new_max = position[2];//保存该位置分数
old_max = new_max;
AIx = position[1]; AIy = position[0];//保存该位置的坐标,注意行列和xy轴的对应关系
judge(1);//判断白子的优势位置
new_max = position[2];//保存分数
if (new_max >= old_max) {//判断哪个位置的分数大,从而判断是堵截还是进攻
AIx = position[1]; AIy = position[0];
}
}
复盘功能主要就是运用一个变量来记录每局下棋的步数和一个结构体数组来记录每一步的具体位置。再重新初始化棋盘数组和棋子颜色,再一步步重现上一局棋局。
void _fupan() {
f_Game_Initialization();
f_Checkerboard_display();
for (int i = 0; i < piece_num; i++) {
Pieceset_fupan(fupan[i].f_x, fupan[i].f_y, Current_color);
Drawpiece(fupan[i].f_y, fupan[i].f_x, Current_color);
Current_color += Change_Chess;
Change_Chess *= -1;
}
MOUSEMSG m;
while (1) {
m = GetMouseMsg();//获取鼠标信息
if (m.mkLButton) //左键按下
{
if (m.x <= 900 && m.y < 550 && m.x >= 800 && m.y > 500) //后手
{
begin();
}
}
}
}
这里面使用的是正常画棋子的函数,就为了有音效且有延迟,可以一步一步展示给玩家看。
背景音乐功能主要包括打开和关闭,我这里设置的是运行程序时默认打开,在开始界面可以点击音乐播放开关那里关闭(只设置了在开始界面开关,有点懒)
void music_open() {
mciSendString(_T("open D:\\背景音乐.mp3 alias m"), NULL, 0, NULL);//打开音乐
mciSendString(_T("play D:\\背景音乐.mp3"), NULL, 0, NULL);//播放音乐
}
void music_close() {
mciSendString(_T("close D:\\背景音乐.mp3"), NULL, 0, NULL);//关闭音乐
}
mciSendString():主要用来打开播放关闭音乐。
落子音效设置,只需要在一个函数内使用mciSendString函数对落子声音实现打开播放关闭即可。
void music_set() {
mciSendString(_T("open D:\\落子.mp3 alias m"), NULL, 0, NULL);//打开播放关闭音乐
mciSendString(_T("play D:\\落子.mp3 wait"), NULL, 0, NULL);
mciSendString(_T("close D:\\落子.mp3"), NULL, 0, NULL);
}
主要将模式展示(开始界面)和人人对战、人机对战等函数封装,再设置鼠标操作来实现函数调用实现。
void begin() {//开始
if (music % 2 != 0) {//用来控制在人机人人对战中人为退出时重新进入begin音乐是否播放以及页面的显示
music_open();
Page_initialization1(); //初始化页面
}
else {
music_close();
Page_initialization2();
}
while (1)
{
Modelshow(); //模式显示
switch (Mouse_message.uMsg)//使用switch-case是为了能够在每次对局结束的break能够重复进入开始界面,而不是直接跳出while循环
{
case WM_LBUTTONDOWN: //鼠标左键按下
if (Mouse_message.x >= 230 && Mouse_message.x <= 690 && Mouse_message.y >= 350 && Mouse_message.y <= 420) //在人机对战区域按下
{
if (music % 2 != 0) {
playerVSAI(); //人机对战
Page_initialization1(); //重新显示页面
break;
}
else {
playerVSAI(); //人机对战
Page_initialization2(); //重新显示页面
break;
}
}
if (Mouse_message.x >= 230 && Mouse_message.x <= 690 && Mouse_message.y >= 430 && Mouse_message.y <= 500) //在人人对战区域按下
{
if (music % 2 != 0) {
playerVSplayer(); //人人对战
Page_initialization1(); //重新显示页面
break;
}
else {
playerVSplayer(); //人人对战
Page_initialization2(); //重新显示页面
break;
}
}
if (Mouse_message.x >= 900 && Mouse_message.x <= 950 && Mouse_message.y >= 750 && Mouse_message.y <= 800)
{
if (music % 2 != 0) {
music_close();
Page_initialization2(); //重新显示页面
music++;
break;
}
else {
music_open();
Page_initialization1(); //重新显示页面
music++;
break;
}
}
}
}
}
需要注意的就是用music来控制不同函数的调用的操作。
主要把游戏初始化,棋盘绘制,模式展示,悔棋,开始函数,结束界面等封装在一起来实现。
void playerVSplayer() {//人人对战
Game_Initialization();//初始化游戏
Checkerboard_display_Player();//初始化界面--绘制棋盘
MOUSEMSG m;//定义一个新的鼠标信息变量,防止使用全局变量冲突使落子慢甚至多次点击无法落子
int x, y;
while (1) {
m = GetMouseMsg();//获取鼠标信息
Modelshow_Player();
if (m.mkLButton) //左键按下
{
if (m.x <= 900 && m.y < 250 && m.x >= 800 && m.y > 200) //悔棋
{
regret_Player();//悔棋
Current_color += Change_Chess;//恢复棋子颜色
Change_Chess *= -1;
continue;
}
if (m.x <= 900 && m.y < 350 && m.x >= 800 && m.y > 300) //重开
{
playerVSplayer();//返回开始状态
}
if (m.x <= 900 && m.y < 450 && m.x >= 800 && m.y > 400) //重开
{
begin();//返回开始界面
}
if (m.x <= 750 && m.y <= 750 && m.x >= 50 && m.y >= 50) {
x = (m.x - 50 + 25) / 50; // 减去50--保证减去边界 加上25保证落子范围在棋子大小范围内
y = (m.y - 50 + 25) / 50;//控制范围:在落子附近一个棋子大小范围内,点击都可以落子。
if (PieceSet(x, y, Current_color))//先判断是否有棋
{
Drawpiece(x, y, Current_color);//无棋子,则画下棋
xx = y;//y对应棋盘的行
yy = x;//x对于棋盘的列
win = winner();//判断胜负
if (win > 0) {//分出胜负则显示结束界面
if (win == 2) {
over_Player(win);//展示结束界面
}
else {
over_Player(win);
}
break;
}
}
//改变棋子
Current_color += Change_Chess;
Change_Chess *= -1;
}
}
}
closegraph();//关闭当前窗口
}
特别注意:鼠标获取的坐标与棋盘坐标之间的转化,x = (m.x - 50 + 25) / 50;y = (m.y - 50 + 25) / 50; // 减去50--保证减去边界 加上25保证落子范围在棋子大小范围内,控制范围:在落子附近一个棋子大小范围内,点击都可以落子。
主要把游戏初始化,棋盘绘制,模式展示,悔棋,开始函数,结束界面等封装在一起来实现。
void playerVSAI() {//人机对战
Game_Initialization(); //游戏初始化
Checkerboard_display_AI();//初始化界面--绘制棋盘
int num = 0;//控制后手只能实现一次
MOUSEMSG m;//定义鼠标信息变量
int x, y;
while (1)
{
m = GetMouseMsg(); //获取一个鼠标消息
Modelshow_AI();
if (m.mkLButton) //左键按下
{
if (m.x <= 900 && m.y < 250 && m.x >= 800 && m.y > 200) //悔棋
{
regret_AI();//悔棋
continue;
}
if (m.x <= 900 && m.y < 350 && m.x >= 800 && m.y > 300) //重开:重新开始->把数据初始化再重新展示界面
{
playerVSAI();//返回开始状态
}
if (m.x <= 900 && m.y < 450 && m.x >= 800 && m.y > 400) //后手:让电脑先下棋
{
if (num < 1) {//用num来控制只能在开头点击后手
AI();
PieceSet(AIx, AIy, Current_color);
Drawpiece(AIx, AIy, Current_color);
AIxx = AIy;//记录位置
AIyy = AIx;
//改变棋子颜色
Current_color += Change_Chess;
Change_Chess *= -1;
num++;
continue;
}
}
if (m.x <= 900 && m.y < 550 && m.x >= 800 && m.y > 500) //退出
{
begin();//返回开始界面
}
if (m.x <= 750 && m.y <= 750 && m.x >= 50 && m.y >= 50) {
x = (m.x - 50 + 25) / 50;//减去50--保证减去边界 加上25保证落子范围在棋子大小范围内
y = (m.y - 50 + 25) / 50;//控制范围:在落子附近一个棋子大小范围内,点击都可以落子。
if (PieceSet(x, y, Current_color))//保证此位置没有棋子且给该位置赋值
{
Drawpiece(x, y, Current_color);//画棋子
xx = y;//记录该步位置:x对应列y对应行
yy = x;
win = winner();//判断胜负
if (win > 0) //分出胜负则显示结束界面
{
if (win == 2)
{
over_AI(win);//展示结束界面
}
else
{
over_AI(win);
}
break;
}
//改变棋子颜色
Current_color += Change_Chess;
Change_Chess *= -1;
//AI判断有利位置
AI();
PieceSet(AIx, AIy, Current_color);//落子画棋
Drawpiece(AIx, AIy, Current_color);
AIxx = AIy;//记录位置
AIyy = AIx;
win = winner();//判断胜负
if (win > 0) //分出胜负则显示结束界面
{
if (win == 2)
{
over_AI(win);//展示结束界面
}
else
{
over_AI(win);
}
break;
}
//改变棋子颜色
Current_color += Change_Chess;
Change_Chess *= -1;
}
}
}
}
closegraph();//关闭该窗口
}
注意事项就是有一个变量num来控制人机对战中的后手功能只能使用一次。然后坐标转化的注意与人人对战相同。
总体来说,实现不怎么困难,就是那个AI算法搞的时间比较久,然后就是各种debug和调试花的时间比较多了。主要这每个函数之间的跳转啊什么的老是出现问题,会导致其他函数出现问题,又要去其他函数里看看是什么原因,然后设置一些变量来阻止这样的影响,这个过程比较麻烦。最后还有一个问题就是下棋的时候不能鼠标不能点的太快,会使程序反应不过来(应该是鼠标信息变量获取信息太灵敏),导致棋子颜色没有改变,会出现连续下相同颜色的棋。然后就是功能还不够完善。