src/commands/round_helpers.py
Rama main · réplica de lectura del repositorio original.
"""回合处理中的纯辅助判断。"""
from __future__ import annotations
from src.engine.modules import game_config
from src.engine import module_hooks
import re
from src.engine.constants import COMBAT_INTENT_KEYWORDS
from src.engine.game_instance import GameInstance
_DECISION_KEYWORDS = ("是否", "选择", "赌上", "决定", "要么")
def should_multi_step(instance: GameInstance, actions_text: str) -> bool:
"""判断是否需要多步推理(仅 WebUI 模式)。
多步分析只允许由“当前”确实存在的复杂条件触发;累计的历史 NPC 数
(instance.npcs 包含全部历史 NPC)不代表当前局势复杂,不能据此增加模型调用。
"""
if game_config.entry_point(instance) != "web":
return False
if instance.puzzle_manager and instance.puzzle_manager.get_active_puzzles():
return True
if any(kw in actions_text for kw in COMBAT_INTENT_KEYWORDS):
return True
if any(kw in actions_text for kw in _DECISION_KEYWORDS):
return True
return _current_scene_has_multiple_npcs(instance)
def _current_scene_has_multiple_npcs(instance: GameInstance) -> bool:
"""当前场景描述中出现 ≥2 名已注册 NPC 的名字时,视为当前场景多名活跃 NPC。
用当前场景文本而非累计 NPC 总数,避免历史 NPC 堆积触发多余的局势分析。
名字长度不足 2 的单字名称容易与场景文本误匹配,不予计数。
"""
scene_text = str(module_hooks.narrative_view(instance)["scene"] or "")
if not scene_text:
return False
present = 0
for key, npc in instance.npcs.items():
name = str(npc.get("character_name") or npc.get("name") or key or "").strip()
if len(name) < 2:
continue
if name in scene_text:
present += 1
if present >= 2:
return True
return False
def validate_dice_constraint(dice_block: str, narration: str) -> bool:
"""校验 LLM 叙事是否与骰子结果矛盾。返回 True=合规, False=矛盾。
只在骰子为大成功/大失败时校验,且仅检测以玩家为主语的描述,
避免 NPC 的失败/成功被误判为矛盾。
"""
if not dice_block or not narration:
return True
is_critical = "大成功" in dice_block
is_fumble = "大失败" in dice_block
if not is_critical and not is_fumble:
return True
player_prefixes = ("你", "你们")
failure_words = ("失败", "没能", "无法", "落空", "失误", "错过", "打偏")
success_words = ("成功", "做到", "完成", "命中", "击倒", "漂亮")
sentences = re.split(r'[。!?\n]', narration)
if is_critical:
for sentence in sentences:
sentence = sentence.strip()
if not sentence:
continue
is_player_subject = any(sentence.startswith(p) for p in player_prefixes) or "你" in sentence[:8]
if not is_player_subject:
continue
if any(word in sentence for word in failure_words):
return False
if is_fumble:
for sentence in sentences:
sentence = sentence.strip()
if not sentence:
continue
is_player_subject = any(sentence.startswith(p) for p in player_prefixes) or "你" in sentence[:8]
if not is_player_subject:
continue
if any(word in sentence for word in success_words):
return False
return True
def default_quick_actions_by_class(class_name: str) -> list[str]:
class_text = (class_name or "").lower()
if any(k in class_text for k in ("战", "war", "fighter", "paladin", "蛮")):
return ["攻击", "防御", "冲锋", "威吓"]
if any(k in class_text for k in ("法", "mage", "wizard", "sorcerer", "术")):
return ["施法", "吟唱", "元素掌控", "法术反制"]
if any(k in class_text for k in ("盗", "rogue", "thief", "贼", "刺客")):
return ["潜行", "侦查", "偷袭", "开锁"]
if any(k in class_text for k in ("牧", "cleric", "priest", "医", "德鲁伊")):
return ["治疗", "祝福", "驱散", "祈祷"]
if any(k in class_text for k in ("游", "ranger", "猎人", "弓")):
return ["射击", "追踪", "设陷阱", "侦查"]
return ["观察", "交谈", "探索", "戒备"]