五子棋简单编程(详细教程附源码)

文章较长,建议收藏后学习!!

1.项目分析

五子棋简单编程(详细教程附源码)(1)

2.项目目标
  • AI的基础应用
  • 算法的设计和实现
  • C语言的图形界面程序开发
3.项目准备
  • VS/VC (任意版本)
  • easyx图形库(直接双击即可安装)
  • 领取素材(传送门)(图片、音效)

五子棋简单编程(详细教程附源码)(2)

4.创建项目
  1. 创建空项目。
  2. 把素材拷贝到项目目录下
5.画棋盘

画棋盘,播放开局提示、播放背景音乐

#include <graphics.h> //easyx图像库的头文件 #include <windows.h> #include <mmsystem.h> //播放音乐的头文件 #pragma comment(lib, "winmm.lib") void init() { initgraph(897, 895); loadimage(0, "res/棋盘.jpg"); mciSendString("play res/start.wav", 0, 0, 0); mciSendString("play res/bg.mp3 repeat", 0, 0, 0); } int main(void) { init(); system("pause"); return 0; }

6.画棋子

鼠标点击后,在点击位置画棋子

IMAGE chessBlackImg; IMAGE chessWhiteImg; const float BLOCK_SIZE = 67.4; // 格子的大小 void init() { ...... loadimage(&chessBlackImg, "res/black.png", BLOCK_SIZE, BLOCK_SIZE, true); loadimage(&chessWhiteImg, "res/white.png", BLOCK_SIZE, BLOCK_SIZE, true); } int main(void) { init(); while (1) { MOUSEMSG msg = GetMouseMsg(); if (msg.uMsg == WM_LBUTTONDOWN) { putimage(msg.x, msg.y, &chessBlackImg); } } system("pause"); return 0; }

效果:

五子棋简单编程(详细教程附源码)(3)

黑色区域,透明背景的PNG图片显示不了。

解决方案:

  1. 导入工具库tools.h, tools.cpp
  2. 修改代码

#include "tools.h" int main(void) { init(); while (1) { MOUSEMSG msg = GetMouseMsg(); if (msg.uMsg == WM_LBUTTONDOWN) { //putimage(msg.x, msg.y, &chessBlackImg); drawPNG(&chessBlackImg, msg.x, msg.y); } } system("pause"); return 0; }

效果:

五子棋简单编程(详细教程附源码)(4)

修改:

drawPNG(&chessBlackImg, msg.x, msg.y); drawPNG(&chessBlackImg, msg.x - 0.5 * BLOCK_SIZE, msg.y - 0.5 * BLOCK_SIZE);

五子棋简单编程(详细教程附源码)(5)

看上去,很完美,但是有一个严重的BUG!!

五子棋简单编程(详细教程附源码)(6)

当不在交叉点准确点击时,就会出现以上情况。

解决方案:

需要判断这个点击是否是合法未知的点击,并允许一定的偏差

判断有效的点击定义数据模型

因为点击时,要判断是否在已经有棋子的位置上点击(不能在已经落子的位置点击)

所以需要定义一个数据模型,来表示当前的所有棋子数据。

【模块化开发思想】

创建ChessData.h, 并把main.cpp中的与围棋相关的全局数据,剪贴到ChessData.h中

ChessData.h

#pragma once const float BLOCK_SIZE = 67.4; // 格子的大小 const int BOARD_GRAD_SIZE = 13; //13x13棋盘大小 const int POS_OFFSET = BLOCK_SIZE * 0.4; // 20 鼠标点击的模糊距离上限 struct ChessData { // 存储当前游戏棋盘和棋子的情况,空白为0,黑子1,白子-1 int chessMap[BOARD_GRAD_SIZE][BOARD_GRAD_SIZE]; // 存储各个点位的评分情况,作为AI下棋依据 int scoreMap[BOARD_GRAD_SIZE][BOARD_GRAD_SIZE]; // 标示下棋方, true:黑棋方 false: AI 白棋方(AI方) bool playerFlag; };

