个人主页:日刷百题
系列专栏
:〖C/C++小游戏〗〖Linux〗〖数据结构〗 〖C语言〗
🌎欢迎各位
→点赞
👍+收藏
⭐️+留言
📝
前言:
为了能够快速上手一门语言,我们往往在学习了基本语法后,采用写一个小项目的方式来加深理解语言的语法及运用,本文采用c++去实现对战AI五子棋,采用面向对象开发的一款游戏,里面应用了类和对象以及vector容器等知识。
一、项目效果展示
二、游戏思路
三、游戏框架
我们这里创建四个类(可以简单的理解为加强版的结构体),玩家类、AI类、棋盘类、棋盘控制类,将玩家类、AI类、棋盘类作为参数传给棋盘控制类,棋盘控制类获取这三个类的信息,从而可以控制游戏的运行,而玩家和AI要进行下棋操作时,需要棋盘信息,所以在玩家和AI类设置棋盘类。
根据上面分析,我们搭建好框架,先实现棋盘类功能,再实现AI和玩家类,最后实现棋盘控制类
四、棋盘类实现
4.1 用棋盘类构造函数初始化
4.1.1 checkerboard.h
#include<graphics.h>//eaysx头文件 #include<vector> using namespace std; enum GameResult { BLACK_WIN,WHITE_WIN,DRAW ,CONTINUE}; class checkerboard { public: //构造函数初始化成员变量 checkerboard(int BoardSize, int margin_x, int margin_y, float ChessSize) { this->BoardSize = BoardSize;//几线棋盘 this->margin_x=margin_x; this->margin_y = margin_y; this->ChessSize = ChessSize;//棋子大小 //加载黑子和白子图片到黑子和白子变量 loadimage(&BLACK_IMG, "res/black.png", ChessSize, ChessSize, true); loadimage(&WHITE_IMG, "res/white.png", ChessSize, ChessSize, true); //棋盘初始化 for (int i = 0; i < ChessSize; i++) { vector<int> row; for (int j = 0; j < ChessSize; j++) { row.push_back(0); } BoardMap.push_back(row); } gameresult= CONTINUE; } int BoardSize;//棋盘大小 float ChessSize;//棋子大小 vector<vector<int>> BoardMap;//表示棋盘落子情况 private: IMAGE BLACK_IMG;//黑棋图片变量 IMAGE WHITE_IMG;//白棋图片变量 int margin_x;//左侧边界45 int margin_y;//右侧边界45 };
4.2 棋盘类初始化函数
注:
这个初始化函数和棋盘类构造函数的初始化一样,为什么再初始化一次呢?因为我们后面进行游戏运行时,一局结束,再来一局还需要再调用一次棋盘初始化,而定义棋盘类只能调用一次构造函数,所以再创一个棋盘类初始化函数
4.2.1 checkerboard.h
pubilc: void Init();//棋盘初始化
4.2.2 checkerboard.cpp
void checkerboard::Init() { initgraph(L, W, 1);//窗口大小 //加载到窗口棋盘图片 loadimage(0, "res/棋盘2.jpg", L,W,true); //播放声音 /* mciSendString("play res/start.WAV", 0, 0, 0);*/ //加载黑子和白子图片到黑子和白子变量 //loadimage(&BLACK_IMG, "res/black.png", ChessSize, ChessSize, true); //loadimage(&WHITE_IMG, "res/white.png", ChessSize, ChessSize, true); //后面没有调用构造函数,调用初始化,所有在这里还需要对容器归0 for (int i = 0; i < ChessSize; i++) { for (int j = 0; j < ChessSize; j++) { BoardMap[i][j] = 0; } } gameresult = CONTINUE; }
4.3 检查鼠标点击是否有效
注:
虽然代码很长,但是思路很简单,先计算点击位置(x,y) 附近的4个落棋位置的实际坐标位置,然后再计算点击位置到这四个落棋位置之间的距离,如果落棋位置与点击位置距离小于棋子大小的0.4倍,就认为这个落棋位置是玩家想要落棋的位置,存储在pos中。若此时该位置没有其他棋子,则为有效点击,返回真。
4.3.1 checkerboard.h
棋盘类外:
//落子位置 struct ChessPos { int row; int col; }; enum chess_type{CHESS_WHITE=-1,CHESS_BLACK=1};
棋盘类内:
public: bool ClickBord(int x, int y, ChessPos& pos);//检查有效点击
4.3.2 checkerboard.cpp
bool checkerboard::ClickBord(int x, int y, ChessPos& pos) { //保证在棋盘内 if (x >= margin_x && x <= (L - margin_x) && y >= margin_y && y <= (W - margin_y)) { int col = (x - margin_x) / ChessSize; int row = (y - margin_y) / ChessSize; //该位置左上角的交点的坐标 int LTPos_x = margin_x + ChessSize * col; int LTPos_y = margin_y + ChessSize * row; int critical = ChessSize * 0.4;//临界值 //鼠标点击位置与右上角交点之间的距离 int distance1 = sqrt((x - LTPos_x) * (x - LTPos_x) + (y - LTPos_y) * (y - LTPos_y));//勾股定理 //该位置右上角的交点的坐标 int RTPos_x = LTPos_x + ChessSize; int RTPos_y = LTPos_y; //鼠标点击位置与右上角交点之间的距离 int distance2 = sqrt((x - RTPos_x) * (x - RTPos_x) + (y - RTPos_y) * (y - RTPos_y));//勾股定理 //该位置左下角的交点的坐标 int LDPos_x = LTPos_x; int LDPos_y = LTPos_y + ChessSize; //鼠标点击位置与左下角交点之间的距离 int distance3 = sqrt((x - LDPos_x) * (x - LDPos_x) + (y - LDPos_y) * (y - LDPos_y));//勾股定理 //该位置右下角的交点的坐标 int RDPos_x = LTPos_x + ChessSize; int RDPos_y = LTPos_y + ChessSize; //鼠标点击位置与右下角交点之间的距离 int distance4 = sqrt((x - RDPos_x) * (x - RDPos_x) + (y - RDPos_y) * (y - RDPos_y));//勾股定理 if (distance1 <= critical) { pos.col = col; pos.row = row; if (BoardMap[pos.row][pos.col] == 0)//该坐标没有棋子 { return true; } return false; } else if (distance2 <= critical) { pos.col = col + 1; pos.row = row; if (BoardMap[pos.row][pos.col] == 0)//该坐标没有棋子 { return true; } return false; } else if (distance3 <= critical) { pos.col = col; pos.row = row + 1; if (BoardMap[pos.row][pos.col ] == 0)//该坐标没有棋子 { return true; } return false; } else if (distance4 <= critical) { pos.col = col + 1; pos.row = row + 1; if (BoardMap[pos.row ][pos.col] == 0)//该坐标没有棋子 { return true; } return false; } else { return false; } } else { return false; } }
4.4 下棋
功能:实现记录最后一次落子的位置以及最后一次下棋是玩家方还是AI方,在棋盘二维数组记录落子数据。
4.4.1 checkerboard.h
棋盘类外:
//落子位置 struct ChessPos { int row; int col; }; enum chess_type{CHESS_WHITE=-1,CHESS_BLACK=1};
棋盘类内:
public: void PlayChess(ChessPos& pos,chess_type type);//下棋
4.4.2 checkerboard.cpp
void checkerboard::PlayChess(ChessPos& pos, chess_type type) { int x = margin_x + ChessSize * pos.col-ChessSize*0.5; int y = margin_y + ChessSize * pos.row- ChessSize *0.5; BoardMap[pos.row][pos.col] = type; lastpos.row = pos.row; lastpos.col = pos.col; lasttype = type; if (type == CHESS_BLACK) { putimagePNG(x, y, &BLACK_IMG); } else { putimagePNG(x, y, &WHITE_IMG); } }
4.5 判断棋盘是否下满
4.5.1 checkerboard.h
bool BoardFull();
4.5.2 checkerboard.cpp
bool checkerboard::BoardFull() { for (int row = 0; row < BoardSize; row++) { for (int col = 0; col < BoardSize; col++) { if (BoardMap[row][col] == 0)//棋盘没满 { return false; } } } return true; }
4.6 判断游戏状态
4.6.1 checkerboard.h
棋盘类外:
enum GameResult { BLACK_WIN,WHITE_WIN,DRAW ,CONTINUE};
棋盘类内:
private: GameResult gameresult; GameResult IsWin();
4.6.2 checkerboard.cpp
GameResult checkerboard::IsWin() { bool boardfull = BoardFull(); //每个方向记连续棋子个数 int Black_Num = 0; int White_Num = 0; if (lasttype == CHESS_BLACK)//黑子方 { //计算四个方向是否有5个 for (int i = 0; i <= 1; i++)//分横,竖,上斜,下斜4个方向 { for (int j = -1; j <= 1; j++) { //每个方向记连续棋子个数 Black_Num = 0; White_Num = 0; if ((i == 0 && j == 0) || (i == 0 && j == 1)) { continue; } for (int k = 1; k <= 4; k++)//最多判断五个子,四个方向中的单向 { int Cur_row = lastpos.row + i * k; int Cur_col = lastpos.col + j * k; if (Cur_row >= 0 && Cur_row < BoardSize && Cur_col >= 0 && Cur_col < BoardSize && BoardMap[Cur_row][Cur_col] == CHESS_BLACK) { Black_Num++; } else//超出棋盘或者是白子或者空白 { break; } } for (int k = 1; k <= 4; k++)//最多判断五个子,四个方向中单向的另一个方向 { int Cur_row = lastpos.row - i * k; int Cur_col = lastpos.col - j * k; if (Cur_row >= 0 && Cur_row < BoardSize && Cur_col >= 0 && Cur_col < BoardSize && BoardMap[Cur_row][Cur_col] == CHESS_BLACK) { Black_Num++; } else//超出棋盘或者是白子或者空白 { break; } } //判断游戏状态 if (Black_Num == 4)//5个黑子,游戏结束 { return BLACK_WIN; } else { if (boardfull) { return DRAW; } } } } } else//白子方 { //计算四个方向是否有5个 for (int i = 0; i <= 1; i++)//分横,竖,上斜,下斜4个方向 { for (int j = -1; j <= 1; j++) { //每个方向记连续棋子个数 Black_Num = 0; White_Num = 0; if ((i == 0 && j == 0) || (i == 0 && j == 1)) { continue; } for (int k = 1; k <= 4; k++)//最多判断五个子,四个方向中的单向 { int Cur_row = lastpos.row + i * k; int Cur_col = lastpos.col + j * k; if (Cur_row >= 0 && Cur_row < BoardSize && Cur_col >= 0 && Cur_col < BoardSize && BoardMap[Cur_row][Cur_col] == CHESS_WHITE) { White_Num++; } else//超出棋盘或者是黑子或者空白 { break; } } for (int k = 1; k <= 4; k++)//最多判断五个子,四个方向中单向的另一个方向 { int Cur_row = lastpos.row - i * k; int Cur_col = lastpos.col - j * k; if (Cur_row >= 0 && Cur_row < BoardSize && Cur_col >= 0 && Cur_col < BoardSize && BoardMap[Cur_row][Cur_col] == CHESS_WHITE) { White_Num++; } else//超出棋盘或者是黑子或者空白 { break; } } //判断游戏状态 if (White_Num == 4)//5个白子,游戏结束 { return WHITE_WIN; } else { if (boardfull) { return DRAW; } } } } } return CONTINUE; }
4.7 判断游戏是否结束
4.7.1 checkerboard.h
bool CheckOver();//检查游戏是否结束
4.7.2 checkerboard.cpp
bool checkerboard::CheckOver() { gameresult = IsWin(); if (gameresult == BLACK_WIN) { Sleep(2000); loadimage(0, "res/胜利.jpg",W,L,true); //播放声音 mciSendString("play res/胜利.mp3", 0, 0, 0); _getch(); return true; } else if (gameresult == WHITE_WIN) { Sleep(2000); loadimage(0, "res/失败.jpg",W , L, true); //播放声音 mciSendString("play res/失败.mp3", 0, 0, 0); _getch();//暂停,按任意键继续 return true; } else if (gameresult == DRAW) { Sleep(2000); loadimage(0, "res/平局.png",W , L , true); _getch(); return true; } else//继续游戏 { return false; } }
五、玩家类实现
5.1 用玩家类构造函数初始化
5.1.1 chess_player.h
#include"checkerboard.h" #include"AI.h" class chess_player { public: chess_player(checkerboard& checkerboard) { this->checkerboard = &checkerboard; } void go(); private: checkerboard* checkerboard; };
5.2 玩家下棋
5.2.1 chess_player.h
void go();
5.2.2 chess_player.cpp
void chess_player::go() { ChessPos pos; while (1) { MOUSEMSG mousemsg = GetMouseMsg();//鼠标信息结构体变量 bool click_board = checkerboard->ClickBord(mousemsg.x, mousemsg.y, pos); if (mousemsg.uMsg == WM_LBUTTONDOWN &&click_board )//用到checkboard对象的成员 { printf("%d,%d\n", pos.row, pos.col); break; } } checkerboard->PlayChess(pos, CHESS_BLACK);//黑子下棋的位置(渲染和记录) }
六、AI类实现
6.1 用AI类构造函数初始化
6.1.1 AI.h
#include"checkerboard.h" #include<vector> class AI { public: AI(checkerboard& checkerboard)//AI构造函数 { this->checkerboard = &checkerboard; for (int i = 0; i <checkerboard.ChessSize; i++) { vector<int> row; for (int j = 0; j <checkerboard.ChessSize; j++) { row.push_back(0); } ScoreMap.push_back(row); } } void go(); private: checkerboard* checkerboard; vector<vector<int>> ScoreMap; void CalculateScore(); ChessPos MaxScore(); };
6.2 AI计算权值最高的棋盘空白位置
计算棋盘空白位置的权值,首先对该位置的横、竖、上斜、下斜四个方位做判断,以该位置为起点,每个方位只需要在单方向上判断4个棋子位,反方向判断四个棋子位,统计连续的白子或者黑子个数,根据下面的表格给出相应权重值,选择出累计权值最高的位置为AI落子点。
6.2.1 AI.h
private: vector<vector<int>> ScoreMap; void CalculateScore();
6.2.2 AI.cpp
void AI::CalculateScore() { int Black_Num = 0; int White_Num = 0; int Empty_Num = 0; for(int row=0;row<checkerboard->BoardSize;row++) for (int col = 0; col< checkerboard->BoardSize; col++) { if (checkerboard->BoardMap[row][col] != 0)//有棋子,则跳过判断 { continue; } //先假设下黑子,计分情况 for (int i = 0; i <= 1; i++)//分横,竖,上斜,下斜4个方向 { for (int j = -1; j <= 1; j++) { //每个方向记连续棋子个数 Black_Num = 0; White_Num = 0; Empty_Num = 0; if ((i == 0 && j == 0) || (i ==0 && j == 1)) { continue; } for (int k = 1; k <= 4; k++)//最多判断五个子,四个方向中的单向 { int Cur_row = row + i * k; int Cur_col = col + j * k; if (Cur_row>=0&&Cur_row<checkerboard->BoardSize&& Cur_col>=0&&Cur_col<checkerboard->BoardSize&& checkerboard->BoardMap[Cur_row][Cur_col] == CHESS_BLACK) { Black_Num++; } else if(Cur_row >= 0 && Cur_row < checkerboard->BoardSize && Cur_col >= 0 && Cur_col < checkerboard->BoardSize && checkerboard->BoardMap[Cur_row][Cur_col] ==0) { Empty_Num++; break; } else//超出棋盘或者是白子 { break; } } for (int k = 1; k <= 4; k++)//最多判断五个子,四个方向中单向的另一个方向 { int Cur_row = row - i * k; int Cur_col = col - j * k; if (Cur_row >= 0 && Cur_row < checkerboard->BoardSize && Cur_col >= 0 && Cur_col < checkerboard->BoardSize && checkerboard->BoardMap[Cur_row][Cur_col] == CHESS_BLACK) { Black_Num++; } else if (Cur_row >= 0 && Cur_row < checkerboard->BoardSize && Cur_col >= 0 && Cur_col < checkerboard->BoardSize && checkerboard->BoardMap[Cur_row][Cur_col] == 0) { Empty_Num++; break; } else//超出棋盘或者是白子 { break; } } //该位置的得分情况 if (Black_Num == 1)//2个黑子 { ScoreMap[row][col] += 10; } else if (Black_Num == 2)//连续三个黑子 { if (Empty_Num == 1) { ScoreMap[row][col] += 30; } else if(Empty_Num==2) { ScoreMap[row][col] += 40; } } else if (Black_Num == 3)//连续4个黑子 { if (Empty_Num == 1) { ScoreMap[row][col] += 60; } else if (Empty_Num == 2) { ScoreMap[row][col] += 200; } } else if (Black_Num == 4)//连续5个黑子 { ScoreMap[row][col] += 20000; } } } //假设该位置下白子,计分情况 for (int i = 0; i <= 1; i++)//分横,竖,上斜,下斜4个方向 { for (int j = -1; j <= 1; j++) { //每个方向记连续棋子个数 Black_Num = 0; White_Num = 0; Empty_Num = 0; if ((i == 0 && j == 0) || (i == 0 && j == 1)) { continue; } for (int k = 1; k <= 4; k++)//最多判断五个子,四个方向中的单向 { int Cur_row = row + i * k; int Cur_col = col + j * k; if (Cur_row >= 0 && Cur_row < checkerboard->BoardSize && Cur_col >= 0 && Cur_col < checkerboard->BoardSize && checkerboard->BoardMap[Cur_row][Cur_col] == CHESS_WHITE) { White_Num++; } else if (Cur_row >= 0 && Cur_row < checkerboard->BoardSize && Cur_col >= 0 && Cur_col < checkerboard->BoardSize && checkerboard->BoardMap[Cur_row][Cur_col] == 0) { Empty_Num++; break; } else//超出棋盘或者是黑子 { break; } } for (int k = 1; k <= 4; k++)//最多判断五个子,四个方向中单向的另一个方向 { int Cur_row = row - i * k; int Cur_col = col - j * k; if (Cur_row >= 0 && Cur_row < checkerboard->BoardSize && Cur_col >= 0 && Cur_col < checkerboard->BoardSize && checkerboard->BoardMap[Cur_row][Cur_col] == CHESS_WHITE) { White_Num++; } else if (Cur_row >= 0 && Cur_row < checkerboard->BoardSize && Cur_col >= 0 && Cur_col < checkerboard->BoardSize && checkerboard->BoardMap[Cur_row][Cur_col] == 0) { Empty_Num++; break; } else//超出棋盘或者是黑子 { break; } } //该位置的得分情况 if (White_Num == 0)//1个白子 { ScoreMap[row][col] += 5; } else if (White_Num == 1)//连续2个白子 { ScoreMap[row][col] += 10; } else if (White_Num == 2)//连续3个白子 { if (Empty_Num == 1) { ScoreMap[row][col] += 25; } else if (Empty_Num == 2) { ScoreMap[row][col] += 50; } } else if (White_Num == 3)//连续4个白子 { if (Empty_Num == 1) { ScoreMap[row][col] += 55; } else if (Empty_Num == 2) { ScoreMap[row][col] += 300; } } else if (White_Num == 4)//连续5个白子 { ScoreMap[row][col] += 30000; } } } } }
6.3 选择权值最高的棋盘位置
6.3.1 AI.h
private: ChessPos MaxScore();
6.3.2 AI.cpp
ChessPos AI::MaxScore() { int max = 0; vector<ChessPos> maxscore_pos; ChessPos pos; CalculateScore(); for (int row = 0; row < checkerboard->BoardSize; row++) { for (int col = 0; col < checkerboard->BoardSize; col++) { if (ScoreMap[row][col] > max) { max = ScoreMap[row][col]; maxscore_pos.clear(); pos.row = row; pos.col = col; maxscore_pos.push_back(pos); } else if (ScoreMap[row][col] == max) { pos.row = row; pos.col = col; maxscore_pos.push_back(pos); } } } //计分棋盘归0 for (int i = 0; i < checkerboard->ChessSize; i++) { for (int j = 0; j < checkerboard->ChessSize; j++) { ScoreMap[i][j] = 0; } } int index = rand() % maxscore_pos.size(); return maxscore_pos[index]; }
6.4 AI下棋
6.4.1 AI.h
void go();
6.4.2 AI.cpp
void AI::go() { ChessPos pos = MaxScore(); checkerboard->PlayChess(pos, CHESS_WHITE);//白子下棋的位置(渲染和记录) }
七、棋盘控制类实现
7.1 用棋盘控制类构造函数初始化
7.1.1 ChessGame.h
#include"chess_player.h" #include"AI.h" #include"checkerboard.h" class ChessGame { public: ChessGame(chess_player& chess_player, AI& ai, checkerboard& checkerboard)//构造函数初始化 { this->chess_player = &chess_player; this->ai = &ai; this->checkerboard = &checkerboard; } void play();//开始游戏 //创建数据成员变量 private: chess_player* chess_player; AI* ai; checkerboard* checkerboard; };
7.2 控制游戏进行
7.2.1 ChessGame.h
void play();//开始游戏
7.2.2 ChessGame.cpp
//开始游戏 void ChessGame::play() { again: checkerboard->Init(); while (1) { //棋手先走 chess_player->go(); if (checkerboard->CheckOver()) { goto again; } //AI走 ai->go(); if (checkerboard->CheckOver()) { goto again; } } }
八、主函数
#include<iostream> #include"ChessGame.h" int main() { srand((unsigned int)time(NULL)); checkerboard checkerboard( 13, 45*0.7, 45*0.7,67.25*0.7);//自动调用构造函数 chess_player chess_player(checkerboard);//自动调用构造函数 AI ai(checkerboard);//自动调用构造函数; ChessGame chessgame(chess_player,ai, checkerboard);//引用传值 chessgame.play(); return 0; }
希望大家阅读完可以有所收获,同时也感谢各位铁汁们的支持。文章有任何问题可以在评论区留言,百题一定会认真阅读!