Commit Graph

415 Commits

Author SHA1 Message Date
ctan dc890fc8fd feat(phase2): few-shot prompts + bounded repair loop (P5.2) 2026-09-25 23:24:27 +02:00
ctan d4db4f6d33 docs(phase2): record the P5.1 provider client and plan status 2026-09-25 22:22:30 +02:00
ctan 42802f7bf5 feat(phase2): OpenAI-compatible provider client (P5.1) 2026-09-25 22:22:03 +02:00
ctan 790df17623 docs(phase2): record the P4 robustness measurements and plan status 2026-09-25 21:34:13 +02:00
ctan cd69312ff4 feat(phase2): adversary and robustness cartography (P4) 2026-09-25 21:34:11 +02:00
ctan 016d6e1459 docs(phase2): record the P3 optimizer playbook and plan status 2026-09-22 15:44:06 +02:00
ctan dd64c2ebd6 feat(phase2): optimizer and scenario compiler (P3) 2026-09-22 15:23:05 +02:00
ctan 115f54a7e5 docs(phase2): record the P2 sweep measurements and plan status 2026-09-22 13:48:01 +02:00
ctan dbd1a5822e feat(phase2): strategy compiler and sweep runner (P2) 2026-09-22 13:46:05 +02:00
ctan 15d24be638 feat(phase2): P1c 2026-09-22 12:21:23 +02:00
ctan de4645838b feat(phase2): provider-agnostic LLM client + composite cache (P1b) 2026-09-22 09:45:18 +02:00
ctan f0617a0de6 feat(phase2): external config + versioned prompt pack (P1a)
config.py centralizes phase-2 path/env reads (no hard-coded data paths,
section 15). prompt.py loads the EXTERNAL, versioned prompt pack (section
14): pack version + per-prompt content hash feed prompt_version for the
response-cache composite key (D6); missing/invalid packs raise a clear
PromptPackError, never a silent fallback. Minimal fixture pack under
tests/fixtures/prompt_pack/ pins CI to a local, versioned pack.
2026-09-22 09:21:28 +02:00
ctan 26c5b51b98 docs(phase2): record the runner entry points (P0.4) and replay (P0.5)
run_encounter gains llm_policy and per-run policy_factory (mutually
exclusive with policy/llm_policy); replay.py logs every (observation,
action) keyed by call_seq to JSONL and replay() reproduces the recorded
battle bit-identically (wins/draws/rounds/transcript) with mismatch,
exhausted, duplicate and out-of-order guards. P0 frontier complete: the
recorded run replays identically.
2026-09-22 08:45:52 +02:00
ctan 6c6de16e60 feat(phase2): replay log and replay policy (P0.5)
Add replay.py: a ReplayLog records every (observation, action) pair a
policy produced, keyed by monotonic call_seq, plus the EncounterSpec,
seed and round_cap, and serializes the whole battle to JSONL. replay()
turns a log back into an observation-only LLMPolicy that returns each
recorded action, raising ReplayMismatchError on a differing observation
and ReplayExhaustedError past the last call.

Observations are matched by their canonical JSON (observation_key), not
raw dataclass equality, because JSON collapses tuples and lists and the
snapshot's inner dict[str, object] holds both. Action and the spec are
dumped through Pydantic TypeAdapters (mode=json) so tuple fields survive
as arrays and are coerced back on load.

Move TrickCondition/ReadyTrigger out of the TYPE_CHECKING block in
combat.py: TypeAdapter(Action) must resolve them at runtime to serialize
the dataclass, so the Literal aliases now live at module level.
2026-09-21 15:08:34 +02:00
ctan 334f8f9d05 feat(phase2): runner llm_policy and policy_factory entry points 2026-09-21 14:46:25 +02:00
ctan ac7fe20054 docs(phase2): record the compact view measurements (P0.2) and candidates (P0.3)
Compact render is 15,691 bytes (~3.9k tokens) = 24.4% of the raw 64,204-byte
to_dict() snapshot on the representative 7-combatant encounter, hitting the
~2-4k token target; grids are sparse and determinism is pinned in tests.
candidates() derives plausible actions from the pairs with no second legality
engine (swift/free/full_round excluded).
2026-09-21 14:36:24 +02:00
ctan 8b5043072a feat(phase2): candidate actions 2026-09-21 13:46:05 +02:00
ctan 9da9537995 feat(phase2): compact observation view for LLM prompts
Build a decision-sufficient ObservationView from the raw snapshot and
render it as a deterministic line-based prompt string. The raw to_dict()
is ~64 KB (~16k tokens) for a 7-combatant encounter; the compact view
renders at ~15.7 KB (~3.9k tokens, 24% of raw), under the 4k-token
ceiling of the phase-2 architecture doc.

