Skip to content

Commit 15f2f69

Browse files
pdbethkeclaude
andcommitted
feat: the offers say what they buy — the two that were silent
Six fights each, doctrine and a model reached the SAME three kinds of sixty-two, spending nine actions in ten on plain `attack`. That result rules out the chooser: swapping a catalogue for a frontier model moved the count by one. What is left is the page. Reading it, the diagnosis is narrower than "offers don't say what they buy". Most of them do, and state their COSTS well --- "+3 DCV until your next phase (full DCV bonus, but no attack)", "Cost: -2 OCV / -2 DCV, 1/3 self-damage". Two were silent, and they are the two that matter. THE BASELINE SAID THE LEAST OF ANY OF THEM. `attack` is taken nine times in ten and read "Attack Billy Clanton with Coach gun (3d6K, OCV 7)". No target number --- while every alternative quotes a CV modifier. A -2 OCV is uncomparable without the number it modifies: -2 off a comfortable 14- is a different decision from -2 off a marginal 9-. It now reads "OCV 7 vs DCV 4 — needs 14- on 3d6", which is 6E2's own formula and the number the resolver will actually use. Situational modifiers are deliberately NOT folded in. They belong to the offer that causes them and are quoted there; this is the number they modify. HIDING NEVER MENTIONED WHAT HIDING IS FOR. It read "enemies who can't perceive you can't target you" --- the small half. The large half is 6E2 p.52: an attacker they cannot perceive catches them SURPRISED, at half DCV, and `hide` is the only manoeuvre in the game that causes it. Offered 89 times across six fights and taken zero times by either chooser, which is why the whole Surprised path is unreachable in this benchmark while being fully wired underneath. Doctrine is unchanged by this and must be: `TacticChooser` reads its own catalogue, not these summaries. Only a reader can respond to wording, which is exactly what `coverage.py --chooser model` is for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lvepxj9B2mDc4cRN3jaWwn
1 parent b545a3a commit 15f2f69

2 files changed

Lines changed: 112 additions & 3 deletions

File tree

kirby_combat/enumeration.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,26 @@ def _multi_attack_ocv(base_ocv: int, count: int) -> int:
608608
_DISENGAGE_MIN_GAIN_M = 1.0
609609

610610

