9 Commits

Author SHA1 Message Date
ninemine
278e760fb2 棋盘对齐 2025-10-29 09:29:42 +08:00
57f955f837 修改棋盘格式用于对齐 2025-10-28 17:57:02 +08:00
7d28e2e2aa 修改为发起/接受对局以避开at无法获取id的问题 2025-10-28 17:49:16 +08:00
f217cd958b 修复未成功, 添加调试 2025-10-28 17:43:21 +08:00
93a4882da2 修复at用户格式错误 2025-10-28 17:37:06 +08:00
38cd441908 新增五子棋游戏 2025-10-28 17:22:49 +08:00
b7a57539f5 Merge branch 'task/add-idiom-chain-game_2025-10-28_1' 2025-10-28 16:46:05 +08:00
c4be929b3a 1.全局黑名单2.修复help遗漏 2025-10-28 16:25:04 +08:00
c761a12377 新增成语接龙游戏 2025-10-28 16:12:32 +08:00
11 changed files with 2094 additions and 2 deletions

View File

@@ -0,0 +1,238 @@
# 背景
文件名2025-10-28_1_add-idiom-chain-game.md
创建于2025-10-28_15:43:00
创建者admin
主分支main
任务分支task/add-idiom-chain-game_2025-10-28_1
Yolo模式Off
# 任务描述
在WPS Bot Game项目中新增一个成语接龙游戏功能。
## 核心需求
1. 群内多人游戏,机器人作为裁判和出题者
2. 允许按拼音接龙(包括谐音接龙)
3. 没有时间限制
4. 不需要提示功能
5. 游戏记录保存到.stats统计中
6. 不允许重复使用成语
7. 不需要难度分级(非人机对战)
8. 需要裁判指令用于接受/拒绝玩家回答
## 游戏玩法
- 机器人出题(给出起始成语)
- 群内玩家轮流接龙
- 机器人判断接龙是否有效(拼音/谐音匹配、未重复使用)
- 裁判可以手动接受或拒绝某个回答
- 记录每个玩家的成功接龙次数
# 项目概览
## 项目结构
```
WPSBotGame/
├── app.py # FastAPI主应用
├── config.py # 配置管理
├── core/
│ ├── database.py # SQLite数据库操作
│ ├── middleware.py # 中间件
│ └── models.py # 数据模型
├── games/ # 游戏模块
│ ├── base.py # 游戏基类
│ ├── dice.py # 骰娘游戏
│ ├── rps.py # 石头剪刀布
│ ├── fortune.py # 运势占卜
│ ├── guess.py # 猜数字
│ └── quiz.py # 问答游戏
├── data/ # 数据文件
│ ├── bot.db # SQLite数据库
│ ├── quiz.json # 问答题库
│ └── fortunes.json # 运势数据
├── routers/ # 路由处理
│ ├── callback.py # WPS回调处理
│ └── health.py # 健康检查
└── utils/ # 工具模块
├── message.py # 消息发送
├── parser.py # 指令解析
└── rate_limit.py # 限流控制
```
## 技术栈
- FastAPIWeb框架
- SQLite数据存储
- WPS协作机器人API消息接收与发送
## 现有游戏架构
1. 所有游戏继承`BaseGame`基类
2. 必须实现`handle(command, chat_id, user_id)`方法处理指令
3. 必须实现`get_help()`方法返回帮助信息
4. 游戏状态存储在数据库`game_states`表:`(chat_id, user_id, game_type)`作为联合主键
5. 游戏统计存储在`game_stats`表:记录`wins`, `losses`, `draws`, `total_plays`
6. 指令通过`CommandParser`解析,在`callback.py`中分发到对应游戏处理器
## 数据库设计
### game_states表
- chat_id: 会话ID
- user_id: 用户ID
- game_type: 游戏类型
- state_data: JSON格式的游戏状态数据
- created_at/updated_at: 时间戳
### game_stats表
- user_id: 用户ID
- game_type: 游戏类型
- wins/losses/draws/total_plays: 统计数据
# 分析
## 关键技术挑战
### 1. 群级别vs个人级别状态管理
现有游戏(猜数字、问答)都是个人独立状态,使用`(chat_id, user_id, game_type)`作为主键。
成语接龙是群内共享游戏,需要:
- 群级别的游戏状态:当前成语、已用成语列表、接龙长度、当前轮到谁
- 个人级别的统计:每个玩家的成功接龙次数
**可能方案:**
- 使用特殊user_id如0或-1存储群级别游戏状态
- 或者在state_data中存储所有玩家信息
### 2. 成语词库准备
需要准备:
- 成语列表至少500-1000个常用成语
- 每个成语的拼音信息(用于判断接龙是否匹配)
- 数据格式JSON文件类似quiz.json
### 3. 拼音匹配逻辑
- 需要拼音库支持pypinyin
- 支持谐音匹配(声母韵母匹配)
- 处理多音字情况
### 4. 裁判指令设计
需要额外指令:
- `.idiom accept` - 接受上一个回答
- `.idiom reject` - 拒绝上一个回答
- 需要权限控制(谁可以当裁判?)
### 5. 游戏流程设计
```
1. 开始游戏:.idiom start
- 机器人随机给出起始成语
- 创建群级别游戏状态
2. 玩家接龙:.idiom [成语]
- 检查是否在词库中
- 检查拼音是否匹配(首字拼音 == 上一个成语尾字拼音)
- 检查是否已使用过
- 自动判断或等待裁判确认
3. 裁判操作:.idiom accept/reject
- 手动接受或拒绝最近的回答
4. 查看状态:.idiom status
- 显示当前成语、已用成语数量、参与人数
5. 结束游戏:.idiom stop
- 显示统计信息
- 更新每个玩家的game_stats
```
## 现有代码分析
### CommandParser (utils/parser.py)
需要添加成语接龙指令映射:
```python
'.idiom': 'idiom',
'.成语接龙': 'idiom',
```
### callback.py (routers/callback.py)
需要在`handle_command`函数中添加idiom游戏分支
```python
if game_type == 'idiom':
from games.idiom import IdiomGame
game = IdiomGame()
return await game.handle(command, chat_id, user_id)
```
### base.py (games/base.py)
需要更新`get_help_message()``get_stats_message()`,添加成语接龙信息。
### config.py
可能需要添加成语接龙相关配置:
```python
"idiom": {
"auto_judge": True, # 是否自动判断
"require_approval": False, # 是否需要裁判确认
}
```
# 提议的解决方案
待INNOVATE模式填写
# 当前执行步骤:"1. 创建任务文件"
# 任务进度
## [2025-10-28 15:45:00]
- 已修改requirements.txt, config.py, utils/parser.py, routers/callback.py, games/base.py
- 已创建games/idiom.py
- 更改:
1. 在requirements.txt中添加pypinyin==0.51.0依赖
2. 在config.py的GAME_CONFIG中添加idiom配置起始成语池、历史显示数量
3. 在utils/parser.py的COMMAND_MAP中添加.idiom、.成语接龙、.成语指令映射
4. 创建games/idiom.py实现完整的成语接龙游戏逻辑
- IdiomGame类继承BaseGame
- 实现handle()主指令分发方法
- 实现_start_game()开始游戏
- 实现_make_chain()玩家接龙
- 实现_set_next_user()指定下一位
- 实现_reject_idiom()裁判拒绝
- 实现_show_status()显示状态
- 实现_show_blacklist()显示黑名单
- 实现_stop_game()结束游戏
- 实现_get_pinyin()获取拼音(支持多音字)
- 实现_check_pinyin_match()检查拼音匹配(忽略声调)
- 实现_parse_mentioned_user()解析@用户
- 实现_can_answer()权限检查(防连续、指定轮次)
- 实现_validate_idiom()词语验证4字、拼音匹配、未使用、未黑名单
- 实现get_help()帮助信息
5. 在routers/callback.py的handle_command()中添加idiom游戏分支
6. 在games/base.py的get_help_message()中添加成语接龙帮助信息
7. 在games/base.py的get_stats_message()的game_names字典中添加idiom映射
- 原因:实现成语接龙游戏功能的所有核心代码
- 阻碍因素:无
- 状态:未确认
## [2025-10-28 15:50:00]
- 已修改games/base.py
- 更改在get_help_message()的成语接龙部分添加黑名单相关指令说明
- 添加 `.idiom reject [词语]` - 拒绝词语加入黑名单(仅发起人)
- 添加 `.idiom blacklist` - 查看黑名单
- 原因:用户反馈.help帮助信息中看不到黑名单机制的使用说明
- 阻碍因素:无
- 状态:未确认
## [2025-10-28 15:55:00]
- 已修改games/idiom.py
- 已创建data/idiom_blacklist.json
- 更改:将黑名单机制从游戏状态改为全局永久存储
1. 创建data/idiom_blacklist.json作为全局黑名单数据文件
2. 在IdiomGame.__init__()中添加黑名单文件路径和懒加载变量
3. 添加_load_blacklist()方法从文件懒加载全局黑名单
4. 添加_save_blacklist()方法保存黑名单到文件
5. 修改_validate_idiom()方法检查全局黑名单而非游戏状态中的黑名单
6. 修改_start_game()方法移除state_data中的blacklist字段初始化
7. 修改_reject_idiom()方法将词语添加到全局黑名单并保存到文件
8. 修改_show_blacklist()方法显示全局黑名单,不再依赖游戏状态
9. 更新所有提示信息,明确说明是"永久禁用"
- 原因:用户要求被拒绝的词语应该永久不可用,而不是仅本局游戏不可用
- 阻碍因素:无
- 状态:未确认
# 最终审查
待REVIEW模式完成后填写

