feat(@projects/@magic-civilization): add objectives dashboard tracking

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-04-17 00:19:20 -07:00
parent aaa359e2c5
commit e6a2a067aa
5 changed files with 366 additions and 15 deletions

View file

@ -0,0 +1,58 @@
# Objectives — Dashboard
> **Generated by `tools/objectives-report.py` — do not hand-edit.** Source of truth is per-file YAML frontmatter in this directory.
## Legend
✅ done · 🟡 partial · 🔴 stub · ❌ missing · ⚫ out-of-scope (Game 2)
## Totals
| Status | Count |
|---|---|
| ✅ done | 0 |
| 🟡 partial | 19 |
| 🔴 stub | 1 |
| ❌ missing | 4 |
| ⚫ oos | 0 |
| **total** | **24** |
## P0 — Blockers for "completely playable"
| ID | Status | Title | Updated |
|---|---|---|---|
| [p0-01](p0-01-mcts-wiring.md) | 🟡 partial | Wire MCTS into gameplay AI | 2026-04-17 |
| [p0-02](p0-02-clan-personalities.md) | 🟡 partial | Five AI clan personalities drive distinct playstyles | 2026-04-17 |
| [p0-03](p0-03-pvp-in-turn.md) | 🟡 partial | PvP combat resolved inside the authoritative turn processor | 2026-04-17 |
| [p0-04](p0-04-wonder-tracking.md) | 🟡 partial | World wonder tracking in PlayerState and score victory | 2026-04-17 |
| [p0-05](p0-05-culture-and-borders.md) | 🔴 stub | Culture generation and border expansion | 2026-04-17 |
| [p0-06](p0-06-economy-integration.md) | 🟡 partial | Fold gold income / upkeep / improvement yields into turn loop | 2026-04-17 |
| [p0-07](p0-07-tech-research-costs.md) | 🟡 partial | Tech research costs and science pool pacing | 2026-04-17 |
| [p0-08](p0-08-domination-victory.md) | 🟡 partial | Domination victory path in mc-turn::victory | 2026-04-17 |
| [p0-09](p0-09-ui-completeness.md) | 🟡 partial | City-screen UI completeness (citizen assign, queue controls, promotion picker) | 2026-04-17 |
| [p0-10](p0-10-completion-stability.md) | 🟡 partial | Game-completion stability — ≥7/10 seeds declare a winner | 2026-04-17 |
| [p0-11](p0-11-mystery-item-authoring.md) | ❌ missing | Author the four T8T10 mystery item drops | 2026-04-17 |
## P1 — Ship-readiness
| ID | Status | Title | Updated |
|---|---|---|---|
| [p1-01](p1-01-diplomacy-lite.md) | 🟡 partial | Diplomacy-lite — peace/war toggle plus one trade action | 2026-04-17 |
| [p1-02](p1-02-strategic-resource-yields.md) | 🟡 partial | Strategic resource yields feed into production bonuses | 2026-04-17 |
| [p1-03](p1-03-tutorial-overlay.md) | ❌ missing | First-run tutorial / onboarding overlay | 2026-04-17 |
| [p1-04](p1-04-sound-and-music.md) | ❌ missing | Sound effects and music | 2026-04-17 |
| [p1-05](p1-05-balance-tuning.md) | 🟡 partial | Balance tuning — pop_peak ≥30 median, worker improvements ≥8 min | 2026-04-17 |
| [p1-06](p1-06-options-polish.md) | 🟡 partial | Options screen polish | 2026-04-17 |
| [p1-07](p1-07-chronicle-coverage.md) | 🟡 partial | Chronicle notifications coverage | 2026-04-17 |
| [p1-08](p1-08-victory-screen-content.md) | 🟡 partial | Victory/defeat screen content — recap, banner, replay seed | 2026-04-17 |
## P2 — Polish
| ID | Status | Title | Updated |
|---|---|---|---|
| [p2-01](p2-01-minimap-improvements.md) | 🟡 partial | Minimap — fog reflection and unit markers | 2026-04-17 |
| [p2-02](p2-02-hud-tooltips.md) | 🟡 partial | Tooltips on all HUD elements | 2026-04-17 |
| [p2-03](p2-03-hotkey-cheat-sheet.md) | ❌ missing | Hotkey cheat sheet (F1 / ?) | 2026-04-17 |
| [p2-04](p2-04-localization-audit.md) | 🟡 partial | Localization audit — no hardcoded strings | 2026-04-17 |
| [p2-05](p2-05-turn-latency.md) | 🟡 partial | Sub-second single-player turn latency | 2026-04-17 |

View file