611+
#: 6E2's attack roll: 3d6 <= OCV + 11 - DCV.
612+
BASE_TO_HIT = 11
613+
614+
615+
def _to_hit_number(ocv: int, enemy) -> int:
616+
"""What this attack must roll under, on 3d6.
617+
618+
THE BASELINE OFFER SAID THE LEAST OF ANY OF THEM. `attack` is taken
619+
nine times in ten by every chooser measured, and it read "OCV 7" with
620+
no target number --- while every alternative quoted a CV modifier.
621+
A -2 OCV is uncomparable without the number it modifies: -2 off a
622+
comfortable 14- is a different decision from -2 off a marginal 9-.
623+
624+
Situational modifiers (cover, range, a maneuver) are NOT folded in.
625+
They belong to the offer that causes them and are quoted there; this
626+
is the number they modify.
627+
"""
628+
return int(ocv) + BASE_TO_HIT - int(enemy.combat_stats().dcv)
629+
630+
611631
def enumerate_actions(
612632
actor: HeroCombatant,
613633
enemies: list[HeroCombatant],
@@ -1054,7 +1074,9 @@ def _limitation_label(actor: HeroCombatant, ap: Any) -> str:
10541074
power_name=ap.name or None,
10551075
summary=(
10561076
f"Attack {_friendly(enemy)} with {label} "
1057-
f"({dmg_summary}, OCV {s.ocv}){limit_tag}"
1077+
f"({dmg_summary}, OCV {s.ocv} vs DCV "
1078+
f"{enemy.combat_stats().dcv} — needs "
1079+
f"{_to_hit_number(s.ocv, enemy)}- on 3d6){limit_tag}"
10581080
),
10591081
_attack_view=ap,
10601082
))
@@ -1706,8 +1728,11 @@ def _limitation_label(actor: HeroCombatant, ap: Any) -> str:
17061728
kind="hide",
17071729
target_id=None, power_xmlid=None, power_name=None,
17081730
summary=(
1709-
"Hide — break line-of-sight behind cover; enemies who "
1710-
"can't perceive you can't target you (breaks if you attack)"
1731+
"Hide — break line-of-sight behind cover. An enemy who "
1732+
"can't perceive you can't target you, and is SURPRISED "
1733+
"when you break cover to shoot: half DCV against that "
1734+
"attack (6E2 p52). Hiding is the only way to cause it. "
1735+
"Ends the moment you attack."
17111736
),
17121737
))
17131738
# PR-70 / PR-71 / PR-72: Adjustment + Healing powers.
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
"""An offer has to say what taking it is worth.
2+
3+
Measured over six fights each, doctrine and a model reach the SAME three
4+
kinds of sixty-two, spending nine actions in ten on plain `attack`. That
5+
result rules out the chooser: swapping a catalogue for a frontier model
6+
moved the count by one. What is left is the page.
7+
8+
Reading it, the diagnosis is narrower than "offers don't say what they
9+
buy". Most of them do, and say their COSTS well:
10+
11+
Dodge — +3 DCV until your next phase (full DCV bonus, but no attack)
12+
Move-By: 2d6N ... Cost: -2 OCV / -2 DCV, 1/3 self-damage
13+
14+
Two things are missing, and they are the two that matter:
15+
16+
**The baseline says the least.** `attack` -- the action taken nine times
17+
in ten -- read "Attack Billy Clanton with Coach gun (3d6K, OCV 7)". No
18+
target number. Every alternative quotes a CV modifier, and a -2 OCV means
19+
nothing without the number it modifies: -2 off a comfortable 14- is a
20+
different decision from -2 off a marginal 9-.
21+
22+
**Hiding never mentions what hiding is FOR.** It read "enemies who can't
23+
perceive you can't target you", which is the small half. The large half
24+
is 6E2 p.52: an attacker they cannot perceive catches them Surprised, at
25+
half DCV -- and `hide` is the only manoeuvre in the game that produces
26+
it. Offered 89 times across six fights and taken zero times by either
27+
chooser, so the whole Surprised path is unreachable in this benchmark
28+
while being fully wired underneath.
29+
30+
Example paraphrased; this project ships no rules text.
31+
"""
32+
from __future__ import annotations
33+
34+
import pytest
35+
36+
37+
def _summaries():
38+
from examples.the_shootout_we_can_publish import the_fight
39+
40+
seen: dict[str, str] = {}
41+
42+
class Peek:
43+
def choose(self, situation):
44+
for action in situation.menu:
45+
seen.setdefault(action.kind, action.summary)
46+
return situation.menu[0].action_id
47+
48+
the_fight(59, chooser=Peek())
49+
return seen
50+
51+
52+
@pytest.fixture(scope="module")
53+
def summaries():
54+
return _summaries()
55+
56+
57+
class TestTheBaselineOffer:
58+
def test_an_attack_says_the_number_it_needs(self, summaries):
59+
"""3d6 <= OCV + 11 - DCV. Without it, every CV modifier on every
60+
other offer is uncomparable."""
61+
assert "needs" in summaries["attack"], summaries["attack"]
62+
63+
def test_and_the_number_is_the_engines_own(self, summaries):
64+
"""OCV 7 against Billy Clanton's DCV 4 is 7 + 11 - 4 = 14-."""
65+
assert "14-" in summaries["attack"], summaries["attack"]
66+
67+
def test_it_still_says_what_it_hits_with(self, summaries):
68+
"""Adding the number must not cost the damage."""
69+
assert "3d6K" in summaries["attack"], summaries["attack"]
70+
71+
72+
class TestHiding:
73+
def test_hiding_says_it_causes_surprise(self, summaries):
74+
"""Case-insensitive: the offer shouts it, and which case it
75+
shouts in is not the thing under test."""
76+
assert "surprised" in summaries["hide"].lower(), summaries["hide"]
77+
78+
def test_and_what_surprise_is_worth(self, summaries):
79+
"""Half DCV is the payoff, and it is the reason to spend a Phase."""
80+
assert "DCV" in summaries["hide"], summaries["hide"]
81+
82+
def test_it_still_says_the_cost(self, summaries):
83+
"""It breaks when you attack --- that has to survive the rewrite."""
84+
assert "attack" in summaries["hide"].lower(), summaries["hide"]

0 commit comments

Comments
 (0)