View File

@@ -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)`作为唯一键
- 五子棋需要双人对战,需要同时记录两个玩家
- 需要设计状态数据结构,存储:
- 对战双方IDplayer1_id, player2_id
- 当前轮到谁current_player_id
- 棋盘状态15x15二维数组
- 游戏状态waiting, playing, finished
- 胜者IDwinner_id如果有
### 2. 多轮对战并发
- 允许同一个chat中有多轮对战只要对战双方不同
- 需要一个机制来标识不同的对战局可以用对战双方ID的组合
- 状态查询需要能够找到特定用户参与的对战
### 3. 禁手规则实现
禁手规则(仅对黑方,即先手玩家):
- **三三禁手**:一手棋同时形成两个或以上的活三
- **四四禁手**:一手棋同时形成两个或以上的活四或冲四
- **长连禁手**:一手棋形成六子或以上的连珠
需要实现:
- 判断某个位置的四个方向(横、竖、左斜、右斜)的连珠情况
- 判断活三、活四、冲四的定义
- 在落子时检查是否触发禁手
### 4. 坐标系统
-A-O15列
-1-1515行
- 需要坐标转换函数:`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 @用户格式 `<at user_id="xxx"></at>` 进行解析
- 修改所有用户显示,从 `@用户{user_id}` 改为 `<at user_id="{user_id}"></at>`,以正确显示用户的群名称
- 涉及修改:开始游戏、落子、显示棋盘、认输、列出对战等所有用户显示位置
- 原因:修复用户识别失败和显示错误的问题
- 阻碍因素:用户识别仍然失败
- 状态:不成功
## [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代码块中正确对齐
- 原因:修复用户反馈的棋盘文本对齐问题
- 阻碍因素:无
- 状态:未确认
# 最终审查
(待完成后填写)

