使用VS2019+easyx图形库开发,也可以使用VS的其他版本。
使用VS2019(或VS2022)创建一个新项目,选择空项目模板。
然后再导入图片素材res目录。因网盘链接不稳定,在评论中回复邮件地址,即发送完整素材。也可以使用自己的素材。
使用C语言开发的初学者,往往直接就在main函数中写详细的过程。使用C++面向对象,就需要“脱胎换骨”,改变开发思路了!不写过程,直接写需要几个类!
这里,设计了4个类,分别表示棋手,AI, 棋盘,游戏控制。这应该是最符合现实情况的简单设计了,如果是做网络对战版,就还需要添加其它模块。
按照这个方式,一共创建4个类:Man, AI, Chess, ChessGame. 创建完后,项目的目录结构如:
注意:在给类设计接口时,建议先只考虑对外暴露的“接口”,可以先不用考虑数据成员,对外(public)提供的接口(函数)才是最重要的。
Chess.h
typedef enum {
CHESS_WHITE = -1, // 白方
CHESS_BLACK = 1 // 黑方
} chess_kind_t;
struct ChessPos {
int row;
int col;
};
class Chess
{
public:
// 棋盘的初始化:加载棋盘的图片资源,初始化棋盘的相关数据
void init();
// 判断在指定坐标(x,y)位置,是否是有效点击
// 如果是有效点击,把有效点击的位置(行,列)保存在参数pos中
bool clickBoard(int x, int y, ChessPos* pos);
// 在棋盘的指定位置(pos), 落子(kind)
void chessDown(ChessPos* pos, chess_kind_t kind);
// 获取棋盘的大小(13线、15线、19线)
int getGradeSize();
// 获取指定位置是黑棋,还是白棋,还是空白
int getChessData(ChessPos* pos);
int getChessData(int row, int col);
// 判断棋局是否结束
bool checkOver();
};
AI.h
#include "Chess.h"
class AI
{
public:
void init(Chess* chess);
void go();
};
Man.h
ChessGame.h
class ChessGame
{
public:
void play();
};
可以使用如下方式自动生成各接口的具体实现。先不用考虑各个接口的真正实现,直接使用空函数体代替。
直接调用各个类定义的接口,实现游戏的主体控制。
为了便于调用各个类的功能,在ChessGame中,添加3各数据成员,并再构造函数中初始化这三个数据成员。
#include "Man.h"
#include "AI.h"
#include "Chess.h"
class ChessGame
{
public:
ChessGame(Man*, AI*, Chess*);
void play();
private:
Man* man;
AI* ai;
Chess* chess;
};
ChessGame::ChessGame(Man* man, AI* ai, Chess* chess)
{
this->man = man;
this->ai = ai;
this->chess = chess;
ai->init(chess);
man->init(chess);
}
void ChessGame::play()
{
chess->init();
while (1) {
man->go();
if (chess->checkOver()) {
chess->init();;
continue;
}
ai->go();
if (chess->checkOver()) {
chess->init();
continue;
}
}
}
在main函数中,创建游戏。
#include
#include "ChessGame.h"
int main(void) {
Chess chess;
Man man;
AI ai;
ChessGame game(&man, &ai, &chess);
game.play();
return 0;
}
为棋盘类,添加private权限的“数据成员”。
private:
// 棋盘尺寸
int gradeSize;
float margin_x;//49;
int margin_y;// 49;
float chessSize; //棋子大小(棋盘方格大小)
IMAGE chessBlackImg;
IMAGE chessWhiteImg;
// 存储当前游戏棋盘和棋子的情况,空白为0,黑子1,白子-1
vector> chessMap;
// 标示下棋方, true:黑棋方 false: AI 白棋方(AI方)
bool playerFlag;
再补充一下头文件。
#include
#include
using namespace std;
添加棋盘类的构造函数的定义以及实现。
Chess.h
Chess(int gradeSize, int marginX, int marginY, float chessSize);
Chess.cpp
Chess::Chess(int gradeSize, int marginX, int marginY, float chessSize)
{
this->gradeSize = gradeSize;
this->margin_x = marginX;
this->margin_y = marginY;
this->chessSize = chessSize;
playerFlag = CHESS_BLACK;
for (int i = 0; i < gradeSize; i++) {
vectorrow;
for (int j = 0; j < gradeSize; j++) {
row.push_back(0);
}
chessMap.push_back(row);
}
}
同时修改main函数的Chess对象的创建
//Chess chess;
Chess chess(13, 44, 43, 67.4);
对棋盘进行数据初始化,使得能够看到实际的棋盘。
void Chess::init()
{
initgraph(897, 895);
loadimage(0, "res/棋盘2.jpg");
mciSendString("play res/start.wav", 0, 0, 0); //需要修改字符集为多字节字符集
loadimage(&chessBlackImg, "res/black.png", chessSize, chessSize, true);
loadimage(&chessWhiteImg, "res/white.png", chessSize, chessSize, true);
for (int i = 0; i < chessMap.size(); i++) {
for (int j = 0; j < chessMap[i].size(); j++) {
chessMap[i][j] = 0;
}
}
playerFlag = true;
}
添加头文件和相关库,使得能够播放落子音效。
Chess.cpp
#include
#pragma comment(lib, "winmm.lib")
现在执行程序,除了弹出的棋盘,什么都不能干。因为,棋手的走棋函数,还没有实现哦!现在来实现棋手走棋功能。
为棋手类,添加数据成员,表示棋盘
Man.h
private:
Chess* chess;
实现棋手对象的初始化。
Man.cpp
void Man::init(Chess* chess)
{
this->chess = chess;
}
在ChessGame的构造函数中,实现棋手的初始化。
ChessGame.cpp
ChessGame::ChessGame(Man* man, AI* ai, Chess* chess)
{
this->man = man;
this->ai = ai;
this->chess = chess;
man->init(chess); //初始化棋手
}
Man.cpp
void Man::go(){
// 等待棋士有效落子
MOUSEMSG msg;
ChessPos pos;
while (1) {
msg = GetMouseMsg();
if (msg.uMsg == WM_LBUTTONDOWN && chess->clickBoard(msg.x, msg.y, &pos)) {
break;
}
}
// 落子
chess->chessDown(&pos, CHESS_BLACK);
}
执行程序后,还是没有任何效果,因为落子的有效性还没有判断。
先计算点击位置附近的4个点的位置,然后再计算点击位置到这四个点之间的距离,如果离某个点的距离小于“阈值”,就认为这个点是落子位置。这个“阈值”, 小于棋子大小的一半即可。我们这里取棋子大小的0.4倍。
Chess.cpp
bool Chess::clickBoard(int x, int y, ChessPos* pos)
{
int col = (x - margin_x) / chessSize;
int row = (y - margin_y) / chessSize;
int leftTopPosX = margin_x + chessSize * col;
int leftTopPosY = margin_y + chessSize * row;
int offset = chessSize * 0.4; // 20 鼠标点击的模糊距离上限
int len;
int selectPos = false;
do {
len = sqrt((x - leftTopPosX) * (x - leftTopPosX) + (y - leftTopPosY) * (y - leftTopPosY));
if (len < offset) {
pos->row = row;
pos->col = col;
if (chessMap[pos->row][pos->col] == 0) {
selectPos = true;
}
break;
}
// 距离右上角的距离
len = sqrt((x - leftTopPosX - chessSize) * (x - leftTopPosX - chessSize) + (y - leftTopPosY) * (y - leftTopPosY));
if (len < offset) {
pos->row = row;
pos->col = col + 1;
if (chessMap[pos->row][pos->col] == 0) {
selectPos = true;
}
break;
}
// 距离左下角的距离
len = sqrt((x - leftTopPosX) * (x - leftTopPosX) + (y - leftTopPosY - chessSize) * (y - leftTopPosY - chessSize));
if (len < offset) {
pos->row = row + 1;
pos->col = col;
if (chessMap[pos->row][pos->col] == 0) {
selectPos = true;
}
break;
}
// 距离右下角的距离
len = sqrt((x - leftTopPosX - chessSize) * (x - leftTopPosX - chessSize) + (y - leftTopPosY - chessSize) * (y - leftTopPosY - chessSize));
if (len < offset) {
pos->row = row + 1;
pos->col = col + 1;
if (chessMap[pos->row][pos->col] == 0) {
selectPos = true;
}
break;
}
} while (0);
return selectPos;
}
void Chess::chessDown(ChessPos *pos, chess_kind_t kind)
{
mciSendString("play res/down7.WAV", 0, 0, 0);
int x = margin_x + pos->col * chessSize - 0.5 * chessSize;
int y = margin_y + pos->row * chessSize - 0.5 * chessSize;
if (kind == CHESS_WHITE) {
putimagePNG(x, y, &chessWhiteImg);
}
else {
putimagePNG(x, y, &chessBlackImg);
}
}
棋子背后有黑色背景。这是因为easyx图形库默认不支持背景透明的png格式图片,把透明部分直接渲染为黑色了。解决方案,使用自定义的图形渲染接口,如下:
void putimagePNG(int x, int y, IMAGE* picture) //x为载入图片的X坐标,y为Y坐标
{
// 变量初始化
DWORD* dst = GetImageBuffer(); // GetImageBuffer()函数,用于获取绘图设备的显存指针,EASYX自带
DWORD* draw = GetImageBuffer();
DWORD* src = GetImageBuffer(picture); //获取picture的显存指针
int picture_width = picture->getwidth(); //获取picture的宽度,EASYX自带
int picture_height = picture->getheight(); //获取picture的高度,EASYX自带
int graphWidth = getwidth(); //获取绘图区的宽度,EASYX自带
int graphHeight = getheight(); //获取绘图区的高度,EASYX自带
int dstX = 0; //在显存里像素的角标
// 实现透明贴图 公式: Cp=αp*FP+(1-αp)*BP , 贝叶斯定理来进行点颜色的概率计算
for (int iy = 0; iy < picture_height; iy++)
{
for (int ix = 0; ix < picture_width; ix++)
{
int srcX = ix + iy * picture_width; //在显存里像素的角标
int sa = ((src[srcX] & 0xff000000) >> 24); //0xAArrggbb;AA是透明度
int sr = ((src[srcX] & 0xff0000) >> 16); //获取RGB里的R
int sg = ((src[srcX] & 0xff00) >> 8); //G
int sb = src[srcX] & 0xff; //B
if (ix >= 0 && ix <= graphWidth && iy >= 0 && iy <= graphHeight && dstX <= graphWidth * graphHeight)
{
dstX = (ix + x) + (iy + y) * graphWidth; //在显存里像素的角标
int dr = ((dst[dstX] & 0xff0000) >> 16);
int dg = ((dst[dstX] & 0xff00) >> 8);
int db = dst[dstX] & 0xff;
draw[dstX] = ((sr * sa / 255 + dr * (255 - sa) / 255) << 16) //公式: Cp=αp*FP+(1-αp)*BP ; αp=sa/255 , FP=sr , BP=dr
| ((sg * sa / 255 + dg * (255 - sa) / 255) << 8) //αp=sa/255 , FP=sg , BP=dg
| (sb * sa / 255 + db * (255 - sa) / 255); //αp=sa/255 , FP=sb , BP=db
}
}
}
}
再把chessDown中的putimage更换为putimagePNG, 测试效果如下:
12.2 修改棋盘的棋子数据
在界面上落子之后,还需要修改棋盘的棋子数据。为Chess类添加updateGameMap函数来修改棋子数据。这个方法,是给棋盘对象内部使用的,不需要开放给他人使用,所有把权限设置为private,设置为public也可以,但是从技术角度就不安全了。如果他人直接调用这个函数,就会导致棋盘的数据和界面上看到的数据不一样。
12.2 修改棋盘的棋子数据
在界面上落子之后,还需要修改棋盘的棋子数据。为Chess类添加updateGameMap函数来修改棋子数据。这个方法,是给棋盘对象内部使用的,不需要开放给他人使用,所有把权限设置为private,设置为public也可以,但是从技术角度就不安全了。如果他人直接调用这个函数,就会导致棋盘的数据和界面上看到的数据不一样。
Chess.h
private:
void updateGameMap(ChessPos *pos);
Chess.cpp
void Chess::updateGameMap(ChessPos* pos)
{
lastPos = *pos;
chessMap[pos->row][pos->col] = playerFlag ? 1 : -1;
playerFlag = !playerFlag; // 换手
}
在落子后,调用updateGameMap更新棋子数据。
void Chess::chessDown(ChessPos *pos, chess_kind_t kind)
{
// ......
updateGameMap(pos);
}
棋理格言:敌之好点,即我之好点。
就是说,每个点,都要考虑,如果敌方占领了这个点,会产生多大的价值,如果我方占领了这个点,又会产生多大的价值。如果我方占领这个点,价值只有1000,但是敌方要是占领了这个点,价值有2000,而在自己在其它位置没有价值更高的点,那么建议直接抢占这个敌方的好点。
AI先计算棋手如果在这个位置落子,会有多大的价值。然后再计算自己如果在这个位置落子,有大大价值。具体计算方法,就是计算如果黑棋或者白棋在这个位置落子,那么在这个位置的某个方向上, 一共有连续几个黑子或者连续几个白子。连续的数量越多,价值越大
用代码实现评分计算
AI.h
private:
void calculateScore();
AI.cpp
void AI::calculateScore()
{
// 统计玩家或者电脑连成的子
int personNum = 0; // 玩家连成子的个数
int botNum = 0; // AI连成子的个数
int emptyNum = 0; // 各方向空白位的个数
// 清空评分数组
for (int i = 0; i < scoreMap.size(); i++) {
for (int j = 0; j < scoreMap[i].size(); j++) {
scoreMap[i][j] = 0;
}
}
int size = chess->getGradeSize();
for (int row = 0; row < size; row++)
for (int col = 0; col < size; col++)
{
// 空白点就算
if (chess->getChessData(row, col) == 0) {
// 遍历周围八个方向
for (int y = -1; y <= 1; y++) {
for (int x = -1; x <= 1; x++)
{
// 重置
personNum = 0;
botNum = 0;
emptyNum = 0;
// 原坐标不算
if (!(y == 0 && x == 0))
{
// 每个方向延伸4个子
// 对黑棋评分(正反两个方向)
for (int i = 1; i <= 4; i++)
{
int curRow = row + i * y;
int curCol = col + i * x;
if (curRow >= 0 && curRow < size &&
curCol >= 0 && curCol < size &&
chess->getChessData(curRow, curCol) == 1) // 真人玩家的子
{
personNum++;
}
else if (curRow >= 0 && curRow < size &&
curCol >= 0 && curCol < size &&
chess->getChessData(curRow, curCol) == 0) // 空白位
{
emptyNum++;
break;
}
else // 出边界
break;
}
for (int i = 1; i <= 4; i++)
{
int curRow = row - i * y;
int curCol = col - i * x;
if (curRow >= 0 && curRow < size &&
curCol >= 0 && curCol < size &&
chess->getChessData(curRow, curCol) == 1) // 真人玩家的子
{
personNum++;
}
else if (curRow >= 0 && curRow < size &&
curCol >= 0 && curCol < size &&
chess->getChessData(curRow, curCol) == 0) // 空白位
{
emptyNum++;
break;
}
else // 出边界
break;
}
if (personNum == 1) // 杀二
scoreMap[row][col] += 10;
else if (personNum == 2) // 杀三
{
if (emptyNum == 1)
scoreMap[row][col] += 30;
else if (emptyNum == 2)
scoreMap[row][col] += 40;
}
else if (personNum == 3) // 杀四
{
// 量变空位不一样,优先级不一样
if (emptyNum == 1)
scoreMap[row][col] += 60;
else if (emptyNum == 2)
scoreMap[row][col] += 200;
}
else if (personNum == 4) // 杀五
scoreMap[row][col] += 20000;
// 进行一次清空
emptyNum = 0;
// 对白棋评分
for (int i = 1; i <= 4; i++)
{
int curRow = row + i * y;
int curCol = col + i * x;
if (curRow > 0 && curRow < size &&
curCol > 0 && curCol < size &&
chess->getChessData(curRow, curCol) == -1) // 玩家的子
{
botNum++;
}
else if (curRow > 0 && curRow < size &&
curCol > 0 && curCol < size &&
chess->getChessData(curRow, curCol) == 0) // 空白位
{
emptyNum++;
break;
}
else // 出边界
break;
}
for (int i = 1; i <= 4; i++)
{
int curRow = row - i * y;
int curCol = col - i * x;
if (curRow > 0 && curRow < size &&
curCol > 0 && curCol < size &&
chess->getChessData(curRow, curCol) == -1) // 玩家的子
{
botNum++;
}
else if (curRow > 0 && curRow < size &&
curCol > 0 && curCol < size &&
chess->getChessData(curRow, curCol) == 0) // 空白位
{
emptyNum++;
break;
}
else // 出边界
break;
}
if (botNum == 0) // 普通下子
scoreMap[row][col] += 5;
else if (botNum == 1) // 活二
scoreMap[row][col] += 10;
else if (botNum == 2)
{
if (emptyNum == 1) // 死三
scoreMap[row][col] += 25;
else if (emptyNum == 2)
scoreMap[row][col] += 50; // 活三
}
else if (botNum == 3)
{
if (emptyNum == 1) // 死四
scoreMap[row][col] += 55;
else if (emptyNum == 2)
scoreMap[row][col] += 10000; // 活四
}
else if (botNum >= 4)
scoreMap[row][col] += 30000; // 活五,应该具有最高优先级
}
}
}
}
}
}