diff --git a/.tasks/2025-10-28_1_gomoku.md b/.tasks/2025-10-28_1_gomoku.md
new file mode 100644
index 0000000..9fad4f1
--- /dev/null
+++ b/.tasks/2025-10-28_1_gomoku.md
@@ -0,0 +1,271 @@
+# 背景
+文件名: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棋盘渲染(⚫⚪➕)
+ - 胜负判定
+ - 战绩统计
+- 原因:实现用户需求的双人对战五子棋游戏
+- 阻碍因素:用户识别和显示格式错误
+- 状态:不成功
+
+## [2025-10-28 17:36:07]
+- 已修改:
+ - games/gomoku.py - 修复用户识别和显示格式
+- 更改:
+ - 修复 `_parse_opponent()` 方法,使用正确的WPS @用户格式 `` 进行解析
+ - 修改所有用户显示,从 `@用户{user_id}` 改为 ``,以正确显示用户的群名称
+ - 涉及修改:开始游戏、落子、显示棋盘、认输、列出对战等所有用户显示位置
+- 原因:修复用户识别失败和显示错误的问题
+- 阻碍因素:用户识别仍然失败
+- 状态:不成功
+
+## [2025-10-28 17:42:24]
+- 已修改:
+ - games/gomoku.py - 改进用户ID解析和添加调试日志
+ - routers/callback.py - 增强日志输出
+- 更改:
+ - 改进 `_parse_opponent()` 方法,支持多种@用户格式(双引号、单引号、不同的标签格式)
+ - 在 `handle()` 方法中添加详细的调试日志(command, args, action, opponent_id)
+ - 改进错误提示,显示实际接收到的参数内容
+ - 将 callback.py 中的消息内容日志级别从 DEBUG 改为 INFO,便于追踪
+- 原因:进一步诊断用户ID识别失败的问题,添加调试信息帮助定位问题
+- 阻碍因素:WPS callback不提供被@用户的ID信息
+- 状态:不成功
+
+## [2025-10-28 17:55:00]
+- 已修改:
+ - games/gomoku.py - 重构游戏发起机制,从@用户改为挑战-接受模式
+ - games/base.py - 更新全局帮助信息
+ - routers/callback.py - 添加完整callback数据日志
+- 更改:
+ - **核心架构变更**:从"@用户发起对战"改为"挑战-接受"机制
+ - 新增 `_create_challenge()` 方法 - 用户发起挑战
+ - 新增 `_accept_challenge()` 方法 - 其他用户接受挑战
+ - 新增 `_cancel_challenge()` 方法 - 取消自己的挑战
+ - 删除 `_parse_opponent()` 方法(不再需要)
+ - 删除 `_start_game()` 方法(由新方法替代)
+ - 更新游戏池数据结构,添加 `challenges` 列表
+ - 更新所有帮助信息和错误提示
+ - 指令变更:
+ - `.gomoku challenge` / `.gomoku start` - 发起挑战
+ - `.gomoku accept` / `.gomoku join` - 接受挑战
+ - `.gomoku cancel` - 取消挑战
+- 原因:WPS callback消息内容中@用户只是文本形式(如"@揭英飙"),不包含user_id,无法实现@用户发起对战
+- 阻碍因素:无
+- 状态:成功
+
+## [2025-10-28 17:56:03]
+- 已修改:
+ - games/gomoku_logic.py - 修复棋盘对齐问题
+- 更改:
+ - 优化 `render_board()` 函数的格式化逻辑
+ - 列标题:每个字母后面加一个空格,确保与棋子列对齐
+ - 行号:调整前导空格,从 " {row_num} " 改为 "{row_num} "
+ - 棋子:每个emoji后面加一个空格,行尾去除多余空格
+ - 整体对齐:确保列标题、行号、棋子三者在Markdown代码块中正确对齐
+- 原因:修复用户反馈的棋盘文本对齐问题
+- 阻碍因素:无
+- 状态:未确认
+
+# 最终审查
+(待完成后填写)
+
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..f8a3ac5 100644
--- a/games/base.py
+++ b/games/base.py
@@ -73,6 +73,15 @@ def get_help_message() -> str:
- `.idiom reject [词语]` - 拒绝词语加入黑名单(仅发起人)
- `.idiom blacklist` - 查看黑名单
+### ⚫ 五子棋
+- `.gomoku challenge` - 发起挑战
+- `.gomoku accept` - 接受挑战
+- `.gomoku A1` - 落子
+- `.gomoku show` - 显示棋盘
+- `.gomoku resign` - 认输
+- `.gomoku list` - 列出所有对战
+- `.gomoku stats` - 查看战绩
+
### 其他
- `.help` - 显示帮助
- `.stats` - 查看个人统计
@@ -105,7 +114,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..ff5d639
--- /dev/null
+++ b/games/gomoku.py
@@ -0,0 +1,567 @@
+"""五子棋游戏"""
+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()
+
+ # 调试日志
+ logger.info(f"五子棋指令解析 - command: {command}")
+ logger.info(f"五子棋指令解析 - args: {args}")
+
+ # 没有参数,显示帮助
+ if not args:
+ return self.get_help()
+
+ # 解析参数
+ parts = args.split(maxsplit=1)
+ action = parts[0].lower()
+ logger.info(f"五子棋指令解析 - action: {action}")
+
+ # 帮助
+ if action in ['help', '帮助']:
+ return self.get_help()
+
+ # 发起挑战
+ if action in ['challenge', 'start', '挑战', '开始']:
+ return self._create_challenge(chat_id, user_id)
+
+ # 接受挑战
+ if action in ['accept', 'join', '接受', '加入']:
+ return self._accept_challenge(chat_id, user_id)
+
+ # 取消挑战
+ if action in ['cancel', '取消']:
+ return self._cancel_challenge(chat_id, user_id)
+
+ # 列出所有对战
+ 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)
+
+ # 未识别的指令
+ logger.warning(f"五子棋未识别的指令 - args: {args}")
+ return f"❌ 未识别的指令:{args}\n\n💡 提示:\n- 发起挑战:`.gomoku challenge`\n- 接受挑战:`.gomoku accept`\n- 落子:`.gomoku A1`\n- 查看帮助:`.gomoku help`"
+
+ except Exception as e:
+ logger.error(f"处理五子棋指令错误: {e}", exc_info=True)
+ return f"❌ 处理指令出错: {str(e)}"
+
+ 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 _create_challenge(self, chat_id: int, user_id: int) -> str:
+ """创建挑战
+
+ Args:
+ chat_id: 会话ID
+ user_id: 发起者ID
+
+ Returns:
+ 提示消息
+ """
+ # 获取游戏池
+ pool = self._get_game_pool(chat_id)
+ games = pool.get("games", [])
+ challenges = pool.get("challenges", [])
+
+ # 检查用户是否已经在对战中
+ user_game = self._find_user_game(chat_id, user_id)
+ if user_game:
+ return "⚠️ 你已经在对战中!\n\n输入 `.gomoku show` 查看棋盘"
+
+ # 检查用户是否已经发起了挑战
+ for challenge in challenges:
+ if challenge["challenger_id"] == user_id:
+ return "⚠️ 你已经发起了一个挑战!\n\n等待其他人接受,或输入 `.gomoku cancel` 取消挑战"
+
+ # 创建挑战
+ current_time = int(time.time())
+ challenge = {
+ "challenger_id": user_id,
+ "created_at": current_time
+ }
+
+ challenges.append(challenge)
+ pool["challenges"] = challenges
+ self._save_game_pool(chat_id, pool)
+
+ text = f"## 🎯 五子棋挑战\n\n"
+ text += f" 发起了五子棋挑战!\n\n"
+ text += f"💡 想要应战吗?输入 `.gomoku accept` 接受挑战"
+
+ return text
+
+ def _accept_challenge(self, chat_id: int, user_id: int) -> str:
+ """接受挑战
+
+ Args:
+ chat_id: 会话ID
+ user_id: 接受者ID
+
+ Returns:
+ 提示消息
+ """
+ # 获取游戏池
+ pool = self._get_game_pool(chat_id)
+ games = pool.get("games", [])
+ challenges = pool.get("challenges", [])
+
+ if not challenges:
+ return "⚠️ 当前没有挑战可以接受\n\n输入 `.gomoku challenge` 发起挑战"
+
+ # 检查用户是否已经在对战中
+ user_game = self._find_user_game(chat_id, user_id)
+ if user_game:
+ return "⚠️ 你已经在对战中!\n\n输入 `.gomoku show` 查看棋盘"
+
+ # 获取最新的挑战
+ challenge = challenges[-1]
+ challenger_id = challenge["challenger_id"]
+
+ # 不能接受自己的挑战
+ if challenger_id == user_id:
+ return "❌ 不能接受自己的挑战!"
+
+ # 检查是否已达到最大并发数
+ active_games = [g for g in games if g["status"] == "playing"]
+ if len(active_games) >= self.max_concurrent_games:
+ return f"⚠️ 当前聊天已有 {len(active_games)} 局对战,已达到最大并发数限制"
+
+ # 创建游戏
+ current_time = int(time.time())
+ game_id = f"p{challenger_id}_p{user_id}_{current_time}"
+
+ new_game = {
+ "game_id": game_id,
+ "player_black": challenger_id, # 挑战者执黑(先手)
+ "player_white": user_id, # 接受者执白(后手)
+ "current_player": challenger_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)
+
+ # 移除已接受的挑战
+ challenges.remove(challenge)
+
+ pool["games"] = games
+ pool["challenges"] = challenges
+ self._save_game_pool(chat_id, pool)
+
+ text = f"## ⚫ 五子棋对战开始!\n\n"
+ text += f"**黑方(先手)**: ⚫\n\n"
+ text += f"**白方(后手)**: ⚪\n\n"
+ text += f"**轮到**: ⚫\n\n"
+ text += "💡 提示:\n"
+ text += "- 黑方有禁手规则(三三、四四、长连禁手)\n"
+ text += "- 输入 `.gomoku A1` 在A1位置落子\n"
+ text += "- 输入 `.gomoku show` 查看棋盘"
+
+ return text
+
+ def _cancel_challenge(self, chat_id: int, user_id: int) -> str:
+ """取消挑战
+
+ Args:
+ chat_id: 会话ID
+ user_id: 用户ID
+
+ Returns:
+ 提示消息
+ """
+ # 获取游戏池
+ pool = self._get_game_pool(chat_id)
+ challenges = pool.get("challenges", [])
+
+ # 查找用户的挑战
+ user_challenge = None
+ for challenge in challenges:
+ if challenge["challenger_id"] == user_id:
+ user_challenge = challenge
+ break
+
+ if not user_challenge:
+ return "⚠️ 你没有发起挑战"
+
+ # 移除挑战
+ challenges.remove(user_challenge)
+ pool["challenges"] = challenges
+ self._save_game_pool(chat_id, pool)
+
+ return "✅ 已取消挑战"
+
+ 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"⚠️ 现在轮到 落子"
+
+ # 解析坐标
+ 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"**获胜者**: ⚪ 白方\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"**获胜者**: {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_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"**黑方**: ⚫\n\n"
+ text += f"**白方**: ⚪\n\n"
+ text += f"**轮到**: {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" {loser_name} 认输\n\n"
+ text += f"**获胜者**: {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"- **黑方**: ⚫\n"
+ text += f"- **白方**: ⚪\n"
+ text += f"- **轮到**: {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 challenge` - 发起挑战
+- `.gomoku accept` - 接受挑战
+- `.gomoku A1` - 在A1位置落子
+- `.gomoku show` - 显示当前棋盘
+- `.gomoku resign` - 认输
+
+### 其他指令
+- `.gomoku cancel` - 取消自己的挑战
+- `.gomoku list` - 列出所有进行中的对战
+- `.gomoku stats` - 查看个人战绩
+
+### 游戏规则
+- 标准15×15棋盘,五子连珠获胜
+- 黑方先手,但有禁手规则:
+ - **三三禁手**:一手棋同时形成两个活三
+ - **四四禁手**:一手棋同时形成两个四(活四或冲四)
+ - **长连禁手**:一手棋形成六子或以上连珠
+- 触发禁手者判负
+- 允许多轮对战同时进行(对战双方不同即可)
+
+### 坐标系统
+- 列:A-O(15列)
+- 行:1-15(15行)
+- 示例:A1(左上角)、O15(右下角)、H8(中心)
+
+### 示例
+```
+.gomoku challenge # 发起挑战
+.gomoku accept # 接受挑战
+.gomoku H8 # 在中心位置落子
+.gomoku show # 查看棋盘
+.gomoku resign # 认输
+```
+
+💡 提示:黑方虽然先手,但需要注意禁手规则
+"""
+
diff --git a/games/gomoku_logic.py b/games/gomoku_logic.py
new file mode 100644
index 0000000..383d47c
--- /dev/null
+++ b/games/gomoku_logic.py
@@ -0,0 +1,287 @@
+"""五子棋游戏逻辑模块"""
+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 = "\t " + " ".join([chr(ord('A') + i) + "" for i in range(15)])
+ lines.append(col_labels.rstrip())
+
+ # 绘制棋盘
+ 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("⚪")
+
+ # 每个emoji后面加一个空格
+ lines.append(f"{row_num} " + "".join([cell + " " for cell in row_cells]).rstrip())
+
+ return "\n".join(lines)
+
diff --git a/routers/callback.py b/routers/callback.py
index c56edb6..a7c74dd 100644
--- a/routers/callback.py
+++ b/routers/callback.py
@@ -28,7 +28,8 @@ async def callback_receive(request: Request):
# 解析请求数据
data = await request.json()
logger.info(f"收到消息: chatid={data.get('chatid')}, creator={data.get('creator')}")
- logger.debug(f"消息内容: {data.get('content')}")
+ logger.info(f"消息内容: {data.get('content')}")
+ logger.info(f"完整callback数据: {data}")
# 验证请求
try:
@@ -152,6 +153,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',