Bootstrap

中国象棋——基于EasyX图形库

目录

1.数据的定义与类型的搭建

2.打印棋盘与棋子

3.鼠标点击

4.设置下棋规则

 5.游戏结束判断及结束窗口

6.主函数实现逻辑 

7.双缓冲绘图

8.目前存在的不足

        1.代码量过大

        2.无法应对高压测试

9.完整代码


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;
		}
	}


}

效果如下: 

9cc31b7a78444a5e8bfbf91b020a6844.png

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;
	}
}

abdb23c15dae4dc2bd715e37bb58ea76.png

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图形库下载链接:

https://easyx.cn/


制作不易,能否点个赞再走呢qwq

;