五子棋简单编程(详细教程附源码)
文章较长,建议收藏后学习!!
1.项目分析2.项目目标
- AI的基础应用
- 算法的设计和实现
- C语言的图形界面程序开发
- VS/VC (任意版本)
- easyx图形库(直接双击即可安装)
- 领取素材(传送门)(图片、音效)
4.创建项目
- 创建空项目。
- 把素材拷贝到项目目录下
画棋盘,播放开局提示、播放背景音乐
#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;
}
鼠标点击后,在点击位置画棋子
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;
}
效果:
黑色区域,透明背景的PNG图片显示不了。
解决方案:
- 导入工具库tools.h, tools.cpp
- 修改代码
#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;
}
效果:
修改:
drawPNG(&chessBlackImg, msg.x, msg.y);
drawPNG(&chessBlackImg, msg.x - 0.5 * BLOCK_SIZE, msg.y - 0.5 * BLOCK_SIZE);
看上去,很完美,但是有一个严重的BUG!!
当不在交叉点准确点击时,就会出现以上情况。
解决方案:
需要判断这个点击是否是合法未知的点击,并允许一定的偏差
判断有效的点击定义数据模型因为点击时,要判断是否在已经有棋子的位置上点击(不能在已经落子的位置点击)
所以需要定义一个数据模型,来表示当前的所有棋子数据。
【模块化开发思想】
创建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);
}
判断原理
先计算出绿点,然后分别计算出3个黑点位置,计算当前位置离4个点的位置。
如果小于阈值(POS_OFFSET),就认为选择了哪个点。
在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.优化项目架构1.封装画棋子的代码
- 在ChessData.h中添加棋子类型
typedef enum {
CHESS_WHITE = -1,
CHESS_BLACK = 1
} chess_kind_t;
- 在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);
}
}
- 落子
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;
}
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;
}
人(黑方)落子后,还没有修改底层的游戏数据。
在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);
}
连2
活3
死3
活4
死4
连5(赢)
AI走棋原理
计算每个合法的落子点的“权值”,然后再权值最大的点落子
以后,可以在这个基础之上,实现多个层次的计算.
对于每个空白点,分别计算周围的八个方向
因为在计算某个方向时,正向和反向需同时考虑,所以实际上只需计算4个方向即可:
如果黑棋走这个点
产生效果 |
评分 |
连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; // 活五,应该具有最高优先级
}
}
}
}
在各落子点,找到分值最大的点。如果有多个分值相同的点,直接在其中取一个随机点。
在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];
}
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);
}
原理分析:
在4个方向上搜索。
以右下方向为例:(黑色棋子表示刚下的棋子)
从当前棋子开始,向右下方数5个
从当前棋子的左上角开始,向右下方数5个
从当前棋子的左上第2个开始,向右下方数5个
从当前棋子的左上第3个开始,向右下方数5个
从当前棋子的左上第4个开始,向右下方数5个
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;
}
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;
}
通过项目的实战积累,在实战中成长
服务器联网通信开发。
游戏大厅配对功能
服务器端业务开发。
AI迭代
使用搜索树,提高算度。
,免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com