View File

@@ -58,5 +58,19 @@ GAME_CONFIG = {
"quiz": {
"timeout": 60, # 答题超时时间(秒)
},
"idiom": {
"max_history_display": 10, # 状态显示最近N个成语
"starter_idioms": [ # 起始成语池
"一马当先", "龙马精神", "马到成功", "开门见山",
"心想事成", "万事如意", "风调雨顺", "国泰民安",
"四季平安", "安居乐业", "业精于勤", "勤学苦练",
"练达老成", "成竹在胸", "胸有成竹", "竹报平安",
"平步青云", "云程发轫", "刃迎缕解", "解甲归田"
]
},
"gomoku": {
"max_concurrent_games": 5, # 每个聊天最多同时进行的游戏数
"board_size": 15, # 棋盘大小
},
}

View File

@@ -0,0 +1,5 @@
{
"blacklist": [],
"description": "成语接龙游戏全局黑名单,被拒绝的词语将永久不可使用"
}

View File

@@ -64,6 +64,24 @@ def get_help_message() -> str:
- `.quiz` - 随机问题
- `.quiz 答案` - 回答问题
### 🀄 成语接龙
- `.idiom start [成语]` - 开始游戏
- `.idiom [成语]` - 接龙
- `.idiom [成语] @某人` - 接龙并指定下一位
- `.idiom stop` - 结束游戏
- `.idiom status` - 查看状态
- `.idiom reject [词语]` - 拒绝词语加入黑名单(仅发起人)
- `.idiom blacklist` - 查看黑名单
### ⚫ 五子棋
- `.gomoku challenge` - 发起挑战
- `.gomoku accept` - 接受挑战
- `.gomoku A1` - 落子
- `.gomoku show` - 显示棋盘
- `.gomoku resign` - 认输
- `.gomoku list` - 列出所有对战
- `.gomoku stats` - 查看战绩
### 其他
- `.help` - 显示帮助
- `.stats` - 查看个人统计
@@ -95,7 +113,9 @@ def get_stats_message(user_id: int) -> str:
game_names = {
'rps': '✊ 石头剪刀布',
'guess': '🔢 猜数字',
'quiz': '📝 问答游戏'
'quiz': '📝 问答游戏',
'idiom': '🀄 成语接龙',
'gomoku': '⚫ 五子棋'
}
for row in stats:

