From aaa359e2c5c9d4d77a2db524bfa7980d73a9349d Mon Sep 17 00:00:00 2001 From: Natalie Date: Fri, 17 Apr 2026 00:14:17 -0700 Subject: [PATCH] =?UTF-8?q?feat(@projects):=20=E2=9C=A8=20add=20project=20?= =?UTF-8?q?objectives=20roadmap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .gdlintrc | 5 +- .project/{iteration_log.md => CHANGELOG.md} | 8 + .project/README.md | 37 +++ .../20260325_feature_gap_ea_pivot.md} | 0 .../20260412_legacy_migration_plan.md} | 0 .../20260415_final_batch_report.md} | 0 .../20260416_ea_release_summary.md} | 0 .../20260416_gpu_recon.md} | 0 .project/objectives/p0-01-mcts-wiring.md | 33 +++ .../objectives/p0-02-clan-personalities.md | 25 ++ .project/objectives/p0-03-pvp-in-turn.md | 22 ++ .project/objectives/p0-04-wonder-tracking.md | 26 +++ .../objectives/p0-05-culture-and-borders.md | 24 ++ .../objectives/p0-06-economy-integration.md | 26 +++ .../objectives/p0-07-tech-research-costs.md | 22 ++ .../objectives/p0-08-domination-victory.md | 22 ++ .project/objectives/p0-09-ui-completeness.md | 27 +++ .../objectives/p0-10-completion-stability.md | 31 +++ .../p0-11-mystery-item-authoring.md | 25 ++ .project/objectives/p1-01-diplomacy-lite.md | 22 ++ .../p1-02-strategic-resource-yields.md | 21 ++ .project/objectives/p1-03-tutorial-overlay.md | 26 +++ .project/objectives/p1-04-sound-and-music.md | 20 ++ .project/objectives/p1-05-balance-tuning.md | 27 +++ .project/objectives/p1-06-options-polish.md | 21 ++ .../objectives/p1-07-chronicle-coverage.md | 21 ++ .../p1-08-victory-screen-content.md | 27 +++ .../objectives/p2-01-minimap-improvements.md | 20 ++ .project/objectives/p2-02-hud-tooltips.md | 18 ++ .../objectives/p2-03-hotkey-cheat-sheet.md | 19 ++ .../objectives/p2-04-localization-audit.md | 19 ++ .../screenshots/autoplay_01_main_menu.png | Bin .../screenshots/autoplay_02_game_setup.png | Bin .../screenshots/autoplay_turn_001.png | Bin .../screenshots/autoplay_turn_002.png | Bin .../screenshots/autoplay_turn_003.png | Bin .../screenshots/autoplay_turn_011.png | Bin .../screenshots/autoplay_turn_021.png | Bin .../screenshots/autoplay_turn_031.png | Bin .../screenshots/autoplay_turn_041.png | Bin .../screenshots/autoplay_turn_051.png | Bin .../screenshots/autoplay_turn_061.png | Bin .../screenshots/autoplay_turn_071.png | Bin .../screenshots/autoplay_turn_081.png | Bin .../screenshots/autoplay_turn_091.png | Bin .../screenshots/autoplay_turn_101.png | Bin .../screenshots/autoplay_turn_111.png | Bin .../screenshots/autoplay_turn_121.png | Bin .../screenshots/autoplay_turn_131.png | Bin .../screenshots/autoplay_turn_141.png | Bin .../screenshots/autoplay_turn_151.png | Bin .../screenshots/autoplay_turn_161.png | Bin .../screenshots/autoplay_turn_171.png | Bin .../screenshots/autoplay_turn_181.png | Bin .../screenshots/autoplay_turn_191.png | Bin .../screenshots/autoplay_turn_201.png | Bin .../screenshots/autoplay_turn_211.png | Bin .../screenshots/autoplay_turn_221.png | Bin .../screenshots/autoplay_turn_231.png | Bin .../screenshots/autoplay_turn_241.png | Bin .../screenshots/autoplay_turn_251.png | Bin .../screenshots/autoplay_turn_261.png | Bin .../screenshots/autoplay_turn_271.png | Bin .../screenshots/autoplay_turn_281.png | Bin .../screenshots/autoplay_turn_291.png | Bin .../screenshots/autoplay_turn_301.png | Bin .../screenshots/autoplay_turn_311.png | Bin .../screenshots/autoplay_turn_321.png | Bin .../screenshots/autoplay_turn_331.png | Bin .../screenshots/autoplay_turn_341.png | Bin .../screenshots/autoplay_turn_351.png | Bin .../screenshots/autoplay_turn_361.png | Bin .../screenshots/autoplay_turn_371.png | Bin .../screenshots/autoplay_turn_381.png | Bin .../screenshots/autoplay_turn_391.png | Bin .../screenshots/autoplay_turn_401.png | Bin .../screenshots/autoplay_turn_411.png | Bin .../screenshots/autoplay_victory_turn_418.png | Bin .../simulation}/README.md | 0 .../simulation}/balance/axis-viability.md | 0 .../simulation}/balance/tournament.md | 0 .../simulation}/baseline/report.md | 0 .../simulation}/baseline/stats.md | 0 .../simulation}/baseline/story.md | 0 .../simulation}/experiment-log.md | 0 .../simulation}/mechanics.md | 0 .../simulation}/pvp/combat-stats.md | 0 .../simulation}/pvp/conquest-map.md | 0 .../simulation}/pvp/victory-paths.md | 0 .../simulation}/scenarios/0ai/report.md | 0 .../simulation}/scenarios/0ai/stats.md | 0 .../simulation}/scenarios/0ai/story.md | 0 .../simulation}/scenarios/1ai/report.md | 0 .../simulation}/scenarios/1ai/stats.md | 0 .../simulation}/scenarios/1ai/story.md | 0 .../simulation}/scenarios/2ai/report.md | 0 .../simulation}/scenarios/2ai/stats.md | 0 .../simulation}/scenarios/2ai/story.md | 0 .../simulation}/scenarios/3ai/report.md | 0 .../simulation}/scenarios/3ai/stats.md | 0 .../simulation}/scenarios/3ai/story.md | 0 .../simulation}/scenarios/4ai/report.md | 0 .../simulation}/scenarios/4ai/stats.md | 0 .../simulation}/scenarios/4ai/story.md | 0 .../simulation}/scenarios/comparison.md | 0 .../simulation}/tech-debt-audit.md | 0 .../{ => milestones}/m0-foundation/README.md | 0 .../m1-base-mundaneworld/README.md | 0 .../{ => milestones}/m2-flora-fauna/README.md | 0 .../m2-flora-fauna/RUST_IMPLEMENTATION.md | 0 .../m2b-full-ecosystem/README.md | 0 .../m3-natural-events/README.md | 0 .../{ => topics}/climate-balance/README.md | 0 .../{ => topics}/ecology-balance/THESIS.md | 0 .../age-of-dwarves/guide/eslint.config.js | 4 +- .../games/age-of-elves/guide/eslint.config.js | 4 +- .../age-of-kzzkyt/guide/eslint.config.js | 4 +- src/simulator/Cargo.toml | 20 ++ src/simulator/api-gdext/Cargo.toml | 3 + src/simulator/api-wasm/Cargo.toml | 3 + src/simulator/crates/mc-ai/Cargo.toml | 3 + src/simulator/crates/mc-balance/Cargo.toml | 3 + src/simulator/crates/mc-city/Cargo.toml | 3 + src/simulator/crates/mc-climate/Cargo.toml | 3 + src/simulator/crates/mc-combat/Cargo.toml | 3 + src/simulator/crates/mc-compute/Cargo.toml | 3 + src/simulator/crates/mc-core/Cargo.toml | 3 + src/simulator/crates/mc-culture/Cargo.toml | 3 + src/simulator/crates/mc-ecology/Cargo.toml | 3 + src/simulator/crates/mc-economy/Cargo.toml | 3 + src/simulator/crates/mc-flora/Cargo.toml | 3 + src/simulator/crates/mc-happiness/Cargo.toml | 3 + src/simulator/crates/mc-items/Cargo.toml | 3 + src/simulator/crates/mc-magic/Cargo.toml | 3 + src/simulator/crates/mc-mapgen/Cargo.toml | 3 + .../crates/mc-observation/Cargo.toml | 3 + src/simulator/crates/mc-sim/Cargo.toml | 3 + src/simulator/crates/mc-tech/Cargo.toml | 3 + src/simulator/crates/mc-trade/Cargo.toml | 3 + src/simulator/crates/mc-turn/Cargo.toml | 3 + src/simulator/tests/golden/README.md | 97 ++++++++ src/simulator/tests/golden/vectors/.gitkeep | 0 src/simulator/tests/integration/Cargo.toml | 3 + tools/test_personality_winrate.py | 218 ++++++++++++++++++ 144 files changed, 1002 insertions(+), 8 deletions(-) rename .project/{iteration_log.md => CHANGELOG.md} (99%) create mode 100644 .project/README.md rename .project/{FEATURE_GAP.md => history/20260325_feature_gap_ea_pivot.md} (100%) rename .project/{legacy-migration-plan.md => history/20260412_legacy_migration_plan.md} (100%) rename .project/{FINAL_BATCH_REPORT.md => history/20260415_final_batch_report.md} (100%) rename .project/{GAME_COMPLETE.md => history/20260416_ea_release_summary.md} (100%) rename .project/{gpu_recon.md => history/20260416_gpu_recon.md} (100%) create mode 100644 .project/objectives/p0-01-mcts-wiring.md create mode 100644 .project/objectives/p0-02-clan-personalities.md create mode 100644 .project/objectives/p0-03-pvp-in-turn.md create mode 100644 .project/objectives/p0-04-wonder-tracking.md create mode 100644 .project/objectives/p0-05-culture-and-borders.md create mode 100644 .project/objectives/p0-06-economy-integration.md create mode 100644 .project/objectives/p0-07-tech-research-costs.md create mode 100644 .project/objectives/p0-08-domination-victory.md create mode 100644 .project/objectives/p0-09-ui-completeness.md create mode 100644 .project/objectives/p0-10-completion-stability.md create mode 100644 .project/objectives/p0-11-mystery-item-authoring.md create mode 100644 .project/objectives/p1-01-diplomacy-lite.md create mode 100644 .project/objectives/p1-02-strategic-resource-yields.md create mode 100644 .project/objectives/p1-03-tutorial-overlay.md create mode 100644 .project/objectives/p1-04-sound-and-music.md create mode 100644 .project/objectives/p1-05-balance-tuning.md create mode 100644 .project/objectives/p1-06-options-polish.md create mode 100644 .project/objectives/p1-07-chronicle-coverage.md create mode 100644 .project/objectives/p1-08-victory-screen-content.md create mode 100644 .project/objectives/p2-01-minimap-improvements.md create mode 100644 .project/objectives/p2-02-hud-tooltips.md create mode 100644 .project/objectives/p2-03-hotkey-cheat-sheet.md create mode 100644 .project/objectives/p2-04-localization-audit.md rename .project/{ => reports}/screenshots/autoplay_01_main_menu.png (100%) rename .project/{ => reports}/screenshots/autoplay_02_game_setup.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_001.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_002.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_003.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_011.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_021.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_031.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_041.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_051.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_061.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_071.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_081.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_091.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_101.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_111.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_121.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_131.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_141.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_151.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_161.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_171.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_181.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_191.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_201.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_211.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_221.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_231.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_241.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_251.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_261.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_271.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_281.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_291.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_301.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_311.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_321.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_331.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_341.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_351.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_361.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_371.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_381.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_391.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_401.png (100%) rename .project/{ => reports}/screenshots/autoplay_turn_411.png (100%) rename .project/{ => reports}/screenshots/autoplay_victory_turn_418.png (100%) rename .project/{simulation-report => reports/simulation}/README.md (100%) rename .project/{simulation-report => reports/simulation}/balance/axis-viability.md (100%) rename .project/{simulation-report => reports/simulation}/balance/tournament.md (100%) rename .project/{simulation-report => reports/simulation}/baseline/report.md (100%) rename .project/{simulation-report => reports/simulation}/baseline/stats.md (100%) rename .project/{simulation-report => reports/simulation}/baseline/story.md (100%) rename .project/{simulation-report => reports/simulation}/experiment-log.md (100%) rename .project/{simulation-report => reports/simulation}/mechanics.md (100%) rename .project/{simulation-report => reports/simulation}/pvp/combat-stats.md (100%) rename .project/{simulation-report => reports/simulation}/pvp/conquest-map.md (100%) rename .project/{simulation-report => reports/simulation}/pvp/victory-paths.md (100%) rename .project/{simulation-report => reports/simulation}/scenarios/0ai/report.md (100%) rename .project/{simulation-report => reports/simulation}/scenarios/0ai/stats.md (100%) rename .project/{simulation-report => reports/simulation}/scenarios/0ai/story.md (100%) rename .project/{simulation-report => reports/simulation}/scenarios/1ai/report.md (100%) rename .project/{simulation-report => reports/simulation}/scenarios/1ai/stats.md (100%) rename .project/{simulation-report => reports/simulation}/scenarios/1ai/story.md (100%) rename .project/{simulation-report => reports/simulation}/scenarios/2ai/report.md (100%) rename .project/{simulation-report => reports/simulation}/scenarios/2ai/stats.md (100%) rename .project/{simulation-report => reports/simulation}/scenarios/2ai/story.md (100%) rename .project/{simulation-report => reports/simulation}/scenarios/3ai/report.md (100%) rename .project/{simulation-report => reports/simulation}/scenarios/3ai/stats.md (100%) rename .project/{simulation-report => reports/simulation}/scenarios/3ai/story.md (100%) rename .project/{simulation-report => reports/simulation}/scenarios/4ai/report.md (100%) rename .project/{simulation-report => reports/simulation}/scenarios/4ai/stats.md (100%) rename .project/{simulation-report => reports/simulation}/scenarios/4ai/story.md (100%) rename .project/{simulation-report => reports/simulation}/scenarios/comparison.md (100%) rename .project/{simulation-report => reports/simulation}/tech-debt-audit.md (100%) rename .project/tasks/{ => milestones}/m0-foundation/README.md (100%) rename .project/tasks/{ => milestones}/m1-base-mundaneworld/README.md (100%) rename .project/tasks/{ => milestones}/m2-flora-fauna/README.md (100%) rename .project/tasks/{ => milestones}/m2-flora-fauna/RUST_IMPLEMENTATION.md (100%) rename .project/tasks/{ => milestones}/m2b-full-ecosystem/README.md (100%) rename .project/tasks/{ => milestones}/m3-natural-events/README.md (100%) rename .project/tasks/{ => topics}/climate-balance/README.md (100%) rename .project/tasks/{ => topics}/ecology-balance/THESIS.md (100%) create mode 100644 src/simulator/tests/golden/README.md create mode 100644 src/simulator/tests/golden/vectors/.gitkeep create mode 100644 tools/test_personality_winrate.py diff --git a/.gdlintrc b/.gdlintrc index c234c515..00260428 100644 --- a/.gdlintrc +++ b/.gdlintrc @@ -34,9 +34,10 @@ loop-variable-name: _?[a-z][a-z0-9]*(_[a-z0-9]+)* signal-name: '[a-z][a-z0-9]*(_[a-z0-9]+)*' sub-class-name: _?([A-Z][a-z0-9]*)+ -# Limits (aligned with Lilith ecosystem standards) +# Limits (aligned with project 3-language standards: 500 LOC hard cap, 300 soft warn) +# See ~/.claude/instructions/godot-code-standards.md "File Size Limits". max-line-length: 100 -max-file-lines: 600 +max-file-lines: 500 max-public-methods: 100 max-returns: 6 function-arguments-number: 10 diff --git a/.project/iteration_log.md b/.project/CHANGELOG.md similarity index 99% rename from .project/iteration_log.md rename to .project/CHANGELOG.md index eaaee55d..684c3417 100644 --- a/.project/iteration_log.md +++ b/.project/CHANGELOG.md @@ -1,3 +1,11 @@ +# CHANGELOG + +Dated narrative events, append-only. **Newest at bottom.** References objective IDs (e.g. `p0-05`) from `objectives/`; never restates status. For current state see `objectives/`. + +Entry format: `YYYY-MM-DD HH:MM : (files=N) [ref: p0-XX]` + +--- + 2026-04-15 01:00 iter 1: GROWTH, median p0_pop_peak 3→6 (seed 1 smoke), files=3 (city.rs, turn_processor.gd, turn_processor_helpers.gd). Rust: FOOD_PER_POP 2.0→1.5. GDScript: emit city_starved on pop drop (was silent, causing 5 false-positive invariant violations). 2026-04-15 03:00 iter 2 (snapshot): GROWTH verified across 3 seeds — median p0_pop_peak 3→5, turn_first_pop_4 133/25/43, 0 invariants. Next gap: VICTORY (0/3 outcome=victory). Dispatching victory-dev. 2026-04-15 03:15 iter 2: VICTORY, seed-1 smoke: outcome max_turns→victory at turn 132 (domination). Files=3 (city.gd: original_capital_owner field; ai_turn_bridge.gd: owner-before-found reorder; victory_manager.gd: rewrote _check_domination on capital ownership). Full 3-seed verification pending next batch. diff --git a/.project/README.md b/.project/README.md new file mode 100644 index 00000000..6446dfac --- /dev/null +++ b/.project/README.md @@ -0,0 +1,37 @@ +# `.project/` — Directory Map + +Build-process docs for Magic Civilization. Each file owns exactly one responsibility. Status of work-in-flight is tracked **only** in `objectives/` (SSoT). + +## File / dir → responsibility + +| Path | Responsibility | Rule | +|---|---|---| +| `README.md` | This map | Maintained by hand when structure changes | +| `ROADMAP.md` | Phase **sequence** + **scope** per milestone | **Never** carries status; references objective IDs only | +| `TERMINOLOGY.md` | Glossary (terms, acronyms, design vocabulary) | Facts only, no status | +| `CHANGELOG.md` | Dated narrative events (append-only) | References objective IDs; **never** restates status | +| `objectives/` | **Single source of truth** for current state | One `.md` per objective, YAML frontmatter `status:` field | +| `objectives/README.md` | Dashboard index (grouped by P0/P1/P2) | **Generated** by `tools/objectives-report.py` — do not hand-edit | +| `tasks/milestones/` | Per-milestone work packages (scoping docs) | HOW, not WHAT-DONE | +| `tasks/topics/` | Cross-cutting topic work (balance tuning etc.) | HOW, not WHAT-DONE | +| `tasks/deferred/` | Parked work packages | HOW, not WHAT-DONE | +| `handoffs/` | Agent-to-agent context transfer | `YYYYMMDD_slug.md` | +| `history/` | Archived one-off docs (reports, snapshots, obsolete plans) | `YYYYMMDD_slug.md`; immutable once filed | +| `reports/batches/` | Autoplay batch output | Tool artifacts | +| `reports/simulation/` | Simulator reports | Tool artifacts | +| `reports/screenshots/` | Proof screenshots | Tool artifacts | +| `future-games/` | Game 2 design drafts | Out of scope for Game 1 | +| `gdlintrc.local` | Local gdlint overrides | Config | + +## Invariants + +1. **Status lives in `objectives/*.md` frontmatter, nowhere else.** +2. **ROADMAP, CHANGELOG, tasks/** may *reference* objective IDs; they **may not** restate status. +3. `objectives/README.md` is machine-generated from frontmatter. Regenerate after any objective edit: `python3 tools/objectives-report.py`. +4. `history/` is append-only. Archived files get a `YYYYMMDD_` prefix and are never edited in place; if a superseding doc is needed, create a new one. + +## Quick regen + +```bash +python3 tools/objectives-report.py # rebuilds objectives/README.md from frontmatter +``` diff --git a/.project/FEATURE_GAP.md b/.project/history/20260325_feature_gap_ea_pivot.md similarity index 100% rename from .project/FEATURE_GAP.md rename to .project/history/20260325_feature_gap_ea_pivot.md diff --git a/.project/legacy-migration-plan.md b/.project/history/20260412_legacy_migration_plan.md similarity index 100% rename from .project/legacy-migration-plan.md rename to .project/history/20260412_legacy_migration_plan.md diff --git a/.project/FINAL_BATCH_REPORT.md b/.project/history/20260415_final_batch_report.md similarity index 100% rename from .project/FINAL_BATCH_REPORT.md rename to .project/history/20260415_final_batch_report.md diff --git a/.project/GAME_COMPLETE.md b/.project/history/20260416_ea_release_summary.md similarity index 100% rename from .project/GAME_COMPLETE.md rename to .project/history/20260416_ea_release_summary.md diff --git a/.project/gpu_recon.md b/.project/history/20260416_gpu_recon.md similarity index 100% rename from .project/gpu_recon.md rename to .project/history/20260416_gpu_recon.md diff --git a/.project/objectives/p0-01-mcts-wiring.md b/.project/objectives/p0-01-mcts-wiring.md new file mode 100644 index 00000000..60a44be1 --- /dev/null +++ b/.project/objectives/p0-01-mcts-wiring.md @@ -0,0 +1,33 @@ +--- +id: p0-01 +title: Wire MCTS into gameplay AI +priority: p0 +status: partial +scope: game1 +updated_at: 2026-04-17 +evidence: + - src/simulator/crates/mc-ai/src/mcts_tree.rs + - src/simulator/api-gdext/src/ai.rs + - src/game/engine/src/modules/ai/ai_turn_bridge.gd + - src/game/engine/src/modules/ai/simple_heuristic_ai.gd +--- + +## Summary + +`mc-ai/src/mcts_tree.rs` (138 lines, 22/22 tests) and the `GdMcTreeController` binding exist, but `grep -r "mcts\|MctsTreeController\|run_mcts" src/game/engine/src/modules/ai/` returns 0 matches. The game never calls the tree — only `SimpleHeuristicAi` drives AI turns. + +## Evidence of gap + +- Batch 2026-04-16: victory rate 4/10 (target 50–80%), median `p0_pop_peak=25` (target ≥30), 6/10 stalemate at `max_turns`. +- `.project/CHANGELOG.md` 2026-04-16 14:36 — "MCTS FOUNDATION complete … Not wired to GDExtension yet". + +## Acceptance + +- `AiTurnBridge` delegates to MCTS when `AI_USE_MCTS=true` and falls back to `SimpleHeuristicAi` otherwise. +- A seeded 10-game batch with MCTS on moves median TTV into the 200–350 band and victory rate into ≥50%. +- Determinism preserved (same seed → same outcome). + +## Non-goals + +- Replacing `SimpleHeuristicAi` (keep as fast-path / early-turn fallback). +- Per-clan weight variation (that's `p0-02`). diff --git a/.project/objectives/p0-02-clan-personalities.md b/.project/objectives/p0-02-clan-personalities.md new file mode 100644 index 00000000..5e531f33 --- /dev/null +++ b/.project/objectives/p0-02-clan-personalities.md @@ -0,0 +1,25 @@ +--- +id: p0-02 +title: Five AI clan personalities drive distinct playstyles +priority: p0 +status: partial +scope: game1 +updated_at: 2026-04-17 +evidence: + - public/games/age-of-dwarves/data/ai_personalities.json + - src/simulator/crates/mc-ai/src/ +--- + +## Summary + +`ai_personalities.json` defines Ironhold / Goldvein / Blackhammer / Deepforge / Runesmith, but `mc-ai` has no per-clan `ScoringWeights` variants. Every AI plays identically, so the five-clan promise in `CLAUDE.md` isn't delivered. + +## Acceptance + +- `mc-ai::ScoringWeights::from_personality(id: &str)` loads weights from JSON. +- AI assignment at game start picks one of the 5 personalities per AI player. +- Batch of 5 seeds with `AI_PIN_PERSONALITY=` produces measurably different stats per clan (expansion_axis, combat frequency, wealth). + +## Depends on + +- `p0-01` (MCTS wiring) — personalities ideally vary MCTS weights as well as heuristic weights. diff --git a/.project/objectives/p0-03-pvp-in-turn.md b/.project/objectives/p0-03-pvp-in-turn.md new file mode 100644 index 00000000..7d939092 --- /dev/null +++ b/.project/objectives/p0-03-pvp-in-turn.md @@ -0,0 +1,22 @@ +--- +id: p0-03 +title: PvP combat resolved inside the authoritative turn processor +priority: p0 +status: partial +scope: game1 +updated_at: 2026-04-17 +evidence: + - src/simulator/crates/mc-turn/src/processor.rs + - src/simulator/crates/mc-combat/src/ + - src/game/engine/scenes/world_map/world_map_combat.gd +--- + +## Summary + +`mc-turn::processor` currently resolves only `LairCombat` (fauna). Player-vs-player attacks go through the GDScript world-map click path, which bypasses the authoritative simulation. MCTS rollouts (`p0-01`) need deterministic PvP in Rust. + +## Acceptance + +- `processor.rs` resolves queued PvP attacks each turn via `mc-combat::CombatResolver`. +- Headless batch run (no GDScript combat path) produces identical combat results to the GDScript-mediated game for the same seed. +- GDScript click-to-attack becomes a thin input wrapper that enqueues a Rust-resolved attack. diff --git a/.project/objectives/p0-04-wonder-tracking.md b/.project/objectives/p0-04-wonder-tracking.md new file mode 100644 index 00000000..692caf1a --- /dev/null +++ b/.project/objectives/p0-04-wonder-tracking.md @@ -0,0 +1,26 @@ +--- +id: p0-04 +title: World wonder tracking in PlayerState and score victory +priority: p0 +status: partial +scope: game1 +updated_at: 2026-04-17 +evidence: + - src/simulator/crates/mc-turn/src/victory.rs + - public/games/age-of-dwarves/data/buildings/mundane_wonders.json +--- + +## Summary + +`mc-turn/src/victory.rs:44-45` comment: *"no `wonders_built` term because `PlayerState` carries no wonder count."* `mundane_wonders.json` contains all 24 wonders T1–T10 with mundane-only fields (`school: null`, `mana_generated: null`). Data exists; simulator state and score weighting do not. + +## Acceptance + +- `PlayerState.wonders_built: BTreeSet` field added (BTree for determinism). +- Wonder completion in `mc-city::production` appends to the set. +- `victory::calculate_score` folds wonder count (with tier multiplier). +- Encyclopedia/city UI surfaces a "Wonders built" listing per player. + +## Depends on + +- Nothing. Self-contained. diff --git a/.project/objectives/p0-05-culture-and-borders.md b/.project/objectives/p0-05-culture-and-borders.md new file mode 100644 index 00000000..84037a53 --- /dev/null +++ b/.project/objectives/p0-05-culture-and-borders.md @@ -0,0 +1,24 @@ +--- +id: p0-05 +title: Culture generation and border expansion +priority: p0 +status: stub +scope: game1 +updated_at: 2026-04-17 +evidence: + - src/simulator/crates/mc-culture/src/lib.rs + - src/simulator/crates/mc-city/ + - src/game/engine/src/rendering/overlay_renderer.gd +--- + +## Summary + +`mc-culture/src/lib.rs` is **literally 1 line**: `// TODO: culture generation, border expansion`. The renderer has border-overlay capability but nothing drives it. Single largest pure-stub gap. + +## Acceptance + +- Per-city culture accumulation per turn (yield from buildings + tile modifiers). +- Ring-1 → ring-2 → ring-3 border expansion thresholds from JSON. +- `city_border_expanded` event wired to `overlay_renderer.gd`. +- `mc-turn::victory::calculate_score` folds culture tier. +- GDScript `SimpleHeuristicAi` scoring factors in culture yield. diff --git a/.project/objectives/p0-06-economy-integration.md b/.project/objectives/p0-06-economy-integration.md new file mode 100644 index 00000000..2d15ce25 --- /dev/null +++ b/.project/objectives/p0-06-economy-integration.md @@ -0,0 +1,26 @@ +--- +id: p0-06 +title: Fold gold income / upkeep / improvement yields into turn loop +priority: p0 +status: partial +scope: game1 +updated_at: 2026-04-17 +evidence: + - src/simulator/crates/mc-economy/src/lib.rs + - src/simulator/crates/mc-economy/src/gold.rs + - src/simulator/crates/mc-economy/src/treasury.rs + - src/simulator/crates/mc-economy/src/stockpile.rs + - src/simulator/crates/mc-turn/src/processor.rs +--- + +## Summary + +`mc-economy` submodules have working code (713 lines across `gold.rs` 221, `treasury.rs` 314, `stockpile.rs` 178) but `lib.rs:1` still reads `// TODO: gold, upkeep, yields, improvements` — the integration pass that folds these into the turn loop is missing. + +## Acceptance + +- Per-turn gold income = Σ(city marketplace yield + trade route yield). +- Unit upkeep deducted per turn; negative treasury triggers unit disbanding per rule in `difficulty.json`. +- Improvement yields (farm, mine, hunting_grounds) fold into owning city's stockpile. +- Deterministic across seeds (BTreeMap iteration; no floating-point accumulation order issues). +- `mc-turn` tests exercise the full income/upkeep/yield path. diff --git a/.project/objectives/p0-07-tech-research-costs.md b/.project/objectives/p0-07-tech-research-costs.md new file mode 100644 index 00000000..c263ca9e --- /dev/null +++ b/.project/objectives/p0-07-tech-research-costs.md @@ -0,0 +1,22 @@ +--- +id: p0-07 +title: Tech research costs and science pool pacing +priority: p0 +status: partial +scope: game1 +updated_at: 2026-04-17 +evidence: + - src/simulator/crates/mc-tech/src/lib.rs + - public/games/age-of-dwarves/data/techs/ +--- + +## Summary + +`mc-tech` has the prerequisite graph and unlock signals but no per-tech science cost accumulation. Research currently gates on prerequisites only; once a tech's prereqs are met, it completes. Games finish with wildly different tech counts across seeds. + +## Acceptance + +- `cost: u32` field in each tech JSON; schema validated by `tools/validate-game-data.py`. +- Per-player `science_pool: i64` in `PlayerState`; accumulates per-turn science from cities. +- Completion when `science_pool ≥ tech.cost`; pool decremented by cost (not reset). +- Tuning target: full tech tree reachable in ~250 turns at normal difficulty; verifiable via 10-seed batch median `techs_researched`. diff --git a/.project/objectives/p0-08-domination-victory.md b/.project/objectives/p0-08-domination-victory.md new file mode 100644 index 00000000..fae496bb --- /dev/null +++ b/.project/objectives/p0-08-domination-victory.md @@ -0,0 +1,22 @@ +--- +id: p0-08 +title: Domination victory path in mc-turn::victory +priority: p0 +status: partial +scope: game1 +updated_at: 2026-04-17 +evidence: + - src/simulator/crates/mc-turn/src/victory.rs + - src/game/engine/scenes/menus/victory_screen.gd +--- + +## Summary + +ROADMAP declares Domination + Score. Score works (9/9 completable games declare a winner). Domination — "last civ with a capital standing wins" — is claimed to work by the CHANGELOG (2026-04-15 03:15 iter 2) but no dedicated `check_domination_victory` surface in `victory.rs` was confirmed in the audit. + +## Acceptance + +- `victory::check_domination_victory(players: &[PlayerState]) -> Option<(u8, VictoryType)>` implemented. +- `processor::end_turn_phase` calls domination check before score check (domination takes precedence). +- `victory_screen.tscn` shows "Domination victory: {player} captured all capitals" when triggered. +- Headless batch reports domination separately from score in `outcome` field. diff --git a/.project/objectives/p0-09-ui-completeness.md b/.project/objectives/p0-09-ui-completeness.md new file mode 100644 index 00000000..af2bde45 --- /dev/null +++ b/.project/objectives/p0-09-ui-completeness.md @@ -0,0 +1,27 @@ +--- +id: p0-09 +title: City-screen UI completeness (citizen assign, queue controls, promotion picker) +priority: p0 +status: partial +scope: game1 +updated_at: 2026-04-17 +evidence: + - src/game/engine/scenes/city/city_screen.gd + - src/game/engine/scenes/city/production_queue.gd + - src/game/engine/scenes/combat/promotion_picker.gd +--- + +## Summary + +Three UI paths assumed-but-unverified: + +1. **Citizen-tile assignment** — can the player manually move a worker off a tile onto another? +2. **Production queue controls** — reorder, pause, show cost + ETA per item? +3. **Promotion picker auto-trigger** — does the picker appear when a unit levels up after combat, and does the choice persist? + +## Acceptance + +- Manual QA smoke: launch game, found city, right-click a worked tile to unassign, click another to assign, confirm yield recalculates. +- Queue: drag-to-reorder works, each row shows `cost / cost_per_turn = ETA`. +- Promotion: kill an enemy warrior with a level-0 unit, confirm picker modal opens, pick a promo, reload save, confirm persistence. +- Three GUT tests — one per path. diff --git a/.project/objectives/p0-10-completion-stability.md b/.project/objectives/p0-10-completion-stability.md new file mode 100644 index 00000000..ff3b6d67 --- /dev/null +++ b/.project/objectives/p0-10-completion-stability.md @@ -0,0 +1,31 @@ +--- +id: p0-10 +title: Game-completion stability — ≥7/10 seeds declare a winner +priority: p0 +status: partial +scope: game1 +updated_at: 2026-04-17 +evidence: + - tools/autoplay-batch.sh + - tools/checklist-report.py + - .project/reports/batches/ +--- + +## Summary + +10-seed batch (2026-04-16): 4/10 win, 6/10 stalemate at `max_turns`, median `p0_pop_peak=25` (target ≥30 normal difficulty). + +## Acceptance + +Running `PARALLEL=10 bash tools/autoplay-batch.sh 10 300 .local/iter/` against the RUN host yields: + +- ≥ 7/10 seeds with `outcome != max_turns`. +- Median time-to-victory in 200–350 turns (normal). +- Median `p0_pop_peak ≥ 30`. +- 0 invariant violations. +- `tools/checklist-report.py` → 14/14 PASS on normal difficulty. + +## Depends on + +- `p0-01` (MCTS wiring) — currently suspected primary cause. +- `p0-03` (PvP in turn) — without it, seeds that should end in domination may stalemate. diff --git a/.project/objectives/p0-11-mystery-item-authoring.md b/.project/objectives/p0-11-mystery-item-authoring.md new file mode 100644 index 00000000..55c9cdb5 --- /dev/null +++ b/.project/objectives/p0-11-mystery-item-authoring.md @@ -0,0 +1,25 @@ +--- +id: p0-11 +title: Author the four T8–T10 mystery item drops +priority: p0 +status: missing +scope: game1 +updated_at: 2026-04-17 +evidence: + - public/games/age-of-dwarves/data/items/manifest.json + - CLAUDE.md +--- + +## Summary + +`CLAUDE.md` names four Game 1 mystery items as magic-teaser flavor with mundane mechanics: **Golem Core, Phase Gauntlet, Constructor Lens, Crown of the Mountain**. `items/manifest.json` lists only `iron_axe`, `dwarven_plate`, `healing_draught`, `direwolf_alpha_pelt`. The four mystery items are not authored. + +## Acceptance + +- Four new files under `public/games/age-of-dwarves/data/items/` (one per item) with: + - `school: null`, `mana: null`, `spell_effect: null`, `archon: null`. + - Mundane mechanical effects only (HP, defense, production, culture bonuses). + - Flavor text written to feel inexplicable / ancient (Game 2 teaser tone). +- `items/manifest.json` includes the four new IDs. +- Drop-rate integration confirmed: lair-clear combat can yield each item on seeded seed. +- Schema validation passes (`tools/validate-game-data.py`). diff --git a/.project/objectives/p1-01-diplomacy-lite.md b/.project/objectives/p1-01-diplomacy-lite.md new file mode 100644 index 00000000..d90fcaf6 --- /dev/null +++ b/.project/objectives/p1-01-diplomacy-lite.md @@ -0,0 +1,22 @@ +--- +id: p1-01 +title: Diplomacy-lite — peace/war toggle plus one trade action +priority: p1 +status: partial +scope: game1 +updated_at: 2026-04-17 +evidence: + - src/simulator/crates/mc-trade/src/lib.rs + - src/simulator/crates/mc-trade/src/relation.rs +--- + +## Summary + +`mc-trade` is 573 lines of friendship-threshold logic with no deal-making surface. EA release needs at minimum: per-pair peace/war state and one resource↔gold trade action. + +## Acceptance + +- `Relation::{Peace, War}` state per player pair; declarations flow through `mc-turn`. +- `TradeOffer { from, to, give: Resource, want: Gold }` with accept/reject. +- GDScript diplomacy panel exposes declare-war / offer-trade. +- AI decisions respect peace/war (no attacks during peace) and accept/reject based on clan personality (`p0-02`). diff --git a/.project/objectives/p1-02-strategic-resource-yields.md b/.project/objectives/p1-02-strategic-resource-yields.md new file mode 100644 index 00000000..7e9aef40 --- /dev/null +++ b/.project/objectives/p1-02-strategic-resource-yields.md @@ -0,0 +1,21 @@ +--- +id: p1-02 +title: Strategic resource yields feed into production bonuses +priority: p1 +status: partial +scope: game1 +updated_at: 2026-04-17 +evidence: + - public/games/age-of-dwarves/data/deposits/ + - src/simulator/crates/mc-city/ +--- + +## Summary + +Recent commits added deposit resource definitions (iron, coal, gems, etc.). Need to verify the wire-through: owning a tile with a strategic deposit should grant its production/military bonus to the owning city, and should gate production of strategic-requiring units (already partially in place per CHANGELOG 2026-04-16 06:19). + +## Acceptance + +- `mc-city::get_yields` sums deposit bonuses on owned tiles. +- Unit production gated via `requires_resource: [...]` and blocked at enqueue (Rust `QueueError::MissingResource` already exists). +- GUT + Rust test coverage for gating and yielding. diff --git a/.project/objectives/p1-03-tutorial-overlay.md b/.project/objectives/p1-03-tutorial-overlay.md new file mode 100644 index 00000000..658b86f0 --- /dev/null +++ b/.project/objectives/p1-03-tutorial-overlay.md @@ -0,0 +1,26 @@ +--- +id: p1-03 +title: First-run tutorial / onboarding overlay +priority: p1 +status: missing +scope: game1 +updated_at: 2026-04-17 +evidence: [] +--- + +## Summary + +No tutorial scene or onboarding flow exists. 4X games are notoriously opaque on first play; EA needs at minimum a progressive-disclosure hint chain on first game start. + +## Acceptance + +- `src/game/engine/scenes/tutorial/tutorial_overlay.tscn` + controller. +- Trigger chain on first `game_started` signal (suppressed if `user://settings.cfg:tutorial_completed=true`): + 1. Move camera + 2. Select founder + 3. Found first city + 4. Queue a unit + 5. End turn + 6. Open tech tree + 7. Research first tech +- Dismissible per step; all-off from `options.tscn`. diff --git a/.project/objectives/p1-04-sound-and-music.md b/.project/objectives/p1-04-sound-and-music.md new file mode 100644 index 00000000..d0fcff17 --- /dev/null +++ b/.project/objectives/p1-04-sound-and-music.md @@ -0,0 +1,20 @@ +--- +id: p1-04 +title: Sound effects and music +priority: p1 +status: missing +scope: game1 +updated_at: 2026-04-17 +evidence: [] +--- + +## Summary + +No audio audit has been run. Ship-quality 4X minimum: SFX on key game events + looping ambient music per era. + +## Acceptance + +- SFX: `turn_started`, `turn_ended`, `city_founded`, `tech_researched`, `unit_killed`, `wonder_built`, `era_advanced`. +- Ambient track per era (5 tracks); crossfade on era change. +- Options.tscn master / SFX / music volume sliders persist to `user://settings.cfg`. +- Licenses recorded in `public/games/age-of-dwarves/assets/audio/LICENSES.md`. diff --git a/.project/objectives/p1-05-balance-tuning.md b/.project/objectives/p1-05-balance-tuning.md new file mode 100644 index 00000000..e4a2dda9 --- /dev/null +++ b/.project/objectives/p1-05-balance-tuning.md @@ -0,0 +1,27 @@ +--- +id: p1-05 +title: Balance tuning — pop_peak ≥30 median, worker improvements ≥8 min +priority: p1 +status: partial +scope: game1 +updated_at: 2026-04-17 +evidence: + - tools/checklist-report.py + - .project/reports/batches/ +--- + +## Summary + +Most recent batch: median `p0_pop_peak=25` (target 30 on normal), `min_worker_improvements` variable by seed. Once `p0-01/02/03/06/07` land, re-tune food/growth/worker-AI to hit targets. + +## Acceptance + +- 10-seed batch on normal difficulty: + - Median `p0_pop_peak ≥ 30`. + - Min across seeds `worker_improvements ≥ 8`. + - Median `techs_researched ≥ 20`. + - Median `combats ≥ 120`. + +## Depends on + +- `p0-06` (economy), `p0-07` (tech costs) — both affect pop + tech counts. diff --git a/.project/objectives/p1-06-options-polish.md b/.project/objectives/p1-06-options-polish.md new file mode 100644 index 00000000..411eb86a --- /dev/null +++ b/.project/objectives/p1-06-options-polish.md @@ -0,0 +1,21 @@ +--- +id: p1-06 +title: Options screen polish +priority: p1 +status: partial +scope: game1 +updated_at: 2026-04-17 +evidence: + - src/game/engine/scenes/menus/options.tscn + - src/game/engine/src/autoloads/settings_manager.gd +--- + +## Summary + +`options.tscn` exists; content depth unverified. Ship-readiness needs: resolution dropdown, fullscreen toggle, master/SFX/music sliders, autosave interval, tutorial-reset button. + +## Acceptance + +- All settings persist to `user://settings.cfg` via `SettingsManager`. +- Changes apply live (no restart) where possible; restart prompt for resolution only if needed. +- Reset-to-defaults button. diff --git a/.project/objectives/p1-07-chronicle-coverage.md b/.project/objectives/p1-07-chronicle-coverage.md new file mode 100644 index 00000000..2d789b59 --- /dev/null +++ b/.project/objectives/p1-07-chronicle-coverage.md @@ -0,0 +1,21 @@ +--- +id: p1-07 +title: Chronicle notifications coverage +priority: p1 +status: partial +scope: game1 +updated_at: 2026-04-17 +evidence: + - src/game/engine/scenes/hud/chronicle_panel.tscn + - src/game/engine/src/autoloads/event_bus.gd +--- + +## Summary + +`chronicle_panel.tscn` exists. Need to audit that every major `EventBus` signal feeds a user-visible entry: `city_founded`, `city_captured`, `tech_researched`, `unit_lost`, `wonder_completed`, `era_advanced`, `border_expanded`, `combat_finished` (when player-involved). + +## Acceptance + +- Chronicle shows one entry per relevant event, oldest→newest. +- Filter controls (All / Military / Research / City / Diplomacy). +- Entry click scrolls camera to relevant hex if applicable. diff --git a/.project/objectives/p1-08-victory-screen-content.md b/.project/objectives/p1-08-victory-screen-content.md new file mode 100644 index 00000000..14931af8 --- /dev/null +++ b/.project/objectives/p1-08-victory-screen-content.md @@ -0,0 +1,27 @@ +--- +id: p1-08 +title: Victory/defeat screen content — recap, banner, replay seed +priority: p1 +status: partial +scope: game1 +updated_at: 2026-04-17 +evidence: + - src/game/engine/scenes/menus/victory_screen.tscn + - src/game/engine/scenes/menus/defeat_screen.tscn +--- + +## Summary + +Scenes exist; content depth unverified. Ship needs: final score breakdown, wonders built, techs researched, combats won/lost, time-to-victory, seed + map settings displayed, "Replay same seed" button. + +## Acceptance + +- Victory screen shows the structured recap above. +- Defeat screen shows equivalent recap plus top-scoring player. +- "Replay same seed" button returns to `game_setup` with fields pre-filled. +- Both screens respect the player's victory type (`Domination` vs `Score`) via tailored banner copy. + +## Depends on + +- `p0-04` (wonder tracking) — wonders row needs data. +- `p0-08` (domination victory) — victory-type banner needs the enum variant. diff --git a/.project/objectives/p2-01-minimap-improvements.md b/.project/objectives/p2-01-minimap-improvements.md new file mode 100644 index 00000000..58787674 --- /dev/null +++ b/.project/objectives/p2-01-minimap-improvements.md @@ -0,0 +1,20 @@ +--- +id: p2-01 +title: Minimap — fog reflection and unit markers +priority: p2 +status: partial +scope: game1 +updated_at: 2026-04-17 +evidence: + - src/game/engine/scenes/hud/minimap.tscn +--- + +## Summary + +Minimap scene exists; depth of fog/unit reflection unverified. Nice-to-have for navigation on large maps. + +## Acceptance + +- Fog state on minimap matches main map. +- Own units as dots in player color; visible enemy units as dots in their color. +- Click minimap to jump camera. diff --git a/.project/objectives/p2-02-hud-tooltips.md b/.project/objectives/p2-02-hud-tooltips.md new file mode 100644 index 00000000..3cae220d --- /dev/null +++ b/.project/objectives/p2-02-hud-tooltips.md @@ -0,0 +1,18 @@ +--- +id: p2-02 +title: Tooltips on all HUD elements +priority: p2 +status: partial +scope: game1 +updated_at: 2026-04-17 +evidence: [] +--- + +## Summary + +Top bar, unit panel, city-screen headers, tech-tree nodes all benefit from hover tooltips. Current coverage spotty. + +## Acceptance + +- Every interactive HUD element has a tooltip via `Control.tooltip_text` or custom tooltip scene. +- Text resolves through `ThemeVocabulary.lookup()` (no hardcoded strings). diff --git a/.project/objectives/p2-03-hotkey-cheat-sheet.md b/.project/objectives/p2-03-hotkey-cheat-sheet.md new file mode 100644 index 00000000..102f789b --- /dev/null +++ b/.project/objectives/p2-03-hotkey-cheat-sheet.md @@ -0,0 +1,19 @@ +--- +id: p2-03 +title: Hotkey cheat sheet (F1 / ?) +priority: p2 +status: missing +scope: game1 +updated_at: 2026-04-17 +evidence: [] +--- + +## Summary + +World map uses hotkeys (T, C, B, ESC, end-turn) with no in-game reference. F1 or `?` should toggle a non-modal overlay listing all bindings. + +## Acceptance + +- `ui_help` input action bound to F1 and `?`. +- Overlay lists all bindings grouped by context (Map / City / Combat / Menus). +- Closable with same key or ESC. diff --git a/.project/objectives/p2-04-localization-audit.md b/.project/objectives/p2-04-localization-audit.md new file mode 100644 index 00000000..97ace75a --- /dev/null +++ b/.project/objectives/p2-04-localization-audit.md @@ -0,0 +1,19 @@ +--- +id: p2-04 +title: Localization audit — no hardcoded strings +priority: p2 +status: partial +scope: game1 +updated_at: 2026-04-17 +evidence: + - src/game/engine/src/autoloads/theme_vocabulary.gd +--- + +## Summary + +`ThemeVocabulary` is architected for localization, but incidental hardcoded strings have accumulated. Run a pass and route them through `vocabulary.json`. + +## Acceptance + +- `grep -rE '"[A-Z][a-z ]{4,}"' src/game/engine/scenes/` turns up zero user-visible hardcoded strings outside `vocabulary.json` lookups. +- `tools/validate-i18n.py` (new) fails if a `.gd` UI file contains a literal user-visible string. diff --git a/.project/screenshots/autoplay_01_main_menu.png b/.project/reports/screenshots/autoplay_01_main_menu.png similarity index 100% rename from .project/screenshots/autoplay_01_main_menu.png rename to .project/reports/screenshots/autoplay_01_main_menu.png diff --git a/.project/screenshots/autoplay_02_game_setup.png b/.project/reports/screenshots/autoplay_02_game_setup.png similarity index 100% rename from .project/screenshots/autoplay_02_game_setup.png rename to .project/reports/screenshots/autoplay_02_game_setup.png diff --git a/.project/screenshots/autoplay_turn_001.png b/.project/reports/screenshots/autoplay_turn_001.png similarity index 100% rename from .project/screenshots/autoplay_turn_001.png rename to .project/reports/screenshots/autoplay_turn_001.png diff --git a/.project/screenshots/autoplay_turn_002.png b/.project/reports/screenshots/autoplay_turn_002.png similarity index 100% rename from .project/screenshots/autoplay_turn_002.png rename to .project/reports/screenshots/autoplay_turn_002.png diff --git a/.project/screenshots/autoplay_turn_003.png b/.project/reports/screenshots/autoplay_turn_003.png similarity index 100% rename from .project/screenshots/autoplay_turn_003.png rename to .project/reports/screenshots/autoplay_turn_003.png diff --git a/.project/screenshots/autoplay_turn_011.png b/.project/reports/screenshots/autoplay_turn_011.png similarity index 100% rename from .project/screenshots/autoplay_turn_011.png rename to .project/reports/screenshots/autoplay_turn_011.png diff --git a/.project/screenshots/autoplay_turn_021.png b/.project/reports/screenshots/autoplay_turn_021.png similarity index 100% rename from .project/screenshots/autoplay_turn_021.png rename to .project/reports/screenshots/autoplay_turn_021.png diff --git a/.project/screenshots/autoplay_turn_031.png b/.project/reports/screenshots/autoplay_turn_031.png similarity index 100% rename from .project/screenshots/autoplay_turn_031.png rename to .project/reports/screenshots/autoplay_turn_031.png diff --git a/.project/screenshots/autoplay_turn_041.png b/.project/reports/screenshots/autoplay_turn_041.png similarity index 100% rename from .project/screenshots/autoplay_turn_041.png rename to .project/reports/screenshots/autoplay_turn_041.png diff --git a/.project/screenshots/autoplay_turn_051.png b/.project/reports/screenshots/autoplay_turn_051.png similarity index 100% rename from .project/screenshots/autoplay_turn_051.png rename to .project/reports/screenshots/autoplay_turn_051.png diff --git a/.project/screenshots/autoplay_turn_061.png b/.project/reports/screenshots/autoplay_turn_061.png similarity index 100% rename from .project/screenshots/autoplay_turn_061.png rename to .project/reports/screenshots/autoplay_turn_061.png diff --git a/.project/screenshots/autoplay_turn_071.png b/.project/reports/screenshots/autoplay_turn_071.png similarity index 100% rename from .project/screenshots/autoplay_turn_071.png rename to .project/reports/screenshots/autoplay_turn_071.png diff --git a/.project/screenshots/autoplay_turn_081.png b/.project/reports/screenshots/autoplay_turn_081.png similarity index 100% rename from .project/screenshots/autoplay_turn_081.png rename to .project/reports/screenshots/autoplay_turn_081.png diff --git a/.project/screenshots/autoplay_turn_091.png b/.project/reports/screenshots/autoplay_turn_091.png similarity index 100% rename from .project/screenshots/autoplay_turn_091.png rename to .project/reports/screenshots/autoplay_turn_091.png diff --git a/.project/screenshots/autoplay_turn_101.png b/.project/reports/screenshots/autoplay_turn_101.png similarity index 100% rename from .project/screenshots/autoplay_turn_101.png rename to .project/reports/screenshots/autoplay_turn_101.png diff --git a/.project/screenshots/autoplay_turn_111.png b/.project/reports/screenshots/autoplay_turn_111.png similarity index 100% rename from .project/screenshots/autoplay_turn_111.png rename to .project/reports/screenshots/autoplay_turn_111.png diff --git a/.project/screenshots/autoplay_turn_121.png b/.project/reports/screenshots/autoplay_turn_121.png similarity index 100% rename from .project/screenshots/autoplay_turn_121.png rename to .project/reports/screenshots/autoplay_turn_121.png diff --git a/.project/screenshots/autoplay_turn_131.png b/.project/reports/screenshots/autoplay_turn_131.png similarity index 100% rename from .project/screenshots/autoplay_turn_131.png rename to .project/reports/screenshots/autoplay_turn_131.png diff --git a/.project/screenshots/autoplay_turn_141.png b/.project/reports/screenshots/autoplay_turn_141.png similarity index 100% rename from .project/screenshots/autoplay_turn_141.png rename to .project/reports/screenshots/autoplay_turn_141.png diff --git a/.project/screenshots/autoplay_turn_151.png b/.project/reports/screenshots/autoplay_turn_151.png similarity index 100% rename from .project/screenshots/autoplay_turn_151.png rename to .project/reports/screenshots/autoplay_turn_151.png diff --git a/.project/screenshots/autoplay_turn_161.png b/.project/reports/screenshots/autoplay_turn_161.png similarity index 100% rename from .project/screenshots/autoplay_turn_161.png rename to .project/reports/screenshots/autoplay_turn_161.png diff --git a/.project/screenshots/autoplay_turn_171.png b/.project/reports/screenshots/autoplay_turn_171.png similarity index 100% rename from .project/screenshots/autoplay_turn_171.png rename to .project/reports/screenshots/autoplay_turn_171.png diff --git a/.project/screenshots/autoplay_turn_181.png b/.project/reports/screenshots/autoplay_turn_181.png similarity index 100% rename from .project/screenshots/autoplay_turn_181.png rename to .project/reports/screenshots/autoplay_turn_181.png diff --git a/.project/screenshots/autoplay_turn_191.png b/.project/reports/screenshots/autoplay_turn_191.png similarity index 100% rename from .project/screenshots/autoplay_turn_191.png rename to .project/reports/screenshots/autoplay_turn_191.png diff --git a/.project/screenshots/autoplay_turn_201.png b/.project/reports/screenshots/autoplay_turn_201.png similarity index 100% rename from .project/screenshots/autoplay_turn_201.png rename to .project/reports/screenshots/autoplay_turn_201.png diff --git a/.project/screenshots/autoplay_turn_211.png b/.project/reports/screenshots/autoplay_turn_211.png similarity index 100% rename from .project/screenshots/autoplay_turn_211.png rename to .project/reports/screenshots/autoplay_turn_211.png diff --git a/.project/screenshots/autoplay_turn_221.png b/.project/reports/screenshots/autoplay_turn_221.png similarity index 100% rename from .project/screenshots/autoplay_turn_221.png rename to .project/reports/screenshots/autoplay_turn_221.png diff --git a/.project/screenshots/autoplay_turn_231.png b/.project/reports/screenshots/autoplay_turn_231.png similarity index 100% rename from .project/screenshots/autoplay_turn_231.png rename to .project/reports/screenshots/autoplay_turn_231.png diff --git a/.project/screenshots/autoplay_turn_241.png b/.project/reports/screenshots/autoplay_turn_241.png similarity index 100% rename from .project/screenshots/autoplay_turn_241.png rename to .project/reports/screenshots/autoplay_turn_241.png diff --git a/.project/screenshots/autoplay_turn_251.png b/.project/reports/screenshots/autoplay_turn_251.png similarity index 100% rename from .project/screenshots/autoplay_turn_251.png rename to .project/reports/screenshots/autoplay_turn_251.png diff --git a/.project/screenshots/autoplay_turn_261.png b/.project/reports/screenshots/autoplay_turn_261.png similarity index 100% rename from .project/screenshots/autoplay_turn_261.png rename to .project/reports/screenshots/autoplay_turn_261.png diff --git a/.project/screenshots/autoplay_turn_271.png b/.project/reports/screenshots/autoplay_turn_271.png similarity index 100% rename from .project/screenshots/autoplay_turn_271.png rename to .project/reports/screenshots/autoplay_turn_271.png diff --git a/.project/screenshots/autoplay_turn_281.png b/.project/reports/screenshots/autoplay_turn_281.png similarity index 100% rename from .project/screenshots/autoplay_turn_281.png rename to .project/reports/screenshots/autoplay_turn_281.png diff --git a/.project/screenshots/autoplay_turn_291.png b/.project/reports/screenshots/autoplay_turn_291.png similarity index 100% rename from .project/screenshots/autoplay_turn_291.png rename to .project/reports/screenshots/autoplay_turn_291.png diff --git a/.project/screenshots/autoplay_turn_301.png b/.project/reports/screenshots/autoplay_turn_301.png similarity index 100% rename from .project/screenshots/autoplay_turn_301.png rename to .project/reports/screenshots/autoplay_turn_301.png diff --git a/.project/screenshots/autoplay_turn_311.png b/.project/reports/screenshots/autoplay_turn_311.png similarity index 100% rename from .project/screenshots/autoplay_turn_311.png rename to .project/reports/screenshots/autoplay_turn_311.png diff --git a/.project/screenshots/autoplay_turn_321.png b/.project/reports/screenshots/autoplay_turn_321.png similarity index 100% rename from .project/screenshots/autoplay_turn_321.png rename to .project/reports/screenshots/autoplay_turn_321.png diff --git a/.project/screenshots/autoplay_turn_331.png b/.project/reports/screenshots/autoplay_turn_331.png similarity index 100% rename from .project/screenshots/autoplay_turn_331.png rename to .project/reports/screenshots/autoplay_turn_331.png diff --git a/.project/screenshots/autoplay_turn_341.png b/.project/reports/screenshots/autoplay_turn_341.png similarity index 100% rename from .project/screenshots/autoplay_turn_341.png rename to .project/reports/screenshots/autoplay_turn_341.png diff --git a/.project/screenshots/autoplay_turn_351.png b/.project/reports/screenshots/autoplay_turn_351.png similarity index 100% rename from .project/screenshots/autoplay_turn_351.png rename to .project/reports/screenshots/autoplay_turn_351.png diff --git a/.project/screenshots/autoplay_turn_361.png b/.project/reports/screenshots/autoplay_turn_361.png similarity index 100% rename from .project/screenshots/autoplay_turn_361.png rename to .project/reports/screenshots/autoplay_turn_361.png diff --git a/.project/screenshots/autoplay_turn_371.png b/.project/reports/screenshots/autoplay_turn_371.png similarity index 100% rename from .project/screenshots/autoplay_turn_371.png rename to .project/reports/screenshots/autoplay_turn_371.png diff --git a/.project/screenshots/autoplay_turn_381.png b/.project/reports/screenshots/autoplay_turn_381.png similarity index 100% rename from .project/screenshots/autoplay_turn_381.png rename to .project/reports/screenshots/autoplay_turn_381.png diff --git a/.project/screenshots/autoplay_turn_391.png b/.project/reports/screenshots/autoplay_turn_391.png similarity index 100% rename from .project/screenshots/autoplay_turn_391.png rename to .project/reports/screenshots/autoplay_turn_391.png diff --git a/.project/screenshots/autoplay_turn_401.png b/.project/reports/screenshots/autoplay_turn_401.png similarity index 100% rename from .project/screenshots/autoplay_turn_401.png rename to .project/reports/screenshots/autoplay_turn_401.png diff --git a/.project/screenshots/autoplay_turn_411.png b/.project/reports/screenshots/autoplay_turn_411.png similarity index 100% rename from .project/screenshots/autoplay_turn_411.png rename to .project/reports/screenshots/autoplay_turn_411.png diff --git a/.project/screenshots/autoplay_victory_turn_418.png b/.project/reports/screenshots/autoplay_victory_turn_418.png similarity index 100% rename from .project/screenshots/autoplay_victory_turn_418.png rename to .project/reports/screenshots/autoplay_victory_turn_418.png diff --git a/.project/simulation-report/README.md b/.project/reports/simulation/README.md similarity index 100% rename from .project/simulation-report/README.md rename to .project/reports/simulation/README.md diff --git a/.project/simulation-report/balance/axis-viability.md b/.project/reports/simulation/balance/axis-viability.md similarity index 100% rename from .project/simulation-report/balance/axis-viability.md rename to .project/reports/simulation/balance/axis-viability.md diff --git a/.project/simulation-report/balance/tournament.md b/.project/reports/simulation/balance/tournament.md similarity index 100% rename from .project/simulation-report/balance/tournament.md rename to .project/reports/simulation/balance/tournament.md diff --git a/.project/simulation-report/baseline/report.md b/.project/reports/simulation/baseline/report.md similarity index 100% rename from .project/simulation-report/baseline/report.md rename to .project/reports/simulation/baseline/report.md diff --git a/.project/simulation-report/baseline/stats.md b/.project/reports/simulation/baseline/stats.md similarity index 100% rename from .project/simulation-report/baseline/stats.md rename to .project/reports/simulation/baseline/stats.md diff --git a/.project/simulation-report/baseline/story.md b/.project/reports/simulation/baseline/story.md similarity index 100% rename from .project/simulation-report/baseline/story.md rename to .project/reports/simulation/baseline/story.md diff --git a/.project/simulation-report/experiment-log.md b/.project/reports/simulation/experiment-log.md similarity index 100% rename from .project/simulation-report/experiment-log.md rename to .project/reports/simulation/experiment-log.md diff --git a/.project/simulation-report/mechanics.md b/.project/reports/simulation/mechanics.md similarity index 100% rename from .project/simulation-report/mechanics.md rename to .project/reports/simulation/mechanics.md diff --git a/.project/simulation-report/pvp/combat-stats.md b/.project/reports/simulation/pvp/combat-stats.md similarity index 100% rename from .project/simulation-report/pvp/combat-stats.md rename to .project/reports/simulation/pvp/combat-stats.md diff --git a/.project/simulation-report/pvp/conquest-map.md b/.project/reports/simulation/pvp/conquest-map.md similarity index 100% rename from .project/simulation-report/pvp/conquest-map.md rename to .project/reports/simulation/pvp/conquest-map.md diff --git a/.project/simulation-report/pvp/victory-paths.md b/.project/reports/simulation/pvp/victory-paths.md similarity index 100% rename from .project/simulation-report/pvp/victory-paths.md rename to .project/reports/simulation/pvp/victory-paths.md diff --git a/.project/simulation-report/scenarios/0ai/report.md b/.project/reports/simulation/scenarios/0ai/report.md similarity index 100% rename from .project/simulation-report/scenarios/0ai/report.md rename to .project/reports/simulation/scenarios/0ai/report.md diff --git a/.project/simulation-report/scenarios/0ai/stats.md b/.project/reports/simulation/scenarios/0ai/stats.md similarity index 100% rename from .project/simulation-report/scenarios/0ai/stats.md rename to .project/reports/simulation/scenarios/0ai/stats.md diff --git a/.project/simulation-report/scenarios/0ai/story.md b/.project/reports/simulation/scenarios/0ai/story.md similarity index 100% rename from .project/simulation-report/scenarios/0ai/story.md rename to .project/reports/simulation/scenarios/0ai/story.md diff --git a/.project/simulation-report/scenarios/1ai/report.md b/.project/reports/simulation/scenarios/1ai/report.md similarity index 100% rename from .project/simulation-report/scenarios/1ai/report.md rename to .project/reports/simulation/scenarios/1ai/report.md diff --git a/.project/simulation-report/scenarios/1ai/stats.md b/.project/reports/simulation/scenarios/1ai/stats.md similarity index 100% rename from .project/simulation-report/scenarios/1ai/stats.md rename to .project/reports/simulation/scenarios/1ai/stats.md diff --git a/.project/simulation-report/scenarios/1ai/story.md b/.project/reports/simulation/scenarios/1ai/story.md similarity index 100% rename from .project/simulation-report/scenarios/1ai/story.md rename to .project/reports/simulation/scenarios/1ai/story.md diff --git a/.project/simulation-report/scenarios/2ai/report.md b/.project/reports/simulation/scenarios/2ai/report.md similarity index 100% rename from .project/simulation-report/scenarios/2ai/report.md rename to .project/reports/simulation/scenarios/2ai/report.md diff --git a/.project/simulation-report/scenarios/2ai/stats.md b/.project/reports/simulation/scenarios/2ai/stats.md similarity index 100% rename from .project/simulation-report/scenarios/2ai/stats.md rename to .project/reports/simulation/scenarios/2ai/stats.md diff --git a/.project/simulation-report/scenarios/2ai/story.md b/.project/reports/simulation/scenarios/2ai/story.md similarity index 100% rename from .project/simulation-report/scenarios/2ai/story.md rename to .project/reports/simulation/scenarios/2ai/story.md diff --git a/.project/simulation-report/scenarios/3ai/report.md b/.project/reports/simulation/scenarios/3ai/report.md similarity index 100% rename from .project/simulation-report/scenarios/3ai/report.md rename to .project/reports/simulation/scenarios/3ai/report.md diff --git a/.project/simulation-report/scenarios/3ai/stats.md b/.project/reports/simulation/scenarios/3ai/stats.md similarity index 100% rename from .project/simulation-report/scenarios/3ai/stats.md rename to .project/reports/simulation/scenarios/3ai/stats.md diff --git a/.project/simulation-report/scenarios/3ai/story.md b/.project/reports/simulation/scenarios/3ai/story.md similarity index 100% rename from .project/simulation-report/scenarios/3ai/story.md rename to .project/reports/simulation/scenarios/3ai/story.md diff --git a/.project/simulation-report/scenarios/4ai/report.md b/.project/reports/simulation/scenarios/4ai/report.md similarity index 100% rename from .project/simulation-report/scenarios/4ai/report.md rename to .project/reports/simulation/scenarios/4ai/report.md diff --git a/.project/simulation-report/scenarios/4ai/stats.md b/.project/reports/simulation/scenarios/4ai/stats.md similarity index 100% rename from .project/simulation-report/scenarios/4ai/stats.md rename to .project/reports/simulation/scenarios/4ai/stats.md diff --git a/.project/simulation-report/scenarios/4ai/story.md b/.project/reports/simulation/scenarios/4ai/story.md similarity index 100% rename from .project/simulation-report/scenarios/4ai/story.md rename to .project/reports/simulation/scenarios/4ai/story.md diff --git a/.project/simulation-report/scenarios/comparison.md b/.project/reports/simulation/scenarios/comparison.md similarity index 100% rename from .project/simulation-report/scenarios/comparison.md rename to .project/reports/simulation/scenarios/comparison.md diff --git a/.project/simulation-report/tech-debt-audit.md b/.project/reports/simulation/tech-debt-audit.md similarity index 100% rename from .project/simulation-report/tech-debt-audit.md rename to .project/reports/simulation/tech-debt-audit.md diff --git a/.project/tasks/m0-foundation/README.md b/.project/tasks/milestones/m0-foundation/README.md similarity index 100% rename from .project/tasks/m0-foundation/README.md rename to .project/tasks/milestones/m0-foundation/README.md diff --git a/.project/tasks/m1-base-mundaneworld/README.md b/.project/tasks/milestones/m1-base-mundaneworld/README.md similarity index 100% rename from .project/tasks/m1-base-mundaneworld/README.md rename to .project/tasks/milestones/m1-base-mundaneworld/README.md diff --git a/.project/tasks/m2-flora-fauna/README.md b/.project/tasks/milestones/m2-flora-fauna/README.md similarity index 100% rename from .project/tasks/m2-flora-fauna/README.md rename to .project/tasks/milestones/m2-flora-fauna/README.md diff --git a/.project/tasks/m2-flora-fauna/RUST_IMPLEMENTATION.md b/.project/tasks/milestones/m2-flora-fauna/RUST_IMPLEMENTATION.md similarity index 100% rename from .project/tasks/m2-flora-fauna/RUST_IMPLEMENTATION.md rename to .project/tasks/milestones/m2-flora-fauna/RUST_IMPLEMENTATION.md diff --git a/.project/tasks/m2b-full-ecosystem/README.md b/.project/tasks/milestones/m2b-full-ecosystem/README.md similarity index 100% rename from .project/tasks/m2b-full-ecosystem/README.md rename to .project/tasks/milestones/m2b-full-ecosystem/README.md diff --git a/.project/tasks/m3-natural-events/README.md b/.project/tasks/milestones/m3-natural-events/README.md similarity index 100% rename from .project/tasks/m3-natural-events/README.md rename to .project/tasks/milestones/m3-natural-events/README.md diff --git a/.project/tasks/climate-balance/README.md b/.project/tasks/topics/climate-balance/README.md similarity index 100% rename from .project/tasks/climate-balance/README.md rename to .project/tasks/topics/climate-balance/README.md diff --git a/.project/tasks/ecology-balance/THESIS.md b/.project/tasks/topics/ecology-balance/THESIS.md similarity index 100% rename from .project/tasks/ecology-balance/THESIS.md rename to .project/tasks/topics/ecology-balance/THESIS.md diff --git a/public/games/age-of-dwarves/guide/eslint.config.js b/public/games/age-of-dwarves/guide/eslint.config.js index e87ec134..793d5c67 100644 --- a/public/games/age-of-dwarves/guide/eslint.config.js +++ b/public/games/age-of-dwarves/guide/eslint.config.js @@ -31,8 +31,8 @@ export default tseslint.config( prefer: 'type-imports', fixStyle: 'separate-type-imports', }], - // File length — project max 500 lines (CLAUDE.md) - '@lilith/file-length/file-length': ['error', { warnThreshold: 400, errorThreshold: 500 }], + // File length — 500 hard / 300 soft warn (project 3-language standard) + '@lilith/file-length/file-length': ['error', { warnThreshold: 300, errorThreshold: 500 }], // Import alias enforcement — prefer @/ over relative src/ paths 'import-alias/prefer-alias': 'error', diff --git a/public/games/age-of-elves/guide/eslint.config.js b/public/games/age-of-elves/guide/eslint.config.js index e9954a1a..0e089cba 100644 --- a/public/games/age-of-elves/guide/eslint.config.js +++ b/public/games/age-of-elves/guide/eslint.config.js @@ -31,8 +31,8 @@ export default tseslint.config( prefer: 'type-imports', fixStyle: 'separate-type-imports', }], - // File length — project max 500 lines (CLAUDE.md) - '@lilith/file-length/file-length': ['error', { warnThreshold: 400, errorThreshold: 500 }], + // File length — 500 hard / 300 soft warn (project 3-language standard) + '@lilith/file-length/file-length': ['error', { warnThreshold: 300, errorThreshold: 500 }], // Import alias enforcement — prefer @/ over relative src/ paths 'import-alias/prefer-alias': 'error', diff --git a/public/games/age-of-kzzkyt/guide/eslint.config.js b/public/games/age-of-kzzkyt/guide/eslint.config.js index e9954a1a..0e089cba 100644 --- a/public/games/age-of-kzzkyt/guide/eslint.config.js +++ b/public/games/age-of-kzzkyt/guide/eslint.config.js @@ -31,8 +31,8 @@ export default tseslint.config( prefer: 'type-imports', fixStyle: 'separate-type-imports', }], - // File length — project max 500 lines (CLAUDE.md) - '@lilith/file-length/file-length': ['error', { warnThreshold: 400, errorThreshold: 500 }], + // File length — 500 hard / 300 soft warn (project 3-language standard) + '@lilith/file-length/file-length': ['error', { warnThreshold: 300, errorThreshold: 500 }], // Import alias enforcement — prefer @/ over relative src/ paths 'import-alias/prefer-alias': 'error', diff --git a/src/simulator/Cargo.toml b/src/simulator/Cargo.toml index 13f11a33..83793dc4 100644 --- a/src/simulator/Cargo.toml +++ b/src/simulator/Cargo.toml @@ -34,6 +34,26 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" getrandom = "0.2" +# Workspace-wide lint configuration — Python-styled Rust. +# See ~/.claude/instructions/rust-code-standards.md §12 for rationale. +# Each crate inherits via `[lints] workspace = true` in its Cargo.toml. +[workspace.lints.clippy] +pedantic = { level = "warn", priority = -1 } +nursery = { level = "warn", priority = -1 } +unwrap_used = "deny" +expect_used = "warn" +panic = "deny" +todo = "deny" +unimplemented = "deny" +print_stdout = "deny" +print_stderr = "deny" +dbg_macro = "deny" +too_many_lines = "warn" + +[workspace.lints.rust] +unsafe_code = "warn" +missing_docs = "warn" + [profile.release] opt-level = 3 lto = true diff --git a/src/simulator/api-gdext/Cargo.toml b/src/simulator/api-gdext/Cargo.toml index 29b5a892..dd9808e2 100644 --- a/src/simulator/api-gdext/Cargo.toml +++ b/src/simulator/api-gdext/Cargo.toml @@ -22,3 +22,6 @@ mc-compute = { path = "../crates/mc-compute", features = ["gpu", "parallel"] } godot = "0.2" serde.workspace = true serde_json.workspace = true + +[lints] +workspace = true diff --git a/src/simulator/api-wasm/Cargo.toml b/src/simulator/api-wasm/Cargo.toml index 1b0de4b2..0d0b6a8e 100644 --- a/src/simulator/api-wasm/Cargo.toml +++ b/src/simulator/api-wasm/Cargo.toml @@ -17,3 +17,6 @@ serde-wasm-bindgen = "0.6" getrandom = { version = "0.2", features = ["js"] } serde.workspace = true serde_json.workspace = true + +[lints] +workspace = true diff --git a/src/simulator/crates/mc-ai/Cargo.toml b/src/simulator/crates/mc-ai/Cargo.toml index a9ec08f8..a04e5077 100644 --- a/src/simulator/crates/mc-ai/Cargo.toml +++ b/src/simulator/crates/mc-ai/Cargo.toml @@ -8,3 +8,6 @@ mc-core = { path = "../mc-core" } rayon = "1" serde.workspace = true serde_json.workspace = true + +[lints] +workspace = true diff --git a/src/simulator/crates/mc-balance/Cargo.toml b/src/simulator/crates/mc-balance/Cargo.toml index 013ce5e8..a58670c8 100644 --- a/src/simulator/crates/mc-balance/Cargo.toml +++ b/src/simulator/crates/mc-balance/Cargo.toml @@ -8,3 +8,6 @@ mc-ai = { path = "../mc-ai" } serde.workspace = true serde_json.workspace = true thiserror = "1" + +[lints] +workspace = true diff --git a/src/simulator/crates/mc-city/Cargo.toml b/src/simulator/crates/mc-city/Cargo.toml index 81f1d79e..a85eba46 100644 --- a/src/simulator/crates/mc-city/Cargo.toml +++ b/src/simulator/crates/mc-city/Cargo.toml @@ -8,3 +8,6 @@ mc-core = { path = "../mc-core" } mc-economy = { path = "../mc-economy" } serde.workspace = true serde_json.workspace = true + +[lints] +workspace = true diff --git a/src/simulator/crates/mc-climate/Cargo.toml b/src/simulator/crates/mc-climate/Cargo.toml index b60887b1..92b4ace0 100644 --- a/src/simulator/crates/mc-climate/Cargo.toml +++ b/src/simulator/crates/mc-climate/Cargo.toml @@ -8,3 +8,6 @@ mc-core = { path = "../mc-core" } serde.workspace = true serde_json.workspace = true getrandom.workspace = true + +[lints] +workspace = true diff --git a/src/simulator/crates/mc-combat/Cargo.toml b/src/simulator/crates/mc-combat/Cargo.toml index bdcbf2fa..21f74bf4 100644 --- a/src/simulator/crates/mc-combat/Cargo.toml +++ b/src/simulator/crates/mc-combat/Cargo.toml @@ -7,3 +7,6 @@ edition = "2021" mc-core = { path = "../mc-core" } serde.workspace = true serde_json.workspace = true + +[lints] +workspace = true diff --git a/src/simulator/crates/mc-compute/Cargo.toml b/src/simulator/crates/mc-compute/Cargo.toml index f69ebce1..4c2d2a54 100644 --- a/src/simulator/crates/mc-compute/Cargo.toml +++ b/src/simulator/crates/mc-compute/Cargo.toml @@ -20,3 +20,6 @@ rayon = { version = "1.10", optional = true } [dev-dependencies] serde_json = "1" + +[lints] +workspace = true diff --git a/src/simulator/crates/mc-core/Cargo.toml b/src/simulator/crates/mc-core/Cargo.toml index 14e3dca5..42a5945c 100644 --- a/src/simulator/crates/mc-core/Cargo.toml +++ b/src/simulator/crates/mc-core/Cargo.toml @@ -8,3 +8,6 @@ serde.workspace = true serde_json.workspace = true getrandom.workspace = true rayon = "1" + +[lints] +workspace = true diff --git a/src/simulator/crates/mc-culture/Cargo.toml b/src/simulator/crates/mc-culture/Cargo.toml index 3b5ad510..728b0fef 100644 --- a/src/simulator/crates/mc-culture/Cargo.toml +++ b/src/simulator/crates/mc-culture/Cargo.toml @@ -7,3 +7,6 @@ edition = "2021" mc-core = { path = "../mc-core" } serde.workspace = true serde_json.workspace = true + +[lints] +workspace = true diff --git a/src/simulator/crates/mc-ecology/Cargo.toml b/src/simulator/crates/mc-ecology/Cargo.toml index 5b9afde7..77531134 100644 --- a/src/simulator/crates/mc-ecology/Cargo.toml +++ b/src/simulator/crates/mc-ecology/Cargo.toml @@ -19,3 +19,6 @@ rayon = "1" [[bin]] name = "ecology_bench" path = "src/bin/ecology_bench.rs" + +[lints] +workspace = true diff --git a/src/simulator/crates/mc-economy/Cargo.toml b/src/simulator/crates/mc-economy/Cargo.toml index c02a8868..57d184ba 100644 --- a/src/simulator/crates/mc-economy/Cargo.toml +++ b/src/simulator/crates/mc-economy/Cargo.toml @@ -7,3 +7,6 @@ edition = "2021" mc-core = { path = "../mc-core" } serde.workspace = true serde_json.workspace = true + +[lints] +workspace = true diff --git a/src/simulator/crates/mc-flora/Cargo.toml b/src/simulator/crates/mc-flora/Cargo.toml index 0d561c47..6d97efd4 100644 --- a/src/simulator/crates/mc-flora/Cargo.toml +++ b/src/simulator/crates/mc-flora/Cargo.toml @@ -13,3 +13,6 @@ rayon = "1" [[bin]] name = "flora_bench" path = "src/bin/flora_bench.rs" + +[lints] +workspace = true diff --git a/src/simulator/crates/mc-happiness/Cargo.toml b/src/simulator/crates/mc-happiness/Cargo.toml index 57cafda1..0a4ca220 100644 --- a/src/simulator/crates/mc-happiness/Cargo.toml +++ b/src/simulator/crates/mc-happiness/Cargo.toml @@ -7,3 +7,6 @@ edition = "2021" mc-core = { path = "../mc-core" } serde.workspace = true serde_json.workspace = true + +[lints] +workspace = true diff --git a/src/simulator/crates/mc-items/Cargo.toml b/src/simulator/crates/mc-items/Cargo.toml index f5210f7d..963e49a2 100644 --- a/src/simulator/crates/mc-items/Cargo.toml +++ b/src/simulator/crates/mc-items/Cargo.toml @@ -6,3 +6,6 @@ edition = "2021" [dependencies] serde.workspace = true serde_json.workspace = true + +[lints] +workspace = true diff --git a/src/simulator/crates/mc-magic/Cargo.toml b/src/simulator/crates/mc-magic/Cargo.toml index 4f407ab5..49d6ea2c 100644 --- a/src/simulator/crates/mc-magic/Cargo.toml +++ b/src/simulator/crates/mc-magic/Cargo.toml @@ -7,3 +7,6 @@ edition = "2021" mc-core = { path = "../mc-core" } serde.workspace = true serde_json.workspace = true + +[lints] +workspace = true diff --git a/src/simulator/crates/mc-mapgen/Cargo.toml b/src/simulator/crates/mc-mapgen/Cargo.toml index 5a71da3d..5c32f1e8 100644 --- a/src/simulator/crates/mc-mapgen/Cargo.toml +++ b/src/simulator/crates/mc-mapgen/Cargo.toml @@ -8,3 +8,6 @@ mc-core = { path = "../mc-core" } serde.workspace = true serde_json.workspace = true getrandom.workspace = true + +[lints] +workspace = true diff --git a/src/simulator/crates/mc-observation/Cargo.toml b/src/simulator/crates/mc-observation/Cargo.toml index 0236d334..83f76567 100644 --- a/src/simulator/crates/mc-observation/Cargo.toml +++ b/src/simulator/crates/mc-observation/Cargo.toml @@ -7,3 +7,6 @@ edition = "2021" mc-core = { path = "../mc-core" } serde.workspace = true serde_json.workspace = true + +[lints] +workspace = true diff --git a/src/simulator/crates/mc-sim/Cargo.toml b/src/simulator/crates/mc-sim/Cargo.toml index eebcb274..562852bb 100644 --- a/src/simulator/crates/mc-sim/Cargo.toml +++ b/src/simulator/crates/mc-sim/Cargo.toml @@ -43,3 +43,6 @@ path = "src/bin/gpu_bench.rs" [[bin]] name = "disease_validate" path = "src/bin/disease_validate.rs" + +[lints] +workspace = true diff --git a/src/simulator/crates/mc-tech/Cargo.toml b/src/simulator/crates/mc-tech/Cargo.toml index fce9db41..862c0b7a 100644 --- a/src/simulator/crates/mc-tech/Cargo.toml +++ b/src/simulator/crates/mc-tech/Cargo.toml @@ -8,3 +8,6 @@ mc-core = { path = "../mc-core" } serde.workspace = true serde_json.workspace = true thiserror = "1" + +[lints] +workspace = true diff --git a/src/simulator/crates/mc-trade/Cargo.toml b/src/simulator/crates/mc-trade/Cargo.toml index 18f32004..2470b53f 100644 --- a/src/simulator/crates/mc-trade/Cargo.toml +++ b/src/simulator/crates/mc-trade/Cargo.toml @@ -6,3 +6,6 @@ edition = "2021" [dependencies] serde.workspace = true serde_json.workspace = true + +[lints] +workspace = true diff --git a/src/simulator/crates/mc-turn/Cargo.toml b/src/simulator/crates/mc-turn/Cargo.toml index 11847bd9..727299ff 100644 --- a/src/simulator/crates/mc-turn/Cargo.toml +++ b/src/simulator/crates/mc-turn/Cargo.toml @@ -22,3 +22,6 @@ bytemuck = { version = "1", features = ["derive"], optional = true } proptest = "1" mc-economy = { path = "../mc-economy" } mc-happiness = { path = "../mc-happiness" } + +[lints] +workspace = true diff --git a/src/simulator/tests/golden/README.md b/src/simulator/tests/golden/README.md new file mode 100644 index 00000000..020a5032 --- /dev/null +++ b/src/simulator/tests/golden/README.md @@ -0,0 +1,97 @@ +# Golden Vector Test Harness + +Source of truth for **FFI parity** between Rust (native), Rust→WASM (web guide), and Rust→GDExtension (Godot game). + +Each canonical input fixture in `vectors/.json` is consumed by **three** test runners. All three MUST produce **bitwise-identical** output. Divergence = release blocker (marshaling bug, non-determinism, or SOT violation). + +## Why this exists + +The project's Rust simulation compiles to two FFI targets plus runs natively in pure Rust tests: + +``` +src/simulator/crates/mc-* ← SOURCE OF TRUTH (pure Rust) + │ + ├─ native test: cargo test -p mc- --test golden + │ + ├─ api-wasm → pkg/magic_civ_physics_bg.wasm + │ consumed by: pnpm --filter guide-age-of-dwarves test golden + │ (Vitest test runs inside simulation.worker.ts) + │ + └─ api-gdext → src/game/addons/magic_civ_physics/*.so + consumed by: headless Godot + GUT + gut -gtest=res://engine/tests/ffi/test_golden_.gd +``` + +If any of the three emits different output for the same seeded input, we have one of: +1. An FFI marshaling bug (type width, enum numbering drift, serde field rename skew). +2. Non-determinism (wall-clock time, unseeded `rand`, `HashMap` iteration order, parallel-order-sensitive fold). +3. An SOT violation — logic duplicated in GDScript or TypeScript instead of living in Rust. + +All three are release blockers. + +## Vector file shape + +```json +{ + "domain": "combat", + "vector_name": "attacker_wins_ranged_with_first_strike", + "description": "Ranged archer T3 vs melee warrior T2, flat terrain, no promotions", + "rust_version": "0.1.0", + "schema_version": 1, + "seed": 0xDEADBEEF, + "input": { + "attacker": { "unit_def": "archer", "tier": 3, "hp": 10.0, "xp": 0 }, + "defender": { "unit_def": "warrior", "tier": 2, "hp": 10.0, "xp": 0 }, + "terrain": "grassland", + "range": 2 + }, + "expected_output": { + "attacker_hp_after": 10.0, + "defender_hp_after": 6.5, + "defender_died": false, + "xp_awarded": { "attacker": 2, "defender": 1 }, + "hit_rolls": [0.42, 0.18] + } +} +``` + +## What domains must have vectors + +Minimum **3 vectors per domain** (happy path, edge case, stress): + +| Domain | Rust crate | Scope of coverage | +|---|---|---| +| hex math | `mc-core` | distance, ring, spiral, line, neighbor lookup | +| pathfinding | `mc-core` | A* base, A* with ZOC, A* flying, A* road bonus | +| combat | `mc-combat` | resolve, keywords (first_strike, trample, poison), flanking | +| climate | `mc-climate` | one turn of atmosphere, one ecology tick, seeded events | +| mapgen | `mc-mapgen` | seeded continent generation, biome assignment | +| economy | `mc-economy` | one turn's income/upkeep/net, overflow edge | +| happiness | `mc-happiness` | pool recompute across racial tiers, golden-age trigger | +| culture | `mc-culture` | border expansion, city acquisition | +| tech | `mc-tech` | TechWeb state transitions, gating | +| city | `mc-city` | growth, production, building completion | +| turn | `mc-turn` | full 10-turn simulation composition, invariants hold | + +## Adding a new vector + +1. Create `vectors/__.json` following the shape above. +2. Rust consumer: add a `#[test]` in `src/simulator/crates/mc-/tests/golden.rs` that reads the file and asserts. +3. TS/WASM consumer: add a case in `public/games/age-of-dwarves/guide/src/simulation/golden.test.ts`. +4. GDExt consumer: add a case in `src/game/engine/tests/ffi/test_golden_.gd`. +5. Run `./run test:golden` — all three must pass. If any diverge, fix the marshaling / determinism / SOT bug first. + +## Determinism rules + +- All randomness MUST take an explicit `seed` parameter; no wall-clock time, no `SystemTime::now()`. +- No `HashMap` iteration order in output — use `BTreeMap` or sort before serializing. +- No parallel-order-sensitive reductions without a deterministic combine step. +- CI runs each vector **twice** and diffs the output; any mismatch fails the job. + +## Runner contract + +`./run test:golden` is the cross-language driver. It must: +1. Run all three consumers. +2. Diff each consumer's emitted JSON against the fixture's `expected_output`. +3. Diff each consumer's output against the other two consumers' outputs. +4. Exit non-zero on any mismatch, with a three-way diff for the failing vector. diff --git a/src/simulator/tests/golden/vectors/.gitkeep b/src/simulator/tests/golden/vectors/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/simulator/tests/integration/Cargo.toml b/src/simulator/tests/integration/Cargo.toml index 8d1711c7..fe49d760 100644 --- a/src/simulator/tests/integration/Cargo.toml +++ b/src/simulator/tests/integration/Cargo.toml @@ -12,3 +12,6 @@ mc-core = { path = "../../crates/mc-core" } mc-city = { path = "../../crates/mc-city" } mc-happiness = { path = "../../crates/mc-happiness" } mc-combat = { path = "../../crates/mc-combat" } + +[lints] +workspace = true diff --git a/tools/test_personality_winrate.py b/tools/test_personality_winrate.py new file mode 100644 index 00000000..62aefe71 --- /dev/null +++ b/tools/test_personality_winrate.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python3 +"""Tests for personality win-rate tracking in autoplay-report and checklist-report.""" +from __future__ import annotations + +import sys +import io +import importlib.util as _iu +from pathlib import Path + +_TOOLS = Path(__file__).parent + + +def _load(name: str, stem: str): + path = _TOOLS / f"{stem}.py" + spec = _iu.spec_from_file_location(name, path) + mod = _iu.module_from_spec(spec) # type: ignore[arg-type] + spec.loader.exec_module(mod) # type: ignore[union-attr] + return mod + + +ar = _load("autoplay_report", "autoplay-report") +cr = _load("checklist_report", "checklist-report") + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + +def _row(outcome: str, winner_personality: str, winner_index: int = 1) -> dict: + return { + "outcome": outcome, + "winner_index": winner_index, + "winner_personality": winner_personality, + "turns_played": 200, + "victory_type": "score" if outcome == "victory" else "", + "wall_clock_sec": 10.0, + "event_count": 50, + "invariant_violations": 0, + "p0_pop_peak": 20, + "p0_gold_peak": 100, + "agg_total_combats": 30, + } + + +def _result(outcome: str, winner_personality: str) -> tuple[int, dict]: + return (1, { + "outcome": outcome, + "winner_personality": winner_personality, + "turns": 200, + "pop_peak": 20, + "p0_tiles": 25, + "p0_techs": 22, + "combats": 120, + "happy_distinct": 4, + "imp_events": 6, + "loot_events": 2, + "gate_events": 1, + "both_p100": True, + "invariants": 0, + "script_errors": 0, + }) + + +# --------------------------------------------------------------------------- +# autoplay-report: build_personality_win_table +# --------------------------------------------------------------------------- + +def test_win_table_empty_rows(): + assert ar.build_personality_win_table([]) == {} + + +def test_win_table_skips_empty_personality(): + rows = [_row("victory", ""), _row("max_turns", "")] + assert ar.build_personality_win_table(rows) == {} + + +def test_win_table_single_clan_all_wins(): + rows = [_row("victory", "ironhold"), _row("victory", "ironhold")] + table = ar.build_personality_win_table(rows) + assert table["ironhold"]["wins"] == 2 + assert table["ironhold"]["appearances"] == 2 + assert table["ironhold"]["losses"] == 0 + + +def test_win_table_single_clan_no_wins(): + rows = [_row("max_turns", "blackhammer"), _row("max_turns", "blackhammer")] + table = ar.build_personality_win_table(rows) + assert table["blackhammer"]["wins"] == 0 + assert table["blackhammer"]["appearances"] == 2 + assert table["blackhammer"]["losses"] == 2 + + +def test_win_table_multiple_clans(): + rows = [ + _row("victory", "ironhold"), + _row("victory", "goldvein"), + _row("max_turns", "ironhold"), + _row("victory", "runesmith"), + ] + table = ar.build_personality_win_table(rows) + assert table["ironhold"]["wins"] == 1 + assert table["ironhold"]["appearances"] == 2 + assert table["goldvein"]["wins"] == 1 + assert table["goldvein"]["appearances"] == 1 + assert table["runesmith"]["wins"] == 1 + assert table["runesmith"]["appearances"] == 1 + + +def test_win_table_losses_computed(): + rows = [_row("victory", "deepforge"), _row("max_turns", "deepforge")] + table = ar.build_personality_win_table(rows) + assert table["deepforge"]["losses"] == 1 + + +# --------------------------------------------------------------------------- +# autoplay-report: print_personality_summary +# --------------------------------------------------------------------------- + +def test_print_personality_summary_no_data(): + out = io.StringIO() + ar.print_personality_summary([], out=out) + assert "no data" in out.getvalue() + + +def test_print_personality_summary_imbalanced_flag(): + rows = [_row("victory", "blackhammer")] * 3 + [_row("max_turns", "blackhammer")] + out = io.StringIO() + ar.print_personality_summary(rows, out=out) + assert "IMBALANCED" in out.getvalue() + assert "blackhammer" in out.getvalue() + + +def test_print_personality_summary_balanced_no_flag(): + rows = [_row("victory", "runesmith"), _row("max_turns", "runesmith")] + out = io.StringIO() + ar.print_personality_summary(rows, out=out) + assert "IMBALANCED" not in out.getvalue() + + +# --------------------------------------------------------------------------- +# checklist-report: personality_win_balance +# --------------------------------------------------------------------------- + +def test_pwb_no_data(): + ok, detail = cr.personality_win_balance([]) + assert ok is True + assert "no data" in detail + + +def test_pwb_empty_personality_ignored(): + results = [_result("victory", ""), _result("max_turns", "")] + ok, detail = cr.personality_win_balance(results) + assert ok is True + assert "no data" in detail + + +def test_pwb_all_wins_imbalanced(): + results = [_result("victory", "ironhold")] * 3 + ok, detail = cr.personality_win_balance(results) + assert ok is False + assert "ironhold" in detail + + +def test_pwb_exactly_50_percent_passes(): + results = [_result("victory", "goldvein"), _result("max_turns", "goldvein")] + ok, detail = cr.personality_win_balance(results) + assert ok is True + + +def test_pwb_mixed_clans_balanced(): + results = [ + _result("victory", "ironhold"), + _result("max_turns", "ironhold"), + _result("victory", "runesmith"), + _result("max_turns", "runesmith"), + ] + ok, detail = cr.personality_win_balance(results) + assert ok is True + assert "ironhold" in detail + assert "runesmith" in detail + + +def test_pwb_one_clan_over_threshold(): + results = [ + _result("victory", "blackhammer"), + _result("victory", "blackhammer"), + _result("max_turns", "blackhammer"), + _result("victory", "runesmith"), + _result("max_turns", "runesmith"), + ] + ok, detail = cr.personality_win_balance(results) + assert ok is False + assert "blackhammer" in detail + + +def test_pwb_detail_contains_win_counts(): + results = [_result("victory", "deepforge"), _result("max_turns", "deepforge")] + _, detail = cr.personality_win_balance(results) + assert "1/2" in detail + + +# --------------------------------------------------------------------------- +# Runner +# --------------------------------------------------------------------------- + +if __name__ == "__main__": + tests = [v for k, v in sorted(globals().items()) if k.startswith("test_")] + passed = failed = 0 + for t in tests: + try: + t() + print(f" PASS {t.__name__}") + passed += 1 + except Exception as exc: + print(f" FAIL {t.__name__}: {exc}") + failed += 1 + print(f"\n{passed} passed, {failed} failed") + sys.exit(0 if failed == 0 else 1)