From b05d03c6145d1eea5c8101e3ec8a2e8526b68dba Mon Sep 17 00:00:00 2001 From: LHY0125 <3364451258@qq.com> Date: Sun, 29 Jun 2025 22:51:33 +0800 Subject: [PATCH] Add files via upload --- AI_function.md | 198 ++++++++++----------- README.md | 2 +- game_mode.c | 263 +++++++++++++++++++++++++++ game_mode.h | 48 +++++ gobang.c | 228 +++++++++++++++++++++--- gobang.h | 468 +++++++++++++++++++++++++++---------------------- 五子棋.c | 167 +++--------------- 7 files changed, 886 insertions(+), 488 deletions(-) create mode 100644 game_mode.c create mode 100644 game_mode.h diff --git a/AI_function.md b/AI_function.md index b763d73..434759d 100644 --- a/AI_function.md +++ b/AI_function.md @@ -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. **搜索排序**:优先搜索高价值区域 - -## 典型场景处理 - -### 必胜局面 -- 直接落子形成五连 -- 优先进攻而非防守 - -### 必防局面 -- 检测玩家的活四/冲四 -- 强制在关键点落子 - -### 平衡策略 -- 进攻与防守的权重平衡 -- 根据局势动态调整 \ No newline at end of file +2. **扩展方向**: + - 添加开局库 + - 实现并行搜索 + - 优化评估函数 +``` diff --git a/README.md b/README.md index 447915b..3211e91 100644 --- a/README.md +++ b/README.md @@ -143,4 +143,4 @@ chcp 65001 - [ ] **图形用户界面 (GUI)**:使用 `SDL2` 或 `Qt` 等库,将当前的终端界面升级为图形化界面,提升用户体验。 - [ ] **网络对战功能**:增加一个在线对战模式,允许两名玩家通过网络进行对战。 - [ ] **棋谱库集成**:引入开局库,使AI在游戏初期能够选择更优的开局走法。 -- [ ] **代码重构与优化**:持续优化现有代码,提高模块化程度和运行效率,并实现完全的跨平台兼容性。 +- [ ] **代码重构与优化**:持续优化现有代码,提高模块化程度和运行效率,并实现完全的跨平台兼容性。 \ No newline at end of file diff --git a/game_mode.c b/game_mode.c new file mode 100644 index 0000000..2f17c55 --- /dev/null +++ b/game_mode.c @@ -0,0 +1,263 @@ +#include "gobang.h" +#include +#include +#include +#include +#ifdef _WIN32 +#include +#include +#include +#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); + } +} \ No newline at end of file diff --git a/game_mode.h b/game_mode.h new file mode 100644 index 0000000..0cdb0ff --- /dev/null +++ b/game_mode.h @@ -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 \ No newline at end of file diff --git a/gobang.c b/gobang.c index 080478d..d40721d 100644 --- a/gobang.c +++ b/gobang.c @@ -1,7 +1,7 @@ - #include "gobang.h" -#include // 用于目录创建 -#include // 用于时间戳 +#include +#include +#include // 全局变量定义 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; -} +} \ No newline at end of file diff --git a/gobang.h b/gobang.h index 0e0c08f..ee9e94e 100644 --- a/gobang.h +++ b/gobang.h @@ -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 -#include -#include -#include -#include - -// 宏定义 -/** - * @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 +#include +#include +#include +#include +#include + +// 宏定义 +/** + * @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 \ No newline at end of file diff --git a/五子棋.c b/五子棋.c index dde061d..7e509c7 100644 --- a/五子棋.c +++ b/五子棋.c @@ -1,174 +1,47 @@ -#include "gobang.h" +#include "game_mode.h" #include -#include #ifdef _WIN32 #include +#include #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; -} +} \ No newline at end of file