Merge branch 'task/add-idiom-chain-game_2025-10-28_1'

This commit is contained in:
2025-10-28 16:46:05 +08:00
8 changed files with 942 additions and 1 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

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

View File

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

View File

@@ -64,6 +64,15 @@ def get_help_message() -> str:
- `.quiz` - 随机问题 - `.quiz` - 随机问题
- `.quiz 答案` - 回答问题 - `.quiz 答案` - 回答问题
### 🀄 成语接龙
- `.idiom start [成语]` - 开始游戏
- `.idiom [成语]` - 接龙
- `.idiom [成语] @某人` - 接龙并指定下一位
- `.idiom stop` - 结束游戏
- `.idiom status` - 查看状态
- `.idiom reject [词语]` - 拒绝词语加入黑名单(仅发起人)
- `.idiom blacklist` - 查看黑名单
### 其他 ### 其他
- `.help` - 显示帮助 - `.help` - 显示帮助
- `.stats` - 查看个人统计 - `.stats` - 查看个人统计
@@ -95,7 +104,8 @@ def get_stats_message(user_id: int) -> str:
game_names = { game_names = {
'rps': '✊ 石头剪刀布', 'rps': '✊ 石头剪刀布',
'guess': '🔢 猜数字', 'guess': '🔢 猜数字',
'quiz': '📝 问答游戏' 'quiz': '📝 问答游戏',
'idiom': '🀄 成语接龙'
} }
for row in stats: for row in stats:

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 psutil==7.1.2
# 拼音处理
pypinyin==0.51.0
# 注意使用Python标准库sqlite3不引入SQLAlchemy # 注意使用Python标准库sqlite3不引入SQLAlchemy

View File

@@ -146,6 +146,12 @@ async def handle_command(game_type: str, command: str,
game = QuizGame() game = QuizGame()
return await game.handle(command, chat_id, user_id) 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)
# 未知游戏类型 # 未知游戏类型
logger.warning(f"未知游戏类型: {game_type}") logger.warning(f"未知游戏类型: {game_type}")
return "❌ 未知的游戏类型" return "❌ 未知的游戏类型"

View File

@@ -30,6 +30,11 @@ class CommandParser:
'.quiz': 'quiz', '.quiz': 'quiz',
'.问答': 'quiz', '.问答': 'quiz',
# 成语接龙
'.idiom': 'idiom',
'.成语接龙': 'idiom',
'.成语': 'idiom',
# 帮助 # 帮助
'.help': 'help', '.help': 'help',
'.帮助': 'help', '.帮助': 'help',