diff --git a/.tasks/2025-10-28_1_add-idiom-chain-game.md b/.tasks/2025-10-28_1_add-idiom-chain-game.md index 1e497b6..e515da6 100644 --- a/.tasks/2025-10-28_1_add-idiom-chain-game.md +++ b/.tasks/2025-10-28_1_add-idiom-chain-game.md @@ -206,6 +206,32 @@ if game_type == '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模式完成后填写) diff --git a/data/idiom_blacklist.json b/data/idiom_blacklist.json new file mode 100644 index 0000000..6faa731 --- /dev/null +++ b/data/idiom_blacklist.json @@ -0,0 +1,5 @@ +{ + "blacklist": [], + "description": "成语接龙游戏全局黑名单,被拒绝的词语将永久不可使用" +} + diff --git a/games/base.py b/games/base.py index 025cab1..89786f3 100644 --- a/games/base.py +++ b/games/base.py @@ -70,6 +70,8 @@ def get_help_message() -> str: - `.idiom [成语] @某人` - 接龙并指定下一位 - `.idiom stop` - 结束游戏 - `.idiom status` - 查看状态 +- `.idiom reject [词语]` - 拒绝词语加入黑名单(仅发起人) +- `.idiom blacklist` - 查看黑名单 ### 其他 - `.help` - 显示帮助 diff --git a/games/idiom.py b/games/idiom.py index ea16cd6..15a0384 100644 --- a/games/idiom.py +++ b/games/idiom.py @@ -1,9 +1,11 @@ """成语接龙游戏""" +import json import random import logging import time import re -from typing import Optional, Tuple, Dict, Any +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 @@ -23,6 +25,8 @@ class IdiomGame(BaseGame): 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: """处理成语接龙指令 @@ -93,6 +97,40 @@ class IdiomGame(BaseGame): 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: """获取单字拼音 @@ -190,9 +228,10 @@ class IdiomGame(BaseGame): if idiom in state_data.get('used_idioms', []): return False, f"❌ 「{idiom}」已经用过了" - # 检查是否在黑名单 - if idiom in state_data.get('blacklist', []): - return False, f"❌ 「{idiom}」在黑名单中" + # 检查是否在全局黑名单 + blacklist = self._load_blacklist() + if idiom in blacklist: + return False, f"❌ 「{idiom}」在黑名单中(永久禁用)" # 检查拼音匹配 current_idiom = state_data.get('current_idiom', '') @@ -245,7 +284,6 @@ class IdiomGame(BaseGame): 'last_user_id': user_id, # 发起人可以接第一个 'next_user_id': None, 'used_idioms': [idiom], - 'blacklist': [], 'chain_length': 1, 'participants': {}, 'history': [ @@ -402,9 +440,12 @@ class IdiomGame(BaseGame): if state_data.get('creator_id') != user_id: return "❌ 只有游戏发起人可以执行裁判操作" - # 添加到黑名单 - if idiom not in state_data['blacklist']: - state_data['blacklist'].append(idiom) + # 添加到全局黑名单 + 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: @@ -438,7 +479,7 @@ class IdiomGame(BaseGame): # 保存状态 self.db.save_game_state(chat_id, 0, 'idiom', state_data) - text = f"✅ 已将「{idiom}」加入黑名单" + text = f"✅ 已将「{idiom}」加入全局黑名单(永久禁用)" if state_data.get('current_idiom') != idiom: text += f"\n\n当前成语:{state_data['current_idiom']}" else: @@ -495,27 +536,24 @@ class IdiomGame(BaseGame): return text def _show_blacklist(self, chat_id: int) -> str: - """显示黑名单 + """显示全局黑名单 Args: - chat_id: 会话ID + chat_id: 会话ID(保留参数以保持接口一致性) Returns: 黑名单信息 """ - # 获取群状态 - state = self.db.get_game_state(chat_id, 0, 'idiom') - if not state: - return "⚠️ 还没有开始游戏呢!" - - state_data = state['state_data'] - blacklist = state_data.get('blacklist', []) + # 加载全局黑名单 + blacklist = self._load_blacklist() if not blacklist: - return "📋 黑名单为空" + return "📋 全局黑名单为空\n\n💡 发起人可使用 `.idiom reject [词语]` 添加不合适的词语到黑名单" - text = f"## 📋 黑名单词语\n\n" + text = f"## 📋 全局黑名单词语(永久禁用)\n\n" + text += f"**共 {len(blacklist)} 个词语**\n\n" text += "、".join(blacklist) + text += "\n\n💡 这些词语在所有游戏中都不可使用" return text