@ -0,0 +1,20 @@
---
id: p2-05
title: Sub-second single-player turn latency
priority: p2
status: partial
scope: game1
updated_at: 2026-04-17
evidence:
- tools/autoplay-batch.sh
---
## Summary
10-seed parallel batch completes in ~7 minutes wall-clock; single-turn latency on the RUN host is unmeasured. Target: end-of-turn processing ≤1 second on a 512-tile map with 3 AI opponents mid-game.
## Acceptance
- `tools/measure-turn-latency.py` (new) profiles 300 turns and emits `p50 / p90 / p99` latency.
- p99 ≤ 1.0 s for the 3-AI normal difficulty 512-tile scenario.
- Flame graph under `.project/reports/latency/` if regressions appear.

2
run
View file

@ -25,6 +25,7 @@ usage() {
validate Validate game data JSON files against schemas"
echo " format Format all (GDScript + Rust fmt + ESLint fix)"
echo " test Run GUT + Rust + vitest"
echo " test:golden Cross-language golden-vector parity (Rust + WASM + GDExt)"
echo " verify Full pipeline: lint + typecheck + cargo check + tests"
echo " screenshot [name] [scene] [delay] Capture screenshot"
echo " autoplay [seed] Run single seeded auto_play game + report (opt-in)"
@ -72,6 +73,7 @@ case "$COMMAND" in
verify) cmd_verify "$@" ;;
format) cmd_format "$@" ;;
test) cmd_test "$@" ;;
test:golden) cmd_test_golden "$@" ;;
screenshot) cmd_screenshot "$@" ;;
autoplay) cmd_autoplay "$@" ;;
autoplay-batch) cmd_autoplay_batch "$@" ;;

View file

