"""成语接龙游戏""" 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)] user_display_name = self.db.get_user_display_name(user_id) text += f"@{user_display_name} 成功次数:{user_count}\n\n" if mentioned_user_id: mentioned_display_name = self.db.get_user_display_name(mentioned_user_id) text += f"已指定 @{mentioned_display_name} 接龙\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) next_user_display_name = self.db.get_user_display_name(next_user_id) return f"✅ 已指定 @{next_user_display_name} 接龙" 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'): next_user_display_name = self.db.get_user_display_name(state_data['next_user_id']) text += f"**下一位**:@{next_user_display_name}\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): user_display_name = self.db.get_user_display_name(int(uid)) text += f"{idx}. @{user_display_name} - {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): user_display_name = self.db.get_user_display_name(int(uid)) text += f"{idx}. @{user_display_name} - {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 # 结束游戏 ``` 💡 提示:支持多音字和谐音接龙 """