Spaces:
Runtime error
Runtime error
Upload 4 files
Browse files- state_manager.py +287 -1
- story_engine.py +58 -0
state_manager.py
CHANGED
|
@@ -18,6 +18,7 @@ from __future__ import annotations
|
|
| 18 |
|
| 19 |
import copy
|
| 20 |
import logging
|
|
|
|
| 21 |
from typing import Any, Optional
|
| 22 |
from pydantic import BaseModel, Field
|
| 23 |
|
|
@@ -491,10 +492,78 @@ class GameState:
|
|
| 491 |
rest_available=True,
|
| 492 |
ambient_description="溪水潺潺流淌,偶有鸟鸣声在林间回荡,这里是森林中的一片安宁绿洲。",
|
| 493 |
),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 494 |
}
|
| 495 |
|
| 496 |
self.world.discovered_locations = ["村庄广场", "村庄铁匠铺", "村庄旅店", "村庄杂货铺", "村口小路"]
|
| 497 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 498 |
# --- 初始 NPC ---
|
| 499 |
self.world.npcs = {
|
| 500 |
"村长老伯": NPCState(
|
|
@@ -569,6 +638,37 @@ class GameState:
|
|
| 569 |
schedule={"清晨": "村庄旅店", "夜晚": "村口小路"},
|
| 570 |
backstory="似乎在寻找什么。偶尔从斗篷下露出的手指上有奇异的魔法纹路。",
|
| 571 |
),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 572 |
}
|
| 573 |
|
| 574 |
# --- 初始任务 ---
|
|
@@ -612,6 +712,46 @@ class GameState:
|
|
| 612 |
),
|
| 613 |
prerequisites=[],
|
| 614 |
),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 615 |
}
|
| 616 |
|
| 617 |
# --- 初始物品注册表 ---
|
|
@@ -630,6 +770,14 @@ class GameState:
|
|
| 630 |
"地图碎片": ItemInfo(name="地图碎片", item_type="quest_item", description="一片残破的地图,标记着森林深处的某个位置。", quest_related=True, value=0, lore_text="这张地图似乎非常古老,纸张已经泛黄,但上面的墨迹依然清晰。"),
|
| 631 |
"森林之钥": ItemInfo(name="森林之钥", item_type="key", description="一把散发着微弱绿光的古老钥匙,似乎能打开森林深处的某个入口。", rarity="rare", quest_related=True, value=0, lore_text="钥匙上刻着精灵文字,翻译过来是:'唯有勇者可通行'。"),
|
| 632 |
"神秘卷轴": ItemInfo(name="神秘卷轴", item_type="quest_item", description="记载着古老知识的卷轴,散发着微弱的魔力波动。", rarity="rare", quest_related=True, value=0),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 633 |
}
|
| 634 |
|
| 635 |
# --- 初始传闻 ---
|
|
@@ -637,8 +785,25 @@ class GameState:
|
|
| 637 |
"最近森林里的哥布林越来越嚣张了,好几个猎人都不敢进去了。",
|
| 638 |
"听说铁匠格林以前在王都待过,不知道为什么来了这个小村子。",
|
| 639 |
"旅店里来了个奇怪的旅人,整天把自己裹得严严实实的。",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 640 |
]
|
| 641 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 642 |
# --- 玩家初始装备 ---
|
| 643 |
self.player.inventory = ["面包", "面包", "小型治疗药水"]
|
| 644 |
|
|
@@ -1111,7 +1276,18 @@ class GameState:
|
|
| 1111 |
if self.world.rumors:
|
| 1112 |
rumors_desc = "\n【流传的传闻】\n" + "\n".join(f" - {r}" for r in self.world.rumors[-3:])
|
| 1113 |
|
| 1114 |
-
# 8.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1115 |
consistency_rules = (
|
| 1116 |
"\n【一致性约束 —— 你必须严格遵守】\n"
|
| 1117 |
"1. 已死亡的NPC不可再出现或对话(除非有特殊复活剧情)。\n"
|
|
@@ -1145,6 +1321,7 @@ class GameState:
|
|
| 1145 |
move_desc,
|
| 1146 |
event_desc,
|
| 1147 |
rumors_desc,
|
|
|
|
| 1148 |
consistency_rules,
|
| 1149 |
])
|
| 1150 |
|
|
@@ -1248,6 +1425,103 @@ class GameState:
|
|
| 1248 |
|
| 1249 |
return contradictions
|
| 1250 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1251 |
def is_game_over(self) -> bool:
|
| 1252 |
"""
|
| 1253 |
判断游戏是否结束。
|
|
@@ -1319,6 +1593,9 @@ class GameState:
|
|
| 1319 |
# 更新 NPC 位置(根据时间表)
|
| 1320 |
self._update_npc_schedules()
|
| 1321 |
|
|
|
|
|
|
|
|
|
|
| 1322 |
return tick_log
|
| 1323 |
|
| 1324 |
def _apply_status_effects(self) -> list[str]:
|
|
@@ -1385,6 +1662,15 @@ class GameState:
|
|
| 1385 |
|
| 1386 |
return effect_log
|
| 1387 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1388 |
def _check_quest_deadlines(self):
|
| 1389 |
"""检查限时任务是否过期"""
|
| 1390 |
for quest in self.world.quests.values():
|
|
|
|
| 18 |
|
| 19 |
import copy
|
| 20 |
import logging
|
| 21 |
+
import re
|
| 22 |
from typing import Any, Optional
|
| 23 |
from pydantic import BaseModel, Field
|
| 24 |
|
|
|
|
| 492 |
rest_available=True,
|
| 493 |
ambient_description="溪水潺潺流淌,偶有鸟鸣声在林间回荡,这里是森林中的一片安宁绿洲。",
|
| 494 |
),
|
| 495 |
+
# -------- 扩展地点 --------
|
| 496 |
+
"河边渡口": LocationInfo(
|
| 497 |
+
name="河边渡口",
|
| 498 |
+
location_type="wilderness",
|
| 499 |
+
description="一座破旧的木制渡口,宽阔的河流在此缓缓流淌。一艘半沉的渡船拴在码头桩上。",
|
| 500 |
+
connected_to=["村口小路", "废弃矿洞入口", "山麓盗贼营"],
|
| 501 |
+
npcs_present=["渡口老渔夫"],
|
| 502 |
+
danger_level=3,
|
| 503 |
+
is_discovered=False,
|
| 504 |
+
ambient_description="河水拍打着朽烂的木桩,远处有鹰在盘旋,对岸隐约可见矿洞的轮廓。",
|
| 505 |
+
),
|
| 506 |
+
"废弃矿洞入口": LocationInfo(
|
| 507 |
+
name="废弃矿洞入口",
|
| 508 |
+
location_type="dungeon",
|
| 509 |
+
description="荒废多年的铁矿洞,入口被蛛网和碎石半堵。矿道里传来金属碰撞的回声。",
|
| 510 |
+
connected_to=["河边渡口", "矿洞深层"],
|
| 511 |
+
enemies=["骷髅兵", "矿洞蝙蝠群", "锈铁傀儡"],
|
| 512 |
+
danger_level=5,
|
| 513 |
+
is_discovered=False,
|
| 514 |
+
ambient_description="腐朽的矿车轨道延伸向黑暗深处,空气里弥漫着铁锈和硫磺的气味。",
|
| 515 |
+
),
|
| 516 |
+
"矿洞深层": LocationInfo(
|
| 517 |
+
name="矿洞深层",
|
| 518 |
+
location_type="dungeon",
|
| 519 |
+
description="矿洞最深处,一个巨大的地下空间。墙壁上嵌着发光的矿石,中央有一座被遗忘的祭坛。",
|
| 520 |
+
connected_to=["废弃矿洞入口"],
|
| 521 |
+
enemies=["亡灵矿工", "岩石巨像"],
|
| 522 |
+
danger_level=8,
|
| 523 |
+
is_discovered=False,
|
| 524 |
+
is_accessible=False,
|
| 525 |
+
required_item="矿工旧钥匙",
|
| 526 |
+
ambient_description="发光矿石将洞穴映成幽蓝色,祭坛上刻着无人能读的文字,隐约有低沉的嗡鸣。",
|
| 527 |
+
),
|
| 528 |
+
"山麓盗贼营": LocationInfo(
|
| 529 |
+
name="山麓盗贼营",
|
| 530 |
+
location_type="wilderness",
|
| 531 |
+
description="藏在山脚灌木丛后的盗贼据点,几顶破帐篷围着一堆余烬。看起来已被匆忙弃置。",
|
| 532 |
+
connected_to=["河边渡口", "精灵遗迹"],
|
| 533 |
+
enemies=["盗贼斥候", "盗贼头目"],
|
| 534 |
+
danger_level=5,
|
| 535 |
+
is_discovered=False,
|
| 536 |
+
ambient_description="地上散落着翻倒的酒桶和吃了一半的干粮,有人走得很匆忙。",
|
| 537 |
+
),
|
| 538 |
+
"精灵遗迹": LocationInfo(
|
| 539 |
+
name="精灵遗迹",
|
| 540 |
+
location_type="special",
|
| 541 |
+
description="一片被藤蔓覆盖的古老石柱林,精灵文字在月光下隐约发光。空气中有淡淡的魔力涌动。",
|
| 542 |
+
connected_to=["山麓盗贼营"],
|
| 543 |
+
npcs_present=["遗迹守护者"],
|
| 544 |
+
danger_level=4,
|
| 545 |
+
is_discovered=False,
|
| 546 |
+
ambient_description="石柱上的符文随风明灭,仿佛在回应某种古老的感召。脚下的青苔异常柔软。",
|
| 547 |
+
),
|
| 548 |
+
"古塔废墟": LocationInfo(
|
| 549 |
+
name="古塔废墟",
|
| 550 |
+
location_type="dungeon",
|
| 551 |
+
description="一座半坍塌的石塔,据说曾是某位法师的研究所。顶层似乎还有东西在闪烁。",
|
| 552 |
+
connected_to=["村口小路"],
|
| 553 |
+
enemies=["石像鬼", "游荡幽灵"],
|
| 554 |
+
danger_level=6,
|
| 555 |
+
is_discovered=False,
|
| 556 |
+
ambient_description="风从塔身的裂缝中呼啸而过,残破的阶梯上覆满了青苔和鸟粪。",
|
| 557 |
+
),
|
| 558 |
}
|
| 559 |
|
| 560 |
self.world.discovered_locations = ["村庄广场", "村庄铁匠铺", "村庄旅店", "村庄杂货铺", "村口小路"]
|
| 561 |
|
| 562 |
+
# 扩展村口小路的连接 —— 链接到新区域
|
| 563 |
+
self.world.locations["村口小路"].connected_to = [
|
| 564 |
+
"村庄广场", "黑暗森林入口", "河边渡口", "古塔废墟",
|
| 565 |
+
]
|
| 566 |
+
|
| 567 |
# --- 初始 NPC ---
|
| 568 |
self.world.npcs = {
|
| 569 |
"村长老伯": NPCState(
|
|
|
|
| 638 |
schedule={"清晨": "村庄旅店", "夜晚": "村口小路"},
|
| 639 |
backstory="似乎在寻找什么。偶尔从斗篷下露出的手指上有奇异的魔法纹路。",
|
| 640 |
),
|
| 641 |
+
# -------- 扩展 NPC --------
|
| 642 |
+
"渡口老渔夫": NPCState(
|
| 643 |
+
name="渡口老渔���",
|
| 644 |
+
npc_type="quest_giver",
|
| 645 |
+
location="河边渡口",
|
| 646 |
+
attitude="friendly",
|
| 647 |
+
description="一个皮肤黝黑、满脸皱纹的老人,正坐在码头上修补渔网。",
|
| 648 |
+
race="人类",
|
| 649 |
+
occupation="渔夫",
|
| 650 |
+
relationship_level=5,
|
| 651 |
+
can_give_quest=True,
|
| 652 |
+
available_quests=["side_quest_02"],
|
| 653 |
+
memory=[],
|
| 654 |
+
schedule={"清晨": "河边渡口", "正午": "河边渡口", "夜晚": "村庄旅店"},
|
| 655 |
+
backstory="在这条河边住了四十年,对河流两岸的地形了如指掌。最近总念叨对岸矿洞里的怪响。",
|
| 656 |
+
),
|
| 657 |
+
"遗迹守护者": NPCState(
|
| 658 |
+
name="遗迹守护者",
|
| 659 |
+
npc_type="quest_giver",
|
| 660 |
+
location="精灵遗迹",
|
| 661 |
+
attitude="cautious",
|
| 662 |
+
description="一个身形消瘦的半精灵,穿着褪色的绿袍,眼神中有深深的疲惫。",
|
| 663 |
+
race="半精灵",
|
| 664 |
+
occupation="守护者",
|
| 665 |
+
relationship_level=-5,
|
| 666 |
+
can_give_quest=True,
|
| 667 |
+
available_quests=["side_quest_03"],
|
| 668 |
+
memory=[],
|
| 669 |
+
schedule={"清晨": "精灵遗迹", "正午": "精灵遗迹", "夜晚": "精灵遗迹"},
|
| 670 |
+
backstory="最后一位遗迹守护者,独自守护这片先祖的圣地已有三十年。对外来者充满警惕,但内心渴望帮助。",
|
| 671 |
+
),
|
| 672 |
}
|
| 673 |
|
| 674 |
# --- 初始任务 ---
|
|
|
|
| 712 |
),
|
| 713 |
prerequisites=[],
|
| 714 |
),
|
| 715 |
+
# -------- 扩展任务 --------
|
| 716 |
+
"side_quest_02": QuestState(
|
| 717 |
+
quest_id="side_quest_02",
|
| 718 |
+
title="河底的秘密",
|
| 719 |
+
description="渡口老渔夫说他最近总在河里捞到奇怪的骨头,而且对岸矿洞方向夜里总是有光。他请你去查明真相。",
|
| 720 |
+
quest_type="side",
|
| 721 |
+
status="active",
|
| 722 |
+
giver_npc="渡口老渔夫",
|
| 723 |
+
objectives={
|
| 724 |
+
"与渡口老渔夫交谈": False,
|
| 725 |
+
"前往废弃矿洞调查": False,
|
| 726 |
+
"找到矿洞异常的原因": False,
|
| 727 |
+
},
|
| 728 |
+
rewards=QuestRewards(
|
| 729 |
+
gold=60,
|
| 730 |
+
experience=40,
|
| 731 |
+
items=["矿工旧钥匙"],
|
| 732 |
+
reputation_changes={"村庄": 10},
|
| 733 |
+
),
|
| 734 |
+
prerequisites=[],
|
| 735 |
+
),
|
| 736 |
+
"side_quest_03": QuestState(
|
| 737 |
+
quest_id="side_quest_03",
|
| 738 |
+
title="守护者的试炼",
|
| 739 |
+
description="精灵遗迹的守护者提出一个交换条件:通过她设下的试炼,就能获得古老的精灵祝福。",
|
| 740 |
+
quest_type="side",
|
| 741 |
+
status="active",
|
| 742 |
+
giver_npc="遗迹守护者",
|
| 743 |
+
objectives={
|
| 744 |
+
"与遗迹守护者交谈": False,
|
| 745 |
+
"通过守护者的试炼": False,
|
| 746 |
+
},
|
| 747 |
+
rewards=QuestRewards(
|
| 748 |
+
experience=50,
|
| 749 |
+
unlock_skill="精灵祝福",
|
| 750 |
+
karma_change=10,
|
| 751 |
+
title="遗迹认可者",
|
| 752 |
+
),
|
| 753 |
+
prerequisites=[],
|
| 754 |
+
),
|
| 755 |
}
|
| 756 |
|
| 757 |
# --- 初始物品注册表 ---
|
|
|
|
| 770 |
"地图碎片": ItemInfo(name="地图碎片", item_type="quest_item", description="一片残破的地图,标记着森林深处的某个位置。", quest_related=True, value=0, lore_text="这张地图似乎非常古老,纸张已经泛黄,但上面的墨迹依然清晰。"),
|
| 771 |
"森林之钥": ItemInfo(name="森林之钥", item_type="key", description="一把散发着微弱绿光的古老钥匙,似乎能打开森林深处的某个入口。", rarity="rare", quest_related=True, value=0, lore_text="钥匙上刻着精灵文字,翻译过来是:'唯有勇者可通行'。"),
|
| 772 |
"神秘卷轴": ItemInfo(name="神秘卷轴", item_type="quest_item", description="记载着古老知识的卷轴,散发着微弱的魔力波动。", rarity="rare", quest_related=True, value=0),
|
| 773 |
+
# -------- 扩展物品 --------
|
| 774 |
+
"矿工旧钥匙": ItemInfo(name="矿工旧钥匙", item_type="key", description="一把生锈的铜钥匙,上面刻着一个矿镐图案。", rarity="uncommon", quest_related=True, value=0, lore_text="钥匙柄上隐约可见'B-7采掘区'的刻字。"),
|
| 775 |
+
"骷髅碎骨": ItemInfo(name="骷髅碎骨", item_type="material", description="从骷髅兵身上掉落的骨头碎片,泛着不自然的寒光。", rarity="common", value=5),
|
| 776 |
+
"盗贼日志": ItemInfo(name="盗贼日志", item_type="quest_item", description="一本沾满泥渍的笔记本,记录着盗贼团伙近期的行动计划。", rarity="uncommon", quest_related=True, value=0),
|
| 777 |
+
"精灵护符": ItemInfo(name="精灵护符", item_type="accessory", description="由精灵遗迹守护者亲手制作的小型护符,散发着柔和的绿色微光。", rarity="rare", stat_bonus={"perception": 3, "sanity": 5}, value=50, lore_text="佩戴者能感受到来自远古精灵的庇佑。"),
|
| 778 |
+
"锈蚀铁锤": ItemInfo(name="锈蚀铁锤", item_type="weapon", description="矿洞里发现的旧铁锤,虽然锈迹斑斑但依然沉重有力。", rarity="common", stat_bonus={"attack": 4}, value=15),
|
| 779 |
+
"荧光苔藓": ItemInfo(name="荧光苔藓", item_type="consumable", description="矿洞深处生长的发光苔藓,据说有微弱的疗伤效果。", usable=True, use_effect="恢复 15 HP,恢复 5 理智", rarity="uncommon", value=12),
|
| 780 |
+
"古塔法师笔记": ItemInfo(name="古塔法师笔记", item_type="quest_item", description="在古塔废墟中找到的残破笔记,记载着某种仪式的片段。", rarity="rare", quest_related=True, value=0, lore_text="字迹已经模糊,但仍能辨认出几个关键的魔法符号。"),
|
| 781 |
}
|
| 782 |
|
| 783 |
# --- 初始传闻 ---
|
|
|
|
| 785 |
"最近森林里的哥布林越来越嚣张了,好几个猎人都不敢进去了。",
|
| 786 |
"听说铁匠格林以前在王都待过,不知道为什么来了这个小村子。",
|
| 787 |
"旅店里来了个奇怪的旅人,整天把自己裹得严严实实的。",
|
| 788 |
+
"河对岸的旧矿洞晚上闹鬼,渡口的老渔夫说他亲眼看见过蓝色的火光。",
|
| 789 |
+
"山脚下好像有一伙盗贼扎了营,最近有商队被劫的消息。",
|
| 790 |
+
"村子东边的古塔里据说住过一个法师,后来不知为何法师消失了,塔也荒废了。",
|
| 791 |
+
"有人在精灵遗迹附近见到过一个穿绿袍的身影,不知是人是鬼。",
|
| 792 |
]
|
| 793 |
|
| 794 |
+
# --- 随机事件候选池 ---
|
| 795 |
+
self.random_event_pool: list[str] = [
|
| 796 |
+
"一个受伤的旅行商人跌跌撞撞地出现在你面前,他的货物散落一地。",
|
| 797 |
+
"天空忽然暗了下来,远处传来隐约的雷声,一场暴雨似乎即将来临。",
|
| 798 |
+
"你注意到路边有一个被藤蔓遮住的旧箱子,看起来已经放了很久。",
|
| 799 |
+
"一只受伤的猎鹰坠落在你脚边,它的腿上绑着一小卷羊皮纸。",
|
| 800 |
+
"远处传来激烈的打斗声,似乎有人遭到了伏击。",
|
| 801 |
+
"一个衣衫褴褛的流浪汉向你走来,自称知道附近藏宝的秘密。",
|
| 802 |
+
"你经过的路上发现了一串新鲜的、不属于任何已知生物的巨大脚印。",
|
| 803 |
+
"一阵古怪的风带来了焦糊的味道,顺着风向看去,远处有一缕黑烟升起。",
|
| 804 |
+
]
|
| 805 |
+
self.pending_random_event: str | None = None # 本回合触发的随机事件
|
| 806 |
+
|
| 807 |
# --- 玩家初始装备 ---
|
| 808 |
self.player.inventory = ["面包", "面包", "小型治疗药水"]
|
| 809 |
|
|
|
|
| 1276 |
if self.world.rumors:
|
| 1277 |
rumors_desc = "\n【流传的传闻】\n" + "\n".join(f" - {r}" for r in self.world.rumors[-3:])
|
| 1278 |
|
| 1279 |
+
# 8. 随机事件
|
| 1280 |
+
random_event_desc = ""
|
| 1281 |
+
if getattr(self, 'pending_random_event', None):
|
| 1282 |
+
random_event_desc = (
|
| 1283 |
+
f"\n【本回合随机事件 —— 必须融入本次叙事】\n"
|
| 1284 |
+
f"{self.pending_random_event}\n"
|
| 1285 |
+
f"请将此事件自然地融入剧情描写中,作为额外的意外插曲。"
|
| 1286 |
+
f"玩家可以选择回应或忽视此事件。至少一个选项应与此事件相关。"
|
| 1287 |
+
)
|
| 1288 |
+
self.pending_random_event = None # 用后清除
|
| 1289 |
+
|
| 1290 |
+
# 9. 一致性约束指令
|
| 1291 |
consistency_rules = (
|
| 1292 |
"\n【一致性约束 —— 你必须严格遵守】\n"
|
| 1293 |
"1. 已死亡的NPC不可再出现或对话(除非有特殊复活剧情)。\n"
|
|
|
|
| 1321 |
move_desc,
|
| 1322 |
event_desc,
|
| 1323 |
rumors_desc,
|
| 1324 |
+
random_event_desc,
|
| 1325 |
consistency_rules,
|
| 1326 |
])
|
| 1327 |
|
|
|
|
| 1425 |
|
| 1426 |
return contradictions
|
| 1427 |
|
| 1428 |
+
def pre_validate_action(self, intent: dict) -> tuple[bool, str]:
|
| 1429 |
+
"""
|
| 1430 |
+
预校验玩家意图的合法性(在 LLM 调用前立即拦截非法操作)。
|
| 1431 |
+
|
| 1432 |
+
设计思路:
|
| 1433 |
+
- 在任何 API 调用之前就检测明显违反一致性的操作
|
| 1434 |
+
- 不合法时立即驳回,避免浪费 API 调用和回合
|
| 1435 |
+
- 检测维度:物品是否在背包/装备中、技能是否已习得、
|
| 1436 |
+
raw_input 中是否提及使用不存在的物品
|
| 1437 |
+
|
| 1438 |
+
Returns:
|
| 1439 |
+
(is_valid, rejection_reason): 合法返回 (True, ""), 否则返回 (False, "拒绝原因")
|
| 1440 |
+
"""
|
| 1441 |
+
action = intent.get("intent", "")
|
| 1442 |
+
target = intent.get("target", "") or ""
|
| 1443 |
+
details = intent.get("details", "") or ""
|
| 1444 |
+
raw_input = intent.get("raw_input", "") or ""
|
| 1445 |
+
|
| 1446 |
+
inventory = list(self.player.inventory)
|
| 1447 |
+
equipped_items = [v for v in self.player.equipment.values() if v]
|
| 1448 |
+
all_owned = set(inventory) | set(equipped_items)
|
| 1449 |
+
|
| 1450 |
+
# --- 检测 1: USE_ITEM / EQUIP: target 必须在背包或装备中 ---
|
| 1451 |
+
if action in ("USE_ITEM", "EQUIP") and target:
|
| 1452 |
+
if target not in all_owned:
|
| 1453 |
+
return False, f"你的背包中没有「{target}」,无法使用或装备。"
|
| 1454 |
+
if action == "EQUIP" and target not in inventory:
|
| 1455 |
+
if target in equipped_items:
|
| 1456 |
+
return False, f"「{target}」已经装备在身上了。"
|
| 1457 |
+
return False, f"你的背包中没有「{target}」,无法装备。"
|
| 1458 |
+
|
| 1459 |
+
# --- 检测 2: SKILL: 必须已习得 ---
|
| 1460 |
+
if action == "SKILL" and target:
|
| 1461 |
+
if target not in self.player.skills:
|
| 1462 |
+
return False, f"你尚未习得技能「{target}」。"
|
| 1463 |
+
|
| 1464 |
+
# --- 检测 3: 扫描 raw_input 中是否在使用上下文中引用了未拥有的已知物品 ---
|
| 1465 |
+
known_items: set[str] = set(self.world.item_registry.keys())
|
| 1466 |
+
for event in self.event_log:
|
| 1467 |
+
sc = event.state_changes
|
| 1468 |
+
if isinstance(sc, dict):
|
| 1469 |
+
for item in sc.get("items_gained", []):
|
| 1470 |
+
known_items.add(str(item))
|
| 1471 |
+
for item in sc.get("items_lost", []):
|
| 1472 |
+
known_items.add(str(item))
|
| 1473 |
+
|
| 1474 |
+
unavailable_known = {
|
| 1475 |
+
item for item in known_items
|
| 1476 |
+
if item not in all_owned and len(item) >= 2
|
| 1477 |
+
}
|
| 1478 |
+
|
| 1479 |
+
use_verbs = [
|
| 1480 |
+
"使用", "用", "吃", "喝", "装备", "穿上", "戴上",
|
| 1481 |
+
"拿出", "掏出", "挥舞", "举起", "服用", "食用", "拔出", "拿起",
|
| 1482 |
+
]
|
| 1483 |
+
|
| 1484 |
+
for item_name in unavailable_known:
|
| 1485 |
+
if item_name not in raw_input:
|
| 1486 |
+
continue
|
| 1487 |
+
for verb in use_verbs:
|
| 1488 |
+
if verb + item_name in raw_input:
|
| 1489 |
+
return False, f"你的背包中没有「{item_name}」,无法{verb}。"
|
| 1490 |
+
|
| 1491 |
+
# --- 检测 4: 检查 raw_input 中是否提及使用完全未知的物品 ---
|
| 1492 |
+
# 匹配常见的"使用物品"语句模式,提取物品名称并校验
|
| 1493 |
+
extraction_patterns = [
|
| 1494 |
+
(r'(?:掏出|拿出|拔出|举起)(.{2,8}?)(?:$|[,。!?,\s来])', "使用"),
|
| 1495 |
+
(r'吃(?:一个|一块|一份|了个|了一个)?(.{2,8}?)(?:$|[,。!?,\s来])', "吃"),
|
| 1496 |
+
(r'喝(?:一瓶|一杯|一口|了一瓶|了)?(.{2,8}?)(?:$|[,。!?,\s来])', "喝"),
|
| 1497 |
+
(r'用(.{2,6}?)(?:打|攻击|砍|刺|射|劈|挡|切|割)', "使用"),
|
| 1498 |
+
]
|
| 1499 |
+
|
| 1500 |
+
non_item_words = {
|
| 1501 |
+
"拳头", "双手", "手", "脚", "头", "身体",
|
| 1502 |
+
"魔法", "技能", "力量", "勇气", "智慧",
|
| 1503 |
+
"办法", "方法", "速度", "周围", "四周",
|
| 1504 |
+
}
|
| 1505 |
+
|
| 1506 |
+
full_text = raw_input + " " + details
|
| 1507 |
+
|
| 1508 |
+
for pattern, verb_desc in extraction_patterns:
|
| 1509 |
+
match = re.search(pattern, full_text)
|
| 1510 |
+
if match:
|
| 1511 |
+
mentioned = match.group(1).strip()
|
| 1512 |
+
if not mentioned or mentioned in non_item_words:
|
| 1513 |
+
continue
|
| 1514 |
+
if mentioned not in all_owned:
|
| 1515 |
+
# 模糊匹配:"剑" 可能是 "铁剑" 的简称
|
| 1516 |
+
fuzzy_match = any(
|
| 1517 |
+
mentioned in owned or owned in mentioned
|
| 1518 |
+
for owned in all_owned
|
| 1519 |
+
)
|
| 1520 |
+
if not fuzzy_match:
|
| 1521 |
+
return False, f"你并没有「{mentioned}」,请检查你的背包。"
|
| 1522 |
+
|
| 1523 |
+
return True, ""
|
| 1524 |
+
|
| 1525 |
def is_game_over(self) -> bool:
|
| 1526 |
"""
|
| 1527 |
判断游戏是否结束。
|
|
|
|
| 1593 |
# 更新 NPC 位置(根据时间表)
|
| 1594 |
self._update_npc_schedules()
|
| 1595 |
|
| 1596 |
+
# 随机事件(约 25% 概率触发)
|
| 1597 |
+
self._roll_random_event()
|
| 1598 |
+
|
| 1599 |
return tick_log
|
| 1600 |
|
| 1601 |
def _apply_status_effects(self) -> list[str]:
|
|
|
|
| 1662 |
|
| 1663 |
return effect_log
|
| 1664 |
|
| 1665 |
+
def _roll_random_event(self):
|
| 1666 |
+
"""以一定概率从事件池中抽取一个随机事件,注入到下一次 Prompt 中"""
|
| 1667 |
+
import random
|
| 1668 |
+
self.pending_random_event = None
|
| 1669 |
+
if self.random_event_pool and random.random() < 0.25:
|
| 1670 |
+
event = random.choice(self.random_event_pool)
|
| 1671 |
+
self.pending_random_event = event
|
| 1672 |
+
logger.info(f"随机事件触发: {event}")
|
| 1673 |
+
|
| 1674 |
def _check_quest_deadlines(self):
|
| 1675 |
"""检查限时任务是否过期"""
|
| 1676 |
for quest in self.world.quests.values():
|
story_engine.py
CHANGED
|
@@ -367,6 +367,13 @@ MERGED_SYSTEM_PROMPT_TEMPLATE = """【最高优先级指令 ── 输出格式
|
|
| 367 |
- 战斗场景要紧张刺激
|
| 368 |
- 货币统一称"金币"
|
| 369 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 370 |
【选项规则】
|
| 371 |
- 恰好 3 个选项,覆盖不同策略方向(激进/谨慎/探索/社交等)
|
| 372 |
- 选项中的人物/物品/地点必须已在当前或之前剧情中出现过
|
|
@@ -532,6 +539,10 @@ class StoryEngine:
|
|
| 532 |
# 再次检查
|
| 533 |
consistency_issues = self.game_state.check_consistency(state_changes)
|
| 534 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 535 |
# ============================================
|
| 536 |
# 清理状态变更:阻止非消耗品被错误移除
|
| 537 |
# ============================================
|
|
@@ -1007,6 +1018,51 @@ class StoryEngine:
|
|
| 1007 |
|
| 1008 |
return changes, warnings
|
| 1009 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1010 |
def _validate_options(self, options: list[dict]) -> list[dict]:
|
| 1011 |
"""
|
| 1012 |
验证生成的选项:移除引用了玩家不拥有的物品的选项。
|
|
@@ -1315,6 +1371,8 @@ class StoryEngine:
|
|
| 1315 |
|
| 1316 |
if consistency_issues:
|
| 1317 |
logger.warning(f"发现一致性问题: {consistency_issues}")
|
|
|
|
|
|
|
| 1318 |
|
| 1319 |
# 清理状态变更
|
| 1320 |
event_type = outline.get("event_type", "")
|
|
|
|
| 367 |
- 战斗场景要紧张刺激
|
| 368 |
- 货币统一称"金币"
|
| 369 |
|
| 370 |
+
【剧情多样性规则 ── 极其重要】
|
| 371 |
+
- 禁止连续两个回合出现相同类型的敌人(如上回合打了哥布林,本回合不能再打哥布林)
|
| 372 |
+
- 连续战斗不得超过 2 个回合,之后必须穿插非战斗事件(对话/探索/发现/交易)
|
| 373 |
+
- 鼓励引入新NPC、新线索、支线事件,让世界有"活"的感觉
|
| 374 |
+
- 如果世界状态中标注了【本回合随机事件】,必须将其融入叙事
|
| 375 |
+
- 尽量引导玩家前往不同的区域探索,而非反复停留在同一地点
|
| 376 |
+
|
| 377 |
【选项规则】
|
| 378 |
- 恰好 3 个选项,覆盖不同策略方向(激进/谨慎/探索/社交等)
|
| 379 |
- 选项中的人物/物品/地点必须已在当前或之前剧情中出现过
|
|
|
|
| 539 |
# 再次检查
|
| 540 |
consistency_issues = self.game_state.check_consistency(state_changes)
|
| 541 |
|
| 542 |
+
# 移除与非法物品相关的状态变更(安全网)
|
| 543 |
+
if consistency_issues:
|
| 544 |
+
state_changes = self._strip_invalid_item_effects(state_changes)
|
| 545 |
+
|
| 546 |
# ============================================
|
| 547 |
# 清理状态变更:阻止非消耗品被错误移除
|
| 548 |
# ============================================
|
|
|
|
| 1018 |
|
| 1019 |
return changes, warnings
|
| 1020 |
|
| 1021 |
+
def _strip_invalid_item_effects(self, state_changes: dict) -> dict:
|
| 1022 |
+
"""
|
| 1023 |
+
当 LLM 生成了涉及不存在物品的状态变更时,移除相关效果(安全网)。
|
| 1024 |
+
|
| 1025 |
+
例如:LLM 生成了"吃了包子" → hunger_change: +10, items_lost: ["包子"],
|
| 1026 |
+
但包子不在背包中。此方法移除 items_lost 和相关的属性效果。
|
| 1027 |
+
"""
|
| 1028 |
+
changes = dict(state_changes)
|
| 1029 |
+
|
| 1030 |
+
if "items_lost" not in changes:
|
| 1031 |
+
return changes
|
| 1032 |
+
|
| 1033 |
+
items_gained = {str(i) for i in changes.get("items_gained", [])}
|
| 1034 |
+
invalid_count = 0
|
| 1035 |
+
valid_items_lost = []
|
| 1036 |
+
|
| 1037 |
+
for item in changes["items_lost"]:
|
| 1038 |
+
item_str = str(item)
|
| 1039 |
+
# 同回合获得的物品可以立即消耗(如拾取后使用)
|
| 1040 |
+
if item_str in items_gained:
|
| 1041 |
+
valid_items_lost.append(item)
|
| 1042 |
+
continue
|
| 1043 |
+
if item_str in self.game_state.player.inventory:
|
| 1044 |
+
valid_items_lost.append(item)
|
| 1045 |
+
else:
|
| 1046 |
+
invalid_count += 1
|
| 1047 |
+
logger.warning(f"[安全网] 阻止消耗不存在的物品「{item_str}」")
|
| 1048 |
+
|
| 1049 |
+
if invalid_count > 0:
|
| 1050 |
+
if valid_items_lost:
|
| 1051 |
+
changes["items_lost"] = valid_items_lost
|
| 1052 |
+
else:
|
| 1053 |
+
changes.pop("items_lost", None)
|
| 1054 |
+
# 所有 items_lost 都无效 → 属性变更可能全部源于非法物品使用
|
| 1055 |
+
for key in ["hunger_change", "hp_change", "mp_change",
|
| 1056 |
+
"morale_change", "sanity_change"]:
|
| 1057 |
+
if key in changes:
|
| 1058 |
+
logger.warning(f"[安全网] 因物品不合法,阻止 {key}: {changes[key]}")
|
| 1059 |
+
changes.pop(key)
|
| 1060 |
+
if "status_effects_added" in changes:
|
| 1061 |
+
logger.warning("[安全网] 因物品不合法,阻止状态效果添加")
|
| 1062 |
+
changes.pop("status_effects_added")
|
| 1063 |
+
|
| 1064 |
+
return changes
|
| 1065 |
+
|
| 1066 |
def _validate_options(self, options: list[dict]) -> list[dict]:
|
| 1067 |
"""
|
| 1068 |
验证生成的选项:移除引用了玩家不拥有的物品的选项。
|
|
|
|
| 1371 |
|
| 1372 |
if consistency_issues:
|
| 1373 |
logger.warning(f"发现一致性问题: {consistency_issues}")
|
| 1374 |
+
# 移除与非法物品相关的状态变更(安全网)
|
| 1375 |
+
state_changes = self._strip_invalid_item_effects(state_changes)
|
| 1376 |
|
| 1377 |
# 清理状态变更
|
| 1378 |
event_type = outline.get("event_type", "")
|