Skip to content

Commit d6ea798

Browse files
committed
feat(tests): the kitchen sink -- every 6E rule no corpus character takes, oracle-verified
PeterB: "the objects are derived from the rules -- not the corpus. so regardless if a char exercises it, it needs to be future-proofed. there are some obscure powers no one ever takes. but someone will some day." Measured against the full registry and all 655 oracle fixtures, 24 registered 6E rules appeared in no character at all: seven powers (ADJACENT, ADJACENTFIXED, DIFFERINGMODIFIER, DIMENSIONALALL, DIMENSIONALGROUP, ENDURANCERESERVEREC, RAPID), thirteen modifiers (CANBEMISSILEDEFLECTED, CUMULATIVE, DELAYEDEFFECT, EXPLOSION, HALFRANGEMODIFIER, NND, NOTTHROUGHMINDLINK, ONLYONAPPROPRIATETERRAIN, ONLYTOSTARTING, SUBJECTTORANGEMODIFIER, TIMELIMIT, TURNMODE, VISIBLE), CUSTOMTALENT, FAVOR, RESOURCE_POOL and RANGEDDC. tests/kitchen_sink.py generates one character carrying all of them. Deterministic (fixed ids, no timestamps), written to a temp dir at collection, so it is never redistributed and never skips -- it is nobody's build. tests/fixtures/authored/KitchenSink.json is the Java oracle's verdict on it, compared per object by the same harness as Ravel, Bokor and PowerLad: 41 objects, 0 mismatches on cost or display, 468 = 468 total points. It found two defects on its first run, neither reachable from the corpus: * DIFFERINGMODIFIER charged 5 real where HD charges 0. The port never implemented getRealCostPreList, whose last line is `ret -= getLevels()`: the levels are points already paid for, only the increment is charged. Its display was never ported either. * RAPID printed "Rapid ( x10)" without " with Sight Group". Its display override read _selected_option, always None for a sense adder -- the base class's own docstring says so and uses option_alias. Also restored PowerLad.hdc to the authored directory (it was on the Desktop), so the no-skips guard's four skips are gone. 1615 passed, 0 skipped.
1 parent 3b4cacf commit d6ea798

5 files changed

Lines changed: 1394 additions & 3 deletions

File tree

kirby_cost/objects/powers/differing_modifier.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
Differing modifier power.
77
"""
88

9+
from kirby_cost.objects.base import GenericObject
910
from kirby_cost.objects.powers.power import Power
1011
from kirby_cost.util.rounder import round_half_down
1112

@@ -50,3 +51,78 @@ def active_cost(self) -> float:
5051

5152
return active_cost
5253

54+
@property
55+
def real_cost_pre_list(self) -> float:
56+
"""Ported from DifferingModifier.getRealCostPreList.
57+
58+
The shape is the ordinary one -- limitations divide the active
59+
cost, then the multiplier, then the quantity doublings -- with one
60+
line at the end that makes this power what it is: ``ret -=
61+
getLevels()``. A Differing Modifier's levels are the points of the
62+
underlying power it re-modifies, and the character has already
63+
paid for those; what he pays here is only the INCREMENT the new
64+
modifiers add. Five levels with no modifiers therefore cost 5
65+
active and 0 real.
66+
67+
The base Power's real cost had no such subtraction, so this read 5
68+
where HD reads 0. Found by the kitchen-sink fixture, 2026-08-29: no
69+
corpus character had ever bought one.
70+
"""
71+
ret = self.active_cost
72+
limitation_total = 0.0
73+
for mod in self.assigned_modifiers:
74+
if mod.total_value <= 0.0:
75+
limitation_total += mod.total_value
76+
77+
parent = self._parent
78+
if parent is not None and hasattr(parent, "assigned_modifiers"):
79+
from kirby_cost.objects.frameworks.multipower import Multipower
80+
for mod in parent.assigned_modifiers:
81+
if mod.types and "VPP" in mod.types:
82+
continue
83+
if mod.xmlid == "CHARGES" and isinstance(parent, Multipower):
84+
continue
85+
if (GenericObject.find_object_by_id(self.assigned_modifiers, mod.xmlid) is None
86+
or mod.xmlid in ("GENERIC_OBJECT", "CUSTOM_MODIFIER")):
87+
if mod.total_value < 0.0:
88+
limitation_total += mod.total_value
89+
90+
if limitation_total != 0.0:
91+
ret = round_half_down(ret / (1.0 + abs(limitation_total)))
92+
93+
if self.multiplier != 1.0:
94+
ret = round_half_down(ret * self.multiplier)
95+
elif parent is not None and getattr(parent, "multiplier", 1.0) != 1.0:
96+
ret = round_half_down(ret * parent.multiplier)
97+
98+
if self._quantity > 1:
99+
q = float(self._quantity)
100+
doublings = 0
101+
while q > 1.0:
102+
doublings += 1
103+
q /= 2.0
104+
ret += doublings * 5
105+
106+
ret -= self._levels
107+
return ret
108+
109+
@property
110+
def column2_output(self) -> str:
111+
"""``(5 Active Points) for up to 5 Points of Blast`` --
112+
DifferingModifier.getColumn2Output. The line leads with the modifier
113+
string rather than the alias, names the levels as points of the
114+
power given in INPUT, and falls back to the alias plus
115+
``[unknown]`` when no input was recorded.
116+
"""
117+
ret = self.modifier_string or ""
118+
if self.input and self.input.strip():
119+
ret += f" for up to {self._levels} Points of {self.input}"
120+
else:
121+
ret = f"{self.alias}: {ret} for up to {self._levels} Points of [unknown]"
122+
if self._name and self._name.strip():
123+
ret = f"<i>{self._name}:</i> {ret}"
124+
ret = ret.strip()
125+
adders = self.adder_string or ""
126+
if adders.strip():
127+
ret += f" ({adders})"
128+
return ret + self._end_reserve_note()

kirby_cost/objects/powers/rapid.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,19 @@ def column2_output(self) -> str:
3636
if self._name and self._name.strip():
3737
output = f"<i>{self._name}:</i> {output}"
3838

39-
# Build "with" string
39+
# Build "with" string. The sense this applies to is the document's
40+
# OPTION ("Sight Group"), and a sense adder's template lists no
41+
# options, so `_selected_option` is None here -- exactly the bug
42+
# SenseAdder.column2_output documents and fixes with `option_alias`.
43+
# This override still read `_selected_option`, so "Rapid ( x10)"
44+
# printed without its " with Sight Group" while every sibling kept
45+
# it. Found by the kitchen-sink fixture, 2026-08-29: no corpus
46+
# character had ever bought Rapid.
47+
from kirby_cost.objects.base import option_alias
4048
with_str = " with "
41-
if self._selected_option:
42-
with_str += self._selected_option.alias
49+
option = (option_alias(self) or "").strip()
50+
if option:
51+
with_str += option
4352
adder_str = self.adder_string
4453
if adder_str and adder_str.strip():
4554
with_str += ", " + adder_str

0 commit comments

Comments
 (0)