128 lines
4.6 KiB
Python
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()
|