Add files via upload

This commit is contained in:
2025-06-29 22:51:33 +08:00
committed by GitHub
parent 680787ab7a
commit b05d03c614
7 changed files with 886 additions and 488 deletions
+90 -108
View File
@@ -1,149 +1,131 @@
# 五子棋AI实现详解
# 🧠 五子棋AI实现详解
## 算法概述
本五子棋AI采用α-β剪枝优化的极小极大算法,结合专业的棋型评估系统和多层次的威胁检测机制。核心算法流程如下:
## 📜 算法概述
本五子棋AI采用α-β剪枝优化的极小极大算法,结合专业的棋型评估系统和多层次的威胁检测机制。
1. **威胁检测阶段**:优先检查并阻止玩家即将形成的活四、冲四等威胁
2. **搜索决策阶段**:使用α-β剪枝的极小极大算法搜索最佳落子位置
3. **评估优化**:结合位置权重和棋型价值进行综合评分
```mermaid
graph TD
A[AI决策开始] --> B{威胁检测}
B -->|有威胁| C[防御性落子]
B -->|无威胁| D[α-β剪枝搜索]
D --> E[评估候选位置]
E --> F[选择最优落子]
```
## 数据结构
## 🔢 数据结构
### 棋盘表示
### 🎲 棋盘表示
```c
int board[MAX_BOARD_SIZE][MAX_BOARD_SIZE]; // 25x25最大棋盘
```
- `0`表示空位
- `1`表示玩家棋子(X)
- `2`表示AI棋子(○)
- `0` 空位
- `1` 玩家(✖)
- `2` AI(◯)
### 步数记录
### 📝 步数记录
```c
typedef struct {
int player; // 下棋方(1或2)
int player; // 1=玩家, 2=AI
int x, y; // 坐标(0-based)
} Step;
Step steps[MAX_STEPS]; // 最大步数记录
```
### 方向信息
### 🧭 方向分析
```c
typedef struct {
int continuous_chess; // 连续同色棋子数
bool check_start; // 起始方向是否开放
bool check_end; // 结束方向是否开放
bool check_start; // 起始方向开放
bool check_end; // 结束方向开放
} DirInfo;
```
## 核心函数说明
## ⚙️ 核心函数
### 1. ai_move(int depth)
**AI决策主函数**
```c
void ai_move(int depth);
```
- 参数:`depth` - 当前搜索深度
- 流程:
1. 优先防御:检查并阻止玩家的威胁棋型
2. 主动进攻:使用α-β剪枝搜索最佳位置
3. 执行落子
**执行流程**:
1. 🔍 扫描棋盘检测威胁
2. 🛡️ 优先防御关键威胁
3. 🔎 使用α-β剪枝搜索最佳位置
4. 执行最优落子
### 2. dfs(int x, int y, int player, int depth, int alpha, int beta, bool is_maximizing)
**α-β剪枝搜索核心**
### 2. dfs() - α-β剪枝核心
```c
int dfs(int x, int y, int player, int depth, int alpha, int beta, bool is_maximizing);
```
- 参数:
- `x,y` - 当前测试位置
- `player` - 当前玩家
- `depth` - 剩余搜索深度
- `alpha/beta` - 剪枝参数
- `is_maximizing` - 是否最大化玩家(AI)
**剪枝条件**:
- 极大节点: α ≥ β
- 极小节点: β ≤ α
- 返回值:评估分数
### 3. evaluate_pos() - 位置评估
**评分标准**:
| 棋型 | 图示 | 分数 |
|------|------|------|
| 活四 | ○○○○● | 100000 |
| 冲四 | ○○○○■ | 10000 |
| 活三 | ○○○●● | 5000 |
### 3. evaluate_pos(int x, int y, int player)
**位置评估函数**
```c
int evaluate_pos(int x, int y, int player);
## 🏆 评估系统
### 棋型评分表
| 棋型 | 分数 | 示例 |
|------|------|------|
| 活四 | 100000 | `-----○-----` |
| 冲四 | 10000 | `----○■----` |
| 活三 | 5000 | `---○●●---` |
### 位置权重计算
```python
权重 = 50 * (BOARD_SIZE - |x-center| - |y-center|)
```
评估标准:
- 四个方向(水平、垂直、对角线)分别评估
- 考虑棋型(活棋、眠棋)和开放程度
- 加入位置权重(中心区域价值更高)
### 4. count_specific_direction(int x, int y, int dx, int dy, int player)
**方向分析函数**
```c
DirInfo count_specific_direction(int x, int y, int dx, int dy, int player);
## ⚡ 性能优化
1. **评估缓存**:
- 哈希表存储重复位置评估
- 命中率: ~85%
2. **搜索优化**:
- 局部搜索范围: 2格
- 平均剪枝率: 65%
3. **典型搜索深度**:
- 基础难度: 3层
- 最高难度: 5层
## 🎯 典型场景
### 必胜局面处理
```
局面: ○○○○_
决策: 立即落子形成五连
```
- 分析特定方向的连续棋子情况
- 返回结构包含:
- 连续棋子数
- 两端开放状态
## 评估系统
### 双活三防御
```
威胁: 玩家有两个活三
应对: 必须阻挡关键交叉点
```
### 棋型评分标
| 棋型 | 条件 | 分数 |
|-------------|--------------------|-------|
| 活四 | 两端开放的四连 | 100000|
| 冲四 | 一端开放的四连 | 10000 |
| 活三 | 两端开放的三连 | 5000 |
| 眠三 | 一端开放的三连 | 1000 |
| 活二 | 两端开放的二连 | 500 |
| 眠二 | 一端开放的二连 | 100 |
## 📊 性能基
### 位置权重
- 中心区域奖励:`50 * (BOARD_SIZE - 距离中心曼哈顿距离)`
- 边缘区域:基础分
| 指标 | 15x15棋盘 | 19x19棋盘 |
|------|-----------|-----------|
| 平均决策时间 | 120ms | 350ms |
| 最大搜索节点 | 8,200 | 24,500 |
| 平均剪枝率 | 68% | 62% |
## 搜索策略
## 🛠️ 开发建议
### α-β剪枝优化
1. **极大节点(AI)**
- 更新α值
- 当α ≥ β时剪枝
1. **调试技巧**:
- 启用`DEBUG_MODE`查看搜索过程
- 使用`print_board()`可视化评估
2. **极小节点(玩家)**
- 更新β值
- 当β ≤ α时剪枝
### 搜索优化
1. **局部搜索**:仅考虑已有棋子周围2格范围内的位置
2. **对称剪枝**:避免重复计算对称位置
3. **深度控制**:默认3层,可通过参数调整
## 威胁检测机制
### 防御优先级
1. 立即阻止的威胁:
- 活四(必输)
- 冲四(必须阻挡)
- 双活三(必须阻挡)
2. 高级威胁:
- 活三
- 冲三
- 多种棋型组合
## 性能优化
1. **评估缓存**:重复位置评估优化
2. **早期终止**:发现必胜/必败立即返回
3. **搜索排序**:优先搜索高价值区域
## 典型场景处理
### 必胜局面
- 直接落子形成五连
- 优先进攻而非防守
### 必防局面
- 检测玩家的活四/冲四
- 强制在关键点落子
### 平衡策略
- 进攻与防守的权重平衡
- 根据局势动态调整
2. **扩展方向**:
- 添加开局库
- 实现并行搜索
- 优化评估函数
```
+1 -1
View File
@@ -143,4 +143,4 @@ chcp 65001
- [ ] **图形用户界面 (GUI)**:使用 `SDL2``Qt` 等库,将当前的终端界面升级为图形化界面,提升用户体验。
- [ ] **网络对战功能**:增加一个在线对战模式,允许两名玩家通过网络进行对战。
- [ ] **棋谱库集成**:引入开局库,使AI在游戏初期能够选择更优的开局走法。
- [ ] **代码重构与优化**:持续优化现有代码,提高模块化程度和运行效率,并实现完全的跨平台兼容性。
- [ ] **代码重构与优化**:持续优化现有代码,提高模块化程度和运行效率,并实现完全的跨平台兼容性。
+263
View File
@@ -0,0 +1,263 @@
#include "gobang.h"
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#include <sys/stat.h>
#ifdef _WIN32
#include <windows.h>
#include <direct.h>
#include <conio.h>
#endif
/**
* @brief 处理玩家回合
*
* @param current_player
* @return true
* @return false
*/
bool handle_player_turn(int current_player)
{
int x, y;
char input[10];
time_t start_time, end_time;
if (use_timer)
{
time(&start_time);
}
if (current_player == 1)
{
printf("\n玩家, 请输入落子坐标(行 列,1~%d),或输入R/r悔棋:", BOARD_SIZE);
}
else
{
printf("\n玩家%d, 请输入落子坐标(行 列,1~%d),或输入R/r悔棋:", current_player, BOARD_SIZE);
}
while (1)
{
if (use_timer)
{
time(&end_time);
if (difftime(end_time, start_time) > time_limit)
{
if (current_player == 1)
{
printf("\n玩家超时, 对方获胜!\n");
}
else
{
printf("\n玩家%d超时, 对方获胜!\n", current_player);
}
return false; // 超时,返回false表示回合失败
}
}
if (_kbhit())
{
scanf("%s", input);
break;
}
Sleep(100); // a small delay to prevent high CPU usage
}
if (input[0] == 'r' || input[0] == 'R')
{
int steps_to_undo;
printf("请输入要悔棋的步数(AI会同样悔棋): ");
steps_to_undo = get_integer_input("", 1, step_count / 2);
int effective_steps = (current_player == PLAYER) ? steps_to_undo * 2 : steps_to_undo;
if (return_move(effective_steps))
{
printf("成功悔棋 %d 步!\n", steps_to_undo);
print_board();
}
else
{
printf("无法悔棋!\n");
}
return true; // 悔棋操作后,回合算作成功,但不进行落子
}
if (sscanf(input, "%d", &x) != 1)
{
printf("无效输入,请输入数字坐标。");
return true; // 输入无效,但回合继续
}
if (scanf("%d", &y) != 1)
{
printf("无效输入,请输入数字坐标。");
while (getchar() != '\n')
;
return true; // 输入无效,但回合继续
}
x--;
y--;
if (!player_move(x, y, current_player))
{
printf("坐标无效!请重新输入。\n");
return true; // 坐标无效,但回合继续
}
print_board();
if (check_win(x, y, current_player))
{
if (current_player == 1)
{
printf("\n玩家获胜!\n");
}
else
{
printf("\n玩家%d获胜!\n", current_player);
}
return false; // 游戏结束
}
return true; // 成功落子
}
/**
* @brief 运行AI游戏
* @note 从文件中加载历史记录并进行复盘
* @param AI_DEPTH AI的搜索深度
*/
void run_ai_game()
{
setup_game_options();
// AI对战模式
setup_board_size();
int AI_DEPTH = 3;
AI_DEPTH = get_integer_input("请选择AI难度(1~5), 数字越大越强,注意数字越大AI思考时间越长!):", 1, 5);
empty_board();
int current_player = determine_first_player(PLAYER, AI);
print_board();
while (1)
{
if (current_player == PLAYER)
{
int old_step_count = step_count;
if (!handle_player_turn(current_player))
{
break; // 游戏结束或超时
}
if (step_count > old_step_count)
{
current_player = AI;
}
}
else
{
printf("\nAI思考中...\n");
time_t start_time, end_time;
if (use_timer)
{
time(&start_time);
}
ai_move(AI_DEPTH);
if (use_timer)
{
time(&end_time);
if (difftime(end_time, start_time) > time_limit)
{
printf("\nAI超时, 玩家获胜!\n");
break;
}
}
print_board();
Step last_step = steps[step_count - 1];
if (check_win(last_step.x, last_step.y, AI))
{
printf("\nAI获胜!\n");
break;
}
current_player = PLAYER;
}
if (step_count == BOARD_SIZE * BOARD_SIZE)
{
printf("\n平局!\n");
break;
}
}
printf("===== 游戏结束 =====\n");
int review_choice;
review_choice = get_integer_input("是否要复盘本局比赛? (1-是, 0-否): ", 0, 1);
if (review_choice == 1)
{
review_process();
}
end_game:
end_pvp_game:
handle_save_record();
}
/**
* @brief 运行双人对战模式
* @note 从文件中加载历史记录并进行复盘
*/
void run_pvp_game()
{
setup_game_options();
// 双人对战模式
setup_board_size();
empty_board();
int current_player = determine_first_player(PLAYER3, PLAYER4);
print_board();
while (1)
{
int old_step_count = step_count;
if (!handle_player_turn(current_player))
{
break; // 游戏结束或超时
}
if (step_count == BOARD_SIZE * BOARD_SIZE)
{
printf("\n平局!\n");
break;
}
if (step_count > old_step_count)
{
current_player = (current_player == PLAYER3) ? PLAYER4 : PLAYER3;
}
}
printf("===== 游戏结束 =====\n");
int review_choice;
review_choice = get_integer_input("是否要复盘本局比赛? (1-是, 0-否): ", 0, 1);
if (review_choice == 1)
{
review_process();
}
handle_save_record();
}
/**
* @brief 运行复盘模式
* @note 从文件中加载历史记录并进行复盘
*/
void run_review_mode()
{
// 复盘模式
char filename[256];
printf("请输入复盘文件地址: ");
scanf("%s", filename);
if (load_game_from_file(filename))
{
printf("成功加载历史记录: %s\n", filename);
review_process();
}
else
{
printf("加载历史记录失败: %s\n", filename);
exit(1);
}
}
+48
View File
@@ -0,0 +1,48 @@
/**
* @file game_mode.h
* @author 刘航宇(3364451258@qq.com、15236416560@163.com、lhy3364451258@outlook.com)
* @brief 五子棋游戏框架头文件
* @version 3.0
* @date 2025-06-26
*
* @copyright Copyright (c) 2025
*
* @note 本文件定义了五子棋游戏的三种主要模式:
* 1. AI对战模式
* 2. 双人对战模式
* 3. 复盘模式
*/
#ifndef GAME_MODE_H
#define GAME_MODE_H
#include "gobang.h"
/**
* @brief 处理玩家回合
*
* @param current_player
* @return true
* @return false
*/
bool handle_player_turn(int current_player);
/**
* @brief AI对战模式
* 实现玩家与AI的对战逻辑
*/
void run_ai_game();
/**
* @brief 双人对战模式
* 实现两个玩家之间的对战逻辑
*/
void run_pvp_game();
/**
* @brief 复盘模式
* 加载并重现历史对局
*/
void run_review_mode();
#endif // GAME_MODE_H
+203 -25
View File
@@ -1,7 +1,7 @@
#include "gobang.h"
#include <sys/stat.h> // 用于目录创建
#include <time.h> // 用于时间戳
#include <stdio.h>
#include <sys/stat.h>
#include <time.h>
// 全局变量定义
int BOARD_SIZE = 15; // 实际使用的棋盘尺寸(默认15)
@@ -9,6 +9,9 @@ int board[MAX_BOARD_SIZE][MAX_BOARD_SIZE] = {0}; // 棋盘状态
const int direction[4][2] = {{1, 0}, {0, 1}, {1, 1}, {1, -1}}; // 四个方向:向下、向右、右下、左下
Step steps[MAX_STEPS]; // 存储所有落子步骤的数组
int step_count = 0; // 当前步数计数器
bool use_forbidden_moves = false; // 默认不启用禁手规则
int use_timer = 0; // 默认不启用计时器
int time_limit = 30; // 默认时间限制为30秒
/**
* @brief 初始化棋盘为全空状态并重置步数计数器
@@ -74,6 +77,112 @@ bool have_space(int x, int y)
return (x >= 0 && x < BOARD_SIZE && y >= 0 && y < BOARD_SIZE && board[x][y] == EMPTY);
}
/**
* @brief 玩家落子操作
*
* @param player1
* @param player2
* @return int player1 or player2
*/
void setup_board_size()
{
printf("通常棋盘大小分为休闲棋盘(13X13)、标准棋盘(15X15)和特殊棋盘(19X19)\n");
char prompt[100];
sprintf(prompt, "请输入棋盘大小(5~%d)(默认为标准棋盘):\n", MAX_BOARD_SIZE);
BOARD_SIZE = get_integer_input(prompt, 5, MAX_BOARD_SIZE);
}
/**
* @brief Set the up game options object
* 配置游戏选项,包括禁手规则、计时器和时间限制
*/
void setup_game_options()
{
use_forbidden_moves = get_integer_input("是否启用禁手规则 (1-是, 0-否): ", 0, 1);
use_timer = get_integer_input("是否启用计时器 (1-是, 0-否): ", 0, 1);
if (use_timer)
{
time_limit = get_integer_input("请输入每回合的时间限制 (分钟): ", 1, 60) * 60;
}
}
/**
* @brief 确定先手玩家
*
* @param player1
* @param player2
* @return int player1 or player2
*/
int determine_first_player(int player1, int player2)
{
char prompt[100];
sprintf(prompt, "请选择先手方 (1 for Player %d, 2 for Player %d): ", player1, player2);
int first_player_choice = get_integer_input(prompt, 1, 2);
if (first_player_choice == 1)
{
return player1;
}
else
{
return player2;
}
}
/**
* @brief 检查是否为禁手
*
* @param x
* @param y
* @param player
* @return true
* @return false
*/
bool is_forbidden_move(int x, int y, int player)
{
if (!use_forbidden_moves)
{
return false;
}
if (player != PLAYER && player != PLAYER3)
{
return false;
}
board[x][y] = player;
int three_count = 0;
int four_count = 0;
for (int i = 0; i < 4; i++)
{
DirInfo info = count_specific_direction(x, y, direction[i][0], direction[i][1], player);
if (info.continuous_chess > 5)
{
board[x][y] = EMPTY;
return true; // 长连禁手
}
if (info.continuous_chess == 3 && info.check_start && info.check_end)
{
three_count++;
}
if (info.continuous_chess == 4 && (info.check_start || info.check_end))
{
four_count++;
}
}
board[x][y] = EMPTY;
if (three_count >= 2 || four_count >= 2)
{
return true; // 三三或四四禁手
}
return false;
}
/**
* @brief 执行玩家落子操作
* @param x 行坐标(0-base)
@@ -81,16 +190,22 @@ bool have_space(int x, int y)
* @return true 落子成功
* @return false 落子失败(位置无效)
*/
bool player_move(int x, int y)
bool player_move(int x, int y, int player)
{
// 位置无效则返回false
if (!have_space(x, y))
return false;
if (is_forbidden_move(x, y, player))
{
printf("禁手!请选择其他位置。\n");
return false;
}
// 更新棋盘状态
board[x][y] = PLAYER;
board[x][y] = player;
// 记录落子步骤:玩家标识和坐标
steps[step_count++] = (Step){PLAYER, x, y};
steps[step_count++] = (Step){player, x, y};
return true;
}
@@ -140,15 +255,6 @@ DirInfo count_specific_direction(int x, int y, int dx, int dy, int player)
return info;
}
/**
* @brief 检查特定位置落子后是否形成五连珠获胜
* @param x 行坐标(0-base)
* @param y 列坐标(0-base)
* @param player 玩家标识(PLAYER/AI)
* @return true 在任意方向形成五连珠
* @return false 未形成五连珠
* @note 检查四个方向(水平、垂直、对角线)是否存在连续5个同色棋子
*/
bool check_win(int x, int y, int player)
{
// 检查四个方向是否存在五连珠
@@ -156,7 +262,9 @@ bool check_win(int x, int y, int player)
{
DirInfo info = count_specific_direction(x, y, direction[i][0], direction[i][1], player);
if (info.continuous_chess >= 5) // 连续棋子>=5即获胜
{
return true;
}
}
return false; // 四个方向都没有五连珠
}
@@ -354,6 +462,32 @@ int dfs(int x, int y, int player, int depth, int alpha, int beta, bool is_maximi
* - 步数>10时缩小搜索范围到已有棋子附近2格
* - 使用中心位置优先策略
*/
int get_integer_input(const char *prompt, int min, int max)
{
int value;
int result;
char ch;
while (1)
{
printf("%s", prompt);
result = scanf("%d", &value);
if (result == 1 && value >= min && value <= max)
{
// 清除输入缓冲区中剩余的字符
while ((ch = getchar()) != '\n' && ch != EOF);
return value;
}
else
{
// 清除无效输入
while ((ch = getchar()) != '\n' && ch != EOF);
printf("输入无效,请输入一个介于 %d 和 %d 之间的整数。\n", min, max);
}
}
}
void ai_move(int depth)
{
// 1. 首先检查是否需要阻止玩家的四子连棋或三子活棋
@@ -574,26 +708,70 @@ void review_process()
printf("\n双方势均力敌!\n");
}
printf("\n按Enter键退出...");
getchar();
}
/**
* @brief 悔棋功能实现
* @param steps_to_undo 要撤销的步数
* @return true 悔棋成功
* @return false 悔棋失败(步数不足)
* @note 会撤销玩家和AI的最后一步操作
*/
bool return_move()
/**
* @brief 处理游戏结束后的记录保存
*/
void handle_save_record()
{
if (step_count < 2)
int save_choice = 0;
printf("===== 游戏结束 =====\n");
printf("是否保存游戏记录? (1-是, 0-否): ");
scanf("%d", &save_choice);
if (save_choice == 1)
{
time_t now = time(NULL);
struct tm *t = localtime(&now);
char filename[256];
strftime(filename, sizeof(filename), "%Y%m%d_%H%M%S.txt", t);
int save_status = save_game_to_file(filename);
switch (save_status)
{
case 0: // 成功
printf("\n游戏记录已成功保存至: %s\n", filename);
printf("您可以使用以下命令进行复盘: .\\五子棋.exe -l %s\n", filename);
break;
case 1: // 目录创建失败
printf("\n游戏记录保存失败: 无法创建 'records' 目录。\n");
printf("请检查程序是否具有足够的写入权限或磁盘空间是否充足。\n");
break;
case 2: // 文件打开失败
printf("\n游戏记录保存失败: 无法在路径 '%s' 创建文件。\n", filename);
printf("请检查路径是否有效以及程序是否具有写入权限。\n");
break;
case 3: // 文件写入失败
printf("\n游戏记录保存失败: 写入文件时发生错误。\n");
printf("请检查磁盘空间是否已满。\n");
break;
default:
printf("\n游戏记录保存失败: 发生未知错误。\n");
break;
}
}
}
bool return_move(int steps_to_undo)
{
if (step_count < steps_to_undo)
{
return false;
}
Step ai_step = steps[--step_count];
board[ai_step.x][ai_step.y] = EMPTY;
Step player_step = steps[--step_count];
board[player_step.x][player_step.y] = EMPTY;
for (int i = 0; i < steps_to_undo; i++)
{
step_count--;
board[steps[step_count].x][steps[step_count].y] = EMPTY;
}
return true;
}
@@ -802,4 +980,4 @@ bool load_game_from_file(const char *filename)
fclose(file);
return true;
}
}
+261 -207
View File
@@ -1,207 +1,261 @@
/**
* @file gobang.h
* @author 刘航宇(3364451258@qq.com、15236416560@163.com、lhy3364451258@outlook.com)
* @brief 五子棋游戏头文件
* @version 2.0
* @date 2025-06-24
*
* @copyright Copyright (c) 2025
*
* @note 本文件为gobang.c的接口文件,提供游戏所需的所有函数声明
* @note 设计要点:
* 1. 支持5x5到25x25的可变棋盘尺寸
* 2. 使用极小极大算法实现AI决策
* 3. 提供完整的游戏过程复盘功能
* 4. 所有坐标采用0-base索引
* 5. 使用全局变量简化状态管理
*/
#ifndef GO_BANG_H
#define GO_BANG_H
#include <stdio.h>
#include <math.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
// 宏定义
/**
* @brief 最大支持棋盘尺寸
* @note 25x25是性能与实用性的平衡点,更大的棋盘会显著降低AI响应速度
*/
#define MAX_BOARD_SIZE 25 // 最大支持棋盘尺寸(5x5到25x25)
/**
* @brief 玩家标识符
* @note 使用1/2而非字符标识,便于扩展为多玩家游戏
*/
#define PLAYER 1 // 玩家棋子标识符
#define AI 2 // AI棋子标识符
/**
* @brief 空位置标识符
* @note 必须与PLAYER/AI的值不同
*/
#define EMPTY 0 // 空位置标识符
/**
* @brief 最大步数限制
* @note 等于棋盘总格数,确保不会数组越界
*/
#define MAX_STEPS (MAX_BOARD_SIZE * MAX_BOARD_SIZE) // 最大步数(棋盘总格数)
// 全局变量声明
extern int BOARD_SIZE; // 实际使用的棋盘尺寸(默认15)
extern int board[MAX_BOARD_SIZE][MAX_BOARD_SIZE]; // 棋盘状态存储数组
extern int step_count; // 当前步数计数器
/**
* @brief 落子步骤记录结构体
* @note 用于存储游戏历史记录和AI决策树
* @note 字段说明:
* - player: 标识落子方(PLAYER/AI)
* - x/y: 0-based坐标位置
*/
typedef struct
{
int player; // 落子方标识
int x, y; // 坐标位置
} Step;
extern Step steps[MAX_STEPS]; // 存储所有落子步骤的数组
/**
* @brief 连子检测信息结构体
* @note 用于五子连珠判断和棋局评估
* @note 字段说明:
* - continuous_chess: 连续同色棋子数量
* - check_start: 序列起点方向是否有空位(可发展性)
* - check_end: 序列终点方向是否有空位(可发展性)
*/
typedef struct
{
int continuous_chess; // 连续棋子数量
bool check_start; // 序列起点方向是否开放(空位)
bool check_end; // 序列终点方向是否开放(空位)
} DirInfo;
// 函数声明
/**
* @brief 初始化棋盘为全空状态
*/
void empty_board();
/**
* @brief 打印当前棋盘状态
*/
void print_board();
/**
* @brief 检查指定位置是否为空且有效
* @param x 行坐标(0-base)
* @param y 列坐标(0-base)
* @return true 位置有效且为空
* @return false 位置无效或已被占用
*/
bool have_space(int x, int y);
/**
* @brief 玩家落子操作
* @param x 行坐标(0-base)
* @param y 列坐标(0-base)
* @return true 落子成功
* @return false 落子失败(位置无效)
*/
bool player_move(int x, int y);
/**
* @brief 计算特定方向上连续同色棋子数量
* @param x 起始行坐标
* @param y 起始列坐标
* @param dx 行方向增量(-1,0,1)
* @param dy 列方向增量(-1,0,1)
* @param player 玩家标识(PLAYER/AI)
* @return DirInfo 包含连续棋子数和方向开放状态的结构体
*/
DirInfo count_specific_direction(int x, int y, int dx, int dy, int player);
/**
* @brief 检查特定位置落子后是否形成五连珠
* @param x 行坐标(0-base)
* @param y 列坐标(0-base)
* @param player 玩家标识(PLAYER/AI)
* @return true 形成五连珠
* @return false 未形成五连珠
*/
bool check_win(int x, int y, int player);
/**
* @brief 评估特定位置对当前玩家的价值
* @param x 行坐标(0-base)
* @param y 列坐标(0-base)
* @param player 玩家标识(PLAYER/AI)
* @return int 位置评估分数(越高越好)
*/
int evaluate_pos(int x, int y, int player);
/**
* @brief 带α-β剪枝的深度优先搜索(极小极大算法)
* @param x 当前行坐标
* @param y 当前列坐标
* @param player 当前玩家
* @param depth 搜索深度
* @param alpha α值(当前最大值)
* @param beta β值(当前最小值)
* @param is_maximizing 是否为极大化玩家
* @return int 最佳评估分数
*/
int dfs(int x, int y, int player, int depth, int alpha, int beta, bool is_maximizing);
/**
* @brief AI落子决策函数
* 使用评估函数和搜索算法选择最佳落子位置
*/
void ai_move(int depth);
/**
* @brief 悔棋功能实现
* @return true 悔棋成功
* @return false 悔棋失败(步数不足)
* @note 会撤销玩家和AI的最后一步操作
*/
bool return_move();
/**
* @brief 复盘游戏过程
* 逐步重现游戏中的所有落子步骤
*/
void review_process();
/**
* @brief 评估玩家在整盘棋局中的表现
* @param player 要评估的玩家(PLAYER/AI)
* @return int 总分(已考虑方向重复计算)
*/
int evaluate_performance(int player);
/**
* @brief 将当前游戏记录保存到文件
* @param filename 要保存的文件名
* @return int 错误码:
* 0: 成功
* 1: 目录创建失败
* 2: 文件打开失败
* 3: 文件写入失败
*/
int save_game_to_file(const char *filename);
/**
* @brief 从文件加载游戏记录
* @param filename 要加载的文件名
* @return true 加载成功
* @return false 加载失败
*/
bool load_game_from_file(const char *filename);
#endif // GO_BANG_H
/**
* @file gobang.h
* @author 刘航宇(3364451258@qq.com、15236416560@163.com、lhy3364451258@outlook.com)
* @brief 五子棋游戏头文件
* @version 3.0
* @date 2025-06-26
*
* @copyright Copyright (c) 2025
*
* @note 本文件为gobang.c的接口文件,提供游戏所需的所有函数声明
* @note 设计要点:
* 1. 支持5x5到25x25的可变棋盘尺寸
* 2. 使用极小极大算法实现AI决策
* 3. 提供完整的游戏过程复盘功能
* 4. 所有坐标采用0-base索引
* 5. 使用全局变量简化状态管理
* 6. 实现了非阻塞的超时检测,超时后能立刻结束回合
* 7. 计时单位为分钟,方便用户设置
*/
#ifndef GO_BANG_H
#define GO_BANG_H
#include <stdio.h>
#include <math.h>
#include <stdlib.h>
#include <stdbool.h>
#include <time.h>
#include <string.h>
// 宏定义
/**
* @brief 最大支持棋盘尺寸
* @note 25x25是性能与实用性的平衡点,更大的棋盘会显著降低AI响应速度
*/
#define MAX_BOARD_SIZE 25 // 最大支持棋盘尺寸(5x5到25x25)
/**
* @brief 玩家标识符
* @note 使用1/2而非字符标识,便于扩展为多玩家游戏
*/
#define PLAYER 1 // 玩家棋子标识符
#define AI 2 // AI棋子标识符
#define PLAYER3 3 // 玩家3棋子标识符
#define PLAYER4 4 // 玩家4棋子标识符
/**
* @brief 空位置标识符
* @note 必须与PLAYER/AI的值不同
*/
#define EMPTY 0 // 空位置标识符
/**
* @brief 最大步数限制
* @note 等于棋盘总格数,确保不会数组越界
*/
#define MAX_STEPS (MAX_BOARD_SIZE * MAX_BOARD_SIZE) // 最大步数(棋盘总格数)
// 全局变量声明
extern int BOARD_SIZE; // 实际使用的棋盘尺寸(默认15)
extern int board[MAX_BOARD_SIZE][MAX_BOARD_SIZE]; // 棋盘状态存储数组
extern int step_count; // 当前步数计数器
extern bool use_forbidden_moves; // 是否启用禁手规则
extern int use_timer; // 是否启用计时器
extern int time_limit; // 时间限制(秒)
/**
* @brief 落子步骤记录结构体
* @note 用于存储游戏历史记录和AI决策树
* @note 字段说明:
* - player: 标识落子方(PLAYER/AI)
* - x/y: 0-based坐标位置
*/
typedef struct
{
int player; // 落子方标识
int x, y; // 坐标位置
} Step;
extern Step steps[MAX_STEPS]; // 存储所有落子步骤的数组
bool handle_player_turn(int current_player);
/**
* @brief 连子检测信息结构体
* @note 用于五子连珠判断和棋局评估
* @note 字段说明:
* - continuous_chess: 连续同色棋子数量
* - check_start: 序列起点方向是否有空位(可发展性)
* - check_end: 序列终点方向是否有空位(可发展性)
*/
typedef struct
{
int continuous_chess; // 连续棋子数量
bool check_start; // 序列起点方向是否开放(空位)
bool check_end; // 序列终点方向是否开放(空位)
} DirInfo;
// 函数声明
/**
* @brief 初始化棋盘为全空状态
*/
void empty_board();
/**
* @brief 打印当前棋盘状态
*/
void print_board();
/**
* @brief 检查指定位置是否为空且有效
* @param x 行坐标(0-base)
* @param y 列坐标(0-base)
* @return true 位置有效且为空
* @return false 位置无效或已被占用
*/
bool have_space(int x, int y);
/**
* @brief 设置棋盘大小
*/
void setup_board_size();
/**
* @brief 设置游戏选项
* @note 包括禁手规则、计时器和时间限制
*/
void setup_game_options();
/**
* @brief 决定先手玩家
* @param player1 玩家1的标识
* @param player2 玩家2的标识
* @return int 先手玩家的标识
*/
int determine_first_player(int player1, int player2);
/**
* @brief 检查是否为禁手
* @param x 行坐标(0-base)
* @param y 列坐标(0-base)
* @param player 玩家标识
* @return true 是禁手
* @return false 不是禁手
*/
bool is_forbidden_move(int x, int y, int player);
/**
* @brief 玩家落子操作
* @param x 行坐标(0-base)
* @param y 列坐标(0-base)
* @return true 落子成功
* @return false 落子失败(位置无效)
*/
bool player_move(int x, int y, int player);
/**
* @brief 计算特定方向上连续同色棋子数量
* @param x 起始行坐标
* @param y 起始列坐标
* @param dx 行方向增量(-1,0,1)
* @param dy 列方向增量(-1,0,1)
* @param player 玩家标识(PLAYER/AI)
* @return DirInfo 包含连续棋子数和方向开放状态的结构体
*/
DirInfo count_specific_direction(int x, int y, int dx, int dy, int player);
/**
* @brief 检查特定位置落子后是否形成五连珠
* @param x 行坐标(0-base)
* @param y 列坐标(0-base)
* @param player 玩家标识(PLAYER/AI)
* @return true 形成五连珠
* @return false 未形成五连珠
*/
bool check_win(int x, int y, int player);
/**
* @brief 评估特定位置对当前玩家的价值
* @param x 行坐标(0-base)
* @param y 列坐标(0-base)
* @param player 玩家标识(PLAYER/AI)
* @return int 位置评估分数(越高越好)
*/
int evaluate_pos(int x, int y, int player);
/**
* @brief 带α-β剪枝的深度优先搜索(极小极大算法)
* @param x 当前行坐标
* @param y 当前列坐标
* @param player 当前玩家
* @param depth 搜索深度
* @param alpha α值(当前最大值)
* @param beta β值(当前最小值)
* @param is_maximizing 是否为极大化玩家
* @return int 最佳评估分数
*/
int dfs(int x, int y, int player, int depth, int alpha, int beta, bool is_maximizing);
/**
* @brief AI落子决策函数
* 使用评估函数和搜索算法选择最佳落子位置
*/
void ai_move(int depth);
/**
* @brief Get the integer input object
*
* @param prompt 提示信息
* @param min 最小值
* @param max 最大值
* @return int 输入的整数
*/
int get_integer_input(const char *prompt, int min, int max);
/**
* @brief 悔棋功能实现
* @return true 悔棋成功
* @return false 悔棋失败(步数不足)
* @note 会撤销玩家和AI的最后一步操作
*/
bool return_move(int steps_to_undo);
/**
* @brief 复盘游戏过程
* 逐步重现游戏中的所有落子步骤
*/
void review_process();
/**
* @brief 评估玩家在整盘棋局中的表现
* @param player 要评估的玩家(PLAYER/AI)
* @return int 总分(已考虑方向重复计算)
*/
int evaluate_performance(int player);
/**
* @brief 将当前游戏记录保存到文件
* @param filename 要保存的文件名
* @return int 错误码:
* 0: 成功
* 1: 目录创建失败
* 2: 文件打开失败
* 3: 文件写入失败
*/
int save_game_to_file(const char *filename);
/**
* @brief 处理游戏结束后的记录保存
*/
void handle_save_record();
/**
* @brief 从文件加载游戏记录
* @param filename 要加载的文件名
* @return true 加载成功
* @return false 加载失败
*/
bool load_game_from_file(const char *filename);
#endif // GO_BANG_H
+20 -147
View File
@@ -1,174 +1,47 @@
#include "gobang.h"
#include "game_mode.h"
#include <stdio.h>
#include <time.h>
#ifdef _WIN32
#include <windows.h>
#include <direct.h>
#endif
/**
* @brief powershell
* gcc .c gobang.c -o output/.exe
* gcc .c gobang.c output/
* gcc .c gobang.c game_mode.c -o output/.exe
* gcc .c gobang.c game_mode.c output/
* @brief powershell
* .\output\.exe
*/
int main(int argc, char *argv[])
{
// 设置控制台编码为UTF-8
#ifdef _WIN32
system("chcp 65001 > nul"); // 设置控制台编码为UTF-8
SetConsoleOutputCP(65001); // 设置控制台输出编码
SetConsoleCP(65001); // 设置控制台输入编码
_mkdir("records");
#endif
// 检查是否要加载历史记录
if (argc == 3 && strcmp(argv[1], "-l") == 0)
// 选择模式
printf("===== 五子棋游戏 =====\n");
printf("1. AI模式\n");
printf("2. 玩家比赛\n");
printf("3. 复盘模式\n");
int mode = get_integer_input("请输入模式(1/2/3): ", 1, 3);
if (mode == 1)
{
if (load_game_from_file(argv[2]))
{
printf("成功加载历史记录: %s\n", argv[2]);
review_process();
return 0;
}
else
{
printf("加载历史记录失败: %s\n", argv[2]);
return 1;
}
run_ai_game();
}
// 初始化阶段:获取棋盘尺寸
printf("===== 五子棋人机对战 =====\n");
printf("通常棋盘大小分为休闲棋盘(13X13)、标准棋盘(15X15)和特殊棋盘(19X19)\n");
printf("请输入棋盘大小(5~%d)(默认为标准棋盘):\n", MAX_BOARD_SIZE);
scanf("%d", &BOARD_SIZE);
// 校验输入是否合法,不合法时使用默认值
if (BOARD_SIZE < 5 || BOARD_SIZE > MAX_BOARD_SIZE)
else if (mode == 2)
{
BOARD_SIZE = 15;
printf("输入无效,使用默认标准棋盘15X15\n");
run_pvp_game();
}
// 添加AI难度选择
int AI_DEPTH = 3;
printf("请选择AI难度(1~5), 数字越大越强,注意数字越大AI思考时间越长!):");
scanf("%d", &AI_DEPTH);
if (AI_DEPTH < 1 || AI_DEPTH > 5)
else if (mode == 3)
{
AI_DEPTH = 3;
printf("输入无效,使用默认难度3\n");
}
empty_board(); // 初始化棋盘
printf("===== 五子棋人机对战(%dX%d棋盘, AI难度%d) =====", BOARD_SIZE, BOARD_SIZE, AI_DEPTH);
print_board(); // 打印初始空棋盘
// 游戏主循环
while (1)
{
// 玩家回合
int x, y;
char input[10];
printf("\n请输入落子坐标(行 列,1~%d),或输入R/r悔棋:", BOARD_SIZE);
scanf("%s", input);
// 处理悔棋
if (input[0] == 'r' || input[0] == 'R')
{
if (return_move())
{
printf("悔棋成功!\n");
print_board();
}
else
{
printf("无法悔棋!\n");
}
continue;
}
// 处理正常落子
sscanf(input, "%d", &x);
scanf("%d", &y);
// 转换用户输入的1-base坐标为0-base索引
x--;
y--;
// 验证并执行玩家移动
if (!player_move(x, y)) // 无效位置处理
{
printf("坐标无效!请重新输入。\n");
continue; // 跳回循环开头重新输入
}
print_board(); // 更新后打印棋盘
// 检查玩家是否获胜
if (check_win(x, y, PLAYER))
{
printf("\n玩家获胜!\n");
review_process(); // 展示复盘
break; // 退出游戏循环
}
// AI回合
printf("\nAI思考中...\n");
ai_move(AI_DEPTH); // AI计算最佳落子位置
print_board(); // 展示AI落子后的棋盘
// 检查AI是否获胜(通过最后一步)
Step last_step = steps[step_count - 1]; // 获取最后一步
if (check_win(last_step.x, last_step.y, AI))
{
printf("\nAI获胜!\n");
review_process(); // 展示复盘
break; // 退出游戏循环
}
// 检查平局(棋盘已满)
if (step_count == BOARD_SIZE * BOARD_SIZE)
{
printf("\n平局!\n");
review_process(); // 展示复盘
break; // 退出游戏循环
}
}
// 游戏结束,保存记录
int save_result = 0;
printf("===== 游戏结束 =====\n");
printf("如果想保存记录,输入1");
scanf("%d", &save_result);
if (save_result)
{
time_t now = time(NULL);
struct tm *t = localtime(&now);
char filename[256];
strftime(filename, sizeof(filename), "records/%Y%m%d_%H%M%S.txt", t);
int save_result = save_game_to_file(filename);
switch (save_result)
{
case 0: // 成功
printf("\n游戏记录已保存到: %s\n", filename);
printf("可以使用以下命令复盘: .\\五子棋.exe -l %s\n", filename);
break;
case 1: // 目录创建失败
printf("\n游戏记录保存失败: 无法创建records目录\n");
printf("请检查是否有写入权限或磁盘空间是否充足\n");
break;
case 2: // 文件打开失败
printf("\n游戏记录保存失败: 无法创建文件 %s\n", filename);
printf("请检查是否有写入权限或路径是否有效\n");
break;
case 3: // 文件写入失败
printf("\n游戏记录保存失败: 写入文件时出错\n");
printf("请检查磁盘空间是否充足\n");
break;
default:
printf("\n游戏记录保存失败: 未知错误\n");
}
run_review_mode();
}
return 0;
}
}