五子棋AI实战:从零构建具备三种思维层次的智能对手
最近在整理大学时期的代码仓库,翻出了一个尘封已久的五子棋项目。当时为了完成课程设计,硬是用C语言从零搭建了一个带图形界面的五子棋游戏,最让我自豪的是那个花了整整两周调试的AI模块。现在回头看,那个AI的设计思路其实很有意思——它不像现在深度学习模型那样“黑箱”,而是用清晰的逻辑规则模拟了人类下棋时的三种思考层次。今天我就把这个项目的核心AI实现拆解出来,分享给对游戏AI开发感兴趣的朋友们。
如果你已经掌握了C语言的基础语法,想找一个既有挑战性又能真正学到东西的实战项目,那么亲手实现一个五子棋AI会是个绝佳的选择。它不像大型游戏引擎那样复杂,但涉及的状态评估、搜索策略等核心思想,却是所有游戏AI的基石。更重要的是,你能亲眼看到自己写的代码从“胡乱落子”到“步步为营”的进化过程,这种成就感是单纯看书无法比拟的。
1. 项目环境搭建与基础框架
在开始编写AI之前,我们需要先搭建一个能运行的游戏框架。这个框架不需要太复杂,但必须清晰地将界面、逻辑和数据分开,这样后续添加AI时才不会陷入混乱。
1.1 选择图形库与开发环境
对于C语言的图形界面,我推荐使用 EasyX 库。它专为初学者设计,API简单直观,在Windows环境下安装方便,能快速实现窗口、绘图等基础功能。如果你使用的是Visual Studio,可以直接通过其自带的包管理器安装;如果是其他IDE,也可以从官网下载手动配置。
// 示例:EasyX基础窗口初始化
#include <graphics.h>
int main() {
initgraph(640, 720); // 创建640x720的窗口
setbkcolor(RGB(240, 217, 181)); // 设置背景色为米黄色
cleardevice(); // 清屏
// 这里开始绘制棋盘和界面
// ...
getch(); // 等待按键
closegraph(); // 关闭图形窗口
return 0;
}
注意:EasyX主要兼容Windows平台。如果你的开发环境是Linux或macOS,可以考虑使用SDL2或GTK等跨平台库,但API会相对复杂一些。
1.2 设计核心数据结构
棋盘是五子棋游戏的灵魂,如何表示它直接影响后续所有算法的效率。一个15×15的标准棋盘,用二维数组是最直观的选择。
#define BOARD_SIZE 15
// 棋盘状态:0-空位,1-黑棋,2-白棋
int chessBoard[BOARD_SIZE][BOARD_SIZE] = {0};
// 棋子链表节点,用于记录每一步历史,方便悔棋功能
typedef struct ChessNode {
int x;
int y;
int player; // 1或2
struct ChessNode* next;
} ChessNode;
// 游戏全局状态
typedef struct GameState {
int currentPlayer; // 当前该谁下:1-黑棋,2-白棋
int gameMode; // 0-双人,1-人机
int aiDifficulty; // AI难度:0-简单,1-困难,2-地狱
int gameOver; // 游戏是否结束
ChessNode* historyHead; // 历史记录链表头
} GameState;
为什么用链表记录历史而不是数组?因为五子棋最多225步,链表的内存开销可以接受,而它的最大优势是悔棋操作变得极其简单——只需要删除尾节点并恢复棋盘状态即可。这在调试AI时特别有用,你可以随时回退到上一步,观察AI在不同局面下的选择。
1.3 绘制模块与用户交互
图形界面不仅要好看,更要好用。我的经验是,把绘制代码封装成独立的函数,每个函数只负责一个视觉元素。
void drawChessboard() {
setlinecolor(RGB(101, 67, 33)); // 深棕色线条
setlinestyle(PS_SOLID, 2);
// 绘制棋盘网格
for (int i = 0; i < BOARD_SIZE; i++) {
// 横线
line(MARGIN, MARGIN + i * GRID_SIZE,
MARGIN + (BOARD_SIZE-1) * GRID_SIZE,
MARGIN + i * GRID_SIZE);
// 竖线
line(MARGIN + i * GRID_SIZE, MARGIN,
MARGIN + i * GRID_SIZE,
MARGIN + (BOARD_SIZE-1) * GRID_SIZE);
}
// 绘制五个天元和星位
fillcircle(MARGIN + 7 * GRID_SIZE, MARGIN + 7 * GRID_SIZE, 5);
// ... 其他四个星位
}
void drawChessPiece(int x, int y, int player) {
if (player == 1) {
setfillcolor(BLACK);
solidcircle(MARGIN + x * GRID_SIZE,
MARGIN + y * GRID_SIZE,
PIECE_RADIUS);
} else {
setfillcolor(WHITE);
setlinecolor(BLACK);
circle(MARGIN + x * GRID_SIZE,
MARGIN + y * GRID_SIZE,
PIECE_RADIUS);
solidcircle(MARGIN + x * GRID_SIZE,
MARGIN + y * GRID_SIZE,
PIECE_RADIUS - 2);
}
}
用户交互的核心是将鼠标点击坐标转换为棋盘坐标。这里有个细节需要注意:因为棋子是落在交叉点上,不是格子内,所以转换时要四舍五入到最近的交叉点。
// 将鼠标坐标转换为棋盘坐标
int getBoardPos(int mouseX, int mouseY, int *boardX, int *boardY) {
// 检查是否在棋盘范围内
if (mouseX < MARGIN - GRID_SIZE/2 || mouseX > MARGIN + (BOARD_SIZE-1)*GRID_SIZE + GRID_SIZE/2 ||
mouseY < MARGIN - GRID_SIZE/2 || mouseY > MARGIN + (BOARD_SIZE-1)*GRID_SIZE + GRID_SIZE/2) {
return 0; // 不在棋盘内
}
// 计算最近的交叉点
*boardX = (int)round((mouseX - MARGIN) / (double)GRID_SIZE);
*boardY = (int)round((mouseY - MARGIN) / (double)GRID_SIZE);
// 确保坐标在有效范围内
if (*boardX < 0) *boardX = 0;
if (*boardX >= BOARD_SIZE) *boardX = BOARD_SIZE - 1;
if (*boardY < 0) *boardY = 0;
if (*boardY >= BOARD_SIZE) *boardY = BOARD_SIZE - 1;
return 1;
}
2. 胜负判定与基础棋型识别
在让AI学会思考之前,它至少得知道游戏什么时候结束、什么样的棋型是危险的。胜负判定看似简单,但写起来有很多优化空间。
2.1 高效的四方向检测算法
最直观的胜负判定方法是:每落一子,就从该位置向四个方向(横、竖、左斜、右斜)检查是否有连续五个同色棋子。但直接写四个循环会有大量重复代码,维护起来很麻烦。
// 方向数组:横、竖、左斜、右斜
const int dirs[4][2] = {
{1, 0}, // 水平向右
{0, 1}, // 垂直向下
{1, 1}, // 右下对角线
{1, -1} // 右上对角线
};
int checkWin(int board[BOARD_SIZE][BOARD_SIZE], int x, int y) {
int player = board[x][y];
if (player == 0) return 0;
for (int d = 0; d < 4; d++) {
int count = 1; // 当前位置已经有一颗棋子
// 正向检查
int dx = dirs[d][0], dy = dirs[d][1];
int nx = x + dx, ny = y + dy;
while (nx >= 0 && nx < BOARD_SIZE &&
ny >= 0 && ny < BOARD_SIZE &&
board[nx][ny] == player) {
count++;
nx += dx;
ny += dy;
}
// 反向检查
dx = -dirs[d][0];
dy = -dirs[d][1];
nx = x + dx;
ny = y + dy;
while (nx >= 0 && nx < BOARD_SIZE &&
ny >= 0 && ny <

7279

被折叠的 条评论
为什么被折叠?