Compaction: sparse terrain/elevation/threatened grids, per-combatant
key selection (occupied_cells, abilities, skills, initiative_mod/total,
temp_hp_rounds_left, features, carried_items, weapon_damage, aware,
alignment, armor_spell_failure, arcane_caster, run_rounds dropped),
attacks compacted to formula+type damage strings, spells to 7
decision-relevant fields, pair target_index replaced by the stable
combatant id, condition_definitions dropped. Attacks and spells render
as positional tuples declared by attack_fields/spell_fields legend
lines, with '-' for missing values.

Tests pin the size bounds (compact < 0.25 x raw and < 16000 bytes), the
sparse-grid contract, the dropped-key contract and the deterministic
id/render contract on a caster-heavy 3v4 representative battle.
2026-09-21 13:21:33 +02:00
ctan 25418f164f docs(phase2): record the executed spike (measurements + first danger map)
Measures: observation 64 KB (~16k tokens) for 7 combatants / 22 KB for a 2v2;
124 policy calls per battle, i.e. 124k calls per 1000-run sweep cell -> a
per-turn LLM in a sweep is infeasible (D9 validated) and a compact view is
mandatory. Danger map (default vs focus-fire monsters, 100 runs): the default
policy understates danger (PC deaths 13%->24% and 0%->19%, TPK 0%->2%) and the
win rate alone misleads (rises to 98% while deaths rise) -> D12/D13 validated.
Notes the spike limits and the remaining P0.2-P0.5 items.
2026-09-21 12:15:16 +02:00
ctan de88543c5c feat(phase2): danger-cartography spike - per-run outcomes, objectives, focus-fire adversary
run_encounter gains an opt-in collect_runs flag returning one RunOutcome
per battle (winner, rounds, per-combatant id/side/hp/hp_max/dead) next to
the EncounterReport; default behavior is unchanged. objectives.py provides
the pure optimizer primitives: P(survives all), P(TPK), P(>=1 death),
expected hp lost, expected rounds and a worst-decile (CVaR-style) tail
metric. phase2_spike.py holds the spike-level strengthened monster policy
(focus fire on the lowest-AC enemy, ties to the wounded; party side stays
on default_policy). tests/test_phase2_danger_map.py prints the danger-map
sketch: warband and lich scenarios measured default vs focus-fire at
100 runs / seed 11, pinning the delta deterministically.
2026-09-21 12:07:23 +02:00
ctan e5c4892a57 test(phase2): measurement spike - observation size and policy calls per battle
Representative 7-combatant caster-heavy encounter on sample_arena,
measured through the real engine path (engine._build_observation at each
policy call, delegating to default_policy):
- observation: 64,204 bytes at first call, 67,712 max mid-battle (~16.9k tokens)
- policy calls/battle: 124 (7 combatants, round_cap=20), 12 for a 2v2 duel
- projected calls per sweep cell: 124 / 12,400 / 124,000 at runs 1/100/1000
Loose bounds (obs < 200 KB, calls/battle < 200) + seed-determinism pinned.
2026-09-21 11:26:24 +02:00
ctan 3c3e30634e feat(phase2): observation-only LLMPolicy entry (D1) + pinned schema_version (P0.1)
- LLMPolicy is now Callable[[Observation], tuple[Action, ...]]: the snapshot
  is the policy's sole information source; the live CombatantState is no
  longer passed to new-style policies.
- CombatEngine._llm_adapter keeps a compatibility path for pre-D1
  two-argument callables: the signature is inspected ONCE at wrap time
  (_accepts_state) and legacy policies still receive (observation, state).
- Observation.to_dict() carries schema_version (OBSERVATION_SCHEMA_VERSION=1)
  as its first pinned key; bump only on removal/meaning change.
- Pinned keyset tests updated (test_observation, test_observation_enriched);
  new tests: observation-only e2e, legacy 2-arg e2e through the adapter,
  schema_version pin + JSON-serializability.
