From cf8cfceefe3c06c3471eff3cf5e0d64a81e0adbb Mon Sep 17 00:00:00 2001 From: LHY0125 <3364451258@qq.com> Date: Mon, 30 Jun 2025 21:02:13 +0800 Subject: [PATCH] Add files via upload --- README.md | 188 ++++++++++++++--------------- ai.c | 325 +++++++++++++++++++++++++++++++++++++++++++++++++ ai.h | 34 ++++++ game_mode.c | 177 ++++++++++++++++----------- game_mode.h | 25 +++- gobang.c | 340 ++++------------------------------------------------ gobang.exe | Bin 83970 -> 85011 bytes gobang.h | 36 +----- 五子棋.c | 46 ++++--- 9 files changed, 630 insertions(+), 541 deletions(-) create mode 100644 ai.c create mode 100644 ai.h diff --git a/README.md b/README.md index 3211e91..ae413a2 100644 --- a/README.md +++ b/README.md @@ -1,146 +1,146 @@ -# 🎲 五子棋人机对战AI +# ? ˻սAI ![Build Status](https://img.shields.io/badge/build-passing-brightgreen) ![License](https://img.shields.io/badge/license-MIT-blue) ![Code Lines](https://img.shields.io/tokei/lines/github/your-username/your-repo) -## 目录 -- [🎲 五子棋人机对战AI](#-五子棋人机对战ai) - - [目录](#目录) - - [项目简介](#项目简介) - - [✨ 功能特性](#-功能特性) - - [🚀 快速开始](#-快速开始) - - [编译程序](#编译程序) - - [运行游戏](#运行游戏) - - [🎯 游戏玩法](#-游戏玩法) - - [💻 开发环境](#-开发环境) - - [⚠️ 常见问题](#️-常见问题) - - [权限问题](#权限问题) - - [中文显示问题](#中文显示问题) - - [🛠️ 技术实现](#️-技术实现) - - [核心决策算法](#核心决策算法) - - [启发式评估函数](#启发式评估函数) - - [📂 代码结构](#-代码结构) - - [📜 许可证](#-许可证) - - [🙋 反馈与贡献](#-反馈与贡献) - - [🚀 未来计划](#-未来计划) +## Ŀ¼ +- [? ˻սAI](#-˻սai) + - [Ŀ¼](#Ŀ¼) + - [Ŀ](#Ŀ) + - [? ](#-) + - [? ٿʼ](#-ٿʼ) + - [](#) + - [Ϸ](#Ϸ) + - [? Ϸ淨](#-Ϸ淨) + - [? ](#-) + - [?? ](#?-) + - [Ȩ](#Ȩ) + - [ʾ](#ʾ) + - [?? ʵ](#?-ʵ) + - [ľ㷨](#ľ㷨) + - [ʽ](#ʽ) + - [? ṹ](#-ṹ) + - [? ֤](#-֤) + - [? 빱](#-빱) + - [? δƻ](#-δƻ) -## 项目简介 -基于C语言实现的五子棋人机对战系统,采用α-β剪枝优化的极小极大算法,支持自定义棋盘大小、游戏复盘和实时评分。 +## Ŀ +Cʵֵ˻սϵͳæ-¼֦ŻļС㷨֧Զ̴СϷ̺ʵʱ֡ -## ✨ 功能特性 -- 🎮 人机对战模式 -- ⚙️ 可调棋盘尺寸(5x5到25x25) -- 🧠 智能AI决策(1-5级难度) -- 🔍 完整游戏复盘功能 -- 📊 实时对局评分系统 -- ↩️ 悔棋功能(可撤销上一步) -- 🖥️ 清晰的终端界面显示 -- ✔️ 健壮的输入验证(确保所有数字输入都在有效范围内) -- ⏱️ 可选的回合计时器 -- 💾 自动游戏记录保存 +## ? +- ? ˻սģʽ +- ?? ɵ̳ߴ(5x525x25) +- ? AI(1-5Ѷ) +- ? Ϸ̹ +- ? ʵʱԾϵͳ +- ?? 幦(ɳһ) +- ?? ն˽ʾ +- ?? ׳֤(ȷ붼ЧΧ) +- ?? ѡĻغϼʱ +- ? ԶϷ¼ -## 🚀 快速开始 +## ? ٿʼ -![游戏演示](https://your-gif-url.com/demo.gif) +![Ϸʾ](https://your-gif-url.com/demo.gif) -### 编译程序 +### ```bash -gcc 五子棋.c gobang.c game_mode.c -o output/五子棋.exe +gcc .c gobang.c game_mode.c -o output/.exe ``` -### 运行游戏 +### Ϸ ```bash -.\output\五子棋.exe +.\output\.exe ``` -## 🎯 游戏玩法 -1. 启动后设置棋盘大小(默认15x15) -2. 选择AI难度级别(1-5) -3. 输入坐标进行游戏(格式:行 列) - - 输入R/r可悔棋 -4. 游戏结束可查看完整复盘和评分 +## ? Ϸ淨 +1. ̴С(Ĭ15x15) +2. ѡAIѶȼ(1-5) +3. Ϸ(ʽ: ) + - R/rɻ +4. Ϸɲ鿴̺ -## 💻 开发环境 -- 操作系统: Windows (当前版本使用了Windows特有的 `_kbhit()` 和 `Sleep()` 函数,因此仅限Windows平台) -- 编译器: GCC (MinGW(gcc为14.2.0) on Windows) -- 终端: 支持UTF-8编码的终端 +## ? +- ϵͳ: Windows (ǰ汾ʹWindowsе `_kbhit()` `Sleep()` ˽Windowsƽ̨) +- : GCC (MinGW(gccΪ14.2.0) on Windows) +- ն: ֧UTF-8ն -> **跨平台兼容性说明:** +> **ƽ̨˵:** > -> 为了未来能在Linux或macOS等其他操作系统上运行,需要将平台特定的代码(如 `_kbhit()`)替换为跨平台的实现,或使用条件编译(`#ifdef _WIN32`)进行隔离。 +> ΪδLinuxmacOSϵͳУҪƽ̨ضĴ루 `_kbhit()`滻Ϊƽ̨ʵ֣ʹ루`#ifdef _WIN32`и롣 -## ⚠️ 常见问题 +## ?? -### 权限问题 -如果程序在保存游戏记录时提示“无法创建文件”或类似错误,通常是由于缺少写入权限。请尝试以下解决方案: +### Ȩ +ڱϷ¼ʱʾ޷ļƴͨȱдȨޡ볢½ -1. **以管理员身份运行**:右键点击 `五子棋.exe` 或在管理员权限的终端中运行程序。 -2. **检查目录权限**:确保程序所在目录不是系统保护目录(如 `C:\Program Files`)。建议将项目放在用户目录下(如 `D:\Code`)。 -3. **手动创建 `records` 目录**:如果 `records` 目录不存在,请在 `output` 目录下手动创建一个。 +1. **ԹԱ**Ҽ `.exe` ڹԱȨ޵նг +2. **Ŀ¼Ȩ**ȷĿ¼ϵͳĿ¼ `C:\Program Files`齫ĿûĿ¼£ `D:\Code` +3. **ֶ `records` Ŀ¼** `records` Ŀ¼ڣ `output` Ŀ¼ֶһ -### 中文显示问题 -如果在Windows终端中遇到中文字符显示为乱码,是由于终端编码页不匹配导致的。请在运行程序前执行以下命令: +### ʾ +WindowsնַʾΪ룬ն˱ҳƥ䵼µġгǰִ ```bash chcp 65001 ``` -该命令会将当前终端的编码页切换到UTF-8,从而正确显示中文字符。为方便起见,你可以创建一个启动脚本(`.bat` 文件)来自动执行此操作: +Ὣǰն˵ıҳлUTF-8ӶȷʾַΪԴһű`.bat` ļԶִд˲ **start_game.bat** ```batch @echo off chcp 65001 -.\output\五子棋.exe +.\output\.exe ``` -## 🛠️ 技术实现 +## ?? ʵ -项目的AI主要基于以下技术实现: +ĿAIҪ¼ʵ֣ -### 核心决策算法 +### ľ㷨 -- **极小极大算法 (Minimax)**:作为决策的基础,模拟对弈双方的每一步,选择对我方最有利的走法。 -- **α-β 剪枝 (Alpha-Beta Pruning)**:对极小极大算法的關鍵優化,通过剪掉不可能影响最终决策的搜索分支,大幅提升AI的计算效率,使其能够在有限时间内达到更深的搜索深度。 -- **搜索深度**:AI的思考深度,默认为3层,并可根据难度设置进行调整。搜索深度越深,AI的棋力越强,但计算耗时也越长。 +- **С㷨 (Minimax)**ΪߵĻģ˫ÿһѡҷ߷ +- **- ֦ (Alpha-Beta Pruning)**ԼС㷨PIͨӰվߵ֧AIļЧʣʹܹʱڴﵽȡ +- ****AI˼ȣĬΪ3㣬ɸѶýеԽAIԽǿʱҲԽ -### 启发式评估函数 +### ʽ -为了判断棋局的优劣,AI使用了一套复杂的启发式评估函数,主要包括: +ΪжֵӣAIʹһ׸ӵʽҪ -- **棋型识别 (Pattern Recognition)**:能够识别并评估多种关键棋型,如“活四”、“冲四”、“活三”、“眠三”等,并为每种棋型赋予不同权重。 -- **位置权重 (Positional Value)**:棋盘上不同位置的战略价值不同,中心位置通常比边缘位置更有优势。评估函数会为中心区域的落子给予额外加分。 -- **威胁检测 (Threat Detection)**:优先搜索能够直接形成制胜威胁的棋步,如“连五”或“活四”,从而快速响应对手的进攻或抓住制胜机会。 -- **双向延伸评估**:评估一个棋子在特定方向上是否有足够的空间形成连续棋型,避免在被封堵的位置浪费棋步。 +- **ʶ (Pattern Recognition)**ܹʶֹؼͣ硰ġġȣΪÿ͸費ͬȨء +- **λȨ (Positional Value)**ϲͬλõսԼֵͬλͨȱԵλøơΪӸӷ֡ +- **в (Threat Detection)**ֱܹγʤв岽硰塱򡰻ġӶӦֵĽץסʤᡣ +- **˫**һضǷ㹻Ŀռγͣڱµλ˷岽 -## 📂 代码结构 -- `五子棋.c` - 主程序入口,负责初始化和模式选择 -- `gobang.c` - 核心游戏逻辑,包括棋盘操作、胜负判断、AI算法等 -- `gobang.h` - `gobang.c` 的头文件,定义核心数据结构和函数原型 -- `game_mode.c` - 各种游戏模式的实现,如人机对战、双人对战和复盘模式 -- `game_mode.h` - `game_mode.c` 的头文件,定义游戏模式相关函数原型 +## ? ṹ +- `.c` - ڣʼģʽѡ +- `gobang.c` - Ϸ߼̲ʤжϡAI㷨 +- `gobang.h` - `gobang.c` ͷļݽṹͺԭ +- `game_mode.c` - Ϸģʽʵ֣˻ս˫˶ս͸ģʽ +- `game_mode.h` - `game_mode.c` ͷļϷģʽغԭ -## 📜 许可证 +## ? ֤ -该项目采用 [MIT 许可证](https://opensource.org/licenses/MIT)进行授权。 +Ŀ [MIT ֤](https://opensource.org/licenses/MIT)Ȩ -简单来说,你可以自由地使用、复制、修改、合并、出版、分发、再授权和/或销售本软件的副本,只需在你的项目中包含原始的版权和许可声明即可。 +˵ɵʹáơ޸ġϲ桢ַȨ/۱ĸֻĿаԭʼİȨɡ -## 🙋 反馈与贡献 +## ? 빱 -我们非常欢迎任何形式的反馈和贡献!如果你发现了Bug、有功能建议,或者希望改进代码,请随时通过以下方式参与: +Ƿdzӭκʽķ͹ף㷢Bugйܽ飬ϣĽ룬ʱͨ·ʽ룺 -- **提交 Issue**:如果你遇到问题或有新的想法,请在 [GitHub Issues](https://github.com/LHY0125/Gobang-Game/issues) 页面提交详细描述。 -- **发起 Pull Request**:如果你对代码进行了改进,欢迎提交 Pull Request。请确保你的代码风格与项目保持一致,并附上清晰的改动说明。 +- **ύ Issue**µ뷨 [GitHub Issues](https://github.com/LHY0125/Gobang-Game/issues) ҳύϸ +- ** Pull Request**Դ˸Ľӭύ Pull RequestȷĴĿһ£ĸĶ˵ -你的每一次贡献都将使这个项目变得更好! +ÿһι׶ʹĿøã -## 🚀 未来计划 +## ? δƻ -为了让这个项目变得更完善,我们计划在未来实现以下功能: +ΪĿøƣǼƻδʵ¹ܣ -- [ ] **图形用户界面 (GUI)**:使用 `SDL2` 或 `Qt` 等库,将当前的终端界面升级为图形化界面,提升用户体验。 -- [ ] **网络对战功能**:增加一个在线对战模式,允许两名玩家通过网络进行对战。 -- [ ] **棋谱库集成**:引入开局库,使AI在游戏初期能够选择更优的开局走法。 -- [ ] **代码重构与优化**:持续优化现有代码,提高模块化程度和运行效率,并实现完全的跨平台兼容性。 \ No newline at end of file +- [ ] **ͼû (GUI)**ʹ `SDL2` `Qt` ȿ⣬ǰն˽Ϊͼλ棬û顣 +- [ ] **ս**һ߶սģʽͨжս +- [ ] **׿⼯**뿪ֿ⣬ʹAIϷܹѡŵĿ߷ +- [ ] **عŻ**Żд룬ģ黯̶ȺЧʣʵȫĿƽ̨ԡ \ No newline at end of file diff --git a/ai.c b/ai.c new file mode 100644 index 0000000..062472c --- /dev/null +++ b/ai.c @@ -0,0 +1,325 @@ +#include "ai.h" +#include +#include +#include + +extern int BOARD_SIZE; +extern int board[MAX_BOARD_SIZE][MAX_BOARD_SIZE]; +extern int step_count; +extern const int direction[4][2]; +extern Step steps[MAX_STEPS]; + +/** + * @brief 评估特定位置对当前玩家的战略价值。 + * 此函数通过模拟在该位置落子,然后分析形成的棋型来为该位置打分。 + * 分数越高,代表该位置对指定玩家越有利。 + * @param x 要评估的行坐标 (0-based)。 + * @param y 要评估的列坐标 (0-based)。 + * @param player 玩家标识 (PLAYER 或 AI),代表为哪一方进行评估。 + * @return int 返回该位置的综合评估分数。 + * @note 评分系统设计: + * - 核心思想是为不同的棋型赋予不同的权重,棋型越接近胜利,权重越高。 + * - “活”棋型(两端无阻挡)比“眠”棋型(一端有阻挡)或“死”棋型(两端被阻挡)得分高得多, + * 因为它们有更大的发展潜力。 + * - 评分标准 (仅为示例,可调整以优化AI行为): + * - 连五: 1,000,000 (胜利) + * - 活四: 100,000 (下一步胜利) + * - 冲四: 10,000 (下一步可能胜利) + * - 活三: 5,000 (潜力巨大) + * - 眠三: 1,000 + * - 活二: 500 + * - 眠二: 100 + * - 其他: 更低的分数 + * - 位置奖励:棋盘中心区域通常具有更高的战略价值,因此会给予额外加分,鼓励AI占据中心。 + */ +int evaluate_pos(int x, int y, int player) +{ + // 保存原始值用于还原 + int original = board[x][y]; + // 模拟在该位置落子 + board[x][y] = player; + + int total_score = 0; // 总分 + int line_scores[4] = {0}; // 四个方向的得分 + + // 遍历四个方向进行评估 + for (int i = 0; i < 4; i++) + { + int dx = direction[i][0], dy = direction[i][1]; + // 获取当前方向上的棋型信息 + DirInfo info = count_specific_direction(x, y, dx, dy, player); + + // 直接形成五连珠为必胜 + if (info.continuous_chess >= 5) + { + board[x][y] = original; // 还原棋盘 + return 1000000; // 返回最大分 + } + + // 根据连续棋子数评分 + switch (info.continuous_chess) + { + case 4: // 四连珠 + if (info.check_start && info.check_end) // 活四(两端开放) + line_scores[i] = 100000; + else if (info.check_start || info.check_end) // 冲四(一端开放) + line_scores[i] = 10000; + else // 死四(两端封闭) + line_scores[i] = 500; + break; + + case 3: // 三连珠 + if (info.check_start && info.check_end) // 活三 + line_scores[i] = 5000; + else if (info.check_start || info.check_end) // 眠三 + line_scores[i] = 1000; + else // 死三 + line_scores[i] = 50; + break; + + case 2: // 二连珠 + if (info.check_start && info.check_end) // 活二 + line_scores[i] = 500; + else if (info.check_start || info.check_end) // 眠二 + line_scores[i] = 100; + else // 死二 + line_scores[i] = 10; + break; + + case 1: // 单子 + if (info.check_start && info.check_end) // 开放位置 + line_scores[i] = 50; + else if (info.check_start || info.check_end) // 半开放位置 + line_scores[i] = 10; + else // 封闭位置 + line_scores[i] = 1; + break; + } + } + + // 计算总分(最高方向分+其他方向分加权) + int max_score = 0; + int sum_score = 0; + for (int i = 0; i < 4; i++) + { + if (line_scores[i] > max_score) + max_score = line_scores[i]; + sum_score += line_scores[i]; + } + total_score = max_score * 10 + sum_score; // 主方向权重更高 + + // 位置奖励:越靠近中心分数越高 + int center_x = BOARD_SIZE / 2; + int center_y = BOARD_SIZE / 2; + int distance = abs(x - center_x) + abs(y - center_y); // 曼哈顿距离 + int position_bonus = 50 * (BOARD_SIZE - distance); // 距离中心越近奖励越高 + + board[x][y] = original; // 还原棋盘状态 + return total_score + position_bonus; // 返回总评估分 +} + +/** + * @brief 使用带α-β剪枝的深度优先搜索(Minimax算法)来寻找最佳落子点。 + * 该函数递归地探索未来的几步棋,评估不同选择的优劣,并选择最优策略。 + * @param x 上一步落子的行坐标。 + * @param y 上一步落子的列坐标。 + * @param player 当前轮到的玩家 (PLAYER 或 AI)。 + * @param depth 剩余的搜索深度。深度越大,AI看得越远,但计算量也越大。 + * @param alpha α值,极大化玩家(AI)当前能确保的最好结果(下界)。 + * @param beta β值,极小化玩家(对手)当前能确保的最好结果(上界)。 + * @param is_maximizing 布尔值,true表示当前是极大化玩家(AI)的回合,false表示是极小化玩家(对手)的回合。 + * @return int 返回在当前分支下的最佳评估分数。 + * @note 算法核心思想: + * 1. **递归终止条件**: 当游戏出现胜负、平局,或达到预设的搜索深度时,停止递归,并返回当前局面的静态评估分数。 + * 2. **极大化玩家 (AI)**: 尝试所有可能的落子,并选择能使其得分最大化的那一步。此过程中,会不断更新alpha值。 + * 3. **极小化玩家 (对手)**: 模拟对手的走法,并假设对手会选择能使AI得分最小化的那一步。此过程中,会不断更新beta值。 + * 4. **α-β剪枝**: 这是对Minimax算法的关键优化。 + * - **α剪枝**: 在极小化玩家的回合中,如果发现一个走法得到的分数比alpha还低,那么这个分支可以被剪掉, + * 因为极大化玩家绝不会选择进入这个分支(他有更好的选择)。(if beta <= alpha) + * - **β剪枝**: 在极大化玩家的回ah合中,如果发现一个走法得到的分数比beta还高,那么这个分支也可以被剪掉, + * 因为极小化玩家绝不会让游戏进入这个分支(他有更好的选择来阻止)。(if beta <= alpha) + * 通过剪枝,可以避免对大量无效分支的搜索,极大地提高了AI的决策效率。 + */ +int dfs(int x, int y, int player, int depth, int alpha, int beta, bool is_maximizing) +{ + // 检查当前落子是否获胜 + if (check_win(x, y, player)) + { + return (player == AI) ? 1000000 + depth : -1000000 - depth; + } + + // 达到搜索深度或平局 + if (depth == 0 || step_count >= BOARD_SIZE * BOARD_SIZE) + { + return evaluate_pos(x, y, AI) - evaluate_pos(x, y, PLAYER); + } + + int best_score = is_maximizing ? -1000000 : 1000000; + + // 遍历所有可能落子位置 + for (int i = 0; i < BOARD_SIZE; i++) + { + for (int j = 0; j < BOARD_SIZE; j++) + { + if (board[i][j] != EMPTY) + continue; + + // 模拟当前玩家落子 + board[i][j] = player; + step_count++; + + // 递归搜索(切换玩家和搜索深度) + int current_score = dfs(i, j, (player == AI) ? PLAYER : AI, depth - 1, alpha, beta, !is_maximizing); + + // 撤销落子 + board[i][j] = EMPTY; + step_count--; + + // 极大值玩家(AI)逻辑 + if (is_maximizing) + { + best_score = (current_score > best_score) ? current_score : best_score; + alpha = (best_score > alpha) ? best_score : alpha; + // α剪枝 + if (beta <= alpha) + { + break; + } + } + // 极小值玩家(人类)逻辑 + else + { + best_score = (current_score < best_score) ? current_score : best_score; + beta = (best_score < beta) ? best_score : beta; + // β剪枝 + if (beta <= alpha) + { + break; + } + } + } + if ((is_maximizing && best_score >= beta) || (!is_maximizing && best_score <= alpha)) + { + break; // 提前退出外层循环 + } + } + + return best_score; +} + +/** + * @brief AI决策主函数,使用评估函数和搜索算法选择最佳落子位置 + * @note 采用两阶段决策逻辑: + * 1. 防御阶段:检查并阻止玩家即将获胜的位置(活四、冲四、活三) + * 2. 进攻阶段:若无紧急防御需求,使用DFS评估选择最佳进攻位置 + * @note 实现细节: + * - 优先处理玩家活四、冲四等危险局面 + * - 步数>10时缩小搜索范围到已有棋子附近2格 + * - 使用中心位置优先策略 + */ +void ai_move(int depth) +{ + // 1. 首先检查是否需要阻止玩家的四子连棋或三子活棋 + for (int i = 0; i < BOARD_SIZE; i++) + { + for (int j = 0; j < BOARD_SIZE; j++) + { + if (board[i][j] != EMPTY) + continue; + + // 模拟玩家在此位置落子 + board[i][j] = PLAYER; + bool need_block = false; + + // 检查四个方向 + for (int k = 0; k < 4; k++) + { + DirInfo info = count_specific_direction(i, j, direction[k][0], direction[k][1], PLAYER); + + // 如果玩家能形成四子连棋且至少一端开放 + if (info.continuous_chess >= 4 && (info.check_start || info.check_end)) + { + need_block = true; + break; + } + + // 如果玩家能形成三子活棋且两端开放 + if (info.continuous_chess == 3 && info.check_start && info.check_end) + { + need_block = true; + break; + } + } + + board[i][j] = EMPTY; // 恢复棋盘 + + if (need_block) + { + // 必须在此位置落子阻止 + board[i][j] = AI; + steps[step_count++] = (Step){AI, i, j}; + printf("AI落子(%d, %d)\n", i + 1, j + 1); + return; + } + } + } + + // 2. 如果没有需要立即阻止的情况,则正常评估 + int best_score = -1000000; + int best_x = -1, best_y = -1; + + // 遍历棋盘所有空位 + for (int i = 0; i < BOARD_SIZE; i++) + { + for (int j = 0; j < BOARD_SIZE; j++) + { + if (board[i][j] != EMPTY) + continue; + + // 只考虑已有棋子附近(2格范围内) + bool has_nearby_stone = false; + for (int di = -2; di <= 2; di++) + { + for (int dj = -2; dj <= 2; dj++) + { + int ni = i + di; + int nj = j + dj; + if (ni >= 0 && ni < BOARD_SIZE && + nj >= 0 && nj < BOARD_SIZE) + { + if (board[ni][nj] != EMPTY) + { + has_nearby_stone = true; + break; + } + } + } + if (has_nearby_stone) + break; + } + if (!has_nearby_stone && step_count > 10) + continue; + + // 模拟AI落子 + board[i][j] = AI; + int current_score = dfs(i, j, PLAYER, depth, -1000000, 1000000, false); + board[i][j] = EMPTY; + + // 更新最佳位置 + if (current_score > best_score) + { + best_score = current_score; + best_x = i; + best_y = j; + } + } + } + + // 执行最佳落子 + if (best_x != -1 && best_y != -1) + { + board[best_x][best_y] = AI; + steps[step_count++] = (Step){AI, best_x, best_y}; + printf("AI落子(%d, %d)\n", best_x + 1, best_y + 1); + } +} \ No newline at end of file diff --git a/ai.h b/ai.h new file mode 100644 index 0000000..318a9cb --- /dev/null +++ b/ai.h @@ -0,0 +1,34 @@ +#ifndef AI_H +#define AI_H + +#include "gobang.h" + +/** + * @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); + +#endif // AI_H \ No newline at end of file diff --git a/game_mode.c b/game_mode.c index b8950fc..ab1caac 100644 --- a/game_mode.c +++ b/game_mode.c @@ -1,4 +1,6 @@ +#include "game_mode.h" #include "gobang.h" +#include "ai.h" #include #include #include @@ -11,7 +13,7 @@ /** * @brief 从用户获取整数输入 - * + * * @param prompt 提示信息 * @param min 最小值 * @param max 最大值 @@ -47,48 +49,17 @@ int get_integer_input(const char *prompt, int min, int max) /** * @brief 处理玩家回合 - * - * @param current_player - * @return true - * @return false + * + * @param current_player + * @return true + * @return false */ -bool handle_player_turn(int current_player) +bool parse_player_input(int *x, int *y) { - 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); @@ -97,36 +68,103 @@ bool handle_player_turn(int current_player) Sleep(100); // a small delay to prevent high CPU usage } - if (input[0] == 'r' || input[0] == 'R') + if (sscanf(input, "%d", x) == 1) { - 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)) + // Successfully parsed the first number, now parse the second + if (scanf("%d", y) != 1) { - printf("成功悔棋 %d 步!\n", steps_to_undo); - print_board(); + // Check for special commands if second number is not available + if (*x == INPUT_UNDO) + { + int steps_to_undo; + printf("请输入要悔棋的步数(双方各退一步): "); + steps_to_undo = get_integer_input("", 1, step_count / 2); + if (return_move(steps_to_undo * 2)) + { + printf("成功悔棋,双方各退 %d 步!\n", steps_to_undo); + print_board(); + } + else + { + printf("无法悔棋!\n"); + } + return false; // Special command + } + else if (*x == INPUT_SAVE) + { + // ... handle save ... + return false; // Special command + } + else if (*x == INPUT_EXIT) + { + // ... handle exit ... + return false; // Special command + } + printf("无效输入,请输入两个数字坐标。"); + while (getchar() != '\n') + ; + return false; // Invalid input + } + } + else + { + // sscanf failed, check for 'r' or 'R' + if (input[0] == 'r' || input[0] == 'R') + { + int steps_to_undo; + printf("请输入要悔棋的步数(双方各退一步): "); + steps_to_undo = get_integer_input("", 1, step_count / 2); + if (return_move(steps_to_undo * 2)) + { + printf("成功悔棋,双方各退 %d 步!\n", steps_to_undo); + print_board(); + } + else + { + printf("无法悔棋!\n"); + } + return false; // Special command + } + printf("无效输入,请输入数字坐标或'r'悔棋。"); + return false; // Invalid input + } + return true; // Valid coordinates +} + +bool handle_player_turn(int current_player) +{ + int x, y; + time_t start_time, end_time; + if (use_timer) + { + time(&start_time); + } + + 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) + { + printf("\n玩家%d超时, 对方获胜!\n", current_player); + return false; // Timeout + } + } + + if (parse_player_input(&x, &y)) + { + break; // Valid input received } else { - printf("无法悔棋!\n"); + // Special command or invalid input handled, just continue the loop + return true; } - return true; // 悔棋操作后,回合算作成功,但不进行落子 } - if (sscanf(input, "%d", &x) != 1) - { - printf("无效输入,请输入数字坐标。"); - return true; // 输入无效,但回合继续 - } - if (scanf("%d", &y) != 1) - { - printf("无效输入,请输入数字坐标。"); - while (getchar() != '\n') - ; - return true; // 输入无效,但回合继续 - } x--; y--; @@ -139,14 +177,7 @@ bool handle_player_turn(int current_player) if (check_win(x, y, current_player)) { - if (current_player == 1) - { - printf("\n玩家获胜!\n"); - } - else - { - printf("\n玩家%d获胜!\n", current_player); - } + printf("\n玩家%d获胜!\n", current_player); return false; // 游戏结束 } @@ -242,7 +273,7 @@ void run_pvp_game() // 双人对战模式 setup_board_size(); empty_board(); - int current_player = determine_first_player(PLAYER3, PLAYER4); + int current_player = determine_first_player(PLAYER1, PLAYER2); print_board(); while (1) @@ -261,7 +292,7 @@ void run_pvp_game() if (step_count > old_step_count) { - current_player = (current_player == PLAYER3) ? PLAYER4 : PLAYER3; + current_player = (current_player == PLAYER1) ? PLAYER2 : PLAYER1; } } printf("===== 游戏结束 =====\n"); @@ -307,7 +338,7 @@ void run_review_mode() { printf("%d. %s\n", i + 1, record_files[i]); } - + char prompt[150]; sprintf(prompt, "请输入复盘文件编号(1-%d),或输入0以手动输入文件名: ", file_count); int choice = get_integer_input(prompt, 0, file_count); @@ -321,7 +352,8 @@ void run_review_mode() printf("请输入完整文件名: "); scanf("%s", filename); int c; - while ((c = getchar()) != '\n' && c != EOF); + while ((c = getchar()) != '\n' && c != EOF) + ; int possible_choice = atoi(filename); if (possible_choice > 0 && possible_choice <= file_count) @@ -335,7 +367,8 @@ void run_review_mode() printf("未找到任何复盘文件,请输入复盘文件地址: "); scanf("%s", filename); int c; - while ((c = getchar()) != '\n' && c != EOF); + while ((c = getchar()) != '\n' && c != EOF) + ; } int game_mode = load_game_from_file(filename); diff --git a/game_mode.h b/game_mode.h index 466e508..c83d665 100644 --- a/game_mode.h +++ b/game_mode.h @@ -3,7 +3,7 @@ * @author 刘航宇(3364451258@qq.com、15236416560@163.com、lhy3364451258@outlook.com) * @brief 五子棋游戏框架头文件 * @version 3.0 - * @date 2025-06-26 + * @date 2025-06-30 * * @copyright Copyright (c) 2025 * @@ -18,9 +18,14 @@ #include "gobang.h" +// 特殊输入命令 +#define INPUT_UNDO -1 +#define INPUT_SAVE -2 +#define INPUT_EXIT -3 + /** * @brief 从用户获取整数输入 - * + * * @param prompt 提示信息 * @param min 最小值 * @param max 最大值 @@ -30,10 +35,18 @@ int get_integer_input(const char *prompt, int min, int max); /** * @brief 处理玩家回合 - * - * @param current_player - * @return true - * @return false + * + * @param x 玩家输入的横坐标 + * @param y 玩家输入的纵坐标 + * @return true 输入有效 + * @return false 输入无效 + */ +bool parse_player_input(int *x, int *y); + +/** + * @brief 处理AI回合 + * + * @param current_player 当前玩家 */ bool handle_player_turn(int current_player); diff --git a/gobang.c b/gobang.c index bdce2ba..999609d 100644 --- a/gobang.c +++ b/gobang.c @@ -1,14 +1,16 @@ #include "gobang.h" #include "game_mode.h" +#include "ai.h" #include #include #include // 全局变量定义 -int BOARD_SIZE = 15; // 实际使用的棋盘尺寸(默认15) -int board[MAX_BOARD_SIZE][MAX_BOARD_SIZE] = {0}; // 棋盘状态存储数组(默认棋盘全空为0) -const int direction[4][2] = {{1, 0}, {0, 1}, {1, 1}, {1, -1}}; // 四个方向:向下、向右、右下、左下 +int BOARD_SIZE = 15; // 实际使用的棋盘尺寸(默认15) +int board[MAX_BOARD_SIZE][MAX_BOARD_SIZE] = {0}; // 棋盘状态存储数组(默认棋盘全空为0) + Step steps[MAX_STEPS]; // 存储所有落子步骤的数组 +const int direction[4][2] = {{1, 0}, {0, 1}, {1, 1}, {1, -1}}; // 四个方向:向下、向右、右下、左下 int step_count = 0; // 当前步数计数器 bool use_forbidden_moves = false; // 默认不启用禁手规则 int use_timer = 0; // 默认不启用计时器 @@ -144,7 +146,7 @@ bool is_forbidden_move(int x, int y, int player) { return false; } - if (player != PLAYER && player != PLAYER3) + if (player != PLAYER && player != PLAYER1) { return false; } @@ -269,306 +271,6 @@ bool check_win(int x, int y, int player) return false; // 四个方向都没有五连珠 } -/** - * @brief 评估特定位置对当前玩家的战略价值 - * @param x 行坐标(0-base) - * @param y 列坐标(0-base) - * @param player 玩家标识(PLAYER/AI) - * @return int 综合评估分数(越高表示位置越好) - * @note 评分标准: - * - 活四:100000 冲四:10000 死四:500 - * - 活三:5000 眠三:1000 死三:50 - * - 活二:500 眠二:100 死二:10 - * - 单子:50(开放)/10(半开放)/1(封闭) - * - 中心位置有额外加成 - */ -int evaluate_pos(int x, int y, int player) -{ - // 保存原始值用于还原 - int original = board[x][y]; - // 模拟在该位置落子 - board[x][y] = player; - - int total_score = 0; // 总分 - int line_scores[4] = {0}; // 四个方向的得分 - - // 遍历四个方向进行评估 - for (int i = 0; i < 4; i++) - { - int dx = direction[i][0], dy = direction[i][1]; - // 获取当前方向上的棋型信息 - DirInfo info = count_specific_direction(x, y, dx, dy, player); - - // 直接形成五连珠为必胜 - if (info.continuous_chess >= 5) - { - board[x][y] = original; // 还原棋盘 - return 1000000; // 返回最大分 - } - - // 根据连续棋子数评分 - switch (info.continuous_chess) - { - case 4: // 四连珠 - if (info.check_start && info.check_end) // 活四(两端开放) - line_scores[i] = 100000; - else if (info.check_start || info.check_end) // 冲四(一端开放) - line_scores[i] = 10000; - else // 死四(两端封闭) - line_scores[i] = 500; - break; - - case 3: // 三连珠 - if (info.check_start && info.check_end) // 活三 - line_scores[i] = 5000; - else if (info.check_start || info.check_end) // 眠三 - line_scores[i] = 1000; - else // 死三 - line_scores[i] = 50; - break; - - case 2: // 二连珠 - if (info.check_start && info.check_end) // 活二 - line_scores[i] = 500; - else if (info.check_start || info.check_end) // 眠二 - line_scores[i] = 100; - else // 死二 - line_scores[i] = 10; - break; - - case 1: // 单子 - if (info.check_start && info.check_end) // 开放位置 - line_scores[i] = 50; - else if (info.check_start || info.check_end) // 半开放位置 - line_scores[i] = 10; - else // 封闭位置 - line_scores[i] = 1; - break; - } - } - - // 计算总分(最高方向分+其他方向分加权) - int max_score = 0; - int sum_score = 0; - for (int i = 0; i < 4; i++) - { - if (line_scores[i] > max_score) - max_score = line_scores[i]; - sum_score += line_scores[i]; - } - total_score = max_score * 10 + sum_score; // 主方向权重更高 - - // 位置奖励:越靠近中心分数越高 - int center_x = BOARD_SIZE / 2; - int center_y = BOARD_SIZE / 2; - int distance = abs(x - center_x) + abs(y - center_y); // 曼哈顿距离 - int position_bonus = 50 * (BOARD_SIZE - distance); // 距离中心越近奖励越高 - - board[x][y] = original; // 还原棋盘状态 - return total_score + position_bonus; // 返回总评估分 -} - -/** - * @brief 带α-β剪枝的深度优先搜索(极小极大算法实现) - * @param x 当前行坐标 - * @param y 当前列坐标 - * @param player 当前玩家 - * @param depth 剩余搜索深度 - * @param alpha α值(当前最大值) - * @param beta β值(当前最小值) - * @param is_maximizing 是否为极大化玩家(AI) - * @return int 最佳评估分数 - * @note 算法流程: - * 1. 检查是否获胜或达到搜索深度 - * 2. 遍历所有可能落子位置 - * 3. 递归评估每个位置的分数 - * 4. 根据is_maximizing选择最大/最小值 - * 5. 使用α-β剪枝优化搜索过程 - */ -int dfs(int x, int y, int player, int depth, int alpha, int beta, bool is_maximizing) -{ - // 检查当前落子是否获胜 - if (check_win(x, y, player)) - { - return (player == AI) ? 1000000 + depth : -1000000 - depth; - } - - // 达到搜索深度或平局 - if (depth == 0 || step_count >= BOARD_SIZE * BOARD_SIZE) - { - return evaluate_pos(x, y, AI) - evaluate_pos(x, y, PLAYER); - } - - int best_score = is_maximizing ? -1000000 : 1000000; - - // 遍历所有可能落子位置 - for (int i = 0; i < BOARD_SIZE; i++) - { - for (int j = 0; j < BOARD_SIZE; j++) - { - if (board[i][j] != EMPTY) - continue; - - // 模拟当前玩家落子 - board[i][j] = player; - step_count++; - - // 递归搜索(切换玩家和搜索深度) - int current_score = dfs(i, j, (player == AI) ? PLAYER : AI, depth - 1, alpha, beta, !is_maximizing); - - // 撤销落子 - board[i][j] = EMPTY; - step_count--; - - // 极大值玩家(AI)逻辑 - if (is_maximizing) - { - best_score = (current_score > best_score) ? current_score : best_score; - alpha = (best_score > alpha) ? best_score : alpha; - // α剪枝 - if (beta <= alpha) - { - break; - } - } - // 极小值玩家(人类)逻辑 - else - { - best_score = (current_score < best_score) ? current_score : best_score; - beta = (best_score < beta) ? best_score : beta; - // β剪枝 - if (beta <= alpha) - { - break; - } - } - } - if ((is_maximizing && best_score >= beta) || (!is_maximizing && best_score <= alpha)) - { - break; // 提前退出外层循环 - } - } - - return best_score; -} - -/** - * @brief AI决策主函数,使用评估函数和搜索算法选择最佳落子位置 - * @note 采用两阶段决策逻辑: - * 1. 防御阶段:检查并阻止玩家即将获胜的位置(活四、冲四、活三) - * 2. 进攻阶段:若无紧急防御需求,使用DFS评估选择最佳进攻位置 - * @note 实现细节: - * - 优先处理玩家活四、冲四等危险局面 - * - 步数>10时缩小搜索范围到已有棋子附近2格 - * - 使用中心位置优先策略 - */ -void ai_move(int depth) -{ - // 1. 首先检查是否需要阻止玩家的四子连棋或三子活棋 - for (int i = 0; i < BOARD_SIZE; i++) - { - for (int j = 0; j < BOARD_SIZE; j++) - { - if (board[i][j] != EMPTY) - continue; - - // 模拟玩家在此位置落子 - board[i][j] = PLAYER; - bool need_block = false; - - // 检查四个方向 - for (int k = 0; k < 4; k++) - { - DirInfo info = count_specific_direction(i, j, direction[k][0], direction[k][1], PLAYER); - - // 如果玩家能形成四子连棋且至少一端开放 - if (info.continuous_chess >= 4 && (info.check_start || info.check_end)) - { - need_block = true; - break; - } - - // 如果玩家能形成三子活棋且两端开放 - if (info.continuous_chess == 3 && info.check_start && info.check_end) - { - need_block = true; - break; - } - } - - board[i][j] = EMPTY; // 恢复棋盘 - - if (need_block) - { - // 必须在此位置落子阻止 - board[i][j] = AI; - steps[step_count++] = (Step){AI, i, j}; - printf("AI落子(%d, %d)\n", i + 1, j + 1); - return; - } - } - } - - // 2. 如果没有需要立即阻止的情况,则正常评估 - int best_score = -1000000; - int best_x = -1, best_y = -1; - - // 遍历棋盘所有空位 - for (int i = 0; i < BOARD_SIZE; i++) - { - for (int j = 0; j < BOARD_SIZE; j++) - { - if (board[i][j] != EMPTY) - continue; - - // 只考虑已有棋子附近(2格范围内) - bool has_nearby_stone = false; - for (int di = -2; di <= 2; di++) - { - for (int dj = -2; dj <= 2; dj++) - { - int ni = i + di; - int nj = j + dj; - if (ni >= 0 && ni < BOARD_SIZE && - nj >= 0 && nj < BOARD_SIZE) - { - if (board[ni][nj] != EMPTY) - { - has_nearby_stone = true; - break; - } - } - } - if (has_nearby_stone) - break; - } - if (!has_nearby_stone && step_count > 10) - continue; - - // 模拟AI落子 - board[i][j] = AI; - int current_score = dfs(i, j, PLAYER, depth, -1000000, 1000000, false); - board[i][j] = EMPTY; - - // 更新最佳位置 - if (current_score > best_score) - { - best_score = current_score; - best_x = i; - best_y = j; - } - } - } - - // 执行最佳落子 - if (best_x != -1 && best_y != -1) - { - board[best_x][best_y] = AI; - steps[step_count++] = (Step){AI, best_x, best_y}; - printf("AI落子(%d, %d)\n", best_x + 1, best_y + 1); - } -} - /** * @brief 复盘游戏全过程并展示评分 * @note 实现流程: @@ -611,7 +313,7 @@ void review_process(int game_mode) // 打印当前步骤信息 // 根据游戏模式显示不同的标题和玩家信息 if (game_mode == 1) - { + { // 人机对战 printf("\n===== 五子棋人机对战(%dX%d棋盘) =====", BOARD_SIZE, BOARD_SIZE); printf("\n 第%d步/%d步: %s 落子于(%d, %d)\n", @@ -620,12 +322,12 @@ void review_process(int game_mode) s.x + 1, s.y + 1); } else - { + { // 双人对战 printf("\n===== 五子棋双人对战(%dX%d棋盘) =====", BOARD_SIZE, BOARD_SIZE); printf("\n 第%d步/%d步: %s 落子于(%d, %d)\n", i + 1, step_count, - (s.player == PLAYER3) ? "玩家1(黑棋)" : "玩家2(白棋)", + (s.player == PLAYER1) ? "玩家1(黑棋)" : "玩家2(白棋)", s.x + 1, s.y + 1); } @@ -640,9 +342,9 @@ void review_process(int game_mode) printf("%2d ", row + 1); // 行号 for (int col = 0; col < BOARD_SIZE; col++) { - if (temp_board[row][col] == PLAYER || temp_board[row][col] == PLAYER3) + if (temp_board[row][col] == PLAYER || temp_board[row][col] == PLAYER1) printf("x "); - else if (temp_board[row][col] == AI || temp_board[row][col] == PLAYER4) + else if (temp_board[row][col] == AI || temp_board[row][col] == PLAYER2) printf("○ "); else printf("· "); @@ -668,10 +370,10 @@ void review_process(int game_mode) // 遍历所有步数,累积每一步的得分 for (int i = 0; i < step_count; i++) { - if (steps[i].player == PLAYER || steps[i].player == PLAYER3) + if (steps[i].player == PLAYER || steps[i].player == PLAYER1) { player1_score += calculate_step_score(steps[i].x, steps[i].y, steps[i].player); - } + } else { player2_score += calculate_step_score(steps[i].x, steps[i].y, steps[i].player); @@ -688,7 +390,7 @@ void review_process(int game_mode) player1_score, (double)player1_score * 100.0 / sum_score); printf("AI得分: %d, 占比: %.2f%%\n", player2_score, (double)player2_score * 100.0 / sum_score); - } + } else { printf("玩家1(黑棋)得分: %d, 占比: %.2f%%\n", @@ -703,7 +405,7 @@ void review_process(int game_mode) { printf("玩家得分: %d\n", player1_score); printf("AI得分: %d\n", player2_score); - } + } else { printf("玩家1(黑棋)得分: %d\n", player1_score); @@ -752,7 +454,7 @@ void handle_save_record(int game_mode) { case 0: // 成功 printf("\n游戏记录已成功保存至: %s\n", filename); - printf("您可以使用以下命令进行复盘: .\\五子棋.exe -l %s\n", filename); + printf("您可以使用以下命令进行复盘: .\\gobang.exe -l %s\n", filename); break; case 1: // 目录创建失败 printf("\n游戏记录保存失败: 无法创建 'records' 目录。\n"); @@ -775,7 +477,7 @@ void handle_save_record(int game_mode) /** * @brief 悔棋功能实现 - * + * * @param steps_to_undo 要悔棋的步数 * @return true 悔棋成功 * @return false 悔棋失败(步数不足) @@ -789,14 +491,16 @@ bool return_move(int steps_to_undo) for (int i = 0; i < steps_to_undo; i++) { - step_count--; - board[steps[step_count].x][steps[step_count].y] = EMPTY; + if (step_count > 0) + { + step_count--; + board[steps[step_count].x][steps[step_count].y] = EMPTY; + } } return true; } - /** * @brief 评估玩家在整盘棋局中的表现 * @param player 要评估的玩家(PLAYER/AI) diff --git a/gobang.exe b/gobang.exe index f32c0f51c517186b1a3bec4155d635b05f23b572..1537c11b58cb28349628ed02cdda8450e9d40734 100644 GIT binary patch delta 20313 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 diff --git a/gobang.h b/gobang.h index 757ec02..4482189 100644 --- a/gobang.h +++ b/gobang.h @@ -3,7 +3,7 @@ * @author 刘航宇(3364451258@qq.com、15236416560@163.com、lhy3364451258@outlook.com) * @brief 五子棋游戏头文件 * @version 3.0 - * @date 2025-06-26 + * @date 2025-06-30 * * @copyright Copyright (c) 2025 * @@ -41,8 +41,8 @@ */ #define PLAYER 1 // 玩家棋子标识符 #define AI 2 // AI棋子标识符 -#define PLAYER3 3 // 玩家3棋子标识符 -#define PLAYER4 4 // 玩家4棋子标识符 +#define PLAYER1 1 // 玩家1棋子标识符 +#define PLAYER2 2 // 玩家2棋子标识符 /** * @brief 空位置标识符 @@ -63,6 +63,7 @@ extern int step_count; // 当前步数计数器 extern bool use_forbidden_moves; // 是否启用禁手规则 extern int use_timer; // 是否启用计时器 extern int time_limit; // 时间限制(秒) +extern const int direction[4][2]; /** * @brief 落子步骤记录结构体 @@ -175,34 +176,6 @@ DirInfo count_specific_direction(int x, int y, int dx, int dy, int player); */ 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 悔棋成功 @@ -233,7 +206,6 @@ int evaluate_performance(int player); */ int calculate_step_score(int x, int y, int player); - /** * @brief 将当前游戏记录保存到文件 * @param filename 要保存的文件名 diff --git a/五子棋.c b/五子棋.c index 7e509c7..0b36773 100644 --- a/五子棋.c +++ b/五子棋.c @@ -7,10 +7,10 @@ /** * @brief 将指令复制到powershell - * gcc 五子棋.c gobang.c game_mode.c -o output/五子棋.exe + * gcc 五子棋.c gobang.c game_mode.c ai.c -o gobang.exe * gcc 为编译器,五子棋.c gobang.c game_mode.c 为源文件,output/为输出目录 * @brief 将指令复制到powershell - * .\output\五子棋.exe + * .\gobang.exe */ int main(int argc, char *argv[]) @@ -24,24 +24,32 @@ int main(int argc, char *argv[]) #endif // 选择模式 - printf("===== 五子棋游戏 =====\n"); - printf("1. AI模式\n"); - printf("2. 玩家比赛\n"); - printf("3. 复盘模式\n"); - int mode = get_integer_input("请输入模式(1/2/3): ", 1, 3); + while(1) + { + printf("===== 五子棋游戏 =====\n"); + printf("1. AI模式\n"); + printf("2. 玩家比赛\n"); + printf("3. 复盘模式\n"); + printf("4. 退出游戏\n"); + int mode = get_integer_input("请输入模式(1/2/3/4): ", 1, 4); - if (mode == 1) - { - run_ai_game(); + if (mode == 1) + { + run_ai_game(); + } + else if (mode == 2) + { + run_pvp_game(); + } + else if (mode == 3) + { + run_review_mode(); + } + else if (mode == 4) + { + break; + } } - else if (mode == 2) - { - run_pvp_game(); - } - else if (mode == 3) - { - run_review_mode(); - } - + return 0; } \ No newline at end of file