@ -192,19 +192,25 @@ cmd_verify() {
}
# Step 0 — Game data schema validation
_verify_step 0 8 "game data JSON schemas" \
_verify_step 0 9 "game data JSON schemas" \
python3 "$REPO_ROOT/tools/validate-game-data.py"
# Step 1 — Rust build
_verify_step 1 8 "cargo build --workspace" \
# Step 1 — Objectives dashboard freshness
# Fails if .project/objectives/README.md is stale vs the per-objective
# frontmatter. Run `python3 tools/objectives-report.py` to regenerate.
_verify_step 1 9 "objectives dashboard up-to-date" \
python3 "$REPO_ROOT/tools/objectives-report.py" --check
# Step 2 — Rust build
_verify_step 2 9 "cargo build --workspace" \
_verify_run_in_dir "$SIMULATOR_DIR" cargo build --workspace
# Step 2 — Rust tests
_verify_step 2 8 "cargo test --workspace" \
# Step 3 — Rust tests
_verify_step 3 9 "cargo test --workspace" \
_verify_run_in_dir "$SIMULATOR_DIR" cargo test --workspace
# Step 3 — Rust clippy
_verify_step 3 8 "cargo clippy --workspace -D warnings" \
# Step 4 — Rust clippy
_verify_step 4 9 "cargo clippy --workspace -D warnings" \
_verify_run_in_dir "$SIMULATOR_DIR" cargo clippy --workspace -- -D warnings
# Apply project-local gdlint config before linting.
@ -215,20 +221,20 @@ cmd_verify() {
# .project/gdlintrc.local is the source of truth — copy it over before lint.
cp "$REPO_ROOT/.project/gdlintrc.local" "$REPO_ROOT/gdlintrc" 2>/dev/null
# Step 4 — GDScript lint: engine/src/
_verify_step 4 8 "gdlint engine/src/" \
# Step 5 — GDScript lint: engine/src/
_verify_step 5 9 "gdlint engine/src/" \
gdlint "$GAME_DIR/engine/src/"
# Step 5 — GDScript lint: scenes/tests/
_verify_step 5 8 "gdlint engine/scenes/tests/" \
# Step 6 — GDScript lint: scenes/tests/
_verify_step 6 9 "gdlint engine/scenes/tests/" \
gdlint "$GAME_DIR/engine/scenes/tests/"
# Step 6 — GDScript lint: tests/integration/
_verify_step 6 8 "gdlint engine/tests/integration/" \
# Step 7 — GDScript lint: tests/integration/
_verify_step 7 9 "gdlint engine/tests/integration/" \
gdlint "$GAME_DIR/engine/tests/integration/"
# Step 7 — Godot headless boot: GDExtension + script compilation
_verify_step 7 8 "godot headless boot (no script errors)" \
# Step 8 — Godot headless boot: GDExtension + script compilation
_verify_step 8 9 "godot headless boot (no script errors)" \
_godot_headless_boot
_verify_summary
@ -261,6 +267,79 @@ cmd_guide() {
pnpm --prefix "$GUIDE_DIR" dev
}
cmd_test_golden() {
## Cross-language golden-vector parity gate.
##
## Each fixture in src/simulator/tests/golden/vectors/*.json is consumed by
## three runners that MUST produce bitwise-identical output. Divergence =
## release blocker (FFI marshaling / non-determinism / SOT violation).
##
## See src/simulator/tests/golden/README.md for the fixture shape and
## ~/.claude/instructions/rust-code-standards.md §"Testing Strategy" for rationale.
local vectors_dir="$SIMULATOR_DIR/tests/golden/vectors"
local exit_code=0
if [ ! -d "$vectors_dir" ]; then
echo -e "${RED}Golden vectors directory missing: $vectors_dir${NC}"
return 1
fi
local vectors
vectors=$(find "$vectors_dir" -maxdepth 1 -name '*.json' -type f | sort)
if [ -z "$vectors" ]; then
echo -e "${YELLOW}No golden vectors yet — add JSON fixtures to:${NC}"
echo -e " $vectors_dir"
echo -e "${YELLOW}See $SIMULATOR_DIR/tests/golden/README.md for the fixture shape.${NC}"
return 0
fi
local count
count=$(echo "$vectors" | wc -l | tr -d ' ')
echo -e "${BLUE}Found $count golden vector(s) — running 3-consumer parity check${NC}"
echo ""
# Consumer 1: Rust native
echo -e "${BLUE}[1/3] Rust native consumer (cargo test --test golden)${NC}"
if ! (cd "$SIMULATOR_DIR" && cargo test --workspace --test golden 2>&1); then
echo -e "${RED}FAIL: Rust golden tests${NC}"
exit_code=1
fi
echo ""
# Consumer 2: WASM via Vitest (guide simulation worker)
echo -e "${BLUE}[2/3] WASM consumer (pnpm test — golden suite)${NC}"
if ! pnpm --prefix "$GUIDE_DIR" test -- --run golden 2>&1; then
echo -e "${RED}FAIL: WASM golden tests${NC}"
exit_code=1
fi
echo ""
# Consumer 3: GDExtension via headless Godot + GUT
echo -e "${BLUE}[3/3] GDExtension consumer (headless Godot + GUT ffi/)${NC}"
local ffi_dir="$GAME_DIR/engine/tests/ffi"
if [ -d "$ffi_dir" ] && [ -n "$(find "$ffi_dir" -maxdepth 1 -name 'test_golden_*.gd' -print -quit 2>/dev/null)" ]; then
WAYLAND_DISPLAY="${WAYLAND_DISPLAY:-wayland-0}" \
XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" \
$GODOT_BIN --path "$GAME_DIR" --headless \
--script res://addons/gut/gut_cmdln.gd \
-gdir=res://engine/tests/ffi -gprefix=test_golden_ -gexit 2>&1 \
|| exit_code=$?
else
echo -e "${YELLOW}SKIP: No GDExt golden tests yet at $ffi_dir/test_golden_*.gd${NC}"
fi
echo ""
if [ $exit_code -eq 0 ]; then
echo -e "${GREEN}All 3 consumers agree on $count vector(s)${NC}"
else
echo -e "${RED}Divergence detected — release blocker${NC}"
echo -e "${RED}See src/simulator/tests/golden/README.md for triage guidance${NC}"
fi
return $exit_code
}
cmd_autoplay() {
# Single-seed fast feedback: ./run autoplay [seed]
local seed="${1:-1}"

192
tools/objectives-report.py Normal file
View file

@ -0,0 +1,192 @@
#!/usr/bin/env python3
"""Regenerate .project/objectives/README.md from per-objective frontmatter.
SSoT: each .project/objectives/p[012]-NN-*.md file carries its own status in
YAML frontmatter. This script reads every such file, validates the fields, and
emits a grouped dashboard. The dashboard is regenerated, not hand-edited.
Usage:
python3 tools/objectives-report.py # write README.md
python3 tools/objectives-report.py --check # exit 1 if README.md is stale
"""
from __future__ import annotations
import argparse
import sys
from dataclasses import dataclass
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
OBJ_DIR = REPO / ".project" / "objectives"
OUT = OBJ_DIR / "README.md"
VALID_STATUS = {"done", "partial", "stub", "missing", "oos"}
VALID_PRIORITY = {"p0", "p1", "p2"}
VALID_SCOPE = {"game1", "game2"}
STATUS_ICON = {
"done": "",
"partial": "🟡",
"stub": "🔴",
"missing": "",
"oos": "",
}
@dataclass(frozen=True)
class Objective:
id: str
title: str
priority: str
status: str
scope: str
updated_at: str
path: Path
def parse_frontmatter(text: str, path: Path) -> dict[str, str]:
"""Parse a minimal YAML frontmatter block (flat key: value pairs).
We deliberately avoid a PyYAML dependency frontmatter here is flat
`key: value` plus occasional list fields we don't need to index.
"""
if not text.startswith("---\n"):
raise ValueError(f"{path}: missing frontmatter opener '---'")
end = text.find("\n---\n", 4)
if end == -1:
raise ValueError(f"{path}: missing frontmatter closer '---'")
body = text[4:end]
out: dict[str, str] = {}
current_key: str | None = None
for raw in body.splitlines():
line = raw.rstrip()
if not line or line.lstrip().startswith("#"):
continue
# List-item continuation (we ignore list values for now)
if line.startswith((" ", "\t", "-")):
current_key = None
continue
if ":" not in line:
raise ValueError(f"{path}: malformed frontmatter line: {raw!r}")
key, _, value = line.partition(":")
key = key.strip()
value = value.strip()
if value:
out[key] = value
current_key = key
return out
REQUIRED_FIELDS = ("id", "title", "priority", "status", "scope", "updated_at")
def load_objectives() -> list[Objective]:
out: list[Objective] = []
for path in sorted(OBJ_DIR.glob("p[012]-*.md")):
if path.name == "README.md":
continue
text = path.read_text(encoding="utf-8")
fm = parse_frontmatter(text, path)
missing = [k for k in REQUIRED_FIELDS if k not in fm]
if missing:
raise ValueError(f"{path}: missing frontmatter keys: {missing}")
if fm["priority"] not in VALID_PRIORITY:
raise ValueError(f"{path}: invalid priority {fm['priority']!r}")
if fm["status"] not in VALID_STATUS:
raise ValueError(f"{path}: invalid status {fm['status']!r}")
if fm["scope"] not in VALID_SCOPE:
raise ValueError(f"{path}: invalid scope {fm['scope']!r}")
out.append(Objective(
id=fm["id"],
title=fm["title"],
priority=fm["priority"],
status=fm["status"],
scope=fm["scope"],
updated_at=fm["updated_at"],
path=path,
))
out.sort(key=lambda o: (o.priority, o.id))
return out
def render(objectives: list[Objective]) -> str:
by_priority: dict[str, list[Objective]] = {"p0": [], "p1": [], "p2": []}
for o in objectives:
by_priority[o.priority].append(o)
counts = {s: sum(1 for o in objectives if o.status == s) for s in VALID_STATUS}
total = len(objectives)
lines: list[str] = []
lines.append("# Objectives — Dashboard")
lines.append("")
lines.append(
"> **Generated by `tools/objectives-report.py` — do not hand-edit.** "
"Source of truth is per-file YAML frontmatter in this directory."
)
lines.append("")
lines.append("## Legend")
lines.append("")
lines.append("✅ done · 🟡 partial · 🔴 stub · ❌ missing · ⚫ out-of-scope (Game 2)")
lines.append("")
lines.append("## Totals")
lines.append("")
lines.append("| Status | Count |")
lines.append("|---|---|")
for status in ("done", "partial", "stub", "missing", "oos"):
lines.append(f"| {STATUS_ICON[status]} {status} | {counts[status]} |")
lines.append(f"| **total** | **{total}** |")
lines.append("")
priority_heading = {
"p0": "P0 — Blockers for \"completely playable\"",
"p1": "P1 — Ship-readiness",
"p2": "P2 — Polish",
}
for prio in ("p0", "p1", "p2"):
group = by_priority[prio]
if not group:
continue
lines.append(f"## {priority_heading[prio]}")
lines.append("")
lines.append("| ID | Status | Title | Updated |")
lines.append("|---|---|---|---|")
for o in group:
link = f"[{o.id}]({o.path.name})"
icon = STATUS_ICON[o.status]
lines.append(f"| {link} | {icon} {o.status} | {o.title} | {o.updated_at} |")
lines.append("")
return "\n".join(lines) + "\n"
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument(
"--check",
action="store_true",
help="Exit 1 if README.md is stale instead of writing",
)
args = ap.parse_args()
try:
objectives = load_objectives()
except ValueError as e:
print(f"error: {e}", file=sys.stderr)
return 2
rendered = render(objectives)
if args.check:
existing = OUT.read_text(encoding="utf-8") if OUT.exists() else ""
if existing != rendered:
print(f"stale: {OUT.relative_to(REPO)} — run tools/objectives-report.py", file=sys.stderr)
return 1
return 0
OUT.write_text(rendered, encoding="utf-8")
print(f"wrote {OUT.relative_to(REPO)} ({len(objectives)} objectives)")
return 0
if __name__ == "__main__":
raise SystemExit(main())