135 lines
5.1 KiB
Python
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()
|