feat(phase2): P1c
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
"""Offline battles driven by FakeLLMClient through the NL policy (P1.3/P1.4)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from pf1e_simulator.client import FakeLLMClient
|
||||
from pf1e_simulator.loaders import load_combatant
|
||||
from pf1e_simulator.map import load_map
|
||||
from pf1e_simulator.nl_policy import NLPolicy
|
||||
from pf1e_simulator.prompt import PromptPack
|
||||
from pf1e_simulator.prompt_builder import PromptBuilder
|
||||
from pf1e_simulator.runner import EncounterSpec, Side, run_encounter
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
MAP = ROOT / "data" / "maps" / "sample_arena.yaml"
|
||||
|
||||
|
||||
def _c(path: str):
|
||||
return load_combatant(ROOT / "data" / path)
|
||||
|
||||
|
||||
def _duel_spec() -> EncounterSpec:
|
||||
"""A fast 1v1 (goblin archer vs orc) on the sample arena."""
|
||||
return EncounterSpec(
|
||||
map=load_map(MAP),
|
||||
round_cap=20,
|
||||
sides=(
|
||||
Side(name="players", combatants=(_c("monsters/goblin.json"),), zone="A"),
|
||||
Side(name="monsters", combatants=(_c("monsters/orc.json"),), zone="B"),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _pack() -> PromptPack:
|
||||
return PromptPack.load(ROOT / "tests" / "fixtures" / "prompt_pack")
|
||||
|
||||
|
||||
def _builder() -> PromptBuilder:
|
||||
return PromptBuilder(pack=_pack())
|
||||
|
||||
|
||||
def _make_client(script: list[str], *, builder: PromptBuilder) -> FakeLLMClient:
|
||||
return FakeLLMClient(
|
||||
script=script,
|
||||
prompt_version=builder.prompt_version("nl_default"),
|
||||
)
|
||||
|
||||
|
||||
def test_offline_battle_total_defense_draw() -> None:
|
||||
"""Both sides play total_defense → draw at round_cap (20 rounds, 40 calls)."""
|
||||
spec = _duel_spec()
|
||||
builder = _builder()
|
||||
# 60 responses is plenty for 40 turns (20 rounds x 2 combatants)
|
||||
script = ['[{"kind": "total_defense"}]'] * 60
|
||||
client = _make_client(script, builder=builder)
|
||||
policy = NLPolicy(client=client, builder=builder)
|
||||
|
||||
report, _outcomes = run_encounter(spec, runs=1, seed=1, llm_policy=policy, collect_runs=True)
|
||||
|
||||
# Draw at round_cap
|
||||
assert report.draws == 1
|
||||
assert report.wins.get("players", 0) == 0
|
||||
assert report.wins.get("monsters", 0) == 0
|
||||
assert report.avg_rounds == 20
|
||||
# 20 rounds x 2 combatants = 40 policy invocations
|
||||
assert client.stats.calls == 40
|
||||
|
||||
|
||||
def test_offline_battle_fallback() -> None:
|
||||
"""Fallback-driven battle: script of invalid JSON forces fallback on every turn."""
|
||||
spec = _duel_spec()
|
||||
builder = _builder()
|
||||
# Each turn: 2 completes (first parse fail, retry parse fail → fallback).
|
||||
# Max 20 rounds x 2 combatants x 2 = 80 completes. Use 100 for margin.
|
||||
script = ["not json"] * 100
|
||||
client = _make_client(script, builder=builder)
|
||||
policy = NLPolicy(client=client, builder=builder)
|
||||
|
||||
report, _outcomes = run_encounter(spec, runs=1, seed=1, llm_policy=policy, collect_runs=True)
|
||||
|
||||
# The battle should complete (not exhaust script)
|
||||
assert report.runs == 1
|
||||
assert report.draws + sum(report.wins.values()) == 1
|
||||
# At least some calls happened; script not exhausted
|
||||
assert client.stats.calls > 0
|
||||
assert client.stats.calls <= 100
|
||||
Reference in New Issue
Block a user