在main.cpp中添加围棋数据变量game

#include "ChessData.h" ChessData game;

初始化数据模型

ChessData.h

void initChessData(ChessData*); // 开始游戏

ChessData.cpp

void initChessData(ChessData *data) { if (!data)return; memset(data->chessMap, 0, sizeof(data->chessMap)); memset(data->scoreMap, 0, sizeof(data->scoreMap)); data->playerFlag = true; }

main.cpp

void init() { ...... // 初始化游戏模型 initChessData(&game); }

7.判断有效点击

判断原理

先计算出绿点,然后分别计算出3个黑点位置,计算当前位置离4个点的位置。

如果小于阈值(POS_OFFSET),就认为选择了哪个点。

五子棋简单编程(详细教程附源码)(7)

在main.cpp中添加变量,存储有效点击的位置

int clickPosRow, clickPosCol; // 存储点击的位置

判断是否是有效点击,如果是有效点击,返回true并把结果保存到全局变量clickPosRow、 clickPosCol;

ChessData.h

const int POS_OFFSET = BLOCK_SIZE * 0.4; // 20 鼠标点击的模糊距离上限 bool clickBoard(MOUSEMSG msg) { int x = msg.x; int y = msg.y; int col = (x - margin_x) / BLOCK_SIZE; int row = (y - margin_y) / BLOCK_SIZE; int leftTopPosX = margin_x BLOCK_SIZE * col; int leftTopPosY = margin_y BLOCK_SIZE * row; int len; int selectPos = false; do { len = sqrt((x - leftTopPosX) * (x - leftTopPosX) (y - leftTopPosY) * (y - leftTopPosY)); if (len < POS_OFFSET) { clickPosRow = row; clickPosCol = col; if (game.chessMap[clickPosRow][clickPosCol] == 0) { selectPos = true; } break; } // 距离右上角的距离 len = sqrt((x - leftTopPosX - BLOCK_SIZE) * (x - leftTopPosX - BLOCK_SIZE) (y - leftTopPosY) * (y - leftTopPosY)); if (len < POS_OFFSET) { clickPosRow = row; clickPosCol = col 1; if (game.chessMap[clickPosRow][clickPosCol] == 0) { selectPos = true; } break; } // 距离左下角的距离 len = sqrt((x - leftTopPosX) * (x - leftTopPosX) (y - leftTopPosY - BLOCK_SIZE) * (y - leftTopPosY - BLOCK_SIZE)); if (len < POS_OFFSET) { clickPosRow = row 1; clickPosCol = col; if (game.chessMap[clickPosRow][clickPosCol] == 0) { selectPos = true; } break; } // 距离右下角的距离 len = sqrt((x - leftTopPosX - BLOCK_SIZE) * (x - leftTopPosX - BLOCK_SIZE) (y - leftTopPosY - BLOCK_SIZE) * (y - leftTopPosY - BLOCK_SIZE)); if (len < POS_OFFSET) { clickPosRow = row 1; clickPosCol = col 1; if (game.chessMap[clickPosRow][clickPosCol] == 0) { selectPos = true; } break; } } while (0); return selectPos; }

实现有效点击

int main(void) { init(); while (1) { MOUSEMSG msg = GetMouseMsg(); if (msg.uMsg == WM_LBUTTONDOWN && clickBoard(msg)) { //putimage(msg.x, msg.y, &chessBlackImg); //drawPNG(&chessBlackImg, msg.x - 0.5 * BLOCK_SIZE, msg.y - 0.5 * BLOCK_SIZE); int x = margin_x clickPosCol * BLOCK_SIZE - 0.5 * BLOCK_SIZE; int y = margin_y clickPosRow * BLOCK_SIZE - 0.5 * BLOCK_SIZE; drawPNG(&chessBlackImg, x, y); } } system("pause"); return 0; }

测试效果:

五子棋简单编程(详细教程附源码)(8)

8.优化项目架构1.封装画棋子的代码
  1. 在ChessData.h中添加棋子类型

typedef enum { CHESS_WHITE = -1, CHESS_BLACK = 1 } chess_kind_t;

  1. 在main.cpp封装“落子”代码

void chessDown(int row, int col, chess_kind_t kind) { mciSendString("play res/down7.WAV", 0, 0, 0); int x = margin_x col * BLOCK_SIZE - 0.5 * BLOCK_SIZE; int y = margin_y row * BLOCK_SIZE - 0.5 * BLOCK_SIZE; if (kind == CHESS_WHITE) { drawPNG(&chessWhiteImg, x, y); } else { drawPNG(&chessBlackImg, x, y); } }

  1. 落子

int main(void) { init(); while (1) { MOUSEMSG msg = GetMouseMsg(); if (msg.uMsg == WM_LBUTTONDOWN && clickBoard(msg)) { chessDown(clickPosRow, clickPosCol, CHESS_BLACK); } } system("pause"); return 0; }

2.优化项目架构

bool checkOver() { // 检查游戏是否结束 return false; } void AI_GO() { //AI走棋 } void manGo() { // 玩家走棋 chessDown(clickPosRow, clickPosCol, CHESS_BLACK); } int main(void) { init(); while (1) { MOUSEMSG msg = GetMouseMsg(); if (msg.uMsg == WM_LBUTTONDOWN) { manGo(); if (checkOver()) { init(); continue; } AI_GO(); if (checkOver()) { init(); continue; } } } closegraph(); return 0; }

9.更新游戏数据

人(黑方)落子后,还没有修改底层的游戏数据。

在ChessDatat.h添加接口:

void updateGameMap(ChessData* data, int row, int col);

在ChessData.cpp中添加实现。

void updateGameMap(ChessData* data, int row, int col) { if (!data)return; if (data->playerFlag) data->chessMap[row][col] = 1; else data->chessMap[row][col] = -1; data->playerFlag = !data->playerFlag; // 换手 }

应用更新:

void manGo() { // 玩家走棋 chessDown(clickPosRow, clickPosCol, CHESS_BLACK); updateGameMap(&game, clickPosRow, clickPosCol); }

10.实现AI走棋五子棋入门

连2

五子棋简单编程(详细教程附源码)(9)

五子棋简单编程(详细教程附源码)(10)

活3

五子棋简单编程(详细教程附源码)(11)

五子棋简单编程(详细教程附源码)(12)

死3

五子棋简单编程(详细教程附源码)(13)

五子棋简单编程(详细教程附源码)(14)

活4

五子棋简单编程(详细教程附源码)(15)

五子棋简单编程(详细教程附源码)(16)

死4

五子棋简单编程(详细教程附源码)(17)

五子棋简单编程(详细教程附源码)(18)

连5(赢)

五子棋简单编程(详细教程附源码)(19)

五子棋简单编程(详细教程附源码)(20)

AI走棋原理

计算每个合法的落子点的“权值”,然后再权值最大的点落子

以后,可以在这个基础之上,实现多个层次的计算.

对于每个空白点,分别计算周围的八个方向

五子棋简单编程(详细教程附源码)(21)

因为在计算某个方向时,正向和反向需同时考虑,所以实际上只需计算4个方向即可:

五子棋简单编程(详细教程附源码)(22)

如果黑棋走这个点

产生效果

评分

连2

10

死3

30

活3

40

死4

60

活4

200

连5

20000

如果白棋AI走这个点

产生效果

评分

连1(普通)

5

连2

10

死3

25

活3

50

死4

55

活4

300

连5

30000

计算各点的“权值”

权值的计算,放在ChessData模块中。

ChessData.h

void calculateScore(ChessData* data);

ChessData.cpp

#include <string.h> //memset函数 // 最关键的计算评分函数 void calculateScore(ChessData* data) { if (!data) return; // 统计玩家或者电脑连成的子 int personNum = 0; // 玩家连成子的个数 int botNum = 0; // AI连成子的个数 int emptyNum = 0; // 各方向空白位的个数 // 清空评分数组 memset(data->scoreMap, 0, sizeof(data->scoreMap)); for (int row = 0; row < BOARD_GRAD_SIZE; row ) for (int col = 0; col < BOARD_GRAD_SIZE; col ) { // 空白点就算 if (row >= 0 && col >= 0 && data->chessMap[row][col] == 0) { // 遍历周围4个方向,分别计算正反两个方向 int directs[4][2] = { {1,0}, {1,1}, {0,1}, {-1,1 } }; for (int k = 0; k < 4; k ) { int x = directs[k][0]; int y = directs[k][1]; // 重置 personNum = 0; botNum = 0; emptyNum = 0; // 对黑棋评分(正向) for (int i = 1; i <= 4; i ) { if (row i * y >= 0 && row i * y < BOARD_GRAD_SIZE && col i * x >= 0 && col i * x < BOARD_GRAD_SIZE && data->chessMap[row i * y][col i * x] == 1) { // 真人玩家的子 personNum ; } else if (row i * y >= 0 && row i * y < BOARD_GRAD_SIZE && col i * x >= 0 && col i * x < BOARD_GRAD_SIZE && data->chessMap[row i * y][col i * x] == 0) { // 空白位 emptyNum ; break; // 遇到空白位置,停止该方向的搜索 } else // 出边界,或者遇到白棋,就停止该方向的搜索 break; } // 对黑棋评分(反向) for (int i = 1; i <= 4; i ) { if (row - i * y >= 0 && row - i * y < BOARD_GRAD_SIZE && col - i * x >= 0 && col - i * x < BOARD_GRAD_SIZE && data->chessMap[row - i * y][col - i * x] == 1) { // 玩家的子 personNum ; } else if (row - i * y >= 0 && row - i * y < BOARD_GRAD_SIZE && col - i * x >= 0 && col - i * x < BOARD_GRAD_SIZE && data->chessMap[row - i * y][col - i * x] == 0) { // 空白位 emptyNum ; break; } else // 出边界,或者有AI自己的棋子 break; } if (personNum == 1) // 杀二 data->scoreMap[row][col] = 10; else if (personNum == 2) { // 杀三 if (emptyNum == 1) // 死三 data->scoreMap[row][col] = 30; else if (emptyNum == 2) // 活三 data->scoreMap[row][col] = 40; } else if (personNum == 3) { // 杀四 if (emptyNum == 1) //死四 data->scoreMap[row][col] = 60; else if (emptyNum == 2) //活四 data->scoreMap[row][col] = 200; } else if (personNum == 4) // 杀五 data->scoreMap[row][col] = 20000; // 进行一次清空 emptyNum = 0; // 对白棋评分(正向) for (int i = 1; i <= 4; i ) { if (row i * y > 0 && row i * y < BOARD_GRAD_SIZE && col i * x > 0 && col i * x < BOARD_GRAD_SIZE && data->chessMap[row i * y][col i * x] == -1) { // 玩家的子 botNum ; } else if (row i * y > 0 && row i * y < BOARD_GRAD_SIZE && col i * x > 0 && col i * x < BOARD_GRAD_SIZE && data->chessMap[row i * y][col i * x] == 0) { // 空白位 emptyNum ; break; } else break; } // 对白棋评分(反向) for (int i = 1; i <= 4; i ) { if (row - i * y > 0 && row - i * y < BOARD_GRAD_SIZE && col - i * x > 0 && col - i * x < BOARD_GRAD_SIZE && data->chessMap[row - i * y][col - i * x] == -1) { // AI的子 botNum ; } else if (row - i * y > 0 && row - i * y < BOARD_GRAD_SIZE && col - i * x > 0 && col - i * x < BOARD_GRAD_SIZE && data->chessMap[row - i * y][col - i * x] == 0) { // 空白位 emptyNum ; break; } else // 出边界 break; } if (botNum == 0) // 普通下子 data->scoreMap[row][col] = 5; else if (botNum == 1) // 活二 data->scoreMap[row][col] = 10; else if (botNum == 2) { if (emptyNum == 1) // 死三 data->scoreMap[row][col] = 25; else if (emptyNum == 2) data->scoreMap[row][col] = 50; // 活三 } else if (botNum == 3) { if (emptyNum == 1) // 死四 data->scoreMap[row][col] = 55; else if (emptyNum == 2) data->scoreMap[row][col] = 300; // 活四 } else if (botNum >= 4) data->scoreMap[row][col] = 30000; // 活五,应该具有最高优先级 } } } }

AI思考落子点

在各落子点,找到分值最大的点。如果有多个分值相同的点,直接在其中取一个随机点。

在ChesssData模块实现。

ChessData.h

typedef struct point { int row; int col; } point_t; point_t actionByAI(ChessData* data); // 机器执行下棋

ChessData.cpp

#include <time.h> #include <stdlib.h> point_t actionByAI(ChessData *data) { // 计算评分 calculateScore(data); // 从评分中找出最大分数的位置 int maxScore = 0; //std::vector<std::pair<int, int>> maxPoints; point_t maxPoints[BOARD_GRAD_SIZE * BOARD_GRAD_SIZE] = { 0, }; int k=0; for (int row = 0; row < BOARD_GRAD_SIZE; row ) for (int col = 0; col < BOARD_GRAD_SIZE; col ) { // 前提是这个坐标是空的 if (data->chessMap[row][col] == 0) { if (data->scoreMap[row][col] > maxScore) // 找最大的数和坐标 { //maxPoints.clear(); memset(maxPoints, 0, sizeof(maxPoints)); k = 0; maxScore = data->scoreMap[row][col]; //maxPoints.push_back(std::make_pair(row, col)); maxPoints[k].row = row; maxPoints[k].col = col; k ; } else if (data->scoreMap[row][col] == maxScore) { // 如果有多个最大的数,都存起来 //maxPoints.push_back(std::make_pair(row, col)); maxPoints[k].row = row; maxPoints[k].col = col; k ; } } } // 随机落子,如果有多个点的话 srand((unsigned)time(0)); int index = rand() % k; return maxPoints[index]; }

实现AI落子

void AI_GO() { //AI走棋 point_t point = actionByAI(&game); clickPosRow = point.row; clickPosCol = point.col; Sleep(1000); //AI计算的太快,此处以假装思考 chessDown(clickPosRow, clickPosCol, CHESS_WHITE); updateGameMap(&game, clickPosRow, clickPosCol); }

11.判断棋局是否结束在ChessData模块定义判断输赢的接口

原理分析:

在4个方向上搜索。

五子棋简单编程(详细教程附源码)(23)

以右下方向为例:(黑色棋子表示刚下的棋子)

从当前棋子开始,向右下方数5个

五子棋简单编程(详细教程附源码)(24)

从当前棋子的左上角开始,向右下方数5个

五子棋简单编程(详细教程附源码)(25)

从当前棋子的左上第2个开始,向右下方数5个

五子棋简单编程(详细教程附源码)(26)

从当前棋子的左上第3个开始,向右下方数5个

五子棋简单编程(详细教程附源码)(27)

从当前棋子的左上第4个开始,向右下方数5个

五子棋简单编程(详细教程附源码)(28)

ChessData.h

bool checkWin(ChessData* game, int row, int col); //row,col表示当前落子

ChessData.cpp

bool checkWin(ChessData* game, int row, int col) { // 横竖斜四种大情况,每种情况都根据当前落子往后遍历5个棋子,有一种符合就算赢 // 水平方向 for (int i = 0; i < 5; i ) { // 往左5个,往右匹配4个子,20种情况 if (col - i >= 0 && col - i 4 < BOARD_GRAD_SIZE && game->chessMap[row][col - i] == game->chessMap[row][col - i 1] && game->chessMap[row][col - i] == game->chessMap[row][col - i 2] && game->chessMap[row][col - i] == game->chessMap[row][col - i 3] && game->chessMap[row][col - i] == game->chessMap[row][col - i 4]) return true; } // 竖直方向(上下延伸4个) for (int i = 0; i < 5; i ) { if (row - i >= 0 && row - i 4 < BOARD_GRAD_SIZE && game->chessMap[row - i][col] == game->chessMap[row - i 1][col] && game->chessMap[row - i][col] == game->chessMap[row - i 2][col] && game->chessMap[row - i][col] == game->chessMap[row - i 3][col] && game->chessMap[row - i][col] == game->chessMap[row - i 4][col]) return true; } // “/"方向 for (int i = 0; i < 5; i ) { if (row i < BOARD_GRAD_SIZE && row i - 4 >= 0 && col - i >= 0 && col - i 4 < BOARD_GRAD_SIZE && // 第[row i]行,第[col-i]的棋子,与右上方连续4个棋子都相同 game->chessMap[row i][col - i] == game->chessMap[row i - 1][col - i 1] && game->chessMap[row i][col - i] == game->chessMap[row i - 2][col - i 2] && game->chessMap[row i][col - i] == game->chessMap[row i - 3][col - i 3] && game->chessMap[row i][col - i] == game->chessMap[row i - 4][col - i 4]) return true; } // “\“ 方向 for (int i = 0; i < 5; i ) { // 第[row i]行,第[col-i]的棋子,与右下方连续4个棋子都相同 if (row - i >= 0 && row - i 4 < BOARD_GRAD_SIZE && col - i >= 0 && col - i 4 < BOARD_GRAD_SIZE && game->chessMap[row - i][col - i] == game->chessMap[row - i 1][col - i 1] && game->chessMap[row - i][col - i] == game->chessMap[row - i 2][col - i 2] && game->chessMap[row - i][col - i] == game->chessMap[row - i 3][col - i 3] && game->chessMap[row - i][col - i] == game->chessMap[row - i 4][col - i 4]) return true; } return false; }

调用AI接口

main.cpp

#include <stdio.h> bool checkOver() { if (checkWin(&game, clickPosRow, clickPosCol)) { Sleep(1500); if (game.playerFlag == false) { //黑棋赢(玩家赢),此时标记已经反转,轮到白棋落子 mciSendString("play res/不错.mp3", 0, 0, 0); loadimage(0, "res/胜利.jpg"); } else { mciSendString("play res/失败.mp3", 0, 0, 0); loadimage(0, "res/失败.jpg"); } getch(); return true; } return false; }

显示分数

在胜利窗口,或者失败窗口中,显示分数。

main.cpp

#define INIT_SCORE 1000 int score; // 当前分数 void initScore() { // 显示分数的字体设置 settextcolor(WHITE); settextstyle(50, 0, "微软雅黑"); FILE *fp = fopen("score.data", "rb"); if (fp == NULL) { score = INIT_SCORE; } else { fread(&score, sizeof(score), 1, fp); } if (fp)fclose(fp); } void init() { ...... initScore(); }

更新分数

ChessData.cpp

bool checkOver() { if (checkWin(&game, clickPosRow, clickPosCol)) { Sleep(1500); if (game.playerFlag == false) { //黑棋赢(玩家赢),此时标记已经反转,轮到白棋落子 mciSendString("play res/不错.mp3", 0, 0, 0); loadimage(0, "res/胜利.jpg"); score = 100; } else { mciSendString("play res/失败.mp3", 0, 0, 0); loadimage(0, "res/失败.jpg"); score -= 100; } // 显示分数 char scoreText[64]; sprintf(scoreText, "当前分数 :%d", score); outtextxy(310, 800, scoreText); // 记录分数 FILE* fp = fopen("score.data", "wb"); fwrite(&score, sizeof(score), 1, fp); fclose(fp); getch(); return true; } return false; }

项目迭代联网对战功能

通过项目的实战积累,在实战中成长

服务器联网通信开发。

五子棋简单编程(详细教程附源码)(29)

游戏大厅配对功能

服务器端业务开发。

五子棋简单编程(详细教程附源码)(30)

AI迭代

使用搜索树,提高算度。

,

免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com

    分享
    投诉
    首页