Files
aetherbound-guild/tools/validate_design_contracts.py
T

135 lines
5.1 KiB
Python

#!/usr/bin/env python3
"""Validate arithmetic and single-authority boundaries in product contracts."""
from __future__ import annotations
import math
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
PRODUCT = ROOT / "docs"
def cache_loss(lines: list[tuple[int, bool]], fraction: float) -> tuple[int, list[int]]:
total = sum(value for value, _ in lines)
target = math.ceil(total * fraction)
removed = 0
selected: list[int] = []
remaining: list[tuple[int, int]] = []
for index, (value, splittable) in enumerate(lines):
if splittable:
take = min(value, max(0, target - removed))
removed += take
if take:
selected.append(index)
else:
remaining.append((index, value))
for index, value in remaining:
current_error = abs(target - removed)
next_error = abs(target - (removed + value))
if next_error <= current_error:
removed += value
selected.append(index)
if target > 0 and removed == 0 and remaining:
index, value = min(remaining, key=lambda pair: (abs(pair[1] - target), pair[0]))
removed = value
selected = [index]
return removed, selected
def maximum_gap(duration: int, timestamps: list[int]) -> int:
points = [0, *timestamps, duration]
return max(right - left for left, right in zip(points, points[1:]))
def main() -> None:
# The six-authority contract is intentionally consolidated in the current
# review set; keep this validator pointed at the public authority files
# instead of the retired docs/product split.
economy = (PRODUCT / "02_GAMEPLAY_AND_BALANCE.md").read_text()
battle = economy
product = (PRODUCT / "01_GAME_DESIGN.md").read_text()
save = economy
region_totals = [
int(match.group(1))
for match in re.finditer(
r"^\| [1-8] \| [0-9]+ min \| [0-9]+ min \| ([0-9]+) min \| .*? \|$",
economy,
re.MULTILINE,
)
]
assert len(region_totals) == 8, region_totals
assert sum(region_totals) == 972
traces = {
"normal": (1200, [25, 90, 175, 280, 325, 390, 475, 580, 625, 690, 775, 880, 925, 990, 1075, 1180], 105),
"active": (1200, [20, 75, 135, 205, 310, 350, 410, 475, 535, 640, 680, 740, 805, 865, 970, 1015, 1070, 1135], 105),
"efficient": (1200, [35, 125, 240, 295, 385, 500, 555, 645, 760, 815, 905, 1020, 1075, 1165], 115),
"adversarial": (1200, [30, 80, 145, 270, 320, 375, 435, 590, 640, 700, 755, 920, 970, 1030, 1095], 165),
"returning": (1200, [25, 85, 135, 250, 300, 355, 415, 560, 620, 680, 755, 900, 950, 1015, 1085, 1190], 145),
}
for name, (duration, timestamps, expected_gap) in traces.items():
timestamp_literal = "[" + ",".join(str(value) for value in timestamps) + "]"
assert f"| {name.title()} |" in economy
assert f"`{timestamp_literal}` | {expected_gap} s |" in economy
observed_gaps = {
name: maximum_gap(duration, timestamps)
for name, (duration, timestamps, _) in traces.items()
}
assert observed_gaps == {name: expected for name, (*_, expected) in traces.items()}
recruit_cost = min(250, 90 + 12 * 8 + 5 * max(0, 29 - 6))
counter_cost = min(180, 55 + 12 * 8 + 8 * 7 + 10 * 2)
first_clear_xp = math.floor((50 + 25 * 2 + 10 * 3 + 35 + 20) * 1.15)
assert (recruit_cost, counter_cost, first_clear_xp) == (250, 180, 212)
assert math.floor(14 / 2) - 4 == 3
assert 4 * (14 - 1) == 52
cache_vectors = [
([(30, False), (30, False), (40, False)], 0.25, 30),
([(100, True)], 0.25, 25),
([(40, False)], 0.25, 40),
([(18, False), (18, False)], 0.50, 18),
([(10, True), (30, False), (40, False)], 0.50, 40),
]
for lines, fraction, expected in cache_vectors:
actual, _ = cache_loss(lines, fraction)
assert actual == expected, (lines, fraction, actual, expected)
raw = 125
mitigated = raw * (100 / (100 + 500)) * 0.10
damage = max(1, math.ceil(0.04 * raw), math.floor(mitigated))
assert math.isclose(mitigated, 2.0833333333333335)
assert damage == 5
failure_rows = (
"| Wayfinder | 0 Coin | `min(2,current Coin)` | `min(4,current Coin)` |",
"| Standard | `min(2,current Coin)` | `min(4,current Coin)` | `min(6,current Coin)` |",
"| Oathbound | `min(4,current Coin)` | `min(6,current Coin)` | `min(8,current Coin)` |",
)
assert all(value in save for value in failure_rows)
assert "status: pending | applied | rejected" in save
assert "immediate child" not in save
assert "crit = can_crit and critical_roll_bp < CritChanceBp" in battle
assert "DamageFloor = ceil(0.04 * RawDamage)" in battle
assert "Every 20-minute profile trace stays within its stated decision-gap bound." in product
print(
"ABG_DESIGN_CONTRACTS_OK "
f"contract_minutes={sum(region_totals)} "
f"gaps={observed_gaps} "
f"recruit_cap={recruit_cost} counter_cap={counter_cost} "
f"cache_vectors={len(cache_vectors)} damage_floor={damage}"
)
if __name__ == "__main__":
main()