Commit Graph

  • f61fc5c61f docs: reorganize documentation structure main ctan 2026-09-26 20:12:05 +02:00
  • b0bd0b95f4 docs(phase2): record the P5.3 report writer ctan 2026-09-26 10:43:14 +02:00
  • 98985ee6fc feat(phase2): llm report writer (P5.3) ctan 2026-09-26 10:40:58 +02:00
  • c454a67b84 docs(phase2): record the P5.2 few-shot prompts and repair loop ctan 2026-09-25 23:24:29 +02:00
  • dc890fc8fd feat(phase2): few-shot prompts + bounded repair loop (P5.2) ctan 2026-09-25 23:24:27 +02:00
  • d4db4f6d33 docs(phase2): record the P5.1 provider client and plan status ctan 2026-09-25 22:22:30 +02:00
  • 42802f7bf5 feat(phase2): OpenAI-compatible provider client (P5.1) ctan 2026-09-25 22:22:03 +02:00
  • 790df17623 docs(phase2): record the P4 robustness measurements and plan status ctan 2026-09-25 21:34:13 +02:00
  • cd69312ff4 feat(phase2): adversary and robustness cartography (P4) ctan 2026-09-25 21:34:11 +02:00
  • 016d6e1459 docs(phase2): record the P3 optimizer playbook and plan status ctan 2026-09-22 15:44:06 +02:00
  • dd64c2ebd6 feat(phase2): optimizer and scenario compiler (P3) ctan 2026-09-22 15:23:05 +02:00
  • 115f54a7e5 docs(phase2): record the P2 sweep measurements and plan status ctan 2026-09-22 13:48:01 +02:00
  • dbd1a5822e feat(phase2): strategy compiler and sweep runner (P2) ctan 2026-09-22 13:46:05 +02:00
  • 15d24be638 feat(phase2): P1c ctan 2026-09-22 12:21:23 +02:00
  • de4645838b feat(phase2): provider-agnostic LLM client + composite cache (P1b) ctan 2026-09-22 09:45:18 +02:00
  • f0617a0de6 feat(phase2): external config + versioned prompt pack (P1a) ctan 2026-09-22 09:21:28 +02:00
  • 26c5b51b98 docs(phase2): record the runner entry points (P0.4) and replay (P0.5) ctan 2026-09-22 08:45:52 +02:00
  • 6c6de16e60 feat(phase2): replay log and replay policy (P0.5) ctan 2026-09-21 15:08:34 +02:00
  • 334f8f9d05 feat(phase2): runner llm_policy and policy_factory entry points ctan 2026-09-21 14:46:25 +02:00
  • ac7fe20054 docs(phase2): record the compact view measurements (P0.2) and candidates (P0.3) ctan 2026-09-21 14:36:24 +02:00
  • 8b5043072a feat(phase2): candidate actions ctan 2026-09-21 13:46:05 +02:00
  • 9da9537995 feat(phase2): compact observation view for LLM prompts ctan 2026-09-21 13:21:33 +02:00
  • 25418f164f docs(phase2): record the executed spike (measurements + first danger map) ctan 2026-09-21 12:15:16 +02:00
  • de88543c5c feat(phase2): danger-cartography spike - per-run outcomes, objectives, focus-fire adversary ctan 2026-09-21 12:07:23 +02:00
  • e5c4892a57 test(phase2): measurement spike - observation size and policy calls per battle ctan 2026-09-21 11:26:24 +02:00
  • 3c3e30634e feat(phase2): observation-only LLMPolicy entry (D1) + pinned schema_version (P0.1) ctan 2026-09-21 11:10:28 +02:00
  • 3fd9860b5c docs(phase2): record the arbitrated decisions (external prompts, cartography-first) ctan 2026-09-21 10:49:00 +02:00
  • d794828c7e docs(phase2): add the optimizer and adversarial-cartography layers ctan 2026-09-21 10:27:11 +02:00
  • 630f5321e0 docs(phase2): revise the LLM architecture after critique ctan 2026-09-21 09:55:53 +02:00
  • be64451ba4 docs(phase2): architecture and implementation plan for the LLM decision layer ctan 2026-09-21 09:51:05 +02:00
  • 7e4a9f5edf docs(rules): conformance journal round 15 - nonlethal, splash, skills and flight (out of scope -> modeled) ctan 2026-09-20 22:35:55 +02:00
  • 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 ctan 2026-09-20 22:29:05 +02:00
  • 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 ctan 2026-09-20 22:21:40 +02:00
  • 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) ctan 2026-09-20 21:48:02 +02:00
  • 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 ctan 2026-09-20 21:39:54 +02:00
  • 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 ctan 2026-09-20 21:06:12 +02:00
  • 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 ctan 2026-09-20 20:48:26 +02:00
  • 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 ctan 2026-09-20 19:33:12 +02:00
  • a1b1c29ca3 feat(rules): expose splash in observation attack reference + AttackResult.splash, document splash weapon rule and JSON schema in README (SRD Throw Splash Weapon) ctan 2026-09-20 19:23:15 +02:00
  • 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) ctan 2026-09-20 19:18:46 +02:00
  • 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) ctan 2026-09-20 19:11:39 +02:00
  • 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 ctan 2026-09-20 18:54:22 +02:00
  • cba7bbf3c9 feat(rules): nonlethal damage track - separate total, derived staggered/unconscious, max-hp overflow, undead/construct immunity, healing removal (SRD Nonlethal Damage) ctan 2026-09-20 18:41:53 +02:00
  • 3d0fd0c101 docs(rules): conformance journal round 14 - transcript/format audit (14 bugs fixed) ctan 2026-09-20 18:08:29 +02:00
  • dd0b42b73b docs(map): make omitted move_cost explicitly mean impassable ctan 2026-09-20 18:03:46 +02:00
  • ee22f7548a fix(map): reject duplicate YAML keys via a SafeLoader duplicate-check ctan 2026-09-20 18:00:27 +02:00
  • 53c6dc0b43 fix(models): reject combatant ids that misparse the transcript format ctan 2026-09-20 17:51:40 +02:00
  • bce0a59e60 fix(map): fail on blank rows inside a layer, keep trailing-space cells verbatim ctan 2026-09-20 17:48:47 +02:00
  • 205ddece0b fix(map): require single-character legend keys ctan 2026-09-20 17:43:50 +02:00
  • c46e636d12 fix(cli): require --round-cap >= 1 ctan 2026-09-20 17:40:29 +02:00
  • 10f811e80e fix(models): require a positive range_increment_ft (reject 0 and negatives) ctan 2026-09-20 17:36:45 +02:00
  • e9c2a63122 fix(runner): disambiguation suffix skips ids already taken by explicit combatants ctan 2026-09-20 17:33:35 +02:00
  • de9309ca43 fix(cli): reject duplicate --side names with a clear error ctan 2026-09-20 17:29:10 +02:00
  • 130087a556 fix(map): reject a zones layer without a deployment section ctan 2026-09-20 17:15:29 +02:00
  • a8c7e37f8e fix(loaders): resource pools validated consistently before grant merge ctan 2026-09-20 17:13:09 +02:00
  • 9adc87c9e1 fix(loaders): coerce combatant level with Pydantic-consistent rules ctan 2026-09-20 17:05:36 +02:00
  • b8e96294a6 fix(loaders): non-list features and blank/non-string entries fail cleanly ctan 2026-09-20 17:02:39 +02:00
  • 9656ae820a fix(loaders): malformed spell_slots key raises CombatantLoadError, not bare ValueError ctan 2026-09-20 16:58:57 +02:00
  • 5911602736 docs(rules): conformance journal round 13 - objects/inventory (out of scope -> modeled) ctan 2026-09-20 16:10:00 +02:00
  • d0f7f3a107 feat(rules): carried items + real steal transfers, unarmed-disarm auto-pickup ctan 2026-09-20 16:05:07 +02:00
  • 74f5d64c43 feat(rules): sunder deals real weapon damage vs object hardness/hp; broken weapons usable at -2 with restricted crits ctan 2026-09-20 15:43:45 +02:00
  • 544d870688 docs(rules): conformance journal round 12 - spell components and arcane spell failure ctan 2026-09-20 14:38:42 +02:00
  • 9f72948d23 docs(rules): README - component/ASF data now encoded, dedup snapshot bullet ctan 2026-09-20 14:34:33 +02:00
  • f6292f8bdc feat(rules): encode arcane_caster and armor_spell_failure from the sheets ctan 2026-09-20 14:30:57 +02:00
  • 91666d84b9 feat(rules): encode SRD spell components for all 108 registry spells ctan 2026-09-20 14:21:15 +02:00
  • 1f8f068dd1 feat(rules): spell components + deafened verbal failure and arcane spell failure rolls ctan 2026-09-20 14:02:10 +02:00
  • a483070e13 docs(rules): conformance journal round 11 - alignment subsystem (out of scope -> modeled) ctan 2026-09-20 13:02:15 +02:00
  • 3be22b9c55 feat(rules): alignment-gated protection from evil/law wards via attacker context ctan 2026-09-20 12:57:29 +02:00
  • 6a18d456f6 feat(rules): alignment-gated spell damage for Shard of Chaos and Spear of Purity ctan 2026-09-20 12:32:14 +02:00
  • f9e51e377a feat(rules): add alignment field to Combatant with axis gating flags ctan 2026-09-20 11:49:04 +02:00
  • 40088e1869 docs(rules): conformance journal round 10 - utility spells (guidance, resistance, virtue, stabilize) ctan 2026-09-20 10:52:22 +02:00
  • f7886135bd fix(rules): stabilize is a StabilizeEffect that auto-stabilizes a dying creature ctan 2026-09-20 10:48:08 +02:00
  • a8a65b18cd fix(rules): virtue is 1 temporary hit point for 1 minute ctan 2026-09-20 10:32:05 +02:00
  • 1238c40e43 fix(rules): resistance is a +1 resistance bonus on fort/ref/will saves for 1 minute - SRD spells/resistance.md grants the subject a +1 resistance bonus on saving throws for 1 minute (Will negates, harmless); the resistance bonus type already exists in the stacking model (effects.py BonusType), so the buff is expressible as a BuffEffect and was only deferred because _find_buff_spell returns the FIRST BuffEffect in known-spell order and cantrips cost no slots - encoding it would have made default_policy cast Resistance every round (for Tammara it would preempt Divine Favor and never kill anything); resistance.json now carries a BuffEffect (+1 resistance on fort/ref/will, 10 rounds = 1 minute) and _find_buff_spell skips level-0 spells (cantrips have no spell slot in the engine's budget - spell_slots_left has no level-0 key - and the policy deliberately never auto-casts them, a cantrip buff would be cast every round degrading the policy), so Resistance cannot preempt a leveled buff (Divine Favor) in the known-spell order; leveled buffs keep the historical first-match behavior (if the first one's slots are exhausted the buff branch gives up - the golden pins that); the resistance bonus type does not stack with itself (resolve_modifiers keeps the higher value); +9 tests (test_resistance.py: buff shape, cast on a willing ally consumes no d20 and applies without a save roll, the bonus rides save rolls, no self-stacking, expiry at 10 rounds, _find_buff_spell skips the slot-less cantrip / returns None without a leveled buff, _choose_spell_action proposes Divine Favor and never Resistance); golden untouched; 1714 tests green ctan 2026-09-20 10:10:32 +02:00
  • 08f9af9e3e fix(rules): guidance is a one-shot +1 competence bonus on a single attack roll - SRD spells/guidance.md grants '+1 competence bonus on a SINGLE attack roll, saving throw, or skill check' (duration 1 minute or until discharged); the engine had no spell effect that could write the one-shot attack track (aid_attack_bonus is hardwired to aid-another +2 and expires at the start of the beneficiary's next turn), and a persistent BuffEffect would misrepresent the discharge rule (it would boost every roll for a minute) and would become the first buff spell in the casters' known-spell order, degrading default_policy; a new OneShotEffect type is added (track/value/bonus_type/duration_rounds, parser 'one_shot', SpellEffect union, summary 'one-shot +X type on track'), stored as ActiveOneShot on CombatantState (one_shot_bonuses, decremented with the other duration-tracked effects, exposed in the snapshot), discharged by the FIRST roll of the encoded track via _discharge_one_shot (best value wins, each casting is its own charge) and folded into the resolve_modifiers pool so bonus-type stacking applies; the attack track rides the attack roll only, NOT the crit-confirm roll (the SRD grants it to a SINGLE attack roll, so unlike aid another the confirmation is untouched - _attack_roll_modifiers returns (pool_mod, attack_roll_mod) and resolve_attack uses the latter for the total while the confirm keeps the former); the save and skill tracks are wired in the mechanism (resolve_save folds a save-track shot into save_mods, resolve_skill discharges a skill-track shot into _skill_static_bonus via extra_mods) but guidance.json encodes the attack track only - the SRD 'attack OR save OR skill' choice has no declaration channel in the engine, so the attack-roll variant is encoded (documented); _apply_targeted_spell_effect dispatches OneShotEffect to _apply_one_shot_effect (a successful save negates it, 'one-shot negated'); guidance.json now carries the effect (1 minute = 10 rounds) and default_policy never casts it (cantrips have no spell slot and the one-shot type is not a buff/heal/offensive pick); _spell_effect_summary split into _damage_effect_summary/_non_damage_effect_summary to stay under the PLR0911 return cap; test_spell_registry_batch2._EFFECT_TYPES gains OneShotEffect; +14 tests (test_one_shot_guidance.py: cast-and-discharge on the next attack, not on the crit-confirm roll, save/skill tracks, stacking by bonus type, expiry at 10 rounds, save negation, snapshot exposure, no auto-cast by default_policy); golden untouched; 1705 tests green ctan 2026-09-20 09:53:53 +02:00
  • 154c3f69a0 docs(rules): conformance journal round 9 - assumed residuals closed (10 fixes, 1 deferred) ctan 2026-09-20 00:46:08 +02:00
  • d5ad3c3aac fix(rules): a grazed caster cell no longer shortens a line spell - SRD Aiming a Spell: a line 'starts from any corner of your square and extends to the limit of its range' (Lightning Bolt: 'The bolt begins at your fingertips'), so the caster's own squares are origin space the line starts PAST and must not consume one of the length_cells squares; line_cells took a fixed ray[1:length_cells+1] window and dropped caster cells inside it, so a 2x2 caster's line came out one square short per grazed footprint cell (the old code documented this as a simplification); the ray is now extended (reach = length_cells + 1 + len(caster_cells)) and caster cells are skipped WITHOUT spending a length slot, so a multi-cell line runs its full length starting just past the footprint; 1x1 casters are bit-identical (the only skipped cell is the origin at ray[0], already outside the counted window); the barrier/LoE stop and out-of-bounds semantics are unchanged (a straight ray never re-enters a convex map); golden untouched (no line spell in it); +3 tests (grazed-cell full-length template, 1x1 bit-identical regression, engine-level reach of a foe the short line missed) ctan 2026-09-20 00:41:02 +02:00
  • d347dde3fc fix(rules): a multi-cell creature moving under mixed terrain pays the difficult rate for its whole footprint - SRD Movement, Position and Distance: 'If you occupy squares with different kinds of terrain, you can move only as fast as the most difficult terrain you occupy will allow'; the charge/run/5-foot-step already gated on the whole footprint (_footprint_touches_difficult) but the move/withdraw DESCENT priced every step by the anchor cell alone (grid.step_cost), so a 2x2 creature straddling rubble crossed at full speed; the descent now prices a step by the most difficult square under the footprint via _footprint_step_cost -> a new grid.step_cost_with_terrain (the 5-10-5 + elevation formula stays centralized in grid.py, step_cost delegates to it); 1x1 creatures are bit-identical (footprint == anchor cell); the direction Dijkstra field remains anchor-priced (making it creature-aware rewrites grid.reachable wholesale - the documented deferral); golden untouched (open-floor arena, no difficult terrain); +7 tests (trailing-square orthogonal/diagonal cost, 1x1 bit-identical regression, full-lane move budget 3-vs-6 squares, withdraw hampering) ctan 2026-09-20 00:29:36 +02:00
  • b775d46850 fix(rules): document the burst center-to-center convention as a deferred SRD approximation - the SRD (aiming-a-spell.md) measures a burst from a grid INTERSECTION with the far-edge rule ('count from intersection to intersection'; a square is inside only 'if the far edge of a square is within the spell's area'), but the engine's burst origin is a CELL (target_pos, centered_on_caster, resolved target) counted center-to-center 5-10-5 in _creatures_in_range; matching RAW exactly needs an intersection coordinate added to AreaEffect/Action.target_pos/the observation area snapshot contract plus a per-caster corner choice (the far-edge boundary result depends entirely on which of the cell's four corners is the origin), so it is DEFERRED with a precise explanation in areas.py and a pointer from _creatures_in_range; no behavior change, golden untouched ctan 2026-09-20 00:16:18 +02:00
  • e6021a6dda fix(rules): expose healing_done in per-combatant stats and the CLI report - the channel/spell healing deliberately removed from damage_dealt (2e2873f) was invisible; CombatantStats/_LiveStats gain a healing_done counter booked with the hp actually restored (capped at the target's hp_max, overheal uncounted) by _channel_heal and by spell HealEffect on living targets, while cure-as-damage on undead stays in damage_dealt and the damage counters keep their exact semantics; the runner aggregates and averages the new field and the French report prints it as 'soins prodigués'; +4 tests (channel heal, cure spell, undead cure guard, run_encounter aggregate) +1 CLI assertion ctan 2026-09-19 23:23:00 +02:00
  • 5e7436b210 fix(rules): duplicate summons get disambiguated ids - the engine now counts id occurrences (seeded from the deployed roster) and gives each new summon of the same creature a build_states-style suffix (celestial_wolf, celestial_wolf-2, ...), so two summons no longer share one _LiveStats slot (the second summon used to reset the first one's counters and their attrition merged); run_encounter aggregation verified to report each slot separately; runner 'known limitation' comment updated; +4 tests (id split, distinct slots, per-slot attrition, run_encounter aggregate) ctan 2026-09-19 23:13:06 +02:00
  • 4f8a131ba0 fix(rules): lich stat block aligned to the KB entry - bestiary/lich.md gives hp 111 (11d6+55 plus 15 false life), Str 10, Int 22, Init +2 and 'Aura fear (60-ft. radius, DC 18)'; the JSON carried scenario-tuned 100 hp / Str 12 / Int 20 / Init +6 and the engine's feature-keyed aura constants carried 30 ft / DC 22 (BAB/touch/AC/saves/CON already conformed); the sim's own spell list is kept (the KB's prepared list includes spells outside data/spells/) and the choice is documented in the JSON notes and the README; golden tammara-vs-lich re-pinned for the two SRD-correct line changes (initiative d20=10+2=12 from the KB +2, Magic Missile DC17 = 10+1+6 from Int 22; damage unchanged - the 75 ft deployment distance stays beyond even the widened 60 ft aura, so no save die is consumed); +2 aura range tests (75 ft outside, 60 ft inside) ctan 2026-09-19 23:05:25 +02:00
  • 5fe2b72339 fix(rules): selective channeling modeled as a policy-chosen exclusion - Action(kind=channel_energy) gains channel_exclude, honored only with the Selective Channeling feat and capped at the Charisma modifier (SRD feats/selective-channeling.md: 'choose a number of targets in the area up to your Charisma modifier... not affected by your channeled energy'); without the feat the list is ignored and logged (SRD Normal: all targets affected); observable in current data because heal mode also heals living ENEMIES in the burst (deny them the heal) and harm mode can spare an undead ally; default_policy never excludes (opt-in, like weapon_id/destination/metamagic); golden transcripts unchanged; +5 tests ctan 2026-09-19 22:49:40 +02:00
  • 9f0d86273a fix(rules): self-centered bursts for bless and bane - AreaEffect gains a data-driven 'centered_on_caster' flag (bless.json/bane.json set it per SRD bless.md '50-ft. burst, centered on the caster' and bane.md 'centered on you'); the burst origin is the caster's square regardless of the resolved target or a U7b target_pos, and with no chosen origin there is no primary save - every creature in the area rolls its own (mirror of the point-cast path); snapshot area key exposes centered_on_caster; golden transcript unchanged; +6 tests ctan 2026-09-19 22:38:39 +02:00
  • 4665a1c245 fix(rules): harmless spells on willing targets forgo the save roll - SpellSpec gains a data-driven 'harmless' flag set on the 29 SRD '(harmless)' spells (verified one by one against the KB); a same-side/self target consumes NO save die (SRD saving-throw.md: '(harmless) ... can attempt a saving throw if it desires' + 'Voluntarily Giving up a Saving Throw'), while hostile targets and undead receiving a cure (harmful to them, Will half for undead damage) keep the roll; snapshot spell summaries expose 'harmless'; golden transcript unchanged (no harmless-save cast in it); +8 tests, 3 fixtures re-pinned for the removed die ctan 2026-09-19 22:27:46 +02:00
  • ea835293a0 fix(rules): run is bounded by Constitution endurance and the Run feat - after a number of consecutive run rounds equal to the CON score each further round requires a Constitution check (DC 10 +1 per prior check, no auto nat-1/20, undead/constructs exempt with no CON score); failure stops the run and forces the SRD 1-minute (10-round) rest; the Run feat grants +4 on the check and keeps the Dex bonus to AC while running (SRD Full-Round Actions - Run); heavy-armor x3 speed clause documented out-of-scope (no armor field); +5 tests ctan 2026-09-19 22:02:36 +02:00
  • 28bc624d1e docs(rules): conformance journal round 8 - adversarial verification and 8 new fixes ctan 2026-09-19 19:15:27 +02:00
  • 1b43f4eef1 fix(docs): README states data/pcs/*.json are the canonical PC sheets and foundry.py is a one-shot incomplete migration tool - re-running it and regenerating the JSON would regress the manual/SRD conformance fixes (natural/secondary/firearm flags, Jeanne hooves reach 10, Ierlieth fly 40, Misty bite 1d6, feats beyond its 8-entry map, spell_slots/resources/casting_ability); refresh dev test count 1634 -> 1647 ctan 2026-09-19 19:10:38 +02:00
  • 238e5f201e fix(combat): witch hex trigger uses the SRD close range scaled by caster level (25 ft + 5 ft per 2 levels, extracted as spells.close_range_ft and shared with spell_range_ft) instead of the fixed _HEX_RANGE_FT 25 - a level-5 witch reaches 35 ft; + scaling regression test ctan 2026-09-19 19:07:41 +02:00
  • 6f01a8ebdd fix(combat): _find_ally_needing_heal counts only living allies (SRD Channel Energy: heal mode heals living creatures, positive energy never heals undead) - an undead ally below half triggered heal mode, spending the channel to heal nobody; + trigger regression test ctan 2026-09-19 19:01:03 +02:00
  • c23fc366e4 fix(combat): _find_ally_needing_heal uses hp*2 < hp_max (strictly below half) - the floored hp < hp_max//2 was off by one for odd hp_max (5 -> threshold 2, so 2/5 hp counted healthy); + threshold regression test ctan 2026-09-19 18:58:03 +02:00
  • f93c55b3fb fix(combat): _swift_bonus_actions emits up_close_and_deadly only when the turn will actually fire a firearm attack within 30 ft - the deed burns 1 grit at execution and its +1d6 rides only a firearm attack, so emitting it alongside a melee swing or a spell cast wasted grit for a lost bonus; + 2 policy tests, deed test now uses a firearm-only tammara ctan 2026-09-19 18:55:17 +02:00
  • e8b7a73d1f fix(dice): parse_dice validates sides >= _MIN_SIDES so d1/1d1 raise DiceParseError instead of leaking the model validator's pydantic ValidationError; drop the dead not-sides_str guard (regex \\d+ already guarantees non-empty) + 3 rejected-notation cases (d1, 1d1, 2D6) ctan 2026-09-19 18:51:38 +02:00
  • bdf15aa8fe fix(combat): _choose_spell_action heal branch gates on spell range + line of effect - a touch-range Cure on a distant ally was rejected by _cast_spell (OUT OF RANGE / NO LINE OF EFFECT) and wasted the caster's whole turn; extracted the offensive branch's reach check as _spell_reaches and reused it (the duplicated below-half check in the branch was a no-op copy of _find_ally_needing_heal, dropped) + 3 policy tests ctan 2026-09-19 18:48:58 +02:00
  • 31d47aee79 fix(combat): _spell_combat_actions gates on _has_slot - with exhausted slots the cast is rejected (NO SPELL SLOT) and spell_combat resolves as a bare full attack at -2, strictly worse than the plain full attack; the policy now falls through (mirrors _choose_spell_action's slot gate) + 2 regression tests ctan 2026-09-19 18:41:46 +02:00
  • 657f48474c docs(rules): conformance journal round 7 - spell registry closed (108/108) and subsystem fixes ctan 2026-09-19 17:21:54 +02:00
  • 9a087a444b fix(docs): refresh stale README example numbers against the live engine - Quick start (3 goblins vs 2 orcs, seed 1): 96.2%/962 band [94.4%,98.0%], monsters 3.6%/36, 2 draws, 6.6 rounds; Exemple verifie (1 goblin vs 2 orcs): 33.0% band [28.5%,37.5%], 6 draws, 7.1 rounds, prose adjusted to the new distribution; dev test count 1596 -> 1634 ctan 2026-09-19 17:18:31 +02:00
  • ba5eb4c95f fix(map): load_map rejects two deployment sides bound to the same zone letter (build_states resets its placed set per side, so shared letters deployed camps on top of each other) - fail-fast MapValidationError + README rule, reject/accept tests ctan 2026-09-19 17:12:42 +02:00
  • 2ee19d34e4 fix(docs): correct the Observation 'frozen' claim - the dataclass is frozen at the top level but the inner per-combatant/pair dicts are mutable copies; wording softened in the docstring and README (deep-freeze rejected: it would change the LLM contract and require touching pinned observation tests; copies are never read back by the engine, so policy mutation is already inert) ctan 2026-09-19 17:09:08 +02:00
  • 9459adee02 fix(actions): add downgraded:immediate_swift to ACTION_RESULT_REASONS (emitted since conformance L10 but absent from the published vocabulary); grep showed no other emitted-but-missing ActionResult reason (no_charge_path is a ChargeResult field) - drift guard test pins every _REASON_* constant against the frozenset ctan 2026-09-19 17:06:30 +02:00