567
games/gomoku.py Normal file
View File

@@ -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"<at user_id=\"{user_id}\"></at> 发起了五子棋挑战!\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"**黑方(先手)**<at user_id=\"{challenger_id}\"></at> ⚫\n\n"
text += f"**白方(后手)**<at user_id=\"{user_id}\"></at> ⚪\n\n"
text += f"**轮到**<at user_id=\"{challenger_id}\"></at> ⚫\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"⚠️ 现在轮到 <at user_id=\"{opponent_id}\"></at> 落子"
# 解析坐标
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"**获胜者**<at user_id=\"{game['player_white']}\"></at> ⚪ 白方\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"**获胜者**<at user_id=\"{user_id}\"></at> {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"**轮到**<at user_id=\"{opponent_id}\"></at> {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"**黑方**<at user_id=\"{game['player_black']}\"></at> ⚫\n\n"
text += f"**白方**<at user_id=\"{game['player_white']}\"></at> ⚪\n\n"
text += f"**轮到**<at user_id=\"{current_id}\"></at> {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"<at user_id=\"{user_id}\"></at> {loser_name} 认输\n\n"
text += f"**获胜者**<at user_id=\"{winner_id}\"></at> {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"- **黑方**<at user_id=\"{game['player_black']}\"></at> ⚫\n"
text += f"- **白方**<at user_id=\"{game['player_white']}\"></at> ⚪\n"
text += f"- **轮到**<at user_id=\"{game['current_player']}\"></at> {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"**胜率**<font color='#4CAF50'>{win_rate:.1f}%</font>"
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-O15列
- 行1-1515行
- 示例A1左上角、O15右下角、H8中心
### 示例
```
.gomoku challenge # 发起挑战
.gomoku accept # 接受挑战
.gomoku H8 # 在中心位置落子
.gomoku show # 查看棋盘
.gomoku resign # 认输
```
💡 提示:黑方虽然先手,但需要注意禁手规则
"""

287
games/gomoku_logic.py Normal file
View File

@@ -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)

664
games/idiom.py Normal file
View File

@@ -0,0 +1,664 @@
"""成语接龙游戏"""
import json
import random
import logging
import time
import re
from pathlib import Path
from typing import Optional, Tuple, Dict, Any, List
from pypinyin import pinyin, Style
from games.base import BaseGame
from utils.parser import CommandParser
from config import GAME_CONFIG
logger = logging.getLogger(__name__)
class IdiomGame(BaseGame):
"""成语接龙游戏"""
def __init__(self):
"""初始化游戏"""
super().__init__()
self.config = GAME_CONFIG.get('idiom', {})
self.max_history_display = self.config.get('max_history_display', 10)
self.starter_idioms = self.config.get('starter_idioms', [
"一马当先", "龙马精神", "马到成功", "开门见山"
])
self._blacklist = None
self.blacklist_file = Path(__file__).parent.parent / "data" / "idiom_blacklist.json"
async def handle(self, command: str, chat_id: int, user_id: int) -> str:
"""处理成语接龙指令
Args:
command: 指令,如 ".idiom start"".idiom 马到成功"
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 ['start', '开始']:
starter = parts[1].strip() if len(parts) > 1 else None
return self._start_game(chat_id, user_id, starter)
# 结束游戏
if action in ['stop', '结束', 'end']:
return self._stop_game(chat_id, user_id)
# 查看状态
if action in ['status', '状态']:
return self._show_status(chat_id)
# 查看黑名单
if action in ['blacklist', '黑名单']:
return self._show_blacklist(chat_id)
# 查看帮助
if action in ['help', '帮助']:
return self.get_help()
# 裁判拒绝
if action in ['reject', '拒绝']:
if len(parts) < 2:
return "❌ 请指定要拒绝的词语,如:`.idiom reject 词语`"
idiom_to_reject = parts[1].strip()
return self._reject_idiom(chat_id, user_id, idiom_to_reject)
# 指定下一位
if action in ['next', '下一位']:
if len(parts) < 2:
return "❌ 请@要指定的用户"
mentioned = self._parse_mentioned_user(args)
if not mentioned:
return "❌ 未识别到@的用户"
return self._set_next_user(chat_id, user_id, mentioned)
# 默认:接龙
# 整个args都是成语可能包含@用户)
return self._make_chain(chat_id, user_id, args)
except Exception as e:
logger.error(f"处理成语接龙指令错误: {e}", exc_info=True)
return f"❌ 处理指令出错: {str(e)}"
def _load_blacklist(self) -> List[str]:
"""懒加载全局黑名单
Returns:
黑名单列表
"""
if self._blacklist is None:
try:
if self.blacklist_file.exists():
with open(self.blacklist_file, 'r', encoding='utf-8') as f:
data = json.load(f)
self._blacklist = data.get('blacklist', [])
else:
self._blacklist = []
logger.info(f"黑名单加载完成,共 {len(self._blacklist)} 个词语")
except Exception as e:
logger.error(f"加载黑名单失败: {e}")
self._blacklist = []
return self._blacklist
def _save_blacklist(self):
"""保存黑名单到文件"""
try:
self.blacklist_file.parent.mkdir(parents=True, exist_ok=True)
data = {
"blacklist": self._blacklist if self._blacklist is not None else [],
"description": "成语接龙游戏全局黑名单,被拒绝的词语将永久不可使用"
}
with open(self.blacklist_file, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
logger.info(f"黑名单已保存,共 {len(data['blacklist'])} 个词语")
except Exception as e:
logger.error(f"保存黑名单失败: {e}")
def _get_pinyin(self, char: str, all_readings: bool = True) -> list:
"""获取单字拼音
Args:
char: 单个汉字
all_readings: 是否返回多音字的所有读音
Returns:
拼音列表
"""
try:
result = pinyin(char, style=Style.NORMAL, heteronym=all_readings)
return result[0] if result else []
except Exception as e:
logger.error(f"获取拼音错误: {e}")
return []
def _check_pinyin_match(self, last_char: str, first_char: str) -> Tuple[bool, str, str]:
"""检查拼音匹配
Args:
last_char: 上一个成语的最后一个字
first_char: 当前成语的第一个字
Returns:
(是否匹配, 上一个拼音, 当前拼音)
"""
last_pinyins = self._get_pinyin(last_char)
first_pinyins = self._get_pinyin(first_char)
# 只要有任何一个读音匹配就算成功
for lp in last_pinyins:
for fp in first_pinyins:
if lp.lower() == fp.lower():
return True, lp, fp
# 没有匹配,返回第一个读音
return False, last_pinyins[0] if last_pinyins else '', first_pinyins[0] if first_pinyins else ''
def _parse_mentioned_user(self, content: str) -> Optional[int]:
"""解析消息中@的用户ID
Args:
content: 消息内容
Returns:
用户ID或None
"""
# 简化实现:查找@后的数字
# 实际WPS API可能有特定格式需要根据文档调整
match = re.search(r'@.*?(\d+)', content)
if match:
try:
return int(match.group(1))
except ValueError:
pass
return None
def _can_answer(self, state_data: Dict, user_id: int) -> Tuple[bool, str]:
"""检查用户是否可以接龙
Args:
state_data: 游戏状态数据
user_id: 用户ID
Returns:
(是否可以, 错误消息)
"""
# 不能是上一个接龙的人
if state_data.get('last_user_id') == user_id:
return False, "❌ 不能连续接龙哦!让其他人来吧"
# 如果指定了下一位,必须是指定的人
if state_data.get('next_user_id') is not None:
if state_data['next_user_id'] != user_id:
return False, f"❌ 现在轮到指定的人接龙了"
return True, ""
def _validate_idiom(self, idiom: str, state_data: Dict) -> Tuple[bool, str]:
"""验证词语有效性
Args:
idiom: 待验证的词语
state_data: 游戏状态数据
Returns:
(是否有效, 错误消息)
"""
# 检查长度
if len(idiom) != 4:
return False, "❌ 词语必须是4个字"
# 检查是否已使用
if idiom in state_data.get('used_idioms', []):
return False, f"❌ 「{idiom}」已经用过了"
# 检查是否在全局黑名单
blacklist = self._load_blacklist()
if idiom in blacklist:
return False, f"❌ 「{idiom}」在黑名单中(永久禁用)"
# 检查拼音匹配
current_idiom = state_data.get('current_idiom', '')
if current_idiom:
last_char = current_idiom[-1]
first_char = idiom[0]
is_match, last_py, first_py = self._check_pinyin_match(last_char, first_char)
if not is_match:
return False, f"❌ 首字「{first_char}」拼音[{first_py}]不匹配上个成语尾字「{last_char}」拼音[{last_py}]"
return True, ""
def _start_game(self, chat_id: int, user_id: int, starter_idiom: Optional[str]) -> str:
"""开始新游戏
Args:
chat_id: 会话ID
user_id: 用户ID
starter_idiom: 起始成语(可选)
Returns:
提示消息
"""
# 检查是否已有进行中的游戏user_id=0表示群级别状态
state = self.db.get_game_state(chat_id, 0, 'idiom')
if state:
return "⚠️ 已经有一个进行中的游戏了!\n\n输入 `.idiom stop` 结束当前游戏"
# 确定起始成语
if starter_idiom:
# 验证起始成语
if len(starter_idiom) != 4:
return "❌ 起始成语必须是4个字"
idiom = starter_idiom
else:
# 随机选择
idiom = random.choice(self.starter_idioms)
# 获取最后一个字的拼音
last_char = idiom[-1]
last_pinyin_list = self._get_pinyin(last_char)
last_pinyin = last_pinyin_list[0] if last_pinyin_list else ''
# 创建游戏状态
state_data = {
'creator_id': user_id,
'current_idiom': idiom,
'current_pinyin_last': last_pinyin,
'last_user_id': user_id, # 发起人可以接第一个
'next_user_id': None,
'used_idioms': [idiom],
'chain_length': 1,
'participants': {},
'history': [
{
'user_id': user_id,
'idiom': idiom,
'timestamp': int(time.time())
}
],
'status': 'playing'
}
# 保存群状态user_id=0
self.db.save_game_state(chat_id, 0, 'idiom', state_data)
text = f"## 🀄 成语接龙开始!\n\n"
text += f"**起始成语**{idiom} [{last_pinyin}]\n\n"
text += f"任何人都可以接龙,输入 `.idiom [成语]` 开始吧!\n\n"
text += f"💡 提示:可以用 `.idiom [成语] @某人` 指定下一位"
return text
def _make_chain(self, chat_id: int, user_id: int, args: str) -> str:
"""玩家接龙
Args:
chat_id: 会话ID
user_id: 用户ID
args: 参数(成语 + 可能的@用户)
Returns:
结果消息
"""
# 获取群状态
state = self.db.get_game_state(chat_id, 0, 'idiom')
if not state:
return "⚠️ 还没有开始游戏呢!\n\n输入 `.idiom start` 开始游戏"
state_data = state['state_data']
# 检查用户权限
can_answer, error_msg = self._can_answer(state_data, user_id)
if not can_answer:
return error_msg
# 解析成语和@用户
# 提取成语(去除@部分)
idiom_match = re.match(r'^([^\s@]+)', args)
if not idiom_match:
return "❌ 请输入4个字的词语"
idiom = idiom_match.group(1).strip()
# 验证词语
is_valid, error_msg = self._validate_idiom(idiom, state_data)
if not is_valid:
return error_msg
# 解析@用户
mentioned_user_id = self._parse_mentioned_user(args)
# 获取拼音
last_char = idiom[-1]
last_pinyin_list = self._get_pinyin(last_char)
last_pinyin = last_pinyin_list[0] if last_pinyin_list else ''
# 更新状态
state_data['current_idiom'] = idiom
state_data['current_pinyin_last'] = last_pinyin
state_data['last_user_id'] = user_id
state_data['next_user_id'] = mentioned_user_id
state_data['used_idioms'].append(idiom)
state_data['chain_length'] += 1
# 更新参与者统计
if str(user_id) not in state_data['participants']:
state_data['participants'][str(user_id)] = 0
state_data['participants'][str(user_id)] += 1
# 记录历史
state_data['history'].append({
'user_id': user_id,
'idiom': idiom,
'timestamp': int(time.time())
})
# 保存状态
self.db.save_game_state(chat_id, 0, 'idiom', state_data)
# 构建回复
text = f"## ✅ 接龙成功!\n\n"
text += f"**{idiom}** [{last_pinyin}]\n\n"
text += f"**当前链长**{state_data['chain_length']}\n\n"
user_count = state_data['participants'][str(user_id)]
text += f"@用户{user_id} 成功次数:{user_count}\n\n"
if mentioned_user_id:
text += f"已指定 @用户{mentioned_user_id} 接龙\n\n"
else:
text += "任何人都可以接龙\n\n"
text += "继续加油!💪"
return text
def _set_next_user(self, chat_id: int, user_id: int, next_user_id: int) -> str:
"""指定下一位接龙者
Args:
chat_id: 会话ID
user_id: 当前用户ID
next_user_id: 指定的下一位用户ID
Returns:
提示消息
"""
# 获取群状态
state = self.db.get_game_state(chat_id, 0, 'idiom')
if not state:
return "⚠️ 还没有开始游戏呢!"
state_data = state['state_data']
# 检查是否是最后接龙的人
if state_data.get('last_user_id') != user_id:
return "❌ 只有最后接龙成功的人可以指定下一位"
# 更新状态
state_data['next_user_id'] = next_user_id
self.db.save_game_state(chat_id, 0, 'idiom', state_data)
return f"✅ 已指定 @用户{next_user_id} 接龙"
def _reject_idiom(self, chat_id: int, user_id: int, idiom: str) -> str:
"""裁判拒绝词语
Args:
chat_id: 会话ID
user_id: 用户ID
idiom: 要拒绝的词语
Returns:
提示消息
"""
# 获取群状态
state = self.db.get_game_state(chat_id, 0, 'idiom')
if not state:
return "⚠️ 还没有开始游戏呢!"
state_data = state['state_data']
# 检查权限(仅发起人)
if state_data.get('creator_id') != user_id:
return "❌ 只有游戏发起人可以执行裁判操作"
# 添加到全局黑名单
blacklist = self._load_blacklist()
if idiom not in blacklist:
blacklist.append(idiom)
self._save_blacklist()
logger.info(f"词语「{idiom}」已加入全局黑名单")
# 如果是最后一个成语,回退状态
if state_data.get('current_idiom') == idiom and len(state_data['history']) > 1:
# 移除最后一条历史
removed = state_data['history'].pop()
removed_user = str(removed['user_id'])
# 减少该用户的计数
if removed_user in state_data['participants']:
state_data['participants'][removed_user] -= 1
if state_data['participants'][removed_user] <= 0:
del state_data['participants'][removed_user]
# 恢复到上一个成语
if state_data['history']:
last_entry = state_data['history'][-1]
last_idiom = last_entry['idiom']
last_char = last_idiom[-1]
last_pinyin_list = self._get_pinyin(last_char)
state_data['current_idiom'] = last_idiom
state_data['current_pinyin_last'] = last_pinyin_list[0] if last_pinyin_list else ''
state_data['last_user_id'] = last_entry['user_id']
state_data['next_user_id'] = None
state_data['chain_length'] -= 1
# 从已使用列表中移除
if idiom in state_data['used_idioms']:
state_data['used_idioms'].remove(idiom)
# 保存状态
self.db.save_game_state(chat_id, 0, 'idiom', state_data)
text = f"✅ 已将「{idiom}」加入全局黑名单(永久禁用)"
if state_data.get('current_idiom') != idiom:
text += f"\n\n当前成语:{state_data['current_idiom']}"
else:
text += "\n\n游戏状态已回退"
return text
def _show_status(self, chat_id: int) -> str:
"""显示游戏状态
Args:
chat_id: 会话ID
Returns:
状态信息
"""
# 获取群状态
state = self.db.get_game_state(chat_id, 0, 'idiom')
if not state:
return "⚠️ 还没有开始游戏呢!\n\n输入 `.idiom start` 开始游戏"
state_data = state['state_data']
text = f"## 🀄 成语接龙状态\n\n"
text += f"**当前成语**{state_data['current_idiom']} [{state_data['current_pinyin_last']}]\n\n"
text += f"**链长**{state_data['chain_length']}\n\n"
# 下一位
if state_data.get('next_user_id'):
text += f"**下一位**@用户{state_data['next_user_id']}\n\n"
else:
text += f"**下一位**:任何人都可以接龙\n\n"
# 参与者排行
if state_data['participants']:
text += f"### 🏆 参与者排行\n\n"
sorted_participants = sorted(
state_data['participants'].items(),
key=lambda x: x[1],
reverse=True
)
for idx, (uid, count) in enumerate(sorted_participants[:5], 1):
text += f"{idx}. @用户{uid} - {count}\n"
text += "\n"
# 最近成语
history = state_data.get('history', [])
if history:
display_count = min(self.max_history_display, len(history))
recent = history[-display_count:]
text += f"### 📜 最近{display_count}个成语\n\n"
text += "".join([h['idiom'] for h in recent])
return text
def _show_blacklist(self, chat_id: int) -> str:
"""显示全局黑名单
Args:
chat_id: 会话ID保留参数以保持接口一致性
Returns:
黑名单信息
"""
# 加载全局黑名单
blacklist = self._load_blacklist()
if not blacklist:
return "📋 全局黑名单为空\n\n💡 发起人可使用 `.idiom reject [词语]` 添加不合适的词语到黑名单"
text = f"## 📋 全局黑名单词语(永久禁用)\n\n"
text += f"**共 {len(blacklist)} 个词语**\n\n"
text += "".join(blacklist)
text += "\n\n💡 这些词语在所有游戏中都不可使用"
return text
def _stop_game(self, chat_id: int, user_id: int) -> str:
"""结束游戏
Args:
chat_id: 会话ID
user_id: 用户ID
Returns:
总结消息
"""
# 获取群状态
state = self.db.get_game_state(chat_id, 0, 'idiom')
if not state:
return "⚠️ 还没有开始游戏呢!"
state_data = state['state_data']
# 构建总结
text = f"## 🎮 游戏结束!\n\n"
text += f"**总链长**{state_data['chain_length']}\n\n"
text += f"**参与人数**{len(state_data['participants'])}\n\n"
# 排行榜
if state_data['participants']:
text += f"### 🏆 排行榜\n\n"
sorted_participants = sorted(
state_data['participants'].items(),
key=lambda x: x[1],
reverse=True
)
for idx, (uid, count) in enumerate(sorted_participants, 1):
text += f"{idx}. @用户{uid} - {count}\n"
# 更新统计
try:
for _ in range(count):
self.db.update_game_stats(int(uid), 'idiom', win=True)
except Exception as e:
logger.error(f"更新统计失败: {e}")
text += "\n"
# 完整接龙
history = state_data.get('history', [])
if history:
text += f"### 📜 完整接龙\n\n"
idioms = [h['idiom'] for h in history]
text += "".join(idioms)
# 删除游戏状态
self.db.delete_game_state(chat_id, 0, 'idiom')
return text
def _format_history(self, history: list, count: int) -> str:
"""格式化历史记录
Args:
history: 历史记录列表
count: 显示数量
Returns:
格式化的字符串
"""
if not history:
return ""
display_count = min(count, len(history))
recent = history[-display_count:]
return "".join([h['idiom'] for h in recent])
def get_help(self) -> str:
"""获取帮助信息"""
return """## 🀄 成语接龙
### 基础用法
- `.idiom start [成语]` - 开始游戏(可指定起始成语)
- `.idiom [成语]` - 接龙
- `.idiom [成语] @某人` - 接龙并指定下一位
- `.idiom stop` - 结束游戏(任何人可执行)
### 其他指令
- `.idiom status` - 查看游戏状态
- `.idiom blacklist` - 查看黑名单
- `.idiom reject [词语]` - 裁判拒绝词语(仅发起人)
- `.idiom next @某人` - 指定下一位(仅最后接龙者)
### 游戏规则
- 词语必须是4个字
- 首字拼音必须匹配上个成语尾字拼音(忽略声调)
- 不能重复使用成语
- 不能连续接龙
- 黑名单词语不可使用
- 任何人都可以结束游戏
### 示例
```
.idiom start 一马当先 # 开始游戏
.idiom 先声夺人 # 接龙
.idiom 人山人海 @张三 # 接龙并指定下一位
.idiom reject 某词 # 发起人拒绝某词
.idiom stop # 结束游戏
```
💡 提示:支持多音字和谐音接龙
"""

View File

@@ -15,5 +15,8 @@ pydantic-settings==2.1.0
# 系统监控
psutil==7.1.2
# 拼音处理
pypinyin==0.51.0
# 注意使用Python标准库sqlite3不引入SQLAlchemy

View File

@@ -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:
@@ -146,6 +147,18 @@ async def handle_command(game_type: str, command: str,
game = QuizGame()
return await game.handle(command, chat_id, user_id)
# 成语接龙
if game_type == 'idiom':
from games.idiom import IdiomGame
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 "❌ 未知的游戏类型"

View File

@@ -30,6 +30,16 @@ class CommandParser:
'.quiz': 'quiz',
'.问答': 'quiz',
# 成语接龙
'.idiom': 'idiom',
'.成语接龙': 'idiom',
'.成语': 'idiom',
# 五子棋
'.gomoku': 'gomoku',
'.五子棋': 'gomoku',
'.gobang': 'gomoku',
# 帮助
'.help': 'help',
'.帮助': 'help',