Add tools/check-no-gdscript-sim-logic.py and wire it as verify step 18 (TOTAL 20→21). Fails if presentation GDScript (src/game/engine/src/**/*.gd) re-introduces catalog yield aggregation (`yield_production += …`) or hand-built spec dicts (`"yield_production": …`) — the exact drift class just moved to Rust. Verified to flag the pre-7e2baa25d aggregation and pass clean on the current tree. Logic belongs in the mc-* crates, reached via the GDExtension bridge (Rail 1). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
81 lines
3.3 KiB
Python
81 lines
3.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Rail-1 guard: no game-data transform/aggregation logic in GDScript.
|
|
|
|
GDScript is presentation only. Simulation logic — stat aggregation, catalog
|
|
construction, balance math — lives in Rust (the `mc-*` crates) and is reached
|
|
through the GDExtension bridge. This gate fails if presentation GDScript
|
|
re-introduces the kind of game-data transform that was deliberately moved to
|
|
Rust, so the single-source-of-truth (Rail 1 + Rail 2) cannot silently regress
|
|
the way the unit/building catalogs did.
|
|
|
|
Flagged in `src/game/engine/src/**/*.gd` (presentation; tests are excluded):
|
|
* yield aggregation — `yield_production += ...` (summing sim yields)
|
|
* spec construction — `"yield_production": ...` (hand-building a catalog spec)
|
|
|
|
Reach for the canonical Rust transform instead:
|
|
`mc_ai::tactical::parse_unit_catalog` / `parse_building_catalog`, exposed via
|
|
`GdItemSystem.{parse_unit_catalog_json, aggregate_building_catalog_json}`, and
|
|
delegate from GDScript.
|
|
|
|
Usage:
|
|
tools/check-no-gdscript-sim-logic.py # report; exit 1 if violations
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
SRC = REPO_ROOT / "src" / "game" / "engine" / "src"
|
|
|
|
# The scalar yield channels the tactical catalogs aggregate. Kept in sync with
|
|
# `TacticalBuildingSpec` (mc-core/tactical_types.rs).
|
|
_YIELD = r"(food|production|gold|science|culture|defense|happiness|gpp|great_work_slots)"
|
|
|
|
_PATTERNS: list[tuple[re.Pattern[str], str]] = [
|
|
# `yield_production += ...` or `item["yield_production"] += ...`
|
|
(re.compile(rf"yield_{_YIELD}\b[\"\]\s]*\+="),
|
|
"yield aggregation — sum sim yields in Rust, not GDScript"),
|
|
# `"yield_production": ...` — a catalog spec dict built by hand.
|
|
(re.compile(rf'"yield_{_YIELD}"\s*:'),
|
|
"catalog spec dict constructed in GDScript — use the Rust transform"),
|
|
]
|
|
|
|
|
|
def main() -> int:
|
|
if not SRC.is_dir():
|
|
print(f"error: GDScript source dir not found: {SRC}", file=sys.stderr)
|
|
return 2
|
|
|
|
violations: list[tuple[str, int, str, str]] = []
|
|
for path in sorted(SRC.rglob("*.gd")):
|
|
text = path.read_text(encoding="utf-8", errors="replace")
|
|
for lineno, line in enumerate(text.splitlines(), start=1):
|
|
stripped = line.strip()
|
|
# Skip comment lines (doc comments legitimately name these fields).
|
|
if stripped.startswith("#"):
|
|
continue
|
|
for rx, why in _PATTERNS:
|
|
if rx.search(line):
|
|
rel = path.relative_to(REPO_ROOT)
|
|
violations.append((str(rel), lineno, why, stripped))
|
|
|
|
if violations:
|
|
print(f"Rail-1 violation: {len(violations)} game-data transform site(s) in presentation GDScript:")
|
|
for rel, lineno, why, stripped in violations:
|
|
print(f" {rel}:{lineno} [{why}]")
|
|
print(f" {stripped}")
|
|
print(
|
|
"\nGDScript is presentation only (Rail 1). Move the logic into a Rust\n"
|
|
"transform (mc_ai::tactical::parse_unit_catalog / parse_building_catalog)\n"
|
|
"and delegate from GDScript via GdItemSystem."
|
|
)
|
|
return 1
|
|
|
|
print("OK: no game-data transform logic in presentation GDScript (Rail 1)")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|