2025-10-28 16:12:32 +08:00
|
|
|
|
"""成语接龙游戏"""
|
2025-10-28 16:25:04 +08:00
|
|
|
|
import json
|
2025-10-28 16:12:32 +08:00
|
|
|
|
import random
|
|
|
|
|
|
import logging
|
|
|
|
|
|
import time
|
|
|
|
|
|
import re
|
2025-10-28 16:25:04 +08:00
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
from typing import Optional, Tuple, Dict, Any, List
|
2025-10-28 16:12:32 +08:00
|
|
|
|
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', [
|
|
|
|
|
|
"一马当先", "龙马精神", "马到成功", "开门见山"
|
|
|
|
|
|
])
|
2025-10-28 16:25:04 +08:00
|
|
|
|
self._blacklist = None
|
|
|
|
|
|
self.blacklist_file = Path(__file__).parent.parent / "data" / "idiom_blacklist.json"
|
2025-10-28 16:12:32 +08:00
|
|
|
|
|
|
|
|
|
|
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)}"
|
|
|
|
|
|
|
2025-10-28 16:25:04 +08:00
|
|
|
|
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}")
|
|
|
|
|
|
|
2025-10-28 16:12:32 +08:00
|
|
|
|
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}」已经用过了"
|
|
|
|
|
|
|
2025-10-28 16:25:04 +08:00
|
|
|
|
# 检查是否在全局黑名单
|
|
|
|
|
|
blacklist = self._load_blacklist()
|
|
|
|
|
|
if idiom in blacklist:
|
|
|
|
|
|
return False, f"❌ 「{idiom}」在黑名单中(永久禁用)"
|
2025-10-28 16:12:32 +08:00
|
|
|
|
|
|
|
|
|
|
# 检查拼音匹配
|
|
|
|
|
|
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)]
|
2025-10-31 11:07:41 +08:00
|
|
|
|
user_display_name = self.db.get_user_display_name(user_id)
|
|
|
|
|
|
text += f"@{user_display_name} 成功次数:{user_count}\n\n"
|
2025-10-28 16:12:32 +08:00
|
|
|
|
|
|
|
|
|
|
if mentioned_user_id:
|
2025-10-31 11:07:41 +08:00
|
|
|
|
mentioned_display_name = self.db.get_user_display_name(mentioned_user_id)
|
|
|
|
|
|
text += f"已指定 @{mentioned_display_name} 接龙\n\n"
|
2025-10-28 16:12:32 +08:00
|
|
|
|
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)
|
|
|
|
|
|
|
2025-10-31 11:07:41 +08:00
|
|
|
|
next_user_display_name = self.db.get_user_display_name(next_user_id)
|
|
|
|
|
|
return f"✅ 已指定 @{next_user_display_name} 接龙"
|
2025-10-28 16:12:32 +08:00
|
|
|
|
|
|
|
|
|
|
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 "❌ 只有游戏发起人可以执行裁判操作"
|
|
|
|
|
|
|
2025-10-28 16:25:04 +08:00
|
|
|
|
# 添加到全局黑名单
|
|
|
|
|
|
blacklist = self._load_blacklist()
|
|
|
|
|
|
if idiom not in blacklist:
|
|
|
|
|
|
blacklist.append(idiom)
|
|
|
|
|
|
self._save_blacklist()
|
|
|
|
|
|
logger.info(f"词语「{idiom}」已加入全局黑名单")
|
2025-10-28 16:12:32 +08:00
|
|
|
|
|
|
|
|
|
|
# 如果是最后一个成语,回退状态
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
2025-10-28 16:25:04 +08:00
|
|
|
|
text = f"✅ 已将「{idiom}」加入全局黑名单(永久禁用)"
|
2025-10-28 16:12:32 +08:00
|
|
|
|
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'):
|
2025-10-31 11:07:41 +08:00
|
|
|
|
next_user_display_name = self.db.get_user_display_name(state_data['next_user_id'])
|
|
|
|
|
|
text += f"**下一位**:@{next_user_display_name}\n\n"
|
2025-10-28 16:12:32 +08:00
|
|
|
|
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):
|
2025-10-31 11:07:41 +08:00
|
|
|
|
user_display_name = self.db.get_user_display_name(int(uid))
|
|
|
|
|
|
text += f"{idx}. @{user_display_name} - {count}次\n"
|
2025-10-28 16:12:32 +08:00
|
|
|
|
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:
|
2025-10-28 16:25:04 +08:00
|
|
|
|
"""显示全局黑名单
|
2025-10-28 16:12:32 +08:00
|
|
|
|
|
|
|
|
|
|
Args:
|
2025-10-28 16:25:04 +08:00
|
|
|
|
chat_id: 会话ID(保留参数以保持接口一致性)
|
2025-10-28 16:12:32 +08:00
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
黑名单信息
|
|
|
|
|
|
"""
|
2025-10-28 16:25:04 +08:00
|
|
|
|
# 加载全局黑名单
|
|
|
|
|
|
blacklist = self._load_blacklist()
|
2025-10-28 16:12:32 +08:00
|
|
|
|
|
|
|
|
|
|
if not blacklist:
|
2025-10-28 16:25:04 +08:00
|
|
|
|
return "📋 全局黑名单为空\n\n💡 发起人可使用 `.idiom reject [词语]` 添加不合适的词语到黑名单"
|
2025-10-28 16:12:32 +08:00
|
|
|
|
|
2025-10-28 16:25:04 +08:00
|
|
|
|
text = f"## 📋 全局黑名单词语(永久禁用)\n\n"
|
|
|
|
|
|
text += f"**共 {len(blacklist)} 个词语**\n\n"
|
2025-10-28 16:12:32 +08:00
|
|
|
|
text += "、".join(blacklist)
|
2025-10-28 16:25:04 +08:00
|
|
|
|
text += "\n\n💡 这些词语在所有游戏中都不可使用"
|
2025-10-28 16:12:32 +08:00
|
|
|
|
|
|
|
|
|
|
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):
|
2025-10-31 11:07:41 +08:00
|
|
|
|
user_display_name = self.db.get_user_display_name(int(uid))
|
|
|
|
|
|
text += f"{idx}. @{user_display_name} - {count}次\n"
|
2025-10-28 16:12:32 +08:00
|
|
|
|
# 更新统计
|
|
|
|
|
|
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 # 结束游戏
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
💡 提示:支持多音字和谐音接龙
|
|
|
|
|
|
"""
|
|
|
|
|
|
|