目录
1.数据的定义与类型的搭建
我们知道,象棋由棋盘和棋子组成,而棋子有红黑两种类型,每种类型又分为16种棋子。所以在最开始,我们可以建立一个结构体代表棋盘上每个点的棋子的各个属性,再使用枚举体枚举每个棋子的名称。而对于一些信息,如落子的顺序和鼠标点击的坐标,就建立一些全局变量和结构体将它们表达出来,如下:
#define ROW 10
#define COL 9
#define INTERSIZE 50 //前瞻间隔
#define CHESSSIZE 70 //格子间隔
static int type = RED;// 设置先手后手
static int g = 1; // 记录判断先后手
struct chess
{
int x; //位于窗口的x,y轴坐标
int y;
int id; //棋子名称
int type; //所属类型
int river = 0; //是否过河
};
enum piece
{
SPACE = -1, //表示无棋子
車, 馬, 象, 士, 将, 砲, 卒,
俥, 马, 相, 仕, 帥, 炮, 兵,
BEGIN, END //起子还是落子
};
struct START 鼠标点击坐标
{
int x;
int y;
};
//定义基本数据
struct chess map[ROW][COL]; //棋盘每一个点,每点都对应了一个棋子属性
struct START begin = { -1, -1 }; //起子坐标
struct START end = { -1, -1 }; //落子坐标
int start = BEGIN; //鼠标状态
enum piece blackpiece[] = { 車, 馬, 象, 士, 将, 砲, 卒 }; //黑棋
enum piece redpiece[] = { 俥, 马, 相, 仕, 帥, 炮, 兵 }; //红棋
const char* chessname[] = { "車", "馬", "象", "士", "将", "砲", "卒", "俥", "马", "相", "仕", "帥", "炮", "兵" }; //用于后面打印名称
2.打印棋盘与棋子
根据中国象棋的棋盘模板和棋子位置,我们可以将棋盘打印出来。EasyX图形库中封装好了许多绘图函数供我们使用。我们可以利用其中函数高效地绘制我们想要的图案,如:
//头文件
#include<easyx.h>
#include <graphics.h>
setbkcolor(YELLOW);
setbkcolor(RGB(180,24,137)); //以指定的颜色填充背景(三原色或已有颜色)
打印文字:
settextstyle(h,w,”楷体”); //设置文字样式
settextcolor(YELLOW); //设置文字颜色
outtextxy(x, y, str); //打印文字,其中str为字符串类型
绘制线段,图形:
setlinecolor(RED);//将线段设为红色
line(x1,y1,x2,y2);//在将坐标为(x1,y1)、(x2,y2)的两点连接起来
rectangle(x1,y1,x2,y2);//矩形左上角坐标(x1,y1)、右下角坐标(x2,y2)为界显示一个矩形
fillcircle(x,y,r);//以圆心为(x,y)、半径为r画圆
这里只列举了其中一部分,还有许多函数读者可以自行查阅资料。
通过使用这些库函数我们可以较为容易的绘制出棋盘与棋子,在将棋盘上每一点上的棋子的属性设置好的前提下,我们可以画出较为美观的窗口。主要代码如下:
void printfboard() //打印棋盘
{
setbkcolor(RGB(252, 215, 162)); //设置背景
cleardevice();
setlinecolor(BLACK);
setlinestyle(PS_SOLID, 2); //设置线形
for (int i = 0; i < 10; i++) //划线
{
line(INTERSIZE, i*CHESSSIZE + INTERSIZE, 8 * CHESSSIZE + INTERSIZE, i*CHESSSIZE + INTERSIZE);
if (i < 9)
{
line(i*CHESSSIZE + INTERSIZE, INTERSIZE, i*CHESSSIZE + INTERSIZE, 9 * CHESSSIZE + INTERSIZE);
}
}
//绘制矩形框
rectangle(INTERSIZE - 5, INTERSIZE - 5, 8 * CHESSSIZE + INTERSIZE + 5, 9 * CHESSSIZE + INTERSIZE + 5);
//设置河道填充
setfillcolor(RGB(252, 215, 162));
fillrectangle(INTERSIZE, 4 * CHESSSIZE + INTERSIZE, 8 * CHESSSIZE + INTERSIZE, 5 * CHESSSIZE + INTERSIZE);
// 打印楚河汉界
char river[20] = "楚河 汉界";
settextcolor(BLACK);
settextstyle(50, 0, "楷体");
setbkmode(TRANSPARENT);// 背景色置为透明
outtextxy(INTERSIZE + 100, 4 * CHESSSIZE + INTERSIZE, river);
//上九宫格
line(3 * CHESSSIZE + INTERSIZE, INTERSIZE, 5 * CHESSSIZE + INTERSIZE, 2 * CHESSSIZE + INTERSIZE);
line(3 * CHESSSIZE + INTERSIZE, 2 * CHESSSIZE + INTERSIZE, 5 * CHESSSIZE + INTERSIZE, INTERSIZE);
//下九宫格
line(3 * CHESSSIZE + INTERSIZE, 7 * CHESSSIZE + INTERSIZE, 5 * CHESSSIZE + INTERSIZE, 9 * CHESSSIZE + INTERSIZE);
line(3 * CHESSSIZE + INTERSIZE, 9 * CHESSSIZE + INTERSIZE, 5 * CHESSSIZE + INTERSIZE, 7 * CHESSSIZE + INTERSIZE);
//下棋方显示
if (g % 2 == 1)
{
settextcolor(RED);
settextstyle(40, 0, "楷体");
outtextxy(640, 600, "红方落子");
}
if (g % 2 == 0)
{
settextcolor(BLACK);
settextstyle(40, 0, "楷体");
outtextxy(640, 110, "黑方落子");
}
for (int i = 0; i < ROW; i++)
{
for (int j = 0; j < COL; j++)
{
if (map[i][j].id != SPACE)
{
fillcircle(map[i][j].x, map[i][j].y, 30);
fillcircle(map[i][j].x, map[i][j].y, 25);
settextstyle(30, 10, "黑体");
setbkmode(TRANSPARENT);
settextcolor(map[i][j].type);
outtextxy(map[i][j].x - 13, map[i][j].y - 13, chessname[map[i][j].id]);
}
}
}
}
void printfchess() //打印棋子
{
for (int i = 0; i < ROW; i++)
{
for (int j = 0; j < COL; j++)
{
enum piece chessid = SPACE;
int chesstype;
if (i <= 4) //打印红子,赋属性
{
chesstype = BLACK;
if (i == 0)// 设置最下面一排
{
if (j <= 4)
{
chessid = blackpiece[j];
}
else
{
chessid = blackpiece[8 - j];
}
}
if (i == 2)// 设置炮
{
if (j == 1 || j == 7)
{
chessid = blackpiece[5];
}
}
if (i == 3)// 设置兵
{
if ((j + 1) % 2 == 1)
{
chessid = blackpiece[6];
}
}
}
else
{
chesstype = RED; //打印黑子,附属性
if (9 - i == 0)
{
if (j <= 4)
{
chessid = redpiece[j];
}
else
{
chessid = redpiece[8 - j];
}
}
if (9 - i == 2)
{
if (j == 1 || j == 7)
{
chessid = redpiece[5];
}
}
if (9 - i == 3)
{
if ((j + 1) % 2 == 1)
{
chessid = redpiece[6];
}
}
}
map[i][j].river = 0;// 赋值各点位属性
map[i][j].id = chessid;
map[i][j].type = chesstype;
map[i][j].x = j*CHESSSIZE + INTERSIZE;
map[i][j].y = i*CHESSSIZE + INTERSIZE;
}
}
}
效果如下:
3.鼠标点击
EasyX图形库也给我们封装好了一些函数用于实现鼠标控制,我们使用提供的函数可以通过鼠标对棋子进行移动。需要注意的是,我们需要设置一个点击范围,当鼠标落在这个范围时,就认为其选中了这块区域,使移动更加灵活,主要代码如下:
void Mousecontrol()
{
int n = 0;
ExMessage m;
peekmessage(&m, EX_MOUSE);// 记录鼠标命令
if (m.message == WM_LBUTTONDOWN)
{
int row = (m.y - INTERSIZE) / CHESSSIZE;
int col = (m.x - INTERSIZE) / CHESSSIZE;
if (m.x > map[row][col].x + 30 && m.y < map[row][col].y + 30)// 调整点击范围
col++;
if (m.x<map[row][col].x + 30 && m.y>map[row][col].y + 30)
row++;
if (m.x>map[row][col].x + 30 && m.y > map[row][col].y + 30)
{
row++;
col++;
}
if (start == BEGIN)// 记录开始结束坐标
{
begin.x = col;
begin.y = row;
if (map[begin.y][begin.x].id != SPACE&&map[begin.y][begin.x].type == type)
{
start = END;
}
}
else if (start == END)
{
start = BEGIN;
end.x = col;
end.y = row;
n = 1;// 设置一个循环
}
}
4.设置下棋规则
我们知道,象棋有一套标准的下棋规则,因此我们需要设置一些条件让棋子按照规则来进行移动,当棋子的移动不满足条件时,禁止棋子活动;当符合规则时,通过更改棋盘上的属性使其打印出新的画面,实现移动效果。这也是整个游戏最核心的环节,由于代码过长,以下列举了“将”和“帅”的行走规则,其余的棋子这里就不一一列举了:
switch (map[begin.y][begin.x].id)// 设置各个棋子走法
{
case piece::将:
case piece::帥:
if (begin.y <= 4)
{
for (int i = 0; i <= 2; i++)
{
for (int j = 3; j <= 5; j++)
{
if (begin.x != -1 && end.x != -1 && !((begin.x == end.x) && (begin.y == end.y))
&& n == 1 && end.y == i&&end.x == j && (begin.x == end.x || begin.y == end.y) &&
(abs(begin.x - end.x) == 1 || abs(begin.y - end.y) == 1) && map[begin.y][begin.x].type == type)
{
if (map[end.y][end.x].id == SPACE || map[begin.y][begin.x].type != map[end.y][end.x].type)
{
map[end.y][end.x].id = map[begin.y][begin.x].id;
map[end.y][end.x].type = map[begin.y][begin.x].type;
map[begin.y][begin.x].id = SPACE;
n++;//用于计数,判断有效次数
}
}
}
}
}
else
{
for (int i = 7; i <= 9; i++)
{
for (int j = 3; j <= 5; j++)
{
if (begin.x != -1 && end.x != -1 && !((begin.x == end.x) && (begin.y == end.y))
&& n == 1 && end.y == i&&end.x == j && (begin.x == end.x || begin.y == end.y) &&
(abs(begin.x - end.x) == 1 || abs(begin.y - end.y) == 1) && map[begin.y][begin.x].type == type)
{
if (map[end.y][end.x].id == SPACE || map[begin.y][begin.x].type != map[end.y][end.x].type)
{
map[end.y][end.x].id = map[begin.y][begin.x].id;
map[end.y][end.x].type = map[begin.y][begin.x].type;
map[begin.y][begin.x].id = SPACE;
n++;
}
}
}
}
}
break;
}
5.游戏结束判断及结束窗口
当每一次落子后,我们需要判断棋盘是否出现胜负了,我们可以通过循环查找九宫格来判断谁胜谁负,然后跳转到结束窗口,让用户选择下一步进行的动作。主要函数与效果如下:
int Judge()
{
int n = 0, m = 0;
for (int i = 0; i <= 2; i++)
{
for (int j = 3; j <= 5; j++)
{
if (map[i][j].id == 4)
n++;
}
}
for (int i = 7; i <= 9; i++)
{
for (int j = 3; j <= 5; j++)
{
if (map[i][j].id == 11)
m++;
}
}
if (m == 0 || n == 0)
{
type = RED;
g = 1;
initgraph(800, 500);
setbkcolor(RGB(5, 225, 250)); //设置背景
cleardevice();
setlinecolor(BLACK);
setfillcolor(RGB(255, 128, 192));
settextcolor(RGB(255, 225, 0));
fillrectangle(100, 300, 300, 400);
fillrectangle(500, 300, 700, 400);
if (n == 0)
{
settextstyle(100, 0, "楷体");
setbkmode(TRANSPARENT);// 背景色置为透明
outtextxy(100, 100, "恭喜红方获胜");
settextstyle(40, 0, "楷体");
outtextxy(120, 330, "继续游戏");
outtextxy(520, 330, "退出游戏");
}
if (m == 0)
{
settextstyle(100, 0, "楷体");
setbkmode(TRANSPARENT);// 背景色置为透明
outtextxy(100, 100, "恭喜黑方获胜");
settextstyle(40, 0, "楷体");
outtextxy(120, 330, "继续游戏");
outtextxy(520, 330, "退出游戏");
}
return 0;
}
else
{
return 1;
}
}
6.主函数实现逻辑
主函数采用多重循环嵌套的思路:先设置窗口,打印棋盘。然后进入循环,如果没有得出胜负,就使其在棋子打印,鼠标控制等等函数下进行死循环,直到得出胜负,跳出循环,进入下一个循环,打印结束窗口,让用户选择继续游戏还是退出游戏。如果用户选择继续游戏,让程序循环回到程序最初位置,如果选择退出游戏,则跳出循环,程序结束:
#include "game.h"
#include<windows.h>
int main()
{
int n = 0; //用于表示结束后的选择,0:无选择;1:继续;2:结束
do
{
n = 0;
initgraph(800, 800);
printfchess();
BeginBatchDraw();//双缓冲画图,防止闪屏
while (1)
{
printfboard();
FlushBatchDraw();
Mousecontrol();
int a = Judge();
if (a == 0)
break;
}
EndBatchDraw();
BeginBatchDraw();
while (n==0)
{
n = choise();
}
} while (n==1);
EndBatchDraw();
closegraph();
return 0;
}
7.双缓冲绘图
在实际编写的过程中,有一个问题值得注意,由于打印棋盘和棋子是利用一个死循环来控制的,导致屏幕刷新的速度过快,出现了闪屏的现象。通过查找资料,发现可以使用EasyX的函数来进行双缓冲绘图,所谓双缓冲绘图,就是不直接在DC上进行绘制,而是先在内存虚拟屏幕中,然后利用相关函数,一次全屏拷贝到OSD实际显示屏幕中,此过程即完成双缓冲绘图。通过这种技术,使得由于刷新速度过快而闪烁的棋盘得以丝滑地呈现出来!
8.目前存在的不足
1.代码量过大
此游戏加起来共有将近800行的代码,通过重新审查,发现有些地方还存在着可以改进和优化的空间,可以进一步缩短代码量。
2.无法应对高压测试
虽然有进行一些条件控制来减少点击过程中出现的bug,但在测试的过程中发现,当用户多次进行无效点击后进行有效点击的时候,还是有一定概率会出现棋子无法移动,begin被识别为end,需要再次点击才能进行移动。还需要进一步进行分析与改进。
9.完整代码
百度网盘链接:
https://pan.baidu.com/s/1qnN0HInjqF9_LwlxhOfuCw
提取码:2003
EasyX图形库下载链接:
制作不易,能否点个赞再走呢qwq