NewProject / demo_rules.py
wzh0617's picture
Upload 3 files
15d68d1 verified
from __future__ import annotations
from collections import deque
from dataclasses import dataclass
from typing import Any
ACTION_TIME_COSTS: dict[str, int] = {
"MOVE": 30,
"ATTACK": 30,
"COMBAT": 30,
"CLAIM_REWARD": 10,
"REST": 30,
"OVERNIGHT_REST": 30,
"TALK": 10,
"SHOP_MENU": 10,
"SCENE_OPTIONS": 10,
"TRADE": 10,
"EQUIP": 10,
"USE_ITEM": 10,
"VIEW_MAP": 10,
"MAP": 10,
"QUEST": 10,
}
MAX_OPTION_COUNT = 6
DEFAULT_OPTION_COUNT = 3
OVERNIGHT_REST_LOCATIONS = {"村庄旅店", "溪边营地"}
MAIN_QUEST_ID = "main_quest_01"
MAIN_QUEST_TROLL_ID = "main_quest_02"
FOREST_GOBLIN_DEFEATED_FLAG = "encounter::dark_forest_gate_goblin_defeated"
FOREST_TROLL_TRACKS_FOUND_FLAG = "clue::forest_troll_tracks_found"
FOREST_TROLL_DEFEATED_FLAG = "encounter::forest_troll_defeated"
FOREST_TROLL_INTRO_SEEN_FLAG = "scene::forest_troll_intro_seen"
FOREST_TROLL_HOARD_PENDING_FLAG = "reward::forest_troll_hoard_pending"
FOREST_TROLL_HOARD_CLAIMED_FLAG = "reward::forest_troll_hoard_claimed"
DEEP_FOREST_BARRIER_SEEN_FLAG = "clue::deep_forest_barrier_seen"
FOREST_CAUSE_OBJECTIVE = "调查怪物活动的原因"
REPORT_TO_CHIEF_OBJECTIVE = "与村长老伯对话汇报发现"
FOREST_TROLL_TRAVEL_OBJECTIVE = "前往森林深处"
FOREST_TROLL_BOSS_OBJECTIVE = "击败森林巨魔"
SIDE_QUEST_TRAVELER_ID = "side_quest_01"
SIDE_QUEST_FERRY_ID = "side_quest_02"
SIDE_QUEST_GUARDIAN_ID = "side_quest_03"
TRAVELER_RUMOR_HEARD_FLAG = "rumor::traveler_lead_heard"
MINE_RUMOR_HEARD_FLAG = "rumor::mine_ghost_heard"
FERRY_ROUTE_UNLOCKED_FLAG = "rumor::ferry_route_unlocked"
TRAVELER_ENCOUNTERED_FLAG = "scene::mysterious_traveler_encountered"
GUARDIAN_INTRO_SEEN_FLAG = "scene::elf_guardian_introduced"
LOCATION_MAP_REQUIREMENTS: dict[str, str] = {
"村庄广场": "村庄地图",
"村庄铁匠铺": "村庄地图",
"村庄旅店": "村庄地图",
"村庄杂货铺": "村庄地图",
"村口小路": "村庄地图",
# 黑暗森林入口 可以用村庄地图到达;击败哥布林后获得黑暗森林地图
"黑暗森林入口": "村庄地图",
# 溪边营地/森林深处 需要黑暗森林地图(森林入口战斗奖励)
"溪边营地": "黑暗森林地图",
"森林深处": "黑暗森林地图",
# 河边渡口 用村庄地图可到达;与老渔夫对话后获得山麓地图
"河边渡口": "村庄地图",
# 以下地点需要山麓地图(老渔夫给予)
"废弃矿洞入口": "山麓地图",
"山麓盗贼营": "山麓地图",
"精灵遗迹": "山麓地图",
# 古塔废墟 从村口小路可直接发现,村庄地图即可
"古塔废墟": "村庄地图",
}
SHOP_LOCATION_TO_MERCHANT: dict[str, str] = {
"村庄铁匠铺": "铁匠格林",
"村庄旅店": "旅店老板娘莉娜",
"村庄杂货铺": "杂货商人阿尔",
}
ARRIVAL_EVENT_CONFIG: dict[str, dict[str, str]] = {
"村庄铁匠铺": {
"event_key": "arrival::village_blacksmith",
"story_text": (
"一间热气腾腾的铁匠铺,炉火正旺。墙上挂满了各式武器和护甲。\n"
"炉火映红了铁匠粗犷的脸庞,空气中弥漫着金属和碳的气味。"
),
},
"村庄旅店": {
"event_key": "arrival::village_inn",
"story_text": (
"一家温馨的小旅店,空气中弥漫着烤肉和麦酒的香气。壁炉里的火焰跳动着。\n"
"壁炉噼啪作响,几位旅客正低声交谈,空气温暖而舒适。"
),
},
"村庄杂货铺": {
"event_key": "arrival::village_general_store",
"story_text": (
"一家琳琅满目的杂货铺,从草药到绳索应有尽有。老板是个精明的商人。\n"
"货架上摆满了稀奇古怪的商品,柜台后的商人正用算盘噼里啪啦地算账。"
),
},
"黑暗森林入口": {
"event_key": "arrival::dark_forest_gate",
"story_text": (
"黑暗森林入口的树冠压低了天色,落叶间散着新鲜爪痕和被拖行过的泥印。\n"
"一只拎着弯刀的哥布林正伏在断木后张望,时不时发出刺耳怪叫,像是在替林中的东西守门。"
),
},
"河边渡口": {
"event_key": "arrival::river_ferry",
"story_text": (
"破旧的渡口被河水拍得吱呀作响,湿冷水汽贴着木桩往上爬。\n"
"披着蓑衣的老渔夫正盯着对岸,矿洞与山麓营地的路径都从这里分开。"
),
},
"山麓盗贼营": {
"event_key": "arrival::bandit_camp",
"story_text": (
"山麓盗贼营的篝火还留着余温,翻倒的酒桶和散落的干粮说明有人刚撤离不久。\n"
"营帐阴影里有个盗贼斥候正贴着木桩窥探四周,显然不打算让外来者轻易通过。"
),
},
"古塔废墟": {
"event_key": "arrival::ancient_tower",
"story_text": (
"半坍塌的古塔在风里发出低鸣,残破石阶和裂墙间还能看到新鲜抓痕。\n"
"塔门内飘着一团幽蓝冷火,游荡幽灵在尘灰间若隐若现,像是在警告你别再向前。"
),
},
"废弃矿洞入口": {
"event_key": "arrival::mine_entrance",
"story_text": (
"矿洞入口被枯枝和碎石半堵着,腐朽的矿车轨道向黑暗深处延伸,铁锈和硫磺气味扑面而来。\n"
"一具骷髅兵从废弃矿车后方立起,锈迹斑斑的武器指向你——矿洞里的东西不欢迎活人。"
),
},
"溪边营地": {
"event_key": "arrival::creek_camp",
"story_text": (
"森林中一处难得的开阔地带,清澈的溪水从旁流过,树冠间的光在水面打出零碎金片。\n"
"篝火余烬和被压平的草丛说明有人不久前在此扎营——这里适合短暂休息和搜寻遗留物资。"
),
},
"精灵遗迹": {
"event_key": "arrival::elf_ruins",
"story_text": (
"石柱在藤蔓间若隐若现,精灵文字的刻痕随你的步伐明灭,像是感应到了来者的意图。\n"
"一个穿着褪色绿袍的消瘦身影从石柱阴影里转出,用警惕的眼神审视着你——遗迹有它的守护者。"
),
},
"森林深处": {
"event_key": "arrival::deep_forest",
"story_text": (
"古树盘根错节,荧光苔藓将深处映成幽蓝,腐朽与魔力的气息混杂难辨。\n"
"远处传来低沉咆哮,树影间有成双眼睛移动——有什么东西已经感知到了你的闯入。"
),
},
}
@dataclass(slots=True)
class BattleSnapshot:
hp: int
attack: int
defense: int
stamina: int
hit_rate: float
dodge_rate: float
live_state_multiplier: float = 1.0
@property
def power(self) -> float:
return (
(self.attack * 0.6 + self.defense * 0.3 + self.stamina * 0.1)
* self.live_state_multiplier
)
BATTLE_ENCOUNTER_CONFIG: dict[tuple[str, str], dict[str, Any]] = {
("黑暗森林入口", "哥布林"): {
"enemy_snapshot": BattleSnapshot(
hp=45,
attack=8,
defense=3,
stamina=28,
hit_rate=0.82,
dodge_rate=0.08,
live_state_multiplier=1.0,
),
"defeated_flag": "encounter::dark_forest_gate_goblin_defeated",
"reward_items": ["黑暗森林地图"],
"quest_objectives": ["击败森林中的怪物"],
},
("山麓盗贼营", "盗贼斥候"): {
"enemy_snapshot": BattleSnapshot(
hp=52,
attack=9,
defense=4,
stamina=30,
hit_rate=0.84,
dodge_rate=0.12,
live_state_multiplier=1.0,
),
"defeated_flag": "encounter::bandit_scout_defeated",
"reward_items": ["山麓地图"],
"quest_objectives": [],
},
("古塔废墟", "游荡幽灵"): {
"enemy_snapshot": BattleSnapshot(
hp=58,
attack=10,
defense=5,
stamina=32,
hit_rate=0.86,
dodge_rate=0.14,
live_state_multiplier=1.0,
),
"defeated_flag": "encounter::ancient_tower_wraith_defeated",
"reward_items": ["古塔地图"],
"quest_objectives": [],
},
("废弃矿洞入口", "骷髅兵"): {
"enemy_snapshot": BattleSnapshot(
hp=38,
attack=7,
defense=4,
stamina=22,
hit_rate=0.75,
dodge_rate=0.05,
live_state_multiplier=1.0,
),
"defeated_flag": "encounter::mine_skeleton_defeated",
"reward_items": ["骷髅碎骨"],
"quest_objectives": ["前往废弃矿洞调查"],
},
("黑暗森林入口", "野狼"): {
"enemy_snapshot": BattleSnapshot(
hp=58,
attack=11,
defense=5,
stamina=36,
hit_rate=0.82,
dodge_rate=0.14,
live_state_multiplier=1.0,
),
"defeated_flag": "",
"reward_items": [],
"quest_objectives": [],
},
("森林深处", "森林巨魔"): {
"enemy_snapshot": BattleSnapshot(
hp=120,
attack=15,
defense=10,
stamina=130,
hit_rate=0.88,
dodge_rate=0.1,
live_state_multiplier=1.0,
),
"defeated_flag": FOREST_TROLL_DEFEATED_FLAG,
"reward_items": [],
"quest_id": MAIN_QUEST_TROLL_ID,
"quest_objectives": [FOREST_TROLL_BOSS_OBJECTIVE],
},
}
def action_time_cost_minutes(action_type: str) -> int:
return ACTION_TIME_COSTS.get(str(action_type or "").upper(), 10)
def resolve_battle(
player: BattleSnapshot,
enemy: BattleSnapshot,
*,
player_unarmed: bool = False,
) -> dict[str, Any]:
enemy_power = max(enemy.power, 1.0)
player_power = player.power
if player_unarmed and player.attack <= enemy.defense:
player_power *= 0.5
ratio = player_power / enemy_power
if ratio < 0.6:
outcome = "forced_retreat"
elif ratio < 1.0:
outcome = "pyrrhic_win"
elif ratio < 1.5:
outcome = "normal_win"
else:
outcome = "dominant_win"
return {
"player_power": round(player_power, 2),
"enemy_power": round(enemy_power, 2),
"ratio": round(ratio, 3),
"outcome": outcome,
}
def resolve_trade(
game_state,
*,
merchant_name: str,
item_name: str,
confirm: bool,
) -> dict[str, Any]:
npc = game_state.world.npcs.get(merchant_name)
if npc is None or not npc.can_trade:
return {"applied": False, "reason": "invalid_merchant"}
if npc.location != game_state.player.location:
return {"applied": False, "reason": "merchant_not_here"}
if item_name not in npc.shop_inventory:
return {"applied": False, "reason": "item_not_sold_here"}
item_info = game_state.world.item_registry.get(item_name)
if item_info is None:
return {"applied": False, "reason": "unknown_item"}
if not confirm:
return {
"applied": False,
"reason": "awaiting_confirmation",
"price": item_info.value,
}
if game_state.player.gold < item_info.value:
return {"applied": False, "reason": "insufficient_gold", "price": item_info.value}
game_state.player.gold -= item_info.value
game_state.player.inventory.append(item_name)
game_state.last_recent_gain = item_name
follow_up_actions = build_contextual_actions(game_state, recent_gain=item_name)
return {
"applied": True,
"reason": "purchased",
"price": item_info.value,
"item_name": item_name,
"follow_up_actions": follow_up_actions,
}
def get_battle_encounter(location_name: str, enemy_name: str) -> dict[str, Any] | None:
return BATTLE_ENCOUNTER_CONFIG.get((str(location_name), str(enemy_name)))
def _has_status(game_state, keyword: str) -> bool:
keyword = str(keyword or "")
return any(keyword in effect.name for effect in game_state.player.status_effects)
def _has_map(game_state) -> bool:
owned_items = set(game_state.player.inventory) | {
str(item)
for item in game_state.player.equipment.values()
if item
}
return any("地图" in item for item in owned_items)
def _has_named_map(game_state, map_name: str) -> bool:
owned_items = set(game_state.player.inventory) | {
str(item)
for item in game_state.player.equipment.values()
if item
}
return str(map_name) in owned_items
def _is_accessible_destination(game_state, destination: str) -> bool:
target_location = game_state.world.locations.get(destination)
if target_location is None:
return False
if target_location.is_accessible:
return True
required_item = str(target_location.required_item or "")
owned_items = set(game_state.player.inventory) | {
str(item)
for item in game_state.player.equipment.values()
if item
}
return bool(required_item) and required_item in owned_items
def _is_route_visible(game_state, destination: str) -> bool:
if destination == "河边渡口":
return bool(game_state.world.global_flags.get(FERRY_ROUTE_UNLOCKED_FLAG))
return True
def _is_forest_troll_hunt_active(game_state) -> bool:
quest = game_state.world.quests.get(MAIN_QUEST_TROLL_ID)
if quest is None or quest.status != "active":
return False
return not bool(game_state.world.global_flags.get(FOREST_TROLL_DEFEATED_FLAG))
def _is_main_quest_report_pending(game_state) -> bool:
quest = game_state.world.quests.get(MAIN_QUEST_ID)
if quest is None or quest.status != "active":
return False
if not game_state.world.global_flags.get(FOREST_TROLL_TRACKS_FOUND_FLAG):
return False
return not bool(quest.objectives.get(REPORT_TO_CHIEF_OBJECTIVE))
def _find_encounter_location(enemy_name: str) -> str | None:
enemy_name = str(enemy_name or "")
if not enemy_name:
return None
for (location_name, encounter_enemy), _config in BATTLE_ENCOUNTER_CONFIG.items():
if str(encounter_enemy) == enemy_name:
return str(location_name)
return None
def _move_action(
target: str,
*,
priority: int,
text: str | None = None,
preserve_text: bool = False,
) -> dict[str, Any]:
action = _make_action(
action_type="MOVE",
target=target,
text=text or f"前往{target}",
priority=priority,
)
if preserve_text:
action["preserve_text"] = True
return action
def _make_action(
*,
action_type: str,
text: str,
target: Any = None,
priority: int = 50,
) -> dict[str, Any]:
return {
"id": 0,
"text": text,
"action_type": action_type,
"target": target,
"priority": priority,
}
def _dedupe_actions(actions: list[dict[str, Any]]) -> list[dict[str, Any]]:
deduped: list[dict[str, Any]] = []
seen: set[tuple[str, str]] = set()
for action in sorted(
actions,
key=lambda item: int(item.get("priority", 0) or 0),
reverse=True,
):
normalized = dict(action)
normalized.setdefault("priority", 0)
key = (
str(normalized.get("action_type")),
str(normalized.get("target")),
)
if key in seen:
continue
seen.add(key)
normalized["id"] = len(deduped) + 1
deduped.append(normalized)
return deduped
def _first_incomplete_objective(game_state):
active_quests = [
quest
for quest in game_state.world.quests.values()
if quest.status == "active"
]
active_quests.sort(key=lambda quest: (quest.quest_type != "main", quest.quest_id))
for quest in active_quests:
for objective, completed in quest.objectives.items():
if not completed:
return quest, objective
return None, None
def _extract_dialogue_target(objective: str) -> str | None:
suffixes = ("对话", "交谈", "确认情报", "了解情况")
text = str(objective or "")
if not text.startswith("与"):
return None
candidate = text[1:]
for separator in ("对话", "交谈"):
if separator in candidate:
candidate = candidate.split(separator, 1)[0]
break
else:
for suffix in suffixes:
if candidate.endswith(suffix):
candidate = candidate[: -len(suffix)]
break
candidate = candidate.strip()
return candidate or None
def _extract_location_target(objective: str) -> str | None:
text = str(objective or "")
for prefix in ("前往",):
if text.startswith(prefix):
candidate = text[len(prefix):]
for suffix in ("调查", "探索", "查看", "侦察"):
if candidate.endswith(suffix):
candidate = candidate[: -len(suffix)]
break
return candidate or None
return None
def _find_next_step(game_state, destination: str) -> str | None:
if destination == game_state.player.location:
return destination
visited = {game_state.player.location}
queue: deque[tuple[str, list[str]]] = deque([(game_state.player.location, [])])
while queue:
current, path = queue.popleft()
current_loc = game_state.world.locations.get(current)
if current_loc is None:
continue
for neighbor in current_loc.connected_to:
if neighbor in visited:
continue
visited.add(neighbor)
new_path = path + [neighbor]
if neighbor == destination:
return new_path[0]
queue.append((neighbor, new_path))
return None
def build_village_chief_follow_up_actions(game_state) -> list[dict[str, Any]]:
blacksmith_text = "前往村庄铁匠铺准备武器"
path_text = None
if _is_forest_troll_hunt_active(game_state):
blacksmith_text = "前往村庄铁匠铺准备武器和防具"
path_text = "沿村口小路赶赴森林深处"
return _dedupe_actions(
[
_make_action(
action_type="VIEW_MAP",
text="查看地图",
priority=120,
),
_move_action(
"森林深处" if path_text else "村口小路",
priority=116,
text=path_text,
preserve_text=bool(path_text),
),
_move_action(
"村庄杂货铺",
priority=112,
text="前往村庄杂货铺准备火把",
preserve_text=True,
),
_move_action(
"村庄铁匠铺",
priority=108,
text=blacksmith_text,
preserve_text=True,
),
]
)
def build_map_actions(game_state) -> list[dict[str, Any]]:
if not _has_map(game_state):
return []
current_location = game_state.world.locations.get(game_state.player.location)
if current_location is None:
return []
special_route_order = {
"村庄广场": ["村庄铁匠铺", "村庄旅店", "村口小路", "村庄杂货铺"],
"黑暗森林入口": ["村口小路", "溪边营地", "森林深处"],
"河边渡口": ["废弃矿洞入口", "山麓盗贼营", "村口小路"],
"山麓盗贼营": ["精灵遗迹", "河边渡口"],
"古塔废墟": ["村口小路"],
}
route_order = special_route_order.get(
current_location.name,
list(current_location.connected_to),
)
actions: list[dict[str, Any]] = []
for index, destination in enumerate(route_order):
# 不显示"前往当前场景"的无效选项
if destination == game_state.player.location:
continue
if destination not in current_location.connected_to:
continue
if not _is_accessible_destination(game_state, destination):
continue
required_map = LOCATION_MAP_REQUIREMENTS.get(destination)
if required_map and not _has_named_map(game_state, required_map):
continue
if not _is_route_visible(game_state, destination):
continue
if (
destination == "森林深处"
and game_state.world.global_flags.get(DEEP_FOREST_BARRIER_SEEN_FLAG)
and "森林之钥" not in game_state.player.inventory
):
continue
actions.append(
_move_action(
destination,
priority=120 - index * 4,
)
)
return _dedupe_actions(actions)
def build_shop_menu_actions(game_state, merchant_name: str) -> list[dict[str, Any]]:
npc = game_state.world.npcs.get(merchant_name)
if npc is None or not npc.can_trade:
return []
actions: list[dict[str, Any]] = []
player_gold = int(getattr(game_state.player, "gold", 0))
for index, item_name in enumerate(npc.shop_inventory):
item_info = game_state.world.item_registry.get(item_name)
price = int(item_info.value) if item_info else 0
affordable = player_gold >= price
if affordable:
label = f"【可购买】购买{item_name}{price}金币)"
priority = 130 - index * 4
else:
label = f"【金币不足】购买{item_name}{price}金币,当前{player_gold})"
priority = 90 - index * 2
actions.append(
_make_action(
action_type="TRADE",
target={"merchant": merchant_name, "item": item_name, "confirm": False},
text=label,
priority=priority,
)
)
actions.append(
_make_action(
action_type="SCENE_OPTIONS",
target=npc.location,
text="暂不购买,先离开柜台",
priority=60,
)
)
return _dedupe_actions(actions)
def build_scene_actions(game_state, location_name: str | None = None) -> list[dict[str, Any]]:
current_name = str(location_name or game_state.player.location)
actions: list[dict[str, Any]] = []
if current_name == "村庄广场":
actions.append(
_make_action(
action_type="TALK",
target="村长老伯",
text="与村长老伯对话",
priority=120,
)
)
actions.append(
_make_action(
action_type="RUMOR",
target={"source": "布告栏", "topic": "rumor_menu"},
text="查看布告栏上的异闻",
priority=108,
)
)
if _has_named_map(game_state, "村庄地图"):
actions.append(
_make_action(
action_type="VIEW_MAP",
text="查看地图",
priority=104,
)
)
actions.append(_move_action("村庄铁匠铺", priority=100))
actions.append(_move_action("村庄旅店", priority=96))
actions.append(_move_action("村口小路", priority=92))
actions.append(_move_action("村庄杂货铺", priority=88))
return _dedupe_actions(actions)
if current_name == "村口小路":
traveler_available = (
game_state.world.global_flags.get(TRAVELER_RUMOR_HEARD_FLAG)
and game_state.world.npcs.get("神秘旅人")
and game_state.world.npcs["神秘旅人"].location == "村口小路"
)
if traveler_available:
actions.append(
_make_action(
action_type="TALK",
target="神秘旅人",
text="与神秘旅人交谈",
priority=120,
)
)
if game_state.world.global_flags.get(FERRY_ROUTE_UNLOCKED_FLAG):
actions.append(_move_action("河边渡口", priority=112))
if _has_named_map(game_state, "村庄地图"):
actions.append(
_make_action(
action_type="VIEW_MAP",
text="查看地图",
priority=108,
)
)
if _is_main_quest_report_pending(game_state):
actions.append(
_move_action(
"村庄广场",
priority=120,
text="回村向村长汇报发现",
preserve_text=True,
)
)
else:
actions.append(_move_action("村庄广场", priority=112))
if _has_named_map(game_state, "村庄地图"):
actions.append(_move_action("黑暗森林入口", priority=104))
return _dedupe_actions(actions)
if current_name == "村庄铁匠铺":
actions.append(
_make_action(
action_type="TALK",
target="铁匠格林",
text="与铁匠格林对话",
priority=120,
)
)
if _has_named_map(game_state, "村庄地图"):
actions.append(
_make_action(
action_type="VIEW_MAP",
text="查看地图",
priority=104,
)
)
actions.append(_move_action("村庄广场", priority=96))
return _dedupe_actions(actions)
if current_name == "村庄旅店":
actions.append(
_make_action(
action_type="TALK",
target="旅店老板娘莉娜",
text="与旅店老板娘莉娜对话",
priority=120,
)
)
actions.append(
_make_action(
action_type="RUMOR",
target={"source": "旅店老板娘莉娜", "topic": "rumor_menu"},
text="向莉娜打听最近的异状",
priority=116,
)
)
actions.append(
_make_action(
action_type="REST",
text="在旅店休息片刻",
priority=110,
)
)
if _has_named_map(game_state, "村庄地图"):
actions.append(
_make_action(
action_type="VIEW_MAP",
text="查看地图",
priority=104,
)
)
actions.append(_move_action("村庄广场", priority=96))
return _dedupe_actions(actions)
if current_name == "村庄杂货铺":
actions.append(
_make_action(
action_type="TALK",
target="杂货商人阿尔",
text="与杂货商人阿尔对话",
priority=120,
)
)
if _has_named_map(game_state, "村庄地图"):
actions.append(
_make_action(
action_type="VIEW_MAP",
text="查看地图",
priority=104,
)
)
actions.append(_move_action("村庄广场", priority=96))
return _dedupe_actions(actions)
if current_name == "黑暗森林入口":
goblin_defeated = game_state.world.global_flags.get(FOREST_GOBLIN_DEFEATED_FLAG)
tracks_found = game_state.world.global_flags.get(FOREST_TROLL_TRACKS_FOUND_FLAG)
barrier_seen = game_state.world.global_flags.get(DEEP_FOREST_BARRIER_SEEN_FLAG)
troll_hunt_active = _is_forest_troll_hunt_active(game_state)
if not goblin_defeated:
actions.append(
_make_action(
action_type="ATTACK",
target="哥布林",
text="与哥布林战斗",
priority=120,
)
)
else:
if not tracks_found:
actions.append(
_make_action(
action_type="EXPLORE",
target="黑暗森林入口",
text="调查哥布林留下的痕迹",
priority=120,
)
)
elif troll_hunt_active and "森林之钥" in game_state.player.inventory:
actions.append(
_move_action(
"森林深处",
priority=120,
text="前往森林深处探索",
preserve_text=True,
)
)
elif not barrier_seen:
actions.append(_move_action("森林深处", priority=120))
elif _is_main_quest_report_pending(game_state):
actions.append(
_move_action(
"村口小路",
priority=120,
text="返回村庄向村长汇报",
preserve_text=True,
)
)
if _has_named_map(game_state, "黑暗森林地图"):
actions.append(
_make_action(
action_type="VIEW_MAP",
text="查看地图",
priority=112 if tracks_found else 108,
)
)
actions.append(_move_action("溪边营地", priority=104))
if tracks_found and not barrier_seen and "森林之钥" in game_state.player.inventory:
actions.append(
_move_action(
"森林深处",
priority=116,
text="前往森林深处探索" if troll_hunt_active else None,
preserve_text=troll_hunt_active,
)
)
if _is_main_quest_report_pending(game_state) and not barrier_seen and not troll_hunt_active:
actions.append(
_move_action(
"村口小路",
priority=114,
text="先返回村庄汇报情况",
preserve_text=True,
)
)
elif not barrier_seen or troll_hunt_active:
actions.append(_move_action("村口小路", priority=96))
return _dedupe_actions(actions)
if current_name == "河边渡口":
actions.append(
_make_action(
action_type="TALK",
target="渡口老渔夫",
text="与渡口老渔夫对话",
priority=120,
)
)
if _has_named_map(game_state, "山麓地图"):
actions.append(
_make_action(
action_type="VIEW_MAP",
text="查看地图",
priority=116,
)
)
actions.append(_move_action("废弃矿洞入口", priority=112))
actions.append(_move_action("山麓盗贼营", priority=108))
actions.append(_move_action("村口小路", priority=104))
return _dedupe_actions(actions)
if current_name == "山麓盗贼营":
if not game_state.world.global_flags.get("encounter::bandit_scout_defeated"):
actions.append(
_make_action(
action_type="ATTACK",
target="盗贼斥候",
text="与盗贼斥候战斗",
priority=120,
)
)
if _has_named_map(game_state, "山麓地图"):
actions.append(
_make_action(
action_type="VIEW_MAP",
text="查看地图",
priority=112,
)
)
actions.append(_move_action("河边渡口", priority=108))
return _dedupe_actions(actions)
if current_name == "古塔废墟":
if not game_state.world.global_flags.get("encounter::ancient_tower_wraith_defeated"):
actions.append(
_make_action(
action_type="ATTACK",
target="游荡幽灵",
text="与游荡幽灵战斗",
priority=120,
)
)
if _has_named_map(game_state, "古塔地图"):
actions.append(
_make_action(
action_type="VIEW_MAP",
text="查看地图",
priority=108,
)
)
actions.append(_move_action("村口小路", priority=104))
return _dedupe_actions(actions)
if current_name == "废弃矿洞入口":
if not game_state.world.global_flags.get("encounter::mine_skeleton_defeated"):
actions.append(
_make_action(
action_type="ATTACK",
target="骷髅兵",
text="与骷髅兵战斗",
priority=120,
)
)
else:
actions.append(
_make_action(
action_type="EXPLORE",
target="废弃矿洞入口",
text="搜查矿洞入口的遗留痕迹",
priority=120,
)
)
if _has_named_map(game_state, "山麓地图"):
actions.append(
_make_action(
action_type="VIEW_MAP",
text="查看地图",
priority=108,
)
)
actions.append(_move_action("河边渡口", priority=104))
return _dedupe_actions(actions)
if current_name == "溪边营地":
actions.append(
_make_action(
action_type="REST",
text="在营地休息恢复体力",
priority=120,
)
)
actions.append(
_make_action(
action_type="EXPLORE",
target="溪边营地",
text="搜寻营地遗留的物资线索",
priority=112,
)
)
if _has_named_map(game_state, "黑暗森林地图"):
actions.append(
_make_action(
action_type="VIEW_MAP",
text="查看地图",
priority=104,
)
)
actions.append(_move_action("黑暗森林入口", priority=96))
return _dedupe_actions(actions)
if current_name == "精灵遗迹":
actions.append(
_make_action(
action_type="TALK",
target="遗迹守护者",
text="与遗迹守护者对话",
priority=120,
)
)
if _has_named_map(game_state, "山麓地图"):
actions.append(
_make_action(
action_type="VIEW_MAP",
text="查看地图",
priority=108,
)
)
actions.append(_move_action("山麓盗贼营", priority=100))
return _dedupe_actions(actions)
if current_name == "森林深处":
if game_state.world.global_flags.get(FOREST_TROLL_HOARD_PENDING_FLAG):
actions.append(
_make_action(
action_type="CLAIM_REWARD",
target={"source": "forest_troll_hoard"},
text="确认拾取洞穴中的战利品",
priority=124,
)
)
elif (
_is_forest_troll_hunt_active(game_state)
and game_state.world.global_flags.get(FOREST_TROLL_INTRO_SEEN_FLAG)
):
actions.append(
_make_action(
action_type="ATTACK",
target="森林巨魔",
text="与森林巨魔战斗",
priority=120,
)
)
else:
actions.append(
_make_action(
action_type="EXPLORE",
target="森林深处",
text="深入调查森林异变的根源",
priority=120,
)
)
if _has_named_map(game_state, "黑暗森林地图"):
actions.append(
_make_action(
action_type="VIEW_MAP",
text="查看地图",
priority=108,
)
)
actions.append(_move_action("黑暗森林入口", priority=100))
return _dedupe_actions(actions)
return build_adjacent_actions(game_state)
def build_arrival_event(game_state, location_name: str) -> dict[str, Any] | None:
config = ARRIVAL_EVENT_CONFIG.get(str(location_name))
if config is None:
return None
return {
"event_key": config["event_key"],
"story_text": config["story_text"],
"options": build_scene_actions(game_state, location_name),
}
def build_goal_directed_actions(game_state) -> list[dict[str, Any]]:
quest, objective = _first_incomplete_objective(game_state)
if not quest or not objective:
return []
actions: list[dict[str, Any]] = []
inventory = set(game_state.player.inventory)
current_location = game_state.world.locations.get(game_state.player.location)
dialogue_target = _extract_dialogue_target(objective)
if dialogue_target:
npc = game_state.world.npcs.get(dialogue_target)
if npc is None:
npc = next(
(
candidate
for candidate in game_state.world.npcs.values()
if dialogue_target in candidate.name or candidate.name in objective
),
None,
)
if npc and npc.location == game_state.player.location:
actions.append(
_make_action(
action_type="TALK",
target=npc.name,
text=f"与{npc.name}对话",
priority=120,
)
)
elif npc:
next_step = _find_next_step(game_state, npc.location)
if next_step:
actions.append(
_make_action(
action_type="MOVE",
target=next_step,
text=f"前往{next_step}",
priority=112,
)
)
location_target = _extract_location_target(objective)
if location_target:
if any("地图" in item for item in inventory):
actions.append(
_make_action(
action_type="VIEW_MAP",
text="查看地图",
priority=110,
)
)
if (
location_target == "森林深处"
and _is_forest_troll_hunt_active(game_state)
and "森林之钥" in inventory
and game_state.player.location != "森林深处"
):
actions.append(
_make_action(
action_type="MOVE",
target="森林深处",
text="赶赴森林深处",
priority=105,
)
)
else:
next_step = _find_next_step(game_state, location_target)
if next_step and next_step != game_state.player.location:
actions.append(
_make_action(
action_type="MOVE",
target=next_step,
text=f"前往{next_step}",
priority=105,
)
)
if location_target == "黑暗森林入口" and game_state.player.location == "村庄广场":
if "火把" not in inventory:
actions.append(
_make_action(
action_type="MOVE",
target="村庄杂货铺",
text="前往村庄杂货铺准备火把",
priority=104,
)
)
if (
not game_state.player.equipment.get("weapon")
and "铁剑" not in inventory
and "短剑" not in inventory
):
actions.append(
_make_action(
action_type="MOVE",
target="村庄铁匠铺",
text="前往村庄铁匠铺准备武器",
priority=103,
)
)
if "击败" in str(objective):
enemy_name = str(objective).replace("击败", "").replace("森林中的", "").replace("矿洞中的", "").replace("的怪物", "").strip()
encounter_location = _find_encounter_location(enemy_name)
if enemy_name in {"怪物", "敌人"} and current_location:
local_enemies = list(current_location.enemies or [])
if local_enemies:
enemy_name = local_enemies[0]
encounter_location = _find_encounter_location(enemy_name)
else:
hunt_step = next(
(
neighbor
for neighbor in current_location.connected_to
if game_state.world.locations.get(neighbor)
and game_state.world.locations[neighbor].enemies
),
None,
)
if hunt_step:
actions.append(
_make_action(
action_type="MOVE",
target=hunt_step,
text=f"前往{hunt_step}搜索怪物",
priority=108,
)
)
enemy_name = ""
if enemy_name and encounter_location and encounter_location != game_state.player.location:
next_step = _find_next_step(game_state, encounter_location)
if next_step and next_step != game_state.player.location:
actions.append(
_make_action(
action_type="MOVE",
target=next_step,
text=f"前往{next_step}",
priority=108,
)
)
elif enemy_name:
actions.append(
_make_action(
action_type="ATTACK",
target=enemy_name,
text=f"与{enemy_name}战斗",
priority=100,
)
)
if "调查" in str(objective) or "找到" in str(objective):
if (
str(objective) == FOREST_CAUSE_OBJECTIVE
and game_state.player.location == "黑暗森林入口"
and game_state.world.global_flags.get(FOREST_GOBLIN_DEFEATED_FLAG)
):
actions.append(
_make_action(
action_type="EXPLORE",
target="黑暗森林入口",
text="调查哥布林留下的痕迹",
priority=118,
)
)
return _dedupe_actions(actions)
actions.append(
_make_action(
action_type="EXPLORE",
target=game_state.player.location,
text=f"围绕“{objective}”继续调查",
priority=92,
)
)
return _dedupe_actions(actions)
def build_adjacent_actions(game_state) -> list[dict[str, Any]]:
current_location = game_state.world.locations.get(game_state.player.location)
if current_location is None:
return []
actions: list[dict[str, Any]] = []
owned_items = set(game_state.player.inventory) | {
str(item)
for item in game_state.player.equipment.values()
if item
}
for neighbor in current_location.connected_to:
# 不显示"前往当前场景"的无效选项
if neighbor == game_state.player.location:
continue
target_location = game_state.world.locations.get(neighbor)
if target_location is None:
continue
if not target_location.is_accessible:
required_item = str(target_location.required_item or "")
if not required_item or required_item not in owned_items:
continue
actions.append(
_make_action(
action_type="MOVE",
target=neighbor,
text=f"前往{neighbor}",
priority=78 if not target_location.is_discovered else 72,
)
)
for npc_name in current_location.npcs_present:
if npc_name not in game_state.world.npcs:
continue
actions.append(
_make_action(
action_type="TALK",
target=npc_name,
text=f"与{npc_name}对话",
priority=68,
)
)
return _dedupe_actions(actions)
def merge_demo_options(
base_options: list[dict[str, Any]],
*extra_option_groups: list[dict[str, Any]],
limit: int = 3,
) -> list[dict[str, Any]]:
merged: list[dict[str, Any]] = [option for option in base_options if isinstance(option, dict)]
for group in extra_option_groups:
merged.extend(group)
deduped = _dedupe_actions(merged)
return deduped[:limit]
def build_contextual_actions(
game_state,
*,
recent_gain: str | None = None,
) -> list[dict[str, Any]]:
actions: list[dict[str, Any]] = []
inventory = set(game_state.player.inventory)
light_level = str(game_state.world.light_level)
location = game_state.world.locations.get(game_state.player.location)
time_of_day = str(game_state.world.time_of_day)
if recent_gain:
item_info = game_state.world.item_registry.get(recent_gain)
if item_info and item_info.item_type in {"weapon", "armor", "accessory"}:
slot = "weapon" if item_info.item_type == "weapon" else "armor"
if game_state.player.equipment.get(slot) != recent_gain:
actions.append(
_make_action(
action_type="EQUIP",
target=recent_gain,
text=f"装备{recent_gain}",
priority=100,
)
)
if "地图" in str(recent_gain):
actions.append(
_make_action(
action_type="VIEW_MAP",
text="查看地图",
priority=95,
)
)
in_dark_area = (
light_level in {"黑暗", "昏暗", "幽暗", "漆黑"}
or (location is not None and location.location_type == "dungeon")
or (
location is not None
and location.location_type in {"wilderness", "special"}
and time_of_day in {"夜晚", "深夜"}
)
)
if "火把" in inventory and in_dark_area and not _has_status(game_state, "火把"):
actions.append(
_make_action(
action_type="USE_ITEM",
target="火把",
text="使用火把照明",
priority=90,
)
)
if game_state.player.hp < max(1, game_state.player.max_hp // 2):
for potion_name in ("小型治疗药水", "治疗药水"):
if potion_name in inventory:
actions.append(
_make_action(
action_type="USE_ITEM",
target=potion_name,
text=f"使用{potion_name}",
priority=85,
)
)
break
if game_state.player.hunger < 50:
for food_name in ("面包", "烤肉", "麦酒", "草药包"):
if food_name in inventory:
actions.append(
_make_action(
action_type="USE_ITEM",
target=food_name,
text=f"食用{food_name}",
priority=80,
)
)
break
if (
game_state.player.location in OVERNIGHT_REST_LOCATIONS
and hasattr(game_state, "can_overnight_rest")
and game_state.can_overnight_rest()
):
actions.append(
_make_action(
action_type="OVERNIGHT_REST",
text="在此处过夜",
priority=88,
)
)
return _dedupe_actions(actions)