Files
WPSBot/games/idiom.py

665 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""成语接龙游戏"""
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 # 结束游戏
```
💡 提示:支持多音字和谐音接龙
"""