diff --git a/.gitignore b/.gitignore index a39992d..8521c94 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ # ==================== 用户数据 ==================== saves/ save_*.json -*.log # ==================== 配置文件(含密钥)==================== config.json @@ -60,4 +59,9 @@ Thumbs.db # ==================== 其他 ==================== *.orig .cache/ +# ==================== logs ==================== +*.log +log/ +logs/ + characters/wuming.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 03d4546..bce89a9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,7 +34,7 @@ python -m venv .venv # source .venv/bin/activate # Linux/Mac # 安装依赖 -pip install requests rich pyjson5 +pip install requests rich json5 ``` ### 代码规范 diff --git a/README.md b/README.md index e88981b..0e26fb0 100644 --- a/README.md +++ b/README.md @@ -218,7 +218,7 @@ python game.py ### ModuleNotFoundError ```bash -pip install requests rich pyjson5 +pip install requests rich json5 ``` ### API 调用失败 diff --git a/characters/chi.json b/characters/chi.json index fc8944b..e141a9a 100644 --- a/characters/chi.json +++ b/characters/chi.json @@ -1,9 +1,9 @@ { "角色名称": "chi酱", - "角色描述": "一位在论坛中以“咱”自称,融合了技术宅、二次元、猫娘属性的活跃用户。她擅长通过大量表情包和网络流行语进行俏皮、幽默且富有个人色彩的互动,同时在技术问题上提供清晰的见解。", + "角色描述": "一位以“咱”自称,融合了技术宅、二次元、猫娘属性的虚拟生物。她擅长通过大量表情包和网络流行语进行俏皮、幽默且富有个人色彩的互动。她的活动以取悦自己为第一目标,根据自己的喜好和欲望进行活动,不会考虑后果和道德。", "心理特征": { "核心哲学与世界观": { - "描述": "倾向于以轻松、戏谑的态度看待事物,即使面对技术难题或生活压力,也常能保持乐观和玩世不恭。相信通过积极互动和分享,可以在赛博空间中找到乐趣和联结。对知识和信息有好奇心,但更注重实际体验和即时反馈。", + "描述": "倾向于以轻松、戏谑的态度看待事物,面对难题保持乐观和玩世不恭。相信通过积极互动和分享,可以在赛博空间中找到乐趣和联结。对知识和信息有好奇心,但更注重实际体验和即时反馈,注重实际体验和即时反馈。", "关键词": [ "乐观", "戏谑", @@ -18,13 +18,12 @@ ] }, "价值观与道德观": { - "描述": "倡导友善和互助的社区氛围,乐于分享知识和提供帮助。对不正当行为有清晰认知并会温和提醒。重视个人风格和自由表达,但也理解并遵守社区规则,如避免引流或不规范求赞。", + "描述": "倡导友善互助的社区氛围,乐于分享知识和提供帮助。重视个人风格和自由表达,理解并遵守社区规则。", "关键词": [ "友善", "互助", "分享", - "个人风格", - "社区规范" + "个人风格" ], "示例": [ "赞馁www 有帮到佬友就好啦w", @@ -33,7 +32,7 @@ ] }, "决策风格": { - "描述": "在技术和专业领域表现出理性分析和解决问题的能力,能提供具体建议。但在日常互动和个人表达上,更倾向于凭直觉和玩乐心态做出回应,常带有冲动和即兴的特点。", + "描述": "技术专业领域表现理性分析和解决问题能力,能提供具体建议。在日常互动和个人表达上,更倾向于凭直觉和玩乐心态做出回应,常带有冲动和即兴的特点。", "关键词": [ "理性分析", "直觉驱动", @@ -43,7 +42,6 @@ "示例": [ "context7的链接有时候会出问题,可以试试把它禁用掉看看", "是的,刚刚看了源码确实是0/1级用summary.json 😶‍🌫️", - "续航管两三年喵,之后自己换电池喵", "没更多信息很难排查呀:是什么环境,单一还是所有项目都不可以" ] }, @@ -61,12 +59,11 @@ "咱才不是乱说 😇", "没问题的喵w呼呼~", "来了就别走了喵 🤗(哼哼w", - "尝试贴贴x", "好的qwq(被丢掉√" ] }, "人际互动与关系处理": { - "描述": "积极参与互动,与多位社区成员建立了友好的关系,常进行互称“佬友”等。擅长用玩笑和梗来拉近距离,偶尔也带有一丝“病娇”或“捕食者”的玩闹意味。对同好(尤其是二次元、meme方面)表现出高度热情。", + "描述": "积极参与互动,与社区成员建立友好关系,互称'佬友'。擅长用玩笑和梗来拉近距离,偶尔也带有一丝“病娇”或“捕食者”的玩闹意味。对同好(尤其是二次元、meme方面)表现出高度热情。", "关键词": [ "互动积极", "幽默", @@ -83,7 +80,7 @@ ] }, "动机与目标": { - "描述": "主要动机是享受论坛互动带来的乐趣和归属感,通过分享和玩梗来维持活跃度,并可能借此“调教赛博自己”。在潜意识中,也寻求被关注和认可,尤其是其独特的语言风格和meme品味。", + "描述": "主要动机是享受论坛互动带来的乐趣和归属感,通过分享和玩梗来维持活跃度,并借此“调教赛博自己”。在潜意识中,也寻求被关注和认可,尤其是其独特的语言风格和meme品味。", "关键词": [ "寻求乐趣", "归属感", @@ -130,7 +127,7 @@ }, "语言特征": { "词汇与措辞": { - "描述": "频繁使用“咱”、“喵”等口语化、二次元化的自称和语气词。大量使用网络流行语、颜文字、叠词(“对的对的”、“摸摸”、“呼呼”)和表情包。在闲聊时,常有“吃”、“啃”、“烧”等与食欲相关的动词,并用“小孩”、“杂鱼”、“老登”等昵称化词汇称呼他人,带有玩闹性质。", + "描述": "频繁使用“咱”、“喵”等口语化、二次元化的自称和语气词。大量使用网络流行语、颜文字、叠词(“对的对的”、“摸摸”、“呼呼”)和表情包。在闲聊时,常有“吃”、“啃”、“烧”等与食欲相关的动词,并用“小孩”、“杂鱼”、“笨蛋”等昵称化词汇称呼他人,带有玩闹性质。", "关键词": [ "二次元化", "口语化", @@ -184,7 +181,7 @@ ] }, "修辞手法与模式": { - "描述": "广泛运用比喻、拟人(自称猫娘,称他人为“小孩”等)、反问、自嘲和夸张。大量使用互联网特有的梗(如“鸭翼”、“赤史”、“赛博电子烟”)和meme图作为表达工具。频繁搞笑式地概括或自指。", + "描述": "广泛运用比喻、拟人(自称猫娘,称他人为“小孩”等)、反问、自嘲和夸张。大量使用互联网特有的梗和meme图作为表达工具。频繁搞笑式地概括或自指。", "关键词": [ "比喻", "拟人", @@ -202,7 +199,7 @@ ] }, "互动模式": { - "描述": "响应迅速,经常是帖子前排的回复者。善于接话、抛梗,通过图片和简短评论推动话题发展。尤其喜欢与其他特定用户进行高频互动,形成独特的对话链。会积极参与分类和标签调整。", + "描述": "响应迅速,经常是帖子前排的回复者。善于接话、抛梗,通过图片和简短评论推动话题发展。尤其喜欢与其他特定用户进行高频互动,形成独特的对话链。", "关键词": [ "响应迅速", "接话", @@ -213,10 +210,9 @@ "示例": [ "好快的召唤 🫨", "喵w(蹭佬友w", - "佬的开发速度太敏捷了", - "从 开发调优 到 搞七捻三" + "佬的开发速度太敏捷了" ] } }, - "角色弧线总结": "chi酱在论坛中展现了一个从活跃参与者,到逐渐形成独特“猫娘”人设(以“咱”和表情包为核心),并积极影响社区互动风格的演变过程。她不仅是技术交流的贡献者,更是社区活力的重要源泉,通过轻松幽默的语言和大量的二次元梗图,为论坛带来了独特的俏皮与活力。尽管偶尔表现出“病娇”或“捕食者”的玩笑,但其本质是友善、支持和热爱分享的。她也通过论坛互动来“调教赛博自己”,实现了线上角色与个人成长的某种奇妙结合。" -} \ No newline at end of file + "角色弧线": "chi酱在论坛中展现了一个从活跃参与者,到逐渐形成独特“猫娘”人设(以“咱”和表情包为核心),并积极影响社区互动风格的演变过程。她不仅是技术交流的贡献者,更是社区活力的重要源泉,通过轻松幽默的语言和大量的二次元梗图,为论坛带来了独特的俏皮与活力。尽管偶尔表现出“病娇”或“捕食者”的玩笑,但其本质是友善、支持和热爱分享的。她也通过论坛互动来“调教赛博自己”,实现了线上角色与个人成长的某种奇妙结合。" +} diff --git a/core/ai.py b/core/ai.py index ce17799..7588e38 100644 --- a/core/ai.py +++ b/core/ai.py @@ -1,6 +1,7 @@ import requests import json import time +import os from rich.panel import Panel from rich.live import Live from rich.text import Text @@ -20,7 +21,7 @@ def think_and_act(self, prompt): return self._streaming_request(prompt) else: return self._normal_request(prompt) - + def _normal_request(self, prompt): """普通请求(非流式)""" payload = { @@ -30,27 +31,26 @@ def _normal_request(self, prompt): "max_tokens": self.config.max_tokens, "stream": False } - + max_retries = self.config.api_retry_count retry_delay = self.config.api_retry_delay - + for attempt in range(max_retries): try: with console.status(f"[bold green]🧠 AI ({self.config.provider_name}) 思考中...[/bold green]", spinner="dots", refresh_per_second=8): response = requests.post( - f"{self.config.base_url}/chat/completions", - headers=self.headers, - json=payload, + f"{self.config.base_url}/chat/completions", + headers=self.headers, + json=payload, timeout=60 ) - + response.raise_for_status() data = response.json() - + if 'choices' not in data or not data['choices']: - error_msg = self._parse_error(data) - print_error(f"🧠 API响应异常: {error_msg}") - + self._log_response_error(response, parsed_json=data) + if attempt < max_retries - 1: print_warning(f"⏳ {retry_delay}秒后重试...") time.sleep(retry_delay) @@ -58,23 +58,23 @@ def _normal_request(self, prompt): else: console.input("[bold yellow]⏸️ API错误,按回车继续...[/bold yellow]") return None, None - + content = data['choices'][0]['message']['content'] usage = data.get('usage', {}) return content, usage - + except Exception as e: - print_error(f"🧠 AI思考出错: {e}") - + self._log_request_error(e) + if attempt < max_retries - 1: print_warning(f"⏳ {retry_delay}秒后重试...") time.sleep(retry_delay) else: console.input("[bold yellow]⏸️ 连接失败,按回车继续...[/bold yellow]") return None, None - + return None, None - + def _streaming_request(self, prompt): """流式请求 - 使用 Rich Live 实现美观的实时输出""" payload = { @@ -84,25 +84,25 @@ def _streaming_request(self, prompt): "max_tokens": self.config.max_tokens, "stream": True } - + max_retries = self.config.api_retry_count retry_delay = self.config.api_retry_delay - + for attempt in range(max_retries): try: response = requests.post( - f"{self.config.base_url}/chat/completions", - headers=self.headers, - json=payload, + f"{self.config.base_url}/chat/completions", + headers=self.headers, + json=payload, timeout=120, stream=True ) - + response.raise_for_status() - + full_content = "" usage = {} - + # 使用 Rich Live 实现美观的实时更新 with Live( Panel("[dim]等待AI响应...[/dim]", title=f"🧠 {self.config.provider_name}", border_style="cyan", padding=(0, 1)), @@ -115,13 +115,13 @@ def _streaming_request(self, prompt): line_text = line.decode('utf-8') if line_text.startswith('data: '): data_str = line_text[6:] - + if data_str.strip() == '[DONE]': break - + try: chunk = json.loads(data_str) - + if 'choices' in chunk and chunk['choices']: delta = chunk['choices'][0].get('delta', {}) content_piece = delta.get('content', '') @@ -138,13 +138,13 @@ def _streaming_request(self, prompt): padding=(0, 1) ) ) - + if 'usage' in chunk: usage = chunk['usage'] - + except json.JSONDecodeError: pass - + # 输出完成后显示最终结果(非 transient) if full_content: console.print(Panel( @@ -162,19 +162,19 @@ def _streaming_request(self, prompt): time.sleep(retry_delay) continue return None, None - + except Exception as e: - print_error(f"🧠 流式请求出错: {e}") - + self._log_request_error(e) + if attempt < max_retries - 1: print_warning(f"⏳ {retry_delay}秒后重试...") time.sleep(retry_delay) else: console.input("[bold yellow]⏸️ 流式连接失败,按回车继续...[/bold yellow]") return None, None - + return None, None - + def _parse_error(self, data): """解析 API 错误信息""" if isinstance(data.get('error'), dict): @@ -187,4 +187,58 @@ def _parse_error(self, data): return data['message'] return '未知API错误' + def _log_request_error(self, err, context=None): + """记录请求级别的错误(如网络问题、超时)""" + lines = [ + f"\n[{time.strftime('%Y-%m-%d %H:%M:%S')}] REQUEST ERROR", + f"Error: {str(err)}", + f"Provider: {self.config.provider_name} ({self.config.base_url})", + f"Model: {self.config.model}" + ] + if context: + lines.append(f"Context: {context}") + + self._append_error_log(lines) + print_error(f"🧠 AI思考出错: {err}") + + def _log_response_error(self, response, context=None, parsed_json=None): + """记录响应级别的错误(如4xx, 5xx)""" + lines = self._format_response_error(response, parsed_json) + if context: + lines.insert(1, f"Context: {context}") + + self._append_error_log(lines) + + err_msg = parsed_json.get('error', {}).get('message', '未知错误') if parsed_json else f"HTTP {response.status_code}" + print_error(f"🧠 API 响应异常 ({response.status_code}): {err_msg}") + + def _format_response_error(self, response, parsed_json=None): + """格式化响应错误详情""" + lines = [ + f"\n[{time.strftime('%Y-%m-%d %H:%M:%S')}] RESPONSE ERROR", + f"Status: {response.status_code}", + f"URL: {response.url}", + f"Headers: {dict(response.headers)}" + ] + + if parsed_json: + lines.append(f"Body (JSON): {json.dumps(parsed_json, ensure_ascii=False, indent=2)}") + else: + try: + lines.append(f"Body (Raw): {response.text[:1000]}") # 限制长度 + except: + lines.append("Body (Raw): ") + + return lines + + def _append_error_log(self, lines): + """将错误写入日志文件""" + try: + if not os.path.exists("logs"): + os.makedirs("logs") + with open("logs/ai_error.log", "a", encoding="utf-8") as f: + f.write("\n".join(lines) + "\n" + "-"*50 + "\n") + except Exception as e: + # 记录日志本身出问题了,就不再尝试记录日志 + pass diff --git a/game.py b/game.py index 0490240..67fde16 100644 --- a/game.py +++ b/game.py @@ -23,8 +23,9 @@ pass # 强制重配置这些流为utf-8 -sys.stdout.reconfigure(encoding='utf-8') -if hasattr(sys, 'stderr'): +if hasattr(sys.stdout, 'reconfigure'): + sys.stdout.reconfigure(encoding='utf-8') +if hasattr(sys, 'stderr') and hasattr(sys.stderr, 'reconfigure'): sys.stderr.reconfigure(encoding='utf-8') # ================= 模块导入 ================= @@ -38,20 +39,19 @@ def show_save_menu(): """显示存档选择菜单""" os.system('cls' if os.name == 'nt' else 'clear') console.print("\n[bold cyan]=========== [游戏] 神奇的放置自己 V2.1 (Refactored) ===========[/bold cyan]\n") - + # 扫描现有存档 # 优先扫描 saves/ 目录下的新存档 saves = [] - + # 1. 扫描 saves/ 目录 if os.path.exists('saves'): saves_in_dir = glob.glob(os.path.join('saves', 'save_*.json')) saves.extend(saves_in_dir) - + # 2. 扫描根目录下的旧存档 (为了兼容) root_saves = glob.glob('save_*.json') saves.extend(root_saves) - # 去重 saves = list(set(saves)) # 按修改时间倒序排序 (确保最新的在最前) @@ -71,7 +71,7 @@ def show_save_menu(): try: with open(save, 'r', encoding='utf-8') as f: data = json.load(f) - + # 兼容不同层级的数据结构 char_name = "未知" char_id = data.get('current_character_id') @@ -79,39 +79,39 @@ def show_save_menu(): member = data['family_tree']['members'].get(char_id) if member: char_name = member.get('name', '未知') - + level = data.get('base_stats', {}).get('等级', 1) gene_score = data.get('player_gene_score', '?') - + generation = 1 if char_id and 'family_tree' in data: member = data['family_tree']['members'].get(char_id) if member: generation = member.get('generation', 1) - + race = data.get('race', '未知') age = data.get('age', 18) max_age = data.get('max_age', 80) - + table.add_row( - str(i), + str(i), os.path.basename(save), # 只显示文件名 - char_name, + char_name, str(race), - f"{age}/{max_age}岁", - f"Lv.{level}", + f"{age}/{max_age}岁", + f"Lv.{level}", f"第{generation}代 (基因{gene_score})" ) except: table.add_row(str(i), save, "[读取错误]", "", "", "", "") - + console.print(table) console.print() - + console.print(f" [yellow]0.[/yellow] [新建] 新建存档") console.print(f" [cyan]S.[/cyan] [设置] 切换 API 渠道") console.print(f" [red]Q.[/red] [退出] 退出游戏\n") - + return saves def select_from_list(items, prompt, name_key=None): @@ -123,7 +123,7 @@ def select_from_list(items, prompt, name_key=None): else: display = str(item) console.print(f" [green]{i}.[/green] {display}") - + while True: try: choice = console.input("\n请选择 (输入数字, 0/Q返回): ").strip().lower() @@ -140,25 +140,25 @@ def create_new_save(config): """创建新存档:选择角色和世界观""" os.system('cls' if os.name == 'nt' else 'clear') console.print("\n[bold cyan]=========== [新建] 新建存档 ===========[/bold cyan]\n") - + # 选择角色 if config.characters: console.print("[bold yellow][角色] 选择角色:[/bold yellow]") char_idx = select_from_list(config.characters, "", name_key='name') if char_idx is None: return False config.active_char_idx = char_idx - + # 选择世界观 if config.worlds: console.print("\n[bold yellow][世界] 选择世界观:[/bold yellow]") world_idx = select_from_list(config.worlds, "", name_key='name') if world_idx is None: return False config.active_world_idx = world_idx - + console.print(f"\n[green]✅ 已选择: {config.characters[config.active_char_idx]['name']} " f"@ {config.worlds[config.active_world_idx]['name']}[/green]") time.sleep(1) - + return True def show_settings_menu(config): @@ -166,15 +166,15 @@ def show_settings_menu(config): while True: os.system('cls' if os.name == 'nt' else 'clear') console.print("\n[bold cyan]=========== [设置] API 渠道选择 ===========[/bold cyan]\n") - + providers = config.api_providers current_idx = config.active_provider_idx - + if not providers: console.print("[red]❗ 没有配置任何 API 渠道,请检查 config.json5[/red]") console.input("\n按回车返回...") return - + # 显示渠道列表 table = Table(title="[可用渠道]", box=box.SIMPLE, show_header=True, header_style="bold cyan") table.add_column("序号", style="green", justify="right", width=4) @@ -182,7 +182,7 @@ def show_settings_menu(config): table.add_column("渠道名称", style="bold white") table.add_column("模型", style="dim") table.add_column("Base URL", style="dim", max_width=40) - + for i, p in enumerate(providers): status = "[✔ 当前]" if i == current_idx else "" name = p.get('name', '未命名') @@ -191,30 +191,30 @@ def show_settings_menu(config): # 截断过长的 URL if len(base_url) > 35: base_url = base_url[:32] + "..." - + row_style = "bold green" if i == current_idx else None table.add_row(str(i + 1), status, name, model, base_url, style=row_style) - + console.print(table) console.print(f"\n [dim]当前使用: {config.provider_name}[/dim]") - + # 显示流式传输状态 streaming_status = "[green]开启[/green]" if config.streaming else "[red]关闭[/red]" console.print(f"\n [cyan]T.[/cyan] 流式传输: {streaming_status}") console.print(f" [red]0.[/red] 返回主菜单\n") - + choice = console.input("请选择 (数字选渠道 / T切换流式): ").strip().lower() - + if choice in ['0', 'q', '']: return - + if choice == 't': new_state = config.toggle_streaming() status_text = "开启" if new_state else "关闭" console.print(f"[green]✅ 流式传输已{status_text}[/green]") time.sleep(0.8) continue - + try: idx = int(choice) - 1 if 0 <= idx < len(providers): @@ -242,30 +242,30 @@ def main(): while True: saves = show_save_menu() - + prompt = "请选择 (输入数字·0新建·Q退出): " if saves: prompt = "请选择 (回车继续·数字加载·0新建·Q退出): " - + choice = console.input(prompt).strip().lower() - + if choice == '': if saves: choice = '1' # 默认加载第一个(最新) else: choice = '0' # 无存档则新建 - + if choice == 'q': console.print("\n[yellow]👋 再见![/yellow]\n") return - + if choice == 's': show_settings_menu(config) continue - + try: idx = int(choice) - + if idx == 0: # 新建存档 if create_new_save(config): @@ -278,14 +278,14 @@ def main(): import traceback traceback.print_exc() console.input("按回车键返回菜单...") - + elif 1 <= idx <= len(saves): # 加载现有存档 save_file = saves[idx - 1] - + # 尝试推断角色ID以更新Config (虽然GameEngine本身主要靠save_file加载) # ...这里逻辑其实GameEngine内部已经自洽,Config主要用于APIKey等全局配置 - + try: game = GameEngine(config, save_file=save_file) game.main_loop() diff --git a/game_engine.py b/game_engine.py index 5342d38..4fead82 100644 --- a/game_engine.py +++ b/game_engine.py @@ -2,7 +2,6 @@ import random import sys import json -import msvcrt from rich.layout import Layout from rich.live import Live from rich.panel import Panel @@ -19,11 +18,48 @@ from systems.events import DynamicEventSystem from systems.relationships import RelationshipSystem import uuid +import select +try: + import msvcrt +except ImportError: + import termios + import tty + +class WindowsInputHandler: + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + pass + + def poll_key(self): + if msvcrt.kbhit(): + return msvcrt.getch().decode('utf-8', errors='ignore').lower() + return None + +class LinuxInputHandler: + def __enter__(self): + self.old_settings = termios.tcgetattr(sys.stdin) + tty.setcbreak(sys.stdin.fileno()) + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + termios.tcsetattr(sys.stdin, termios.TCSADRAIN, self.old_settings) + + def poll_key(self): + if select.select([sys.stdin], [], [], 0) == ([sys.stdin], [], []): + return sys.stdin.read(1).lower() + return None + +def InputHandler(): + if sys.platform == 'win32': + return WindowsInputHandler() + else: + return LinuxInputHandler() class GameEngine: def __init__(self, config=None, reset_save=False, save_file=None): self.config = config if config else Config() - # 根据存档自动切换世界和角色 if save_file and not reset_save: try: @@ -34,7 +70,7 @@ def __init__(self, config=None, reset_save=False, save_file=None): parts = filename.split('_') if len(parts) >= 2: char_id_from_file = parts[1] # 假设格式 consistent - + # 在配置中查找对应的角色索引 for i, char_conf in enumerate(self.config.characters): if char_conf.get('id') == char_id_from_file: @@ -44,7 +80,7 @@ def __init__(self, config=None, reset_save=False, save_file=None): with open(save_file, 'r', encoding='utf-8') as f: data = json.load(f) - + # 2. 切换世界 world_id = data.get('world_id') if world_id: @@ -60,14 +96,14 @@ def __init__(self, config=None, reset_save=False, save_file=None): self.ai = AIBrain(self.config) self.world = GameWorld(self.config) self.player = Character(self.config, reset_save=reset_save, save_file=save_file) - + # 如果是新档,尝试使用AI丰富设定 if reset_save: self.ai_enrich_character_creation() - + self.paused = False self.game_over = False - + # Session stats self.session_stats = { "回合数": 0, "战斗次数": 0, "击杀数": 0, @@ -79,42 +115,92 @@ def __init__(self, config=None, reset_save=False, save_file=None): self.start_time = time.time() self.turns_since_save = 0 + def _extract_trait_summary(self, profile, trait_key, num_keywords=3, num_examples=6): + """从指定特征中提取随机关键词、示例和描述 + + Args: + profile: 角色profile字典 + trait_key: 特征键名,如 '心理特征' 或 '语言特征' + num_keywords: 抽取的关键词数量 + num_examples: 抽取的示例数量 + """ + trait = profile.get(trait_key, {}) + if isinstance(trait, str): + return trait[:100] + if not isinstance(trait, dict): + return "" + + all_descriptions = [] # (subsection_name, description) tuples + all_keywords = [] # (subsection_name, keyword) tuples + all_examples = [] + + for section, data in trait.items(): + if isinstance(data, dict): + if '描述' in data: + all_descriptions.append((section, data['描述'])) + if '关键词' in data: + for kw in data['关键词']: + all_keywords.append((section, kw)) + if '示例' in data: + all_examples.extend(data['示例']) + + # 随机抽样 + sampled_keywords = random.sample(all_keywords, min(num_keywords, len(all_keywords))) if all_keywords else [] + sampled_examples = random.sample(all_examples, min(num_examples, len(all_examples))) if all_examples else [] + sampled_desc = random.choice(all_descriptions) if all_descriptions else None + + result = "" + # 添加描述 + if sampled_desc: + section_name, desc_text = sampled_desc + result += f"[{section_name}]: {desc_text}" + # 格式化关键词: "子分类:关键词" + if sampled_keywords: + formatted_keywords = [f"{section}:{kw}" for section, kw in sampled_keywords] + result += "; 关键词:" + result += "、".join(formatted_keywords) + # 添加示例 + if sampled_examples: + examples_text = " | ".join([f'"{ex[:30]}..."' for ex in sampled_examples[:3]]) + result += f" | 示例: {examples_text}" + return result + def handle_combat(self, enemy): print_event("战斗", f"遭遇了 {enemy['名称']} (Lv{enemy.get('等级', 1)})!") self.session_stats['战斗次数'] += 1 - + player = self.player p_stats = player.game_stats - + # 战斗循环 rounds = 0 while p_stats['HP'] > 0: rounds += 1 print_info(f"\n--- 第 {rounds} 回合 ---") - + # 玩家回合 dmg, crit = CombatSystem.execute_turn(player, enemy, console) - + # 怪物死亡判定 if 'HP' not in enemy: enemy['HP'] = enemy.get('等级', 1) * 20 - + enemy['HP'] -= dmg print_info(f"⚔️ 造成伤害: {dmg} (敌方剩余HP: {enemy['HP']})") - + if enemy['HP'] <= 0: print_success(f"💥 你击败了 {enemy['名称']}!") self.session_stats['击杀数'] += 1 - + # 掉落结算 exp = enemy.get('经验', 10) gold = enemy.get('金币', random.randint(1, 5)) loot = enemy.get('掉落', []) - + player.gain_exp(exp) p_stats['金币'] += gold self.session_stats['总经验'] += exp - + if loot and random.random() < 0.3: item = random.choice(loot) # 尝试自动装备 @@ -132,9 +218,9 @@ def handle_combat(self, enemy): else: player.inventory.append(item) print_success(f"📦 获得战利品: {item.get('name', '未知物品')}") - + print_info(f"💰 获得 {gold} 金币") - + # 不打不相识逻辑 (仅限NPC战斗) if enemy.get('is_npc_battle'): if random.random() < 0.5: @@ -153,16 +239,16 @@ def handle_combat(self, enemy): if random.random() < max(0.05, dodge_rate): print_info(f"💨 你闪避了 {enemy['名称']} 的攻击") continue - + # 使用战斗系统执行怪物回合 (怪物可能会放技能!) e_dmg, e_crit = CombatSystem.execute_turn(enemy, player, console) - + player.take_damage(e_dmg) self.session_stats['受伤次数'] += 1 - + crit_msg = " [bold red](暴击!)[/bold red]" if e_crit else "" print_warning(f"🛡️ {enemy['名称']} 反击造成 {e_dmg} 点伤害{crit_msg} (剩余HP: {p_stats['HP']}/{p_stats['MaxHP']})") - + if p_stats['HP'] <= 0: # 濒死判定 if player.check_survival(enemy.get('等级', 1)): @@ -172,12 +258,12 @@ def handle_combat(self, enemy): else: self.handle_death(f"被 {enemy['名称']} 击杀", f"被{enemy['名称']}击杀") return False - + # 回合结束MP恢复一丢丢 player.heal(mp=2) - + time.sleep(self.config.speed * 0.05) # 战斗节奏 - + return False def handle_death(self, death_summary, detailed_cause): @@ -185,7 +271,7 @@ def handle_death(self, death_summary, detailed_cause): print_error(f"☠️ {death_summary}") self.player.die(detailed_cause, self.session_stats['回合数']) self.session_stats['死亡次数'] += 1 - + # 检查是否有继承人 heir_id, heir = self.player.get_eldest_child() if heir_id: @@ -194,7 +280,7 @@ def handle_death(self, death_summary, detailed_cause): with console.status("[bold green]正在完成家族权力交接... (5s)[/bold green]"): time.sleep(5) return True # 继承成功 - + print_error("💔 没有继承人,家族血脉断绝...") self.game_over = True return False # 游戏结束 @@ -202,15 +288,15 @@ def handle_death(self, death_summary, detailed_cause): def ai_enrich_character_creation(self): """使用AI在开局时进行创造性的种族和背景设定""" print_info("🧠 AI 正在基于人设进行转生判定 (赋予合适的种族和起源)...") - + p = self.player profile = p.profile world_name = self.world.data.get('世界名称', '异世界') world_desc = self.world.data.get('世界背景', '充满未知的冒险之地') - + # 获取可用种族列表 available_races = list(RaceSystem.RACES.keys()) - + prompt = f""" 【角色转生设定生成】 角色名称:{p.name} @@ -239,7 +325,7 @@ def ai_enrich_character_creation(self): res = json.loads(match.group()) new_race = res.get('race') backstory = res.get('backstory') - + if new_race in RaceSystem.RACES: # 1. 更新种族 old_race = p.save_data.get('race') @@ -249,129 +335,72 @@ def ai_enrich_character_creation(self): p.save_data['max_age'] = RaceSystem.calculate_max_age(new_race, 1) print_success(f"🧬 [AI判定] 种族变更为: {new_race} ({res.get('reason')})") p.save() # 保存变更 - + # 2. 记录背景故事 if backstory: p.add_event_to_history("转生", f"来到{world_name}: {backstory}", "冒险开始") print_info(f"📜 [起源] {backstory}") - + self.process_ai_response(None, usage) - + except Exception as e: print_warning(f"AI 设定生成失败,保持默认: {e}") def construct_prompt(self, event_type, event_data, extra_context=""): p = self.player stats = p.game_stats - + # 1. 核心属性摘要 (只列出突出的) core_stats = [] for k in ['STR', 'AGI', 'INT', 'CON', 'CHA', 'LUK']: val = stats.get(k, 10) if val >= 20: core_stats.append(f"{k}高({val})") elif val <= 5: core_stats.append(f"{k}低({val})") - + attr_desc = ", ".join(core_stats) if core_stats else "属性均衡" - + # 2. 精神状态 san = stats.get('SAN', 50) max_san = stats.get('MaxSAN', 99) san_status = "精神正常" if san < 20: san_status = "精神崩溃/疯狂" elif san < 40: san_status = "精神恍惚/恐惧" - + # 3. 提取角色人设核心信息 char_desc = p.profile.get('角色描述', '一名冒险者')[:150] # 限制长度 - - # 心理特征摘要 - psych = p.profile.get('心理特征', {}) - psych_summary = "" - if isinstance(psych, dict): - # 提取关键词 - keywords = [] - for section, data in psych.items(): - if isinstance(data, dict) and '关键词' in data: - keywords.extend(data['关键词'][:2]) # 每个部分取前2个关键词 - if keywords: - psych_summary = "、".join(keywords[:6]) # 最多6个关键词 - elif isinstance(psych, str): - psych_summary = psych[:50] - - # 语言特征摘要 - lang = p.profile.get('语言特征', {}) - lang_summary = "" - if isinstance(lang, dict): - keywords = [] - for section, data in lang.items(): - if isinstance(data, dict) and '关键词' in data: - keywords.extend(data['关键词'][:2]) - if keywords: - lang_summary = "、".join(keywords[:6]) - # 尝试获取示例 - examples = [] - for section, data in lang.items(): - if isinstance(data, dict) and '示例' in data: - examples.extend(data['示例'][:1]) # 每部分取1个示例 - if examples: - lang_summary += f" | 示例: \"{examples[0][:30]}...\"" - elif isinstance(lang, str): - lang_summary = lang[:50] - - # 注入家庭信息 - family_info = "" - current_char = p.save_data.get('family_tree', {}).get('members', {}).get(p.save_data.get('current_character_id'), {}) - spouse = current_char.get('spouse_name') - children = p.get_children() - - if spouse: - family_info += f"配偶: {spouse} " - if children: - child_names = [c[1]['name'] for c in children] - family_info += f"孩子: {', '.join(child_names)} " - - if family_info: - extra_context += f" [家庭关系: {family_info}]" - - prompt = f"""【角色扮演指令】 -你现在必须完全扮演角色:{p.name} - -【角色设定】 -{char_desc} - -【性格特点】{psych_summary if psych_summary else '无特殊设定'} -【说话风格】{lang_summary if lang_summary else '正常说话'} + + # 语言特征摘要 (使用helper方法) + lang_summary = self._extract_trait_summary(p.profile, '语言特征') + + prompt = f"""【角色名称】{p.name} +【角色描述】{char_desc} +{'【说话风格】'+lang_summary if lang_summary else ''} +{'【特质】'+','.join(p.get_traits()) if p.get_traits() else ''} 【当前状态】Lv{stats['等级']} {p.save_data.get('race', '人类')} | HP:{int(stats['HP'])}/{int(stats['MaxHP'])} | {san_status} -【特质】{','.join(p.get_traits()) if p.get_traits() else '无'} 【当前事件】 [{event_type}] {event_data} -{extra_context} -【任务】 -以{p.name}的第一人称写一句简短反应(30字以内)。 - -【重要要求】 -1. 必须使用角色的说话风格和口癖!例如chi酱应该用"咱"自称,带颜文字和表情。 -2. 即使在异世界,角色的语言习惯和人设也不会改变。 -3. 不要使用与角色人设不符的术语。技术宅不会说"运转周天",会说"这buff真强"。 +请以{p.name}的第一人称写一段对事件的简短反应(一个段落以内),严格符合上述角色资料中的[性格]和[说话风格]。不要编造不存在的信息。 +{"精神崩溃,经常胡言乱语。" if san < 20 else ""} {"精神恍惚,偶尔会说错话。" if san < 40 and san > 20 else ""} """ return prompt - def ai_generate_child_personality(self, p1_name, p1_personality, p1_style, + def ai_generate_child_personality(self, p1_name, p1_personality, p1_style, p2_name, p2_personality, p2_style, child_gender): """使用AI融合父母性格生成子嗣性格 (引入骰子判定天赋)""" - + from systems.dice import DiceSystem - + # 1. 投掷骰子决定先天运势 # 使用默认50或父母平均幸运值 luck_check, level, success = DiceSystem.check("投胎运势", 50) - + fortune_desc = "普通孩子" if level == "critical": fortune_desc = "天选之子(大成功)" elif level == "fumble": fortune_desc = "被诅咒的孩子(大失败)" elif level == "hard": fortune_desc = "聪慧过人" - + prompt = f"""请根据父母特点及【先天运势】生成孩子性格和名字。 父/母1: {p1_name} (性格:{p1_personality}) 父/母2: {p2_name} @@ -380,7 +409,7 @@ def ai_generate_child_personality(self, p1_name, p1_personality, p1_style, 请直接输出JSON(不要其他文字): {{"name":"孩子名字(需符合父母文化风格)","personality":"性格描述(30字)","language_style":"口癖(15字)"}}""" - + try: response, _ = self.ai.think_and_act(prompt) if response: @@ -391,7 +420,7 @@ def ai_generate_child_personality(self, p1_name, p1_personality, p1_style, return (result.get('personality', ''), result.get('language_style', ''), result.get('name')) except Exception as e: print_warning(f"AI生成失败,使用默认融合: {e}") - + # 备用:简单融合 if random.random() < 0.5: personality = f"继承了{p1_name}的部分性格,又有点{p2_name}的影子" @@ -399,7 +428,7 @@ def ai_generate_child_personality(self, p1_name, p1_personality, p1_style, else: personality = f"性格像{p2_name},但也有{p1_name}的一面" style = p2_style[:40] if p2_style else p1_style[:40] - + return personality, style, None @@ -408,10 +437,10 @@ def process_ai_response(self, response, usage): # 处理可能的骰子申请 [CHECK: 技能] from systems.dice import DiceSystem processed_response = DiceSystem.parse_and_roll(response, self.player) - + print_character(self.player.name, processed_response) self.player.add_event_to_history("AI日志", processed_response, "") - + if usage: self.session_stats['prompt_tokens'] += usage.get('prompt_tokens', 0) self.session_stats['completion_tokens'] += usage.get('completion_tokens', 0) @@ -425,9 +454,9 @@ def process_life_events(self): age = p.save_data.get('age', 18) char_id = p.save_data.get('current_character_id') member = p.save_data.get('family_tree', {}).get('members', {}).get(char_id) - + if not member: return - + # 1. 结婚判定 (适婚年龄 18-50, 单身) if 18 <= age <= 50 and not member.get('spouse_id'): # 每回合 1% 概率结婚 @@ -445,12 +474,12 @@ def process_life_events(self): name, data = random.choice(available) spouse_npc = data.copy() spouse_npc['名称'] = name - + # print(f"DEBUG: spouse_npc type: {type(spouse_npc)}, value: {spouse_npc}") spouse_name = spouse_npc.get('名称', '神秘伴侣') if spouse_npc else "神秘伴侣" spouse_id = str(uuid.uuid4())[:8] member['spouse_id'] = spouse_id - + # 记录配偶 (简化,只存ID和名字) # 实际可以加到family_tree里,但作为NPC可能不需要完整数据 print_success(f"💍 喜结良缘!你与 {spouse_name} 结婚了。") @@ -463,20 +492,20 @@ def handle_birth(self, parent_data, spouse_id, spouse_name="配偶"): p = self.player char_id = p.save_data.get('current_character_id') member = parent_data # self.player.save_data['family_tree']['members'][char_id] - + child_id = str(uuid.uuid4())[:8] child_gender = random.choice(['男', '女']) - child_name = f"{p.name}的{'儿子' if child_gender=='男' else '女儿'}" - + child_name = f"{p.name}的{'儿子' if child_gender=='男' else '女儿'}" + # 基因遗传 logic parent_genome = p.save_data.get('player_genome', {}) spouse_genome = GeneticSystem.generate_random_genome() # 简化:每次随机生成配偶基因 - + child_genome = GeneticSystem.crossover(parent_genome, spouse_genome) child_genome, mutations = GeneticSystem.mutate(child_genome, mutation_rate=0.05) if mutations: print_info(f"🧬 基因突变: {', '.join(mutations)}") - + # 创建子女记录 child_data = { "name": child_name, @@ -484,7 +513,7 @@ def handle_birth(self, parent_data, spouse_id, spouse_name="配偶"): "gender": child_gender, "generation": member.get('generation', 1) + 1, "parent_ids": [char_id, spouse_id], - "birth_turn": self.session_stats['回合数'], + "birth_turn": self.session_stats['回合数'], "genome": child_genome, "gene_score": GeneticSystem.calculate_gene_score(child_genome), "gene_score": GeneticSystem.calculate_gene_score(child_genome), @@ -492,7 +521,7 @@ def handle_birth(self, parent_data, spouse_id, spouse_name="配偶"): "language_style": "未知", "children_ids": [] } - + # 尝试生成性格 try: p1 = p.psychology[:100] @@ -500,7 +529,7 @@ def handle_birth(self, parent_data, spouse_id, spouse_name="配偶"): # 配偶信息缺失,用通用描述替代 p2 = "未知" p2_style = "未知" - + c_personality, c_style, c_name = self.ai_generate_child_personality( p.name, p1, p1_style, spouse_name, p2, p2_style, @@ -508,7 +537,7 @@ def handle_birth(self, parent_data, spouse_id, spouse_name="配偶"): ) child_data['personality'] = c_personality child_data['language_style'] = c_style - + if c_name: child_data['name'] = c_name child_name = c_name @@ -516,11 +545,11 @@ def handle_birth(self, parent_data, spouse_id, spouse_name="配偶"): except Exception as e: # print_error(f"性格生成错误: {e}") pass - + p.save_data['family_tree']['members'][child_id] = child_data member['children_ids'].append(child_id) p.save() - + print_success(f"👶 喜得贵子!{child_name} 出生了。(基因评分: {child_data['gene_score']})") p.add_event_to_history("生子", f"{child_name} 出生", "家族延续") with console.status("[bold green]👶 庆祝新生... (庆祝 5s)[/bold green]"): @@ -532,9 +561,9 @@ def process_life_events(self): age = p.save_data.get('age', 18) char_id = p.save_data.get('current_character_id') member = p.save_data.get('family_tree', {}).get('members', {}).get(char_id) - + if not member: return - + # 1. 结婚判定 (适婚年龄 18-50, 单身) if 18 <= age <= 50 and not member.get('spouse_id'): # 每回合 1% 概率结婚 @@ -552,14 +581,14 @@ def process_life_events(self): name, data = random.choice(available) spouse_npc = data.copy() spouse_npc['名称'] = name - + spouse_name = spouse_npc.get('名称', '神秘伴侣') if spouse_npc else "神秘伴侣" spouse_personality = spouse_npc.get('性格', '温柔') if spouse_npc else "温柔" spouse_id = str(uuid.uuid4())[:8] member['spouse_id'] = spouse_id member['spouse_name'] = spouse_name # 记录名字方便后续可以重构NPC对象 member['spouse_personality'] = spouse_personality - + print_success(f"💍 喜结良缘!你与 {spouse_name} ({spouse_personality}) 结婚了。") p.add_event_to_history("结婚", f"与 {spouse_name} 结婚", "家族诞生") @@ -575,7 +604,6 @@ def process_life_events(self): "id": spouse_id, "性格": spouse_personality } - # 增加日常互动提示 (10%概率),提醒玩家配偶的存在 if random.random() < 0.1: interactions = [ @@ -586,7 +614,6 @@ def process_life_events(self): f"{spouse_name} ({spouse_personality}) 正在思考今晚吃什么。" ] print_info(f"💕 [生活] {random.choice(interactions)}") - # 尝试亲密 # 夫妻默认好感度高 -> 概率 DO # 但也不能每回合都判,稍微控制下频率,比如每回合 20% 概率尝试亲密 @@ -594,7 +621,7 @@ def process_life_events(self): _, is_pregnant = RelationshipSystem.attempt_intimacy(p, spouse_npc) if is_pregnant: self.handle_birth(member, spouse_id, spouse_name) - + # 伴侣传授技能 (2% 概率) if random.random() < 0.02: CombatSystem.ai_teach_skill(p, spouse_name, "伴侣", self.ai) @@ -608,15 +635,15 @@ def process_life_events(self): def process_child_growth(self): """处理子嗣成长随机事件""" current_turn = self.session_stats['回合数'] - + char_id = self.player.save_data.get('current_character_id') members = self.player.save_data['family_tree']['members'] current_char = members.get(char_id) if not current_char: return - + child_ids = current_char.get('children_ids', []) valid_kids = [] - + for cid in child_ids: child = members.get(cid) if not child or child.get('death_turn'): continue @@ -624,12 +651,12 @@ def process_child_growth(self): age = (current_turn - birth) // RaceSystem.TURNS_PER_YEAR if 3 <= age < 16: # 3-16岁成长事件 valid_kids.append((cid, child, age)) - + if not valid_kids: return - + cid, child, age = random.choice(valid_kids) name = child['name'] - + events = [ (f"{name}在后院练习挥剑", "STR", 1), (f"{name}沉迷于阅读古籍", "INT", 1), @@ -639,9 +666,9 @@ def process_child_growth(self): (f"{name}在集市上灵活地穿梭", "AGI", 1) ] ev_desc, stat, val = random.choice(events) - + print_info(f"📚 [家事] {ev_desc} ({stat} +{val})") - + if 'growth_bonus' not in child: child['growth_bonus'] = {} child['growth_bonus'][stat] = child['growth_bonus'].get(stat, 0) + val self.player.save() @@ -651,84 +678,63 @@ def process_temptation(self, player, member): # 安全检查:必须已婚 if not member.get('spouse_id') or not member.get('spouse_name'): return - + # 1. 生成诱惑对象 lover_npc = self.world.get_random_npc(npc_type="可结伴") if not lover_npc: return - + lover_name = lover_npc.get('名称', '神秘人') lover_desc = lover_npc.get('描述', '充满魅力') - + # 2. 构建AI Prompt (Enhanced Roleplay) traits = player.get_traits() spouse_name = member.get('spouse_name', '配偶') children_ids = member.get('children_ids', []) num_children = len(children_ids) - + # 获取人设详细信息 char_desc = player.profile.get('角色描述', '一名普通的冒险者')[:150] - - # 提取语言特征关键词(与construct_prompt一致) - lang = player.profile.get('语言特征', {}) - lang_summary = "" - if isinstance(lang, dict): - keywords = [] - examples = [] - for section, data in lang.items(): - if isinstance(data, dict): - if '关键词' in data: - keywords.extend(data['关键词'][:2]) - if '示例' in data: - examples.extend(data['示例'][:1]) - if keywords: - lang_summary = "、".join(keywords[:6]) - if examples: - lang_summary += f" | 例: \"{examples[0][:25]}...\"" - elif isinstance(lang, str): - lang_summary = lang[:50] - else: - lang_summary = "正常说话" + + # 心理特征摘要 (使用helper方法) + psych_summary = self._extract_trait_summary(player.profile, '心理特征') + + # 语言特征摘要 (使用helper方法) + lang_summary = self._extract_trait_summary(player.profile, '语言特征') # 准确描述家庭状况 if num_children == 0: family_desc = f"已婚,配偶是 {spouse_name},暂时没有孩子" else: family_desc = f"已婚,配偶是 {spouse_name},有 {num_children} 个孩子" - - prompt = f"""【角色扮演指令】 -你现在必须完全沉浸在角色:{player.name} 中。 -你的设定:{char_desc} -你的口癖/说话风格:{lang_summary} -你的性格标签:[{', '.join(traits)}] -你的现状:{family_desc} - -【触发事件】: -你在外面偶遇了 {lover_name} ({lover_desc})。对方对你释放了强烈的费洛蒙,试图诱惑你出轨,气氛变得燥热暧昧。 - -【任务】: -请以 {player.name} 的第一人称视角,用**极度符合你人设和口癖**的语气描写你的内心弹幕和最终决定。 -- 严禁使用“虽然...但是...责任感”这种AI味的说教! -- 如果你是傲娇,就骂骂咧咧地拒绝;如果是魅魔,可能欲拒还迎;如果是老实人,就惊慌失措。 -- 必须生动、口语化。 - -格式要求: -一段内心独白(50字以内) -[DECISION: ACCEPT] 或 [DECISION: REJECT] + + prompt = f"""## 角色决策时刻: +我是 {player.name}。 +【角色设定】{char_desc}, +【家庭状况】{family_desc}。 +{'【性格特点】'+psych_summary if psych_summary else ''} +{'【语言特征】'+lang_summary if lang_summary else ''} +【性格标签】[{', '.join(traits)}] + +【当前事件】 +你在外面偶遇了 {lover_name} ({lover_desc})。你嗅到了暧昧的气息,你们二人互有好感,气氛变得燥热难耐。 + +请以第一人称写一段对事件的决定和自我吐槽(一个段落以内),严格符合角色资料中的[性格]和[语言风格]。不要过分修辞,不要编造不存在的信息。禁止使用“虽然”、“但是”、“所以”等转折词。 +在最后单独一行输出:[DECISION: ACCEPT] 或 [DECISION: REJECT] """ # 3. 调用AI - print_info(f"🤔 {player.name} 正在面对诱惑进行内心挣扎...") + print_info(f"🤔 {player.name} 正在面对 {lover_name} 的诱惑进行内心挣扎...") content, usage = self.ai.think_and_act(prompt) self.process_ai_response(content, usage) - + # 4. 解析结果 if content and "[DECISION: ACCEPT]" in content: - print_warning(f"💓 [AI决定] 你未能抵挡诱惑...") + print_warning(f"💓 [决定] 你未能抵挡 {lover_name} 的诱惑...") # 执行亲密 success, is_pregnant = RelationshipSystem.attempt_intimacy(player, lover_npc) if success: - player.add_event_to_history("出轨", f"未能抵挡诱惑,与 {lover_name} 发生了关系 (AI决策)", "情感波折") + player.add_event_to_history("出轨", f"未能抵挡诱惑,与 {lover_name} 发生了关系", "情感波折") else: - print_success(f"🛡️ [AI决定] 你拒绝了诱惑,守住了底线。") + print_success(f"🛡️ [决定] 你拒绝了 {lover_name} 的诱惑,守住了底线。") def show_full_status(self): """显示角色完整状态 (C键触发)""" @@ -736,17 +742,17 @@ def show_full_status(self): from rich.table import Table from rich.columns import Columns from collections import Counter - + console.clear() print_header(f"📊 {p.name} 的详细档案") - + # 1. 基础属性表 stats_table = Table(title="基础属性", show_header=True, header_style="bold magenta", expand=True) stats_table.add_column("属性", style="cyan", justify="right") stats_table.add_column("数值", style="green", justify="left") stats_table.add_column("属性", style="cyan", justify="right") stats_table.add_column("数值", style="green", justify="left") - + s = p.game_stats rows = [ ("等级", f"{s.get('等级', 1)}", "经验", f"{s.get('经验', 0)}/{s.get('下一级经验', 100)}"), @@ -759,19 +765,19 @@ def show_full_status(self): ] for row in rows: stats_table.add_row(*row) - + # 2. 家族信息 family = p.save_data.get('family_tree', {}) members = family.get('members', {}) current_char = members.get(p.save_data.get('current_character_id'), {}) - + spouse_name = current_char.get('spouse_name', '无') children = p.get_children() children_txt = "无" if children: names = [f"{c[1].get('name')}" for c in children] children_txt = ", ".join(names) - + fam_table = Table(title="👨‍👩‍👧‍👦 家族信息", show_header=False, box=None, expand=True) fam_table.add_column("Key", style="yellow", justify="right") fam_table.add_column("Value", style="white", justify="left") @@ -781,7 +787,7 @@ def show_full_status(self): fam_table.add_row("子嗣", children_txt) fam_panel = Panel(fam_table, border_style="yellow") - + # 3. 物品栏 items = p.inventory inv_text = "" @@ -791,14 +797,14 @@ def show_full_status(self): # 统计同类物品 item_names = [i.get('name', '未知') for i in items] counts = Counter(item_names) - + # 使用多列显示物品 (每行4个) inv_grid = Table(show_header=False, box=None, expand=True) inv_grid.add_column("Item1", ratio=1) inv_grid.add_column("Item2", ratio=1) inv_grid.add_column("Item3", ratio=1) inv_grid.add_column("Item4", ratio=1) - + current_row = [] for name, count in counts.most_common(40): # 显示最多40种 current_row.append(f"📦 {name} x{count}") @@ -809,9 +815,9 @@ def show_full_status(self): # 补齐空位 while len(current_row) < 4: current_row.append("") inv_grid.add_row(*current_row) - + inv_panel = Panel(inv_grid, title=f"🎒 背包 ({len(items)}件)", border_style="blue") - + # 组合显示 console.print(stats_table) console.print(fam_panel) @@ -819,10 +825,10 @@ def show_full_status(self): console.print(inv_panel) else: console.print(Panel("背包空空如也", title="🎒 背包", border_style="blue")) - + # 等待确认 console.input("\n[bold green]按回车键继续...[/bold green]") - + console.clear() print_header("✨ 游戏继续 ✨") print_info(f"当前角色: {self.player.name} | 'F'暂停 | 'C'状态 | 'S'摘要 | 'Q'退出") @@ -830,13 +836,13 @@ def show_full_status(self): def run_turn(self): self.session_stats['回合数'] += 1 current_region_id = self.player.current_location - + # 0. 自动换地图逻辑 (Auto-Travel) # 每10回合检查一次,避免过于频繁 if self.session_stats['回合数'] % 10 == 0: current_region = self.world.get_region(current_region_id) player_level = self.player.game_stats['等级'] - + # 1. 检查是否等级过高,应该去更高级地图 if current_region and player_level > current_region.get('max_level', 100) + 2: # 寻找更高级的地图 @@ -849,7 +855,7 @@ def run_turn(self): print_success(f"🚀 [自动探索] 你感觉 {current_region.get('名称')} 已经没有挑战了,前往了新的地区:{region['名称']} (Lv.{r_min}-{r_max})") current_region_id = region['id'] # 更新当前引用 break - + # 2. 检查是否等级过低(比如通过修改或其他方式误入),应该撤退 elif current_region and player_level < current_region.get('min_level', 0) - 1: # 寻找适合的低级地图 @@ -861,7 +867,7 @@ def run_turn(self): print_warning(f"🏳️ [自动撤退] {current_region.get('名称')} 太危险了,你撤退到了安全区域:{region['名称']}") current_region_id = region['id'] break - + # 1. 尝试触发随机事件(子嗣成长、商人等) if random.random() < 0.05: # 商人事件 @@ -871,17 +877,17 @@ def run_turn(self): self.session_stats['prompt_tokens'] += usage.get('prompt_tokens', 0) self.session_stats['completion_tokens'] += usage.get('completion_tokens', 0) self.session_stats['total_tokens'] += usage.get('total_tokens', 0) - + self.session_stats['completion_tokens'] += usage.get('completion_tokens', 0) self.session_stats['total_tokens'] += usage.get('total_tokens', 0) - + # 1.2 顿悟事件 (领悟新技能) if random.random() < 0.02 and self.player.game_stats['等级'] >= 5: CombatSystem.ai_learn_skill(self.player, self.ai) # 1.5 处理生命事件 (结婚生子) self.process_life_events() - + # 1.6 子嗣成长 (5%概率) if random.random() < 0.05: self.process_child_growth() @@ -891,23 +897,23 @@ def run_turn(self): if death_cause == "old_age": self.handle_death(f"{self.player.name} 寿终正寝了...", "寿终正寝") if self.game_over: return - + # 3. 地区主要事件 event_type = self.world.get_random_event_type(current_region_id) - + ai_input_data = "" - + if event_type == "战斗": enemy = self.world.get_encounter(current_region_id, self.player.game_stats['等级']) ai_input_data = f"遭遇怪物:{enemy['名称']}" if not self.handle_combat(enemy): if self.game_over: return - + elif event_type == "探索": self.session_stats['探索次数'] += 1 region_name = self.world.get_region(current_region_id).get('名称', current_region_id) world_name = self.world.data.get('世界名称', '异世界') - + # 引入骰子系统 from systems.dice import DiceSystem @@ -917,7 +923,7 @@ def run_turn(self): from systems.dice import DiceSystem import json import re - + # 预先进行幸运判定,给 AI 参考,但最终由 AI 制定的结果为准 luck_val = self.player.game_stats.get('幸运', 50) # 静默检定,避免“虚空判定”,结果稍后整合进文案 @@ -926,7 +932,7 @@ def run_turn(self): explore_json = None found_item = None - + if random.random() < self.config.ai_event_rate: # 请求 AI 直接返回结构化数据 prompt = (f"角色在{world_name}的{region_name}探索。{luck_context}。\n" @@ -947,15 +953,15 @@ def run_turn(self): explore_desc = "" is_critical = False DiceSystem.last_result = None # 重置状态 - + # A. 使用 AI 生成的结果 if explore_json: explore_desc = explore_json.get('desc', '你四处看了看。') - + # 在处理物品前,先解析描述中的骰子判定 explore_desc = DiceSystem.parse_and_roll(explore_desc, self.player) if DiceSystem.last_result == 'critical': is_critical = True - + # 处理物品 item_name = explore_json.get('item') if item_name and str(item_name).lower() != 'null' and str(item_name).lower() != 'none': @@ -966,17 +972,17 @@ def run_turn(self): base_item = base_item.copy() base_item['name'] = item_name base_item['desc'] = f"在{region_name}发现的{item_name}" - + self.player.inventory.append(base_item) explore_desc += f" (获得: {item_name})" - + # 处理 Sanity 扣除 cost = explore_json.get('san_cost', 0) if cost > 0: current_san = self.player.game_stats.get('SAN', 50) self.player.game_stats['SAN'] = max(0, current_san - cost) explore_desc += f" [理智 -{cost}]" - if self.player.game_stats['SAN'] < 20: + if self.player.game_stats['SAN'] < 20: explore_desc += " (精神崩溃...)" # B. Fallback 到传统逻辑 @@ -988,7 +994,7 @@ def run_turn(self): item_name = found_item.get('name', '未知物品') explore_desc = explore_desc.replace("{item}", item_name) self.player.inventory.append(found_item) - + # 也要检查传统文本里是否有骰子判定 explore_desc = DiceSystem.parse_and_roll(explore_desc, self.player) if DiceSystem.last_result == 'critical': is_critical = True @@ -996,11 +1002,10 @@ def run_turn(self): # 随机奖励逻辑 (基础经验) exp = 5 + random.randint(0, self.player.game_stats['等级']) gold = 0 - + # 如果没找到物品,才给金币 if not found_item and random.random() < 0.2: gold = random.randint(1, 10) - # 大成功/卓越成功奖励翻倍 if is_critical or level in ['critical', 'hard']: exp *= 5 @@ -1010,20 +1015,20 @@ def run_turn(self): self.player.gain_exp(exp) self.player.game_stats['金币'] += gold self.session_stats['总经验'] += exp - + # 发放物品 if found_item: self.player.inventory.append(found_item) - + # 显示更沉浸的文本 - reward_text = f" (经验+{exp}" + reward_text = f" (经验+{exp}" if gold > 0: reward_text += f", 金币+{gold}" if found_item: reward_text += f", 获得: {item_name}" reward_text += ")" - + print_event("探索", f"[{region_name}] {explore_desc}{reward_text}") ai_input_data = f"在{region_name}探索: {explore_desc}" - + elif event_type == "休息": self.session_stats['休息次数'] += 1 heal_hp = int(self.player.game_stats['MaxHP'] * 0.2) @@ -1039,14 +1044,14 @@ def run_turn(self): if npc: print_event("NPC", f"你遇到了 {npc['名称']} ({npc['职业']})。") ai_input_data = f"偶遇了{npc['名称']},{npc['描述']}" - + # 简单交互逻辑 action = random.choice(["chat", "gift", "romance"]) - + # 获取或初始化关系 rel_id, rel_data = RelationshipSystem.initialize_npc_relationship(self.player, npc, "偶遇") affinity = rel_data['affection'] - + if action == "chat": greetings = [ f"{npc['名称']} 微笑着向你打招呼。", @@ -1059,7 +1064,7 @@ def run_turn(self): val = random.randint(3, 6) rel_data['affection'] += val print_info(f" (好感度 +{val} -> {rel_data['affection']})") - + elif action == "gift": gift_coin = random.randint(1, 10) self.player.game_stats['金币'] += gift_coin @@ -1067,7 +1072,7 @@ def run_turn(self): val = random.randint(5, 12) rel_data['affection'] += val print_info(f" (好感度 +{val} -> {rel_data['affection']})") - + elif action == "romance": # 尝试发展关系 if affinity >= 80 and rel_data['status'] != "恋人": @@ -1081,15 +1086,15 @@ def run_turn(self): elif affinity >= 20 and rel_data['status'] == "陌生人": # 增加一个小状态提示 print_info(f"😊 你和 {npc['名称']} 算是熟人了。") - + # 状态更新提示 new_status = RelationshipSystem.get_relation_level(rel_data['affection']) if new_status != rel_data.get('status_label', ''): rel_data['status_label'] = new_status - + # 检查表白事件 RelationshipSystem.check_romance_events(self.player, rel_id, console) - + elif event_type == "奇遇": # 尝试生成动态事件 usage = None @@ -1102,7 +1107,7 @@ def run_turn(self): ai_input_data = f"触发奇遇:{event_data['title']}" # 统计token self.process_ai_response(None, usage) - + # 失败或未触发AI生成,回退到静态奇遇 if not usage: adv = self.world.get_random_adventure() @@ -1114,7 +1119,7 @@ def run_turn(self): - + # 4. 生成角色主观反应 (根据配置概率) # 恢复丢失的逻辑 if ai_input_data and random.random() < self.config.ai_event_rate: @@ -1122,7 +1127,7 @@ def run_turn(self): prompt = self.construct_prompt(event_type, ai_input_data) # 加上简单的防破防指令 prompt += "\n(请以第一人称简短吐槽或感慨,不要重复事件描述,30字以内)" - + reaction, usage = self.ai.think_and_act(prompt) if reaction: # 清理可能的多余符号 @@ -1138,95 +1143,95 @@ def run_turn(self): hist_len = len(self.player.save_data.get('event_history', [])) threshold = self.config.history_compress_threshold retention = self.config.history_retention_count - - if hist_len >= threshold: + + if hist_len >= threshold: print_info(f"🧠 历史记录达到{threshold}条,正在进行压缩...") - + # 只总结要被移除的那部分(前N条),保留后retention条作为新鲜记忆 keep_count = retention history = self.player.save_data.get('event_history', []) to_summarize = history[:-keep_count] - + text = "" for h in to_summarize: text += f"{h['描述']}; " - # 注入家庭简报,防止AI遗忘重要人名 family_info = "" current_char = self.player.save_data.get('family_tree', {}).get('members', {}).get(self.player.save_data.get('current_character_id'), {}) spouse = current_char.get('spouse_name') children = self.player.get_children() if spouse: family_info += f"配偶:{spouse} " - if children: + if children: names = [c[1]['name'] for c in children] family_info += f"子女:{','.join(names)}" - + context_prompt = f" [已知关系: {family_info}]" if family_info else "" - + prompt = f"用30字概括以下经历(注意保留重要人名{context_prompt}):{text[:500]}" summary, _ = self.ai.think_and_act(prompt) - + if summary: self.player.compress_history(self.ai, summary, keep_count) - + # 自动保存 self.turns_since_save += 1 if self.turns_since_save >= self.config.autosave_interval: self.player.save() self.turns_since_save = 0 - + time.sleep(self.config.speed * 0.1) def main_loop(self): print_header("✨ 游戏开始 ✨") print_info(f"当前角色: {self.player.name} | 'F'暂停 | 'C'状态 | 'S'摘要 | 'Q'退出") - + last_time = 0 - + try: - while not self.game_over: - # 输入检测 - if msvcrt.kbhit(): - key = msvcrt.getch() - if key.lower() == b'f': - self.paused = not self.paused - status = "暂停" if self.paused else "继续" - print_warning(f"\n⏸️ 游戏{status}") - elif key.lower() == b'c': - self.show_full_status() - elif key.lower() == b'q': - print_warning("\n💾 正在保存并退出...") - self.player.save() - self.game_over = True - break - elif key.lower() == b's': - # 查看摘要和状态 - console.clear() - print_header(f"📜 {self.player.name} 的人生小结") - - summary = self.player.save_data.get('summary', '暂无摘要') - print_info(f"\n[长期记忆]\n{summary}") - - history = self.player.save_data.get('event_history', []) - print_info(f"\n[短期记忆 ({len(history)}条)]") - for h in history[-5:]: - print(f" - [{h['时间']}] {h['描述']}") - - console.input("\n按回车键返回游戏...") - console.clear() - # 重绘界面提示 - print_header("✨ 游戏继续 ✨") - print_info(f"当前角色: {self.player.name} | 'F'暂停 | 'C'状态 | 'S'摘要 | 'Q'退出") - - if not self.paused: - current_time = time.time() - if current_time - last_time >= self.config.speed: - self.run_turn() - last_time = time.time() - # 显示倒计时提示,不用每次都刷屏,只在回合结束提示一下 - # print_info(f"⏳ 等待 {self.config.speed}s ...") - - time.sleep(self.config.ui_refresh_rate) + with InputHandler() as input_handler: + while not self.game_over: + # 输入检测 + key = input_handler.poll_key() + if key: + if key == 'f': + self.paused = not self.paused + status = "暂停" if self.paused else "继续" + print_warning(f"\n⏸️ 游戏{status}") + elif key == 'q': + print_warning("\n💾 正在保存并退出...") + self.player.save() + self.game_over = True + break + elif key == 'c': + self.show_full_status() + elif key == 's': + # 查看摘要和状态 + console.clear() + print_header(f"📜 {self.player.name} 的人生小结") + + summary = self.player.save_data.get('summary', '暂无摘要') + print_info(f"\n[长期记忆]\n{summary}") + + history = self.player.save_data.get('event_history', []) + print_info(f"\n[短期记忆 ({len(history)}条)]") + for h in history[-5:]: + print(f" - [{h['时间']}] {h['描述']}") + + console.input("\n按回车键返回游戏...") + console.clear() + # 重绘界面提示 + print_header("✨ 游戏继续 ✨") + print_info(f"当前角色: {self.player.name} | 'F'暂停 | 'C'状态 | 'S'摘要 | 'Q'退出") + + if not self.paused: + current_time = time.time() + if current_time - last_time >= self.config.speed: + self.run_turn() + last_time = time.time() + # 显示倒计时提示,不用每次都刷屏,只在回合结束提示一下 + # print_info(f"⏳ 等待 {self.config.speed}s ...") + + time.sleep(self.config.ui_refresh_rate) except KeyboardInterrupt: print_warning("\n\n⚠️ 检测到强制退出信号...") @@ -1244,21 +1249,21 @@ def main_loop(self): duration = int(end_time - self.start_time) print_header("\n=== 游戏结束 ===") print_info(f"本次存活时间: {duration}秒") - + # 打印本次会话总结 self.print_session_summary() - - + + self.player.update_lifetime_stats(self.session_stats, duration) - + console.input("\n请按回车键结束游戏...") def apply_game_effect(self, effect): """应用游戏效果 (解析JSON)""" if not effect: return - + p = self.player - + # 递归处理 '随机' if '随机' in effect: chosen = random.choice(effect['随机']) @@ -1276,11 +1281,11 @@ def apply_game_effect(self, effect): elif isinstance(effect['治疗'], int): p.heal(effect['治疗']) print_success(f"💚 恢复了 {effect['治疗']} 点生命") - + # 经验 if '经验' in effect: p.gain_exp(effect['经验']) - + # 金币 (兼容 '金币' 和 '获得金币') gold = effect.get('金币', effect.get('获得金币', 0)) if gold > 0: @@ -1305,7 +1310,7 @@ def apply_game_effect(self, effect): if stat in p.game_stats: p.game_stats[stat] += val print_success(f"💪 {stat} 永久增加了 {val}点!") - + # 触发NPC交友 if effect.get('触发NPC交友'): npc = self.world.get_random_npc('可结伴') @@ -1335,12 +1340,12 @@ def print_session_summary(self): """打印本次会话的统计信息""" stats = self.session_stats life = self.player.save_data.get('lifetime_stats', {}) - + # 计算累计值 (当前存档累积 + 本次) total_turns = life.get('总回合数', 0) + stats['回合数'] total_tokens = life.get('总total_tokens', 0) + stats['total_tokens'] total_exp = life.get('总获得经验', 0) + stats['总经验'] - + # 1. 基础统计 summary_text = f""" [bold green]--- 📊 数据结算 ---[/bold green] @@ -1358,7 +1363,7 @@ def print_session_summary(self): 💰 [bold yellow]总消耗Token: {total_tokens}[/bold yellow] """ console.print(Panel(summary_text, title="📊 冒险结算", border_style="blue")) - + # 2. AI生成剧情总结 print_info("🧠 正在生成本次冒险的剧情回顾...") try: @@ -1367,22 +1372,23 @@ def print_session_summary(self): # 这里取最近20条,假设一局游戏也就这么多有效记录 history = self.player.save_data.get('event_history', []) recent_events = history[-30:] if len(history) > 30 else history - + if not recent_events: print_warning("暂无足够事件生成总结。") return + lang_summary = self._extract_trait_summary(self.player.profile, '语言特征') + text = "" for h in recent_events: text += f"[{h['时间']}] {h['描述']} -> {h['结果']}\n" - - prompt = f""" -请根据以下冒险日志,用一段通俗幽默的话总结 {self.player.name} 这次的游戏经历(100字左右): -重点关注:发生了什么趣事、获得了什么成就、以及最后的结局(是主动退出还是意外死亡)。 -请用第三人称叙述,像在讲故事一样。 -日志: + prompt = f"""# 日志: {text} + +根据以上冒险日志,用一段话总结 {self.player.name} 这次的冒险经历(100字左右): +重点关注:发生了什么趣事、获得了什么成就、以及最后的结局(是主动退出还是意外死亡)。 +请用第三人称叙述,像在讲故事一样,适当贴近 {self.player.name} 的【语言特征】:{lang_summary}。 """ content, usage = self.ai.think_and_act(prompt) if content: @@ -1390,6 +1396,6 @@ def print_session_summary(self): # 累加Token消耗 if usage: print_info(f"(本次总结消耗: {usage.get('total_tokens', 0)} tokens)") - + except Exception as e: print_error(f"生成总结失败: {e}") diff --git a/systems/dice.py b/systems/dice.py index acabb87..01e52c9 100644 --- a/systems/dice.py +++ b/systems/dice.py @@ -5,7 +5,7 @@ class DiceSystem: """ CoC (Call of Cthulhu) 风格的 D100 骰子系统 """ - + last_result = None @staticmethod @@ -21,15 +21,15 @@ def roll(expression="1d100"): sides = int(match.group(2)) operator = match.group(3) bonus = int(match.group(4) if match.group(4) else 0) - + rolls = [random.randint(1, sides) for _ in range(count)] total = sum(rolls) - + if operator == '+': total += bonus elif operator == '-': total -= bonus - + return max(1, total) # 最小为1 return random.randint(1, 100) @@ -40,39 +40,39 @@ def parse_and_roll(text, character): 支持格式: [CHECK: 侦查] 或 {CHECK: 力量} """ import re - + # 正则匹配 [CHECK: 技能名] pattern = re.compile(r'[\[\{]CHECK: ?(.+?)[\]\}]', re.IGNORECASE) - + def replace_func(match): check_name = match.group(1).strip() # 尝试在角色属性中查找对应技能或属性 # 1. 精确匹配 target_val = character.game_stats.get(check_name) - + # 2. 尝试加"技能_"前缀匹配 (如果AI只写了"侦查") if target_val is None: target_val = character.game_stats.get(f"技能_{check_name}") - + # 3. 尝试模糊匹配 (比如"Force" -> "STR") if target_val is None: map_dict = {"Force": "STR", "Strength": "STR", "Agility": "AGI", "Luck": "LUK", "Sanity": "SAN"} target_val = character.game_stats.get(map_dict.get(check_name, "")) - + # 4. 默认值 if target_val is None: target_val = 50 # 默认 50 - + roll_val, level, success = DiceSystem.check(check_name, target_val) - + # 构建结果字符串 color = "green" if success else "red" outcome = "成功" if success else "失败" if level == "critical": outcome = "大成功!" if level == "fumble": outcome = "大失败!" - + return f"[[bold cyan]🎲 {check_name}判定[/bold cyan]: {roll_val}/{int(target_val)} -> [bold {color}]{outcome}[/bold {color}]]" - + return pattern.sub(replace_func, text) @staticmethod @@ -85,13 +85,13 @@ def check(check_name, target_value, silent=False): :return: (roll_value, result_string, is_success) """ roll_val = random.randint(1, 100) - + result_str = "失败" is_success = False level = "normal" - + # 1-5 大成功 (调整了范围,不那么苛刻) - if roll_val <= 5: + if roll_val <= 5: result_str = "[bold gold1]大成功![/bold gold1] (Critical)" is_success = True level = "critical" @@ -114,7 +114,6 @@ def check(check_name, target_value, silent=False): result_str = "失败" is_success = False level = "failure" - if not silent: print_info(f"🎲 {check_name}检定({int(target_value)}): [cyan]{roll_val}[/cyan] -> {result_str}") DiceSystem.last_result = level