- README: documents the new entry signature and the schema_version key.
2026-09-21 11:10:28 +02:00
ctan 3fd9860b5c docs(phase2): record the arbitrated decisions (external prompts, cartography-first)
All recommendations accepted by the user except prompt location: prompts live
outside this repo (separate pack, configurable path, versioned; prompt_version
feeds the cache key and the replay record; a pinned pack for integration, a
minimal local fixture for unit tests, no silent fallback). Also records the
repo-scope decision (code only: calculation + LLM + report) with the final
detachment of data/sheets/prompts as a backlog item and the design constraint
to avoid hardcoded data paths in the new code. Priority: cartography first.
2026-09-21 10:49:00 +02:00
ctan d794828c7e docs(phase2): add the optimizer and adversarial-cartography layers
The execution tiers answer 'how to run a tactic', not 'which tactic is best'
nor 'where is the danger'. Adds two decision layers: an optimizer over
StrategySpec (objectives incl. tail risk, coarse-to-fine search, LLM as
candidate seeder) and an adversarial cartography (AdversarySpec + alternating
min-max + scenario sweeps + danger frontier + sensitivity), giving the danger
view for BOTH sides. Reframes the LLM as hypothesis generator / red team /
explicator / reporter, never the optimizer or per-turn executor in sweeps.
Adds D12-D14 and phases P3 (optimizer) / P4 (robustness).
2026-09-21 10:27:11 +02:00
ctan 630f5321e0 docs(phase2): revise the LLM architecture after critique
Key correction: the LLM must not sit on the per-turn hot path of a Monte
Carlo sweep (runs x rounds x combatants calls, no cross-run cache reuse).
Adopts a two-tier design - a compiled, parameterised deterministic Policy for
quantitative sweeps (tier A) and a per-turn NLPolicy for a handful of showcase
battles (tier B). Also fixes: 'engine unchanged' -> rules unchanged/additive
boundary; replay keyed by a monotonic call_seq (delay re-queues, summons);
composite response-cache key incl. model id; ObservationView as a structured
object; candidates derived from pairs instead of a second legality engine;
per-run PolicyFactory; cost guardrails in P1; concurrency; D1 to settle in P0.
2026-09-21 09:55:53 +02:00
ctan be64451ba4 docs(phase2): architecture and implementation plan for the LLM decision layer
Design-only document (no code): target architecture (compact versioned
observation view, legal-actions API, LLM client + cache, NL policy with
parse/validate/repair/fallback, strategy sweep, replay/eval harness), the
concrete gaps in the current runner/CLI wiring, the decisions to settle and a
phased implementation plan (P0 foundations, P1 client+policy, P2 strategy
sweep, P3 provider+report) with exit criteria and risks.
2026-09-21 09:51:05 +02:00
ctan 7e4a9f5edf docs(rules): conformance journal round 15 - nonlethal, splash, skills and flight (out of scope -> modeled)
Brings in the last four out-of-scope mechanics: the nonlethal damage track
(unarmed strikes, minimum damage), splash weapons (direct + scatter),
combat skills (skill checks, Acrobatics Tumbling, Stealth/opposed Perception)
and 3D flight over ground terrain (vertical line of effect deferred). Updates
the out-of-scope and residual tables to drop the now-modeled items and marks
the round-2 disposition tables as historical. Gate 1981.
2026-09-20 22:35:55 +02:00
ctan 9210a54589 docs(rules): document 3D flight over ground terrain (airborne view: difficult terrain cost 1, cross non-LoS impassable cells, blocks_los still stops flight; elevation climb still applies; LoE/cover/high-ground unchanged) and the deferred 3D line-of-effect/maneuverability with SRD citations (Movement, Position and Distance; Aiming a Spell); map legend + grid.py notes, test count 1981 2026-09-20 22:29:05 +02:00
ctan 2ae86ab29f feat(rules): 3D flight over ground terrain - airborne creatures (fly >= land, V4 budget from fly speed) move on Grid.airborne_view: ground difficult terrain costs 1 and cells impassable to walkers (pits, water, rubble, low obstacles) are crossable at cost 1, EXCEPT blocks_los cells (solid barriers, assumed full height) which still stop flight; run/charge/5-foot-step gates and diagonal corner rule follow the same view (flying past a pit is legal, past a wall corner is not); elevation climb cost still applies (step heights, not wall heights - documented); line of effect, cover and high ground use the unchanged real grid so combat geometry is unaffected by flight (SRD Movement, Position and Distance: flying creatures not hampered by difficult terrain, able to avoid most obstacles), 11 tests 2026-09-20 22:21:40 +02:00
ctan 1257bb3849 docs(rules): document skill checks, Acrobatics Tumbling and Stealth in README (rule list, observation schema hidden_from, out-of-scope updates, test count 1970) 2026-09-20 21:48:02 +02:00
ctan 2c40994610 feat(rules): Stealth + opposed Perception - opt-in hide move action (Stealth d20 + trained class + ACP + size mod vs each enemy Perception with -1/10ft distance; observed creatures cannot be hidden from), per-observer hidden_from state treated as total concealment + unseen-attacker +2/deny-Dex for failed observers, attacking or running/charging breaks stealth, move/5-foot-step/withdraw re-check the opposed roll, observation exposes hidden_from (SRD Stealth/Perception/Skill Checks), 13 tests 2026-09-20 21:39:54 +02:00
ctan 61686b5f4e feat(rules): Acrobatics TUMBLING opt-in on move - half speed budget, one Acrobatics check per crossed opponent (DC = CMD, +5 fresh entry into its space trained-only, +2 per additional opponent beyond the first), success spends the opportunity without a strike, failure provokes the AoO and ends the movement (SRD Acrobatics Tumble / Moving Through a Square), transcript logs each check, 9 tests 2026-09-20 21:06:12 +02:00
ctan e0fb5beae2 feat(rules): skill-check subsystem foundation - public skill_check helper (d20 + ranks + ability + trained-class-skill +3 + armor check penalty + typed bonuses, one-shot skill track discharged, no natural 1/20 auto-success/fail per SRD Skill Checks), resolve_skill delegates to it, class-skill +3 now requires invested ranks (Table 4-2 Trained Class Skill row), 8 tests 2026-09-20 20:48:26 +02:00
ctan 678a4d8b31 feat(rules): splash weapon test suite - direct hit full damage + 1 splash to adjacent only, miss scatters d8 clockwise one range increment dealing splash alone (thrower can be caught), energy resistance/immunity apply, DR does not, splash never crit-multiplied, no precision damage, non-splash RNG stream bit-identical, schema rejects splash on melee, observation exposes splash; transcript logs splash after the damage segment 2026-09-20 19:33:12 +02:00
ctan a1b1c29ca3 feat(rules): expose splash in observation attack reference + AttackResult.splash, document splash weapon rule and JSON schema in README (SRD Throw Splash Weapon) 2026-09-20 19:23:15 +02:00
ctan 2e60bbb419 feat(rules): splash weapon resolution - direct hit splashes 1 energy to all within 5 ft (target excluded), miss scatters d8 (1=short toward thrower, 2-8 clockwise) one range increment and splashes the landing + adjacent squares; energy resistance applies, DR and crit never; no precision damage (SRD Throw Splash Weapon) 2026-09-20 19:18:46 +02:00
ctan 9cfcd67f70 feat(rules): SplashSpec data model - data-driven splash marker on thrown weapons, Alchemist's Fire splash 1 fire on oni/tammara (SRD Throw Splash Weapon, Alchemist's fire) 2026-09-20 19:11:39 +02:00
ctan 83c6e541ac feat(rules): nonlethal sources - unarmed strikes deal nonlethal by default (SRD Unarmed Attacks), Minimum Damage floor lands 1 nonlethal (CRB Damage), transcript + AttackResult expose the track 2026-09-20 18:54:22 +02:00
ctan cba7bbf3c9 feat(rules): nonlethal damage track - separate total, derived staggered/unconscious, max-hp overflow, undead/construct immunity, healing removal (SRD Nonlethal Damage) 2026-09-20 18:41:53 +02:00
ctan 3d0fd0c101 docs(rules): conformance journal round 14 - transcript/format audit (14 bugs fixed)
A read-only audit of the transcript rendering, map and loader formats found
14 confirmed bugs (4 Med, 10 Low: duplicate --side, id-disambiguation
collision, range_increment_ft division by zero, silent spell_slots/features
drops, negative round-cap, multi-char legend keys, blank rows, id parsing,
duplicate YAML keys, move_cost default) - all fixed. The transcript/format
area is audited; only the structural out-of-scope items remain. Gate 1900.
2026-09-20 18:08:29 +02:00
ctan dd0b42b73b docs(map): make omitted move_cost explicitly mean impassable 2026-09-20 18:03:46 +02:00
ctan ee22f7548a fix(map): reject duplicate YAML keys via a SafeLoader duplicate-check 2026-09-20 18:00:27 +02:00
ctan 53c6dc0b43 fix(models): reject combatant ids that misparse the transcript format 2026-09-20 17:51:40 +02:00
ctan bce0a59e60 fix(map): fail on blank rows inside a layer, keep trailing-space cells verbatim 2026-09-20 17:48:47 +02:00
ctan 205ddece0b fix(map): require single-character legend keys 2026-09-20 17:43:50 +02:00
ctan c46e636d12 fix(cli): require --round-cap >= 1 2026-09-20 17:40:29 +02:00
ctan 10f811e80e fix(models): require a positive range_increment_ft (reject 0 and negatives) 2026-09-20 17:36:45 +02:00
ctan e9c2a63122 fix(runner): disambiguation suffix skips ids already taken by explicit combatants 2026-09-20 17:33:35 +02:00
ctan de9309ca43 fix(cli): reject duplicate --side names with a clear error 2026-09-20 17:29:10 +02:00
ctan 130087a556 fix(map): reject a zones layer without a deployment section
The README documents deployment as required when zones exist, but load_map
only enforced the reverse direction (deployment without zones). A map with
a zones layer and no deployment now fails with MapValidationError instead
of loading with unreachable deployment zones.
2026-09-20 17:15:29 +02:00