From 38cd4419080df76a15add1b005bf853a0b467a37 Mon Sep 17 00:00:00 2001 From: ninemine <1371605831@qq.com> Date: Tue, 28 Oct 2025 17:22:49 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E4=BA=94=E5=AD=90=E6=A3=8B?= =?UTF-8?q?=E6=B8=B8=E6=88=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .tasks/2025-10-28_1_gomoku.md | 212 +++++++++++++++ config.py | 4 + games/base.py | 11 +- games/gomoku.py | 496 ++++++++++++++++++++++++++++++++++ games/gomoku_logic.py | 286 ++++++++++++++++++++ routers/callback.py | 6 + utils/parser.py | 5 + 7 files changed, 1019 insertions(+), 1 deletion(-) create mode 100644 .tasks/2025-10-28_1_gomoku.md create mode 100644 games/gomoku.py create mode 100644 games/gomoku_logic.py diff --git a/.tasks/2025-10-28_1_gomoku.md b/.tasks/2025-10-28_1_gomoku.md new file mode 100644 index 0000000..ed08a03 --- /dev/null +++ b/.tasks/2025-10-28_1_gomoku.md @@ -0,0 +1,212 @@ +# 背景 +文件名:2025-10-28_1_gomoku.md +创建于:2025-10-28_17:08:29 +创建者:User +主分支:main +任务分支:task/gomoku_2025-10-28_1 +Yolo模式:Off + +# 任务描述 +创建一个五子棋(Gomoku)游戏模块,支持双人对战功能。 + +## 核心需求 +1. **游戏模式**:双人对战(两个用户在同一个聊天中对战) +2. **棋盘规格**:标准15x15棋盘 +3. **禁手规则**:需要实现禁手规则(三三禁手、四四禁手、长连禁手) +4. **超时规则**:不需要回合时间限制 +5. **并发对战**:允许多轮对战同时存在,只要交战双方不同即可 +6. **显示方式**:使用emoji绘制棋盘(⚫⚪➕)+ 坐标系统(A-O列,1-15行) + +## 功能清单 +- 开始游戏:`.gomoku start @对手` 或 `.gomoku @对手` +- 落子:`.gomoku A1` 或 `.gomoku 落子 A1` +- 认输:`.gomoku resign` 或 `.gomoku 认输` +- 查看棋盘:`.gomoku show` 或 `.gomoku 查看` +- 查看战绩:`.gomoku stats` 或 `.gomoku 战绩` +- 帮助信息:`.gomoku help` 或 `.gomoku 帮助` + +## 技术要点 +1. 继承`BaseGame`基类 +2. 游戏状态存储在数据库中(使用chat_id + 对战双方ID作为键) +3. 需要实现五子棋禁手规则的判定逻辑 +4. 需要实现胜负判定(五子连珠) +5. 棋盘使用二维数组表示,支持坐标转换(A-O, 1-15) + +# 项目概览 + +## 现有架构 +- **框架**:FastAPI +- **数据库**:SQLite(使用标准库sqlite3) +- **游戏基类**:`games/base.py - BaseGame` +- **路由处理**:`routers/callback.py` +- **数据库操作**:`core/database.py - Database类` + +## 现有游戏 +- 石头剪刀布(rps) +- 问答游戏(quiz) +- 猜数字(guess) +- 成语接龙(idiom) +- 骰娘系统(dice) +- 运势占卜(fortune) + +## 数据库表结构 +1. **users**:用户基本信息 +2. **game_states**:游戏状态(支持chat_id, user_id, game_type的唯一约束) +3. **game_stats**:游戏统计(wins, losses, draws, total_plays) + +# 分析 + +## 核心挑战 + +### 1. 游戏状态管理 +- 现有的`game_states`表使用`(chat_id, user_id, game_type)`作为唯一键 +- 五子棋需要双人对战,需要同时记录两个玩家 +- 需要设计状态数据结构,存储: + - 对战双方ID(player1_id, player2_id) + - 当前轮到谁(current_player_id) + - 棋盘状态(15x15二维数组) + - 游戏状态(waiting, playing, finished) + - 胜者ID(winner_id,如果有) + +### 2. 多轮对战并发 +- 允许同一个chat中有多轮对战,只要对战双方不同 +- 需要一个机制来标识不同的对战局(可以用对战双方ID的组合) +- 状态查询需要能够找到特定用户参与的对战 + +### 3. 禁手规则实现 +禁手规则(仅对黑方,即先手玩家): +- **三三禁手**:一手棋同时形成两个或以上的活三 +- **四四禁手**:一手棋同时形成两个或以上的活四或冲四 +- **长连禁手**:一手棋形成六子或以上的连珠 + +需要实现: +- 判断某个位置的四个方向(横、竖、左斜、右斜)的连珠情况 +- 判断活三、活四、冲四的定义 +- 在落子时检查是否触发禁手 + +### 4. 坐标系统 +- 列:A-O(15列) +- 行:1-15(15行) +- 需要坐标转换函数:`parse_coord("A1") -> (0, 0)` +- 需要显示转换函数:`format_coord(0, 0) -> "A1"` + +### 5. 棋盘显示 +使用emoji: +- ⚫ 黑子(先手) +- ⚪ 白子(后手) +- ➕ 空位 +- 需要添加行号和列号标注 + +示例: +``` + A B C D E F G H I J K L M N O + 1 ➕➕➕➕➕➕➕➕➕➕➕➕➕➕➕ + 2 ➕➕➕➕➕➕➕➕➕➕➕➕➕➕➕ + 3 ➕➕⚫➕➕➕➕➕➕➕➕➕➕➕➕ + ... +``` + +## 数据结构设计 + +### state_data结构 +```python +{ + "player1_id": 123456, # 黑方(先手) + "player2_id": 789012, # 白方(后手) + "current_player": 123456, # 当前轮到谁 + "board": [[0]*15 for _ in range(15)], # 0:空, 1:黑, 2:白 + "status": "playing", # waiting, playing, finished + "winner_id": None, # 胜者ID + "moves": [], # 历史落子记录 [(row, col, player_id), ...] + "last_move": None # 最后一手 (row, col) +} +``` + +### 游戏状态存储策略 +- 使用chat_id作为会话ID +- 使用较小的user_id作为主键中的user_id(保证唯一性) +- 在state_data中存储完整的对战信息 +- 查询时需要检查用户是否是player1或player2 + +# 提议的解决方案 + +## 方案选择 +使用现有的数据库表结构,通过精心设计state_data来支持双人对战。 + +## 实现方案 + +### 1. 游戏类:`games/gomoku.py` +继承`BaseGame`,实现以下方法: +- `handle()` - 主处理逻辑 +- `get_help()` - 帮助信息 +- `_start_game()` - 开始游戏 +- `_make_move()` - 落子 +- `_show_board()` - 显示棋盘 +- `_resign()` - 认输 +- `_get_stats()` - 查看战绩 + +### 2. 五子棋逻辑:单独模块或工具类 +- `_parse_coord()` - 解析坐标 +- `_format_coord()` - 格式化坐标 +- `_render_board()` - 渲染棋盘 +- `_check_win()` - 检查胜负 +- `_check_forbidden()` - 检查禁手 +- `_is_valid_move()` - 检查落子是否合法 + +### 3. 禁手检测逻辑 +实现辅助方法: +- `_count_line()` - 统计某方向的连珠情况 +- `_is_live_three()` - 判断活三 +- `_is_live_four()` - 判断活四 +- `_is_rush_four()` - 判断冲四 +- `_check_three_three()` - 检查三三禁手 +- `_check_four_four()` - 检查四四禁手 +- `_check_overline()` - 检查长连禁手 + +### 4. 状态管理 +- 使用`min(player1_id, player2_id)`作为数据库中的user_id +- 在state_data中完整存储对战信息 +- 提供辅助方法查找用户当前参与的游戏 + +### 5. 路由注册 +在`routers/callback.py`的`handle_command()`函数中添加: +```python +if game_type == 'gomoku': + from games.gomoku import GomokuGame + game = GomokuGame() + return await game.handle(command, chat_id, user_id) +``` + +### 6. 指令解析 +在`utils/parser.py`的`CommandParser`类中添加gomoku指令识别 + +### 7. 配置更新 +在`config.py`中添加五子棋相关配置(如果需要) + +# 当前执行步骤:"已完成所有实施步骤" + +# 任务进度 + +## [2025-10-28 17:18:21] +- 已修改: + - config.py - 添加gomoku配置 + - utils/parser.py - 添加gomoku指令映射 + - games/gomoku_logic.py - 创建五子棋逻辑模块(新文件) + - games/gomoku.py - 创建五子棋游戏类(新文件) + - routers/callback.py - 添加gomoku路由 + - games/base.py - 更新帮助信息和统计信息 +- 更改:完成五子棋游戏的完整实现,包括: + - 群级游戏池管理(支持多轮对战并存) + - 标准15x15棋盘 + - 完整的禁手规则(三三、四四、长连) + - 坐标系统(A-O列,1-15行) + - emoji棋盘渲染(⚫⚪➕) + - 胜负判定 + - 战绩统计 +- 原因:实现用户需求的双人对战五子棋游戏 +- 阻碍因素:无 +- 状态:未确认 + +# 最终审查 +(待完成后填写) + diff --git a/config.py b/config.py index 43d6e5e..f03858e 100644 --- a/config.py +++ b/config.py @@ -68,5 +68,9 @@ GAME_CONFIG = { "平步青云", "云程发轫", "刃迎缕解", "解甲归田" ] }, + "gomoku": { + "max_concurrent_games": 5, # 每个聊天最多同时进行的游戏数 + "board_size": 15, # 棋盘大小 + }, } diff --git a/games/base.py b/games/base.py index 89786f3..bcf48e4 100644 --- a/games/base.py +++ b/games/base.py @@ -73,6 +73,14 @@ def get_help_message() -> str: - `.idiom reject [词语]` - 拒绝词语加入黑名单(仅发起人) - `.idiom blacklist` - 查看黑名单 +### ⚫ 五子棋 +- `.gomoku @对手` - 发起对战 +- `.gomoku A1` - 落子 +- `.gomoku show` - 显示棋盘 +- `.gomoku resign` - 认输 +- `.gomoku list` - 列出所有对战 +- `.gomoku stats` - 查看战绩 + ### 其他 - `.help` - 显示帮助 - `.stats` - 查看个人统计 @@ -105,7 +113,8 @@ def get_stats_message(user_id: int) -> str: 'rps': '✊ 石头剪刀布', 'guess': '🔢 猜数字', 'quiz': '📝 问答游戏', - 'idiom': '🀄 成语接龙' + 'idiom': '🀄 成语接龙', + 'gomoku': '⚫ 五子棋' } for row in stats: diff --git a/games/gomoku.py b/games/gomoku.py new file mode 100644 index 0000000..ad9d9a6 --- /dev/null +++ b/games/gomoku.py @@ -0,0 +1,496 @@ +"""五子棋游戏""" +import time +import re +import logging +from typing import Optional, Dict, Any +from games.base import BaseGame +from games import gomoku_logic as logic +from utils.parser import CommandParser +from config import GAME_CONFIG + +logger = logging.getLogger(__name__) + + +class GomokuGame(BaseGame): + """五子棋游戏""" + + def __init__(self): + """初始化游戏""" + super().__init__() + self.config = GAME_CONFIG.get('gomoku', {}) + self.max_concurrent_games = self.config.get('max_concurrent_games', 5) + self.board_size = self.config.get('board_size', 15) + + async def handle(self, command: str, chat_id: int, user_id: int) -> str: + """处理五子棋指令 + + Args: + command: 指令,如 ".gomoku @对手" 或 ".gomoku A1" + chat_id: 会话ID + user_id: 用户ID + + Returns: + 回复消息 + """ + try: + # 提取参数 + _, args = CommandParser.extract_command_args(command) + args = args.strip() + + # 没有参数,显示帮助 + if not args: + return self.get_help() + + # 解析参数 + parts = args.split(maxsplit=1) + action = parts[0].lower() + + # 帮助 + if action in ['help', '帮助']: + return self.get_help() + + # 列出所有对战 + if action in ['list', '列表', '查看']: + return self._list_games(chat_id) + + # 查看战绩 + if action in ['stats', '战绩', '统计']: + return self._get_stats(user_id) + + # 显示棋盘 + if action in ['show', '显示', '棋盘']: + return self._show_board(chat_id, user_id) + + # 认输 + if action in ['resign', '认输', '投降']: + return self._resign(chat_id, user_id) + + # 尝试解析为坐标(落子) + coord = logic.parse_coord(action) + if coord is not None: + return self._make_move(chat_id, user_id, action) + + # 尝试解析为@对手(开始游戏) + opponent_id = self._parse_opponent(args) + if opponent_id is not None: + return self._start_game(chat_id, user_id, opponent_id) + + # 未识别的指令 + return f"❌ 未识别的指令\n\n{self.get_help()}" + + except Exception as e: + logger.error(f"处理五子棋指令错误: {e}", exc_info=True) + return f"❌ 处理指令出错: {str(e)}" + + def _parse_opponent(self, args: str) -> Optional[int]: + """解析@对手的用户ID + + Args: + args: 参数字符串 + + Returns: + 用户ID或None + """ + # 查找@后的数字 + match = re.search(r'@.*?(\d+)', args) + if match: + try: + return int(match.group(1)) + except ValueError: + pass + return None + + def _get_game_pool(self, chat_id: int) -> Dict[str, Any]: + """获取游戏池 + + Args: + chat_id: 会话ID + + Returns: + 游戏池数据 + """ + state = self.db.get_game_state(chat_id, 0, 'gomoku') + if state: + return state['state_data'] + else: + return { + "games": [], + "max_concurrent_games": self.max_concurrent_games + } + + def _save_game_pool(self, chat_id: int, pool_data: Dict[str, Any]): + """保存游戏池 + + Args: + chat_id: 会话ID + pool_data: 游戏池数据 + """ + self.db.save_game_state(chat_id, 0, 'gomoku', pool_data) + + def _find_user_game(self, chat_id: int, user_id: int) -> Optional[Dict[str, Any]]: + """查找用户参与的游戏 + + Args: + chat_id: 会话ID + user_id: 用户ID + + Returns: + 游戏数据或None + """ + pool = self._get_game_pool(chat_id) + + for game in pool.get("games", []): + if game["status"] == "playing": + if game["player_black"] == user_id or game["player_white"] == user_id: + return game + + return None + + def _start_game(self, chat_id: int, user_id: int, opponent_id: int) -> str: + """开始新游戏 + + Args: + chat_id: 会话ID + user_id: 发起者ID + opponent_id: 对手ID + + Returns: + 提示消息 + """ + # 检查是否与自己对战 + if user_id == opponent_id: + return "❌ 不能和自己下棋哦!" + + # 获取游戏池 + pool = self._get_game_pool(chat_id) + games = pool.get("games", []) + + # 检查是否已达到最大并发数 + active_games = [g for g in games if g["status"] == "playing"] + if len(active_games) >= self.max_concurrent_games: + return f"⚠️ 当前聊天已有 {len(active_games)} 局对战,已达到最大并发数限制" + + # 检查这两个用户是否已经在对战 + for game in active_games: + players = {game["player_black"], game["player_white"]} + if user_id in players and opponent_id in players: + return "⚠️ 你们已经有一局正在进行的对战了!\n\n输入 `.gomoku show` 查看棋盘" + + # 检查用户是否已经在其他对战中 + user_game = self._find_user_game(chat_id, user_id) + if user_game: + opponent = user_game["player_white"] if user_game["player_black"] == user_id else user_game["player_black"] + return f"⚠️ 你已经在与 @用户{opponent} 对战中!\n\n输入 `.gomoku show` 查看棋盘" + + opponent_game = self._find_user_game(chat_id, opponent_id) + if opponent_game: + other = opponent_game["player_white"] if opponent_game["player_black"] == opponent_id else opponent_game["player_black"] + return f"⚠️ 对手已经在与 @用户{other} 对战中!" + + # 创建新游戏 + current_time = int(time.time()) + game_id = f"p{user_id}_p{opponent_id}_{current_time}" + + new_game = { + "game_id": game_id, + "player_black": user_id, # 发起者执黑(先手) + "player_white": opponent_id, # 对手执白(后手) + "current_player": user_id, # 黑方先手 + "board": logic.create_empty_board(), + "status": "playing", + "winner": None, + "moves": [], + "last_move": None, + "created_at": current_time, + "updated_at": current_time + } + + games.append(new_game) + pool["games"] = games + self._save_game_pool(chat_id, pool) + + text = f"## ⚫ 五子棋对战开始!\n\n" + text += f"**黑方(先手)**:@用户{user_id} ⚫\n\n" + text += f"**白方(后手)**:@用户{opponent_id} ⚪\n\n" + text += f"**轮到**:@用户{user_id} ⚫\n\n" + text += "💡 提示:\n" + text += "- 黑方有禁手规则(三三、四四、长连禁手)\n" + text += "- 输入 `.gomoku A1` 在A1位置落子\n" + text += "- 输入 `.gomoku show` 查看棋盘" + + return text + + def _make_move(self, chat_id: int, user_id: int, coord: str) -> str: + """落子 + + Args: + chat_id: 会话ID + user_id: 用户ID + coord: 坐标字符串 + + Returns: + 结果消息 + """ + # 查找用户的游戏 + game = self._find_user_game(chat_id, user_id) + if not game: + return "⚠️ 你当前没有进行中的对战\n\n输入 `.gomoku @对手` 开始新游戏" + + # 检查是否轮到该用户 + if game["current_player"] != user_id: + opponent_id = game["player_white"] if game["player_black"] == user_id else game["player_black"] + return f"⚠️ 现在轮到 @用户{opponent_id} 落子" + + # 解析坐标 + position = logic.parse_coord(coord) + if position is None: + return f"❌ 无效的坐标:{coord}\n\n坐标格式如:A1, O15" + + row, col = position + + # 检查位置是否已有棋子 + if game["board"][row][col] != 0: + return f"❌ 位置 {coord.upper()} 已有棋子" + + # 确定当前玩家颜色 + player = 1 if game["player_black"] == user_id else 2 + player_name = "黑方" if player == 1 else "白方" + player_emoji = "⚫" if player == 1 else "⚪" + + # 检查黑方禁手 + if player == 1: + is_forbidden, forbidden_type = logic.check_forbidden(game["board"], row, col) + if is_forbidden: + text = f"## ❌ {forbidden_type}!\n\n" + text += f"位置 {coord.upper()} 触发禁手,黑方判负!\n\n" + text += f"**获胜者**:@用户{game['player_white']} ⚪ 白方\n\n" + text += f"📊 战绩已更新" + + # 更新战绩 + self.db.update_game_stats(game['player_white'], 'gomoku', win=True) + self.db.update_game_stats(game['player_black'], 'gomoku', loss=True) + + # 移除游戏 + pool = self._get_game_pool(chat_id) + pool["games"] = [g for g in pool["games"] if g["game_id"] != game["game_id"]] + self._save_game_pool(chat_id, pool) + + return text + + # 落子 + game["board"][row][col] = player + game["moves"].append((row, col, player)) + game["last_move"] = (row, col) + game["updated_at"] = int(time.time()) + + # 检查是否获胜 + if logic.check_win(game["board"], row, col, player): + text = f"## 🎉 五连珠!游戏结束!\n\n" + text += f"**获胜者**:@用户{user_id} {player_emoji} {player_name}\n\n" + + # 渲染棋盘 + board_str = logic.render_board(game["board"], game["last_move"]) + text += f"```\n{board_str}\n```\n\n" + + text += f"📊 战绩已更新" + + # 更新战绩 + opponent_id = game["player_white"] if player == 1 else game["player_black"] + self.db.update_game_stats(user_id, 'gomoku', win=True) + self.db.update_game_stats(opponent_id, 'gomoku', loss=True) + + # 移除游戏 + pool = self._get_game_pool(chat_id) + pool["games"] = [g for g in pool["games"] if g["game_id"] != game["game_id"]] + self._save_game_pool(chat_id, pool) + + return text + + # 切换玩家 + opponent_id = game["player_white"] if player == 1 else game["player_black"] + game["current_player"] = opponent_id + opponent_emoji = "⚪" if player == 1 else "⚫" + opponent_name = "白方" if player == 1 else "黑方" + + # 更新游戏池 + pool = self._get_game_pool(chat_id) + for i, g in enumerate(pool["games"]): + if g["game_id"] == game["game_id"]: + pool["games"][i] = game + break + self._save_game_pool(chat_id, pool) + + # 渲染棋盘 + board_str = logic.render_board(game["board"], game["last_move"]) + + text = f"## ✅ 落子成功!\n\n" + text += f"**位置**:{coord.upper()} {player_emoji}\n\n" + text += f"**轮到**:@用户{opponent_id} {opponent_emoji} {opponent_name}\n\n" + text += f"```\n{board_str}\n```" + + return text + + def _show_board(self, chat_id: int, user_id: int) -> str: + """显示棋盘 + + Args: + chat_id: 会话ID + user_id: 用户ID + + Returns: + 棋盘显示 + """ + game = self._find_user_game(chat_id, user_id) + if not game: + return "⚠️ 你当前没有进行中的对战\n\n输入 `.gomoku @对手` 开始新游戏" + + # 渲染棋盘 + board_str = logic.render_board(game["board"], game["last_move"]) + + # 获取当前玩家信息 + current_id = game["current_player"] + current_emoji = "⚫" if game["player_black"] == current_id else "⚪" + current_name = "黑方" if game["player_black"] == current_id else "白方" + + text = f"## ⚫ 五子棋对战\n\n" + text += f"**黑方**:@用户{game['player_black']} ⚫\n\n" + text += f"**白方**:@用户{game['player_white']} ⚪\n\n" + text += f"**轮到**:@用户{current_id} {current_emoji} {current_name}\n\n" + text += f"**手数**:{len(game['moves'])}\n\n" + text += f"```\n{board_str}\n```" + + return text + + def _resign(self, chat_id: int, user_id: int) -> str: + """认输 + + Args: + chat_id: 会话ID + user_id: 用户ID + + Returns: + 结果消息 + """ + game = self._find_user_game(chat_id, user_id) + if not game: + return "⚠️ 你当前没有进行中的对战" + + # 确定胜者 + if game["player_black"] == user_id: + winner_id = game["player_white"] + loser_name = "黑方" + winner_emoji = "⚪" + else: + winner_id = game["player_black"] + loser_name = "白方" + winner_emoji = "⚫" + + text = f"## 🏳️ 认输\n\n" + text += f"@用户{user_id} {loser_name} 认输\n\n" + text += f"**获胜者**:@用户{winner_id} {winner_emoji}\n\n" + text += f"📊 战绩已更新" + + # 更新战绩 + self.db.update_game_stats(winner_id, 'gomoku', win=True) + self.db.update_game_stats(user_id, 'gomoku', loss=True) + + # 移除游戏 + pool = self._get_game_pool(chat_id) + pool["games"] = [g for g in pool["games"] if g["game_id"] != game["game_id"]] + self._save_game_pool(chat_id, pool) + + return text + + def _list_games(self, chat_id: int) -> str: + """列出所有进行中的游戏 + + Args: + chat_id: 会话ID + + Returns: + 游戏列表 + """ + pool = self._get_game_pool(chat_id) + active_games = [g for g in pool.get("games", []) if g["status"] == "playing"] + + if not active_games: + return "📋 当前没有进行中的对战\n\n输入 `.gomoku @对手` 开始新游戏" + + text = f"## 📋 进行中的对战 ({len(active_games)}/{self.max_concurrent_games})\n\n" + + for idx, game in enumerate(active_games, 1): + current_emoji = "⚫" if game["player_black"] == game["current_player"] else "⚪" + text += f"### {idx}. 对战\n" + text += f"- **黑方**:@用户{game['player_black']} ⚫\n" + text += f"- **白方**:@用户{game['player_white']} ⚪\n" + text += f"- **轮到**:@用户{game['current_player']} {current_emoji}\n" + text += f"- **手数**:{len(game['moves'])}\n\n" + + return text + + def _get_stats(self, user_id: int) -> str: + """获取用户战绩 + + Args: + user_id: 用户ID + + Returns: + 战绩信息 + """ + stats = self.db.get_game_stats(user_id, 'gomoku') + + total = stats['total_plays'] + if total == 0: + return "📊 你还没有五子棋对战记录\n\n快来挑战吧!输入 `.gomoku @对手` 开始游戏" + + wins = stats['wins'] + losses = stats['losses'] + win_rate = (wins / total * 100) if total > 0 else 0 + + text = f"## 📊 五子棋战绩\n\n" + text += f"**总局数**:{total} 局\n\n" + text += f"**胜利**:{wins} 次 🎉\n\n" + text += f"**失败**:{losses} 次\n\n" + text += f"**胜率**:{win_rate:.1f}%" + + return text + + def get_help(self) -> str: + """获取帮助信息""" + return """## ⚫ 五子棋 + +### 基础用法 +- `.gomoku @对手` - 向对手发起对战 +- `.gomoku A1` - 在A1位置落子 +- `.gomoku show` - 显示当前棋盘 +- `.gomoku resign` - 认输 + +### 其他指令 +- `.gomoku list` - 列出所有进行中的对战 +- `.gomoku stats` - 查看个人战绩 + +### 游戏规则 +- 标准15×15棋盘,五子连珠获胜 +- 黑方先手,但有禁手规则: + - **三三禁手**:一手棋同时形成两个活三 + - **四四禁手**:一手棋同时形成两个四(活四或冲四) + - **长连禁手**:一手棋形成六子或以上连珠 +- 触发禁手者判负 +- 允许多轮对战同时进行(对战双方不同即可) + +### 坐标系统 +- 列:A-O(15列) +- 行:1-15(15行) +- 示例:A1(左上角)、O15(右下角)、H8(中心) + +### 示例 +``` +.gomoku @123456 # 向用户123456发起对战 +.gomoku H8 # 在中心位置落子 +.gomoku show # 查看棋盘 +.gomoku resign # 认输 +``` + +💡 提示:黑方虽然先手,但需要注意禁手规则 +""" + diff --git a/games/gomoku_logic.py b/games/gomoku_logic.py new file mode 100644 index 0000000..f20e547 --- /dev/null +++ b/games/gomoku_logic.py @@ -0,0 +1,286 @@ +"""五子棋游戏逻辑模块""" +from typing import Optional, Tuple, List, Dict, Any + + +def create_empty_board() -> List[List[int]]: + """创建空棋盘 + + Returns: + 15x15的二维列表,0表示空位 + """ + return [[0] * 15 for _ in range(15)] + + +def parse_coord(coord_str: str) -> Optional[Tuple[int, int]]: + """解析坐标字符串 + + Args: + coord_str: 如 "A1", "O15", "h8" + + Returns: + (row, col) 或 None + """ + coord_str = coord_str.strip().upper() + + if len(coord_str) < 2: + return None + + # 解析列(A-O) + col_char = coord_str[0] + if not ('A' <= col_char <= 'O'): + return None + col = ord(col_char) - ord('A') + + # 解析行(1-15) + try: + row = int(coord_str[1:]) - 1 + if not (0 <= row <= 14): + return None + except ValueError: + return None + + return (row, col) + + +def format_coord(row: int, col: int) -> str: + """格式化坐标 + + Args: + row: 0-14 + col: 0-14 + + Returns: + 如 "A1", "O15" + """ + col_char = chr(ord('A') + col) + row_num = row + 1 + return f"{col_char}{row_num}" + + +def is_valid_position(row: int, col: int) -> bool: + """检查坐标是否在棋盘范围内 + + Args: + row: 行号 + col: 列号 + + Returns: + 是否有效 + """ + return 0 <= row <= 14 and 0 <= col <= 14 + + +def count_consecutive(board: List[List[int]], row: int, col: int, + direction: Tuple[int, int], player: int) -> int: + """统计某方向连续同色棋子数(包括当前位置) + + Args: + board: 棋盘状态 + row, col: 起始位置 + direction: 方向向量 (dr, dc) + player: 玩家 (1:黑, 2:白) + + Returns: + 连续棋子数 + """ + dr, dc = direction + count = 1 # 包括当前位置 + + # 正方向 + r, c = row + dr, col + dc + while is_valid_position(r, c) and board[r][c] == player: + count += 1 + r += dr + c += dc + + # 反方向 + r, c = row - dr, col - dc + while is_valid_position(r, c) and board[r][c] == player: + count += 1 + r -= dr + c -= dc + + return count + + +def check_win(board: List[List[int]], row: int, col: int, player: int) -> bool: + """检查是否获胜(恰好五连珠) + + Args: + board: 棋盘状态 + row, col: 最后落子位置 + player: 玩家 (1:黑, 2:白) + + Returns: + 是否五连珠获胜 + """ + # 四个方向:横、竖、左斜、右斜 + directions = [(0, 1), (1, 0), (1, 1), (1, -1)] + + for direction in directions: + count = count_consecutive(board, row, col, direction, player) + if count == 5: + return True + + return False + + +def analyze_line(board: List[List[int]], row: int, col: int, + direction: Tuple[int, int], player: int) -> Dict[str, Any]: + """分析某方向的棋型 + + Args: + board: 棋盘状态 + row, col: 待分析位置(假设已落子) + direction: 方向向量 + player: 玩家 + + Returns: + { + "consecutive": int, # 连续数 + "left_open": bool, # 左侧是否开放 + "right_open": bool, # 右侧是否开放 + "pattern": str # 棋型类型 + } + """ + dr, dc = direction + + # 统计正方向连续数 + right_count = 0 + r, c = row + dr, col + dc + while is_valid_position(r, c) and board[r][c] == player: + right_count += 1 + r += dr + c += dc + right_open = is_valid_position(r, c) and board[r][c] == 0 + + # 统计反方向连续数 + left_count = 0 + r, c = row - dr, col - dc + while is_valid_position(r, c) and board[r][c] == player: + left_count += 1 + r -= dr + c -= dc + left_open = is_valid_position(r, c) and board[r][c] == 0 + + # 总连续数(包括当前位置) + consecutive = left_count + 1 + right_count + + # 判定棋型 + pattern = "none" + + if consecutive >= 6: + pattern = "overline" + elif consecutive == 5: + pattern = "five" + elif consecutive == 4: + if left_open and right_open: + pattern = "live_four" + elif left_open or right_open: + pattern = "rush_four" + elif consecutive == 3: + if left_open and right_open: + pattern = "live_three" + elif left_open or right_open: + pattern = "sleep_three" + + return { + "consecutive": consecutive, + "left_open": left_open, + "right_open": right_open, + "pattern": pattern + } + + +def check_forbidden(board: List[List[int]], row: int, col: int) -> Tuple[bool, str]: + """检查黑方禁手 + + Args: + board: 棋盘状态(不包含待落子) + row, col: 待落子位置 + + Returns: + (是否禁手, 禁手类型) + """ + # 只有黑方(玩家1)有禁手 + player = 1 + + # 临时落子 + original_value = board[row][col] + board[row][col] = player + + # 四个方向 + directions = [(0, 1), (1, 0), (1, 1), (1, -1)] + + live_threes = 0 + fours = 0 + has_overline = False + + for direction in directions: + analysis = analyze_line(board, row, col, direction, player) + + if analysis["pattern"] == "overline": + has_overline = True + elif analysis["pattern"] == "live_three": + live_threes += 1 + elif analysis["pattern"] in ["live_four", "rush_four"]: + fours += 1 + + # 恢复棋盘 + board[row][col] = original_value + + # 判定禁手 + if has_overline: + return True, "长连禁手" + if live_threes >= 2: + return True, "三三禁手" + if fours >= 2: + return True, "四四禁手" + + return False, "" + + +def render_board(board: List[List[int]], last_move: Optional[Tuple[int, int]] = None) -> str: + """渲染棋盘为字符串 + + Args: + board: 棋盘状态 + last_move: 最后落子位置(可选,用于标记) + + Returns: + 棋盘的字符串表示 + """ + lines = [] + + # 列标题 + col_labels = " " + " ".join([chr(ord('A') + i) for i in range(15)]) + lines.append(col_labels) + + # 绘制棋盘 + for row in range(15): + row_num = f"{row + 1:2d}" # 右对齐行号 + row_cells = [] + + for col in range(15): + cell = board[row][col] + + # 标记最后落子 + if last_move and last_move == (row, col): + if cell == 1: + row_cells.append("⚫") + elif cell == 2: + row_cells.append("⚪") + else: + row_cells.append("➕") + else: + if cell == 0: + row_cells.append("➕") + elif cell == 1: + row_cells.append("⚫") + elif cell == 2: + row_cells.append("⚪") + + lines.append(f" {row_num} {' '.join(row_cells)}") + + return "\n".join(lines) + diff --git a/routers/callback.py b/routers/callback.py index c56edb6..c3f56be 100644 --- a/routers/callback.py +++ b/routers/callback.py @@ -152,6 +152,12 @@ async def handle_command(game_type: str, command: str, game = IdiomGame() return await game.handle(command, chat_id, user_id) + # 五子棋 + if game_type == 'gomoku': + from games.gomoku import GomokuGame + game = GomokuGame() + return await game.handle(command, chat_id, user_id) + # 未知游戏类型 logger.warning(f"未知游戏类型: {game_type}") return "❌ 未知的游戏类型" diff --git a/utils/parser.py b/utils/parser.py index d6f8b7a..950d1c1 100644 --- a/utils/parser.py +++ b/utils/parser.py @@ -35,6 +35,11 @@ class CommandParser: '.成语接龙': 'idiom', '.成语': 'idiom', + # 五子棋 + '.gomoku': 'gomoku', + '.五子棋': 'gomoku', + '.gobang': 'gomoku', + # 帮助 '.help': 'help', '.帮助': 'help',