Files
aetherbound-guild/tools/validate_design_contracts.py

128 lines
4.6 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" / "product"
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:
economy = (PRODUCT / "ECONOMY_AND_BALANCE.md").read_text()
battle = (PRODUCT / "SYSTEMS_AND_BATTLE.md").read_text()
product = (PRODUCT / "GAME_PRODUCT_CONTRACT.md").read_text()
save = (PRODUCT / "SAVE_AND_FAILURE_CONTRACT.md").read_text()
chapter_totals = [
int(match.group(1))
for match in re.finditer(
r"^\| [1-8] \| .*? \| ([0-9]+) \| [0-9]+ \| [0-7](?:-[0-7])? \|$",
economy,
re.MULTILINE,
)
]
assert len(chapter_totals) == 8, chapter_totals
assert sum(chapter_totals) == 1700
traces = {
"normal": (1200, [90, 240, 390, 540, 690, 840, 990, 1140], 150),
"active": (1200, [60, 170, 280, 390, 500, 610, 720, 830, 940, 1050, 1160], 110),
"efficient": (1200, [120, 280, 440, 600, 760, 920, 1080], 160),
"adversarial": (1200, [90, 220, 350, 480, 610, 740, 870, 1000, 1130], 130),
"returning": (720, [60, 200, 340, 480, 620], 140),
}
for duration, timestamps, expected_gap in traces.values():
timestamp_literal = "[" + ",".join(str(value) for value in timestamps) + "]"
assert f"| {duration:,} | `{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 = 120
mitigated = raw * (100 / (100 + 500)) * 0.10
damage = max(1, math.ceil(0.05 * raw), math.floor(mitigated))
assert math.isclose(mitigated, 2.0)
assert damage == 6
failure_values = ("Lose 15%", "Lose 25%", "Lose 30%", "Lose 50%", "Lose 60%", "Lose 100%")
assert all(value in save for value in failure_values)
assert all(value not in economy for value in failure_values)
assert "status: pending | applied | rejected" not in save
assert "immediate child" not in save
assert "ResolvedCritMultiplier" in battle
assert "DamageFloor = ceil(0.05 * Raw)" in battle
assert "active profile's\nmaximum gap" in product
print(
"ABG_DESIGN_CONTRACTS_OK "
f"chapter_minutes={sum(chapter_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()