From 2d9554d9ff31c1c67b5599d091343cf9089752d0 Mon Sep 17 00:00:00 2001 From: Natalie Date: Fri, 17 Apr 2026 13:06:14 -0700 Subject: [PATCH] =?UTF-8?q?feat(@projects):=20=E2=9C=A8=20update=20wasm=20?= =?UTF-8?q?build=20and=20guide=20deployment=20workflows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .forgejo/workflows/release.yml | 2 +- .gitignore | 4 + .../20260417_guide_web_deploy_audit.md | 13 ++ .project/objectives/README.md | 32 +++-- .../objectives/p0-20-gpu-mcts-rollouts.md | 31 +++++ ...d => p0-23-sprite-rendering-capability.md} | 2 +- .project/objectives/p2-09-guide-web-deploy.md | 4 +- .../objectives/p2-14-additional-races-oos.md | 2 +- .../games/age-of-dwarves/data/objectives.json | 122 +++++++++--------- public/games/age-of-dwarves/guide/CLAUDE.md | 9 +- .../age-of-dwarves/guide/src/ambient.d.ts | 5 +- scripts/run/build.sh | 2 +- scripts/run/dev.sh | 12 ++ scripts/run/verify.sh | 29 ++++- .../engine/src/rendering/unit_renderer.gd | 56 ++++---- src/packages/guide/src/types/ambient.d.ts | 13 +- src/simulator/package.json | 8 +- src/simulator/tests/golden/README.md | 4 +- tooling/claude/CLAUDE.md | 2 +- tooling/claude/dot-claude/agents/guide-web.md | 7 +- .../dot-claude/agents/simulator-infra.md | 28 ++-- .../claude/dot-claude/instructions/README.md | 2 +- .../instructions/build-output-locations.md | 63 ++++++++- .../instructions/rust-source-of-truth.md | 7 +- tools/deploy-guide.sh | 15 ++- tools/objectives-report.py | 53 +++++++- 26 files changed, 382 insertions(+), 145 deletions(-) rename .project/objectives/{p0-22-sprite-rendering-capability.md => p0-23-sprite-rendering-capability.md} (99%) diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml index 53cb8dca..20b91426 100644 --- a/.forgejo/workflows/release.yml +++ b/.forgejo/workflows/release.yml @@ -244,7 +244,7 @@ jobs: archive_name="magic-civilization-$version-wasm.tar.gz" archive_path="$out_dir/$archive_name" mkdir -p "$out_dir/staging/pkg" "$out_dir/staging/guide" - cp -r src/simulator/pkg/. "$out_dir/staging/pkg/" + cp -r .local/build/wasm/. "$out_dir/staging/pkg/" cp -r public/games/age-of-dwarves/guide/dist/. "$out_dir/staging/guide/" tar -C "$out_dir/staging" -czf "$archive_path" . rm -rf "$out_dir/staging" diff --git a/.gitignore b/.gitignore index 36c61091..9de006da 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,10 @@ build/ # Rust build artifacts src/simulator/target/ +# Safety net: src/ is source-only. wasm-pack's default --out-dir is +# /pkg/; build-wasm.sh overrides to .local/build/wasm/, but pin this +# path down in case anyone runs wasm-pack directly. +src/simulator/pkg/ .local/ # Python bytecode diff --git a/.project/history/20260417_guide_web_deploy_audit.md b/.project/history/20260417_guide_web_deploy_audit.md index 09fc58a2..ce2eb47e 100644 --- a/.project/history/20260417_guide_web_deploy_audit.md +++ b/.project/history/20260417_guide_web_deploy_audit.md @@ -94,3 +94,16 @@ TypeScript errors") is measurably false at 32 errors. Fix is scoped to `guide-web`, not `devops-engineer`. Dashboard citation for this pass shows the deploy script + one-level-deep import fix + partial re-exports; full `done` unblocks after guide-web closes the drift. + +--- + +## Footer β€” 2026-04-17 (tourguide, path relocation) + +References above to `src/simulator/pkg/` are historical as of this date. +The wasm-pack output was relocated to `.local/build/wasm/` per objective +p1-11 (`build-output-src-purge`) as part of enforcing the project-wide +"build output never under `src/`" rule. See +`.claude/instructions/build-output-locations.md` for the canonical rule +and `./run verify` step 16 (`_verify_no_build_in_src`) for the +mechanical enforcement. Existing entries above are preserved verbatim +as a record of the pre-relocation audit and are not rewritten. diff --git a/.project/objectives/README.md b/.project/objectives/README.md index d1214baa..95faf197 100644 --- a/.project/objectives/README.md +++ b/.project/objectives/README.md @@ -8,14 +8,30 @@ ## Totals -| Status | Count | +
+ +**By Priority** + +| Priority | βœ… | 🟑 | πŸ”΄ | ❌ | ⚫ | Total | +|---|---|---|---|---|---|---| +| **P0** | 19 | 4 | 0 | 0 | 0 | 23 | +| **P1** | 7 | 2 | 0 | 3 | 0 | 12 | +| **P2** | 7 | 6 | 0 | 2 | 0 | 15 | +| **P3 (oos)** | 0 | 0 | 0 | 0 | 9 | 9 | +| **total** | **33** | **12** | **0** | **5** | **9** | **59** | + + + +**Left To Do by Lead** + +| Team Lead | Remaining | |---|---| -| βœ… done | 33 | -| 🟑 partial | 12 | -| πŸ”΄ stub | 0 | -| ❌ missing | 5 | -| ⚫ oos | 9 | -| **total** | **59** | +| [shipwright](../team-leads/shipwright.md) | 6 | +| [warcouncil](../team-leads/warcouncil.md) | 3 | +| [tourguide](../team-leads/tourguide.md) | 3 | +| [testwright](../team-leads/testwright.md) | 2 | + +
## P0 β€” Blockers for "completely playable" @@ -42,8 +58,8 @@ | [p0-19](p0-19-biome-economy-integration.md) | βœ… done | Biome-driven collectibles β†’ tile yields β†’ happiness end-to-end | β€” | 2026-04-16 | | [p0-20](p0-20-gpu-mcts-rollouts.md) | 🟑 partial | GPU-accelerated MCTS rollouts for look-ahead decision-making | [warcouncil](../team-leads/warcouncil.md) | 2026-04-17 | | [p0-21](p0-21-audio-system-capability.md) | βœ… done | Audio system capability β€” manifest + autoload + EventBus wiring | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | -| [p0-22](p0-22-sprite-rendering-capability.md) | 🟑 partial | Sprite rendering capability β€” replace procedural draw_* with texture rendering | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | | [p0-22](p0-22-ultimate-ai-stress-test.md) | 🟑 partial | Ultimate AI stress test β€” 5 clans, huge map, deep lookahead | [warcouncil](../team-leads/warcouncil.md) | 2026-04-17 | +| [p0-23](p0-23-sprite-rendering-capability.md) | 🟑 partial | Sprite rendering capability β€” replace procedural draw_* with texture rendering | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | ## P1 β€” Ship-readiness diff --git a/.project/objectives/p0-20-gpu-mcts-rollouts.md b/.project/objectives/p0-20-gpu-mcts-rollouts.md index dfd9c526..c405bfab 100644 --- a/.project/objectives/p0-20-gpu-mcts-rollouts.md +++ b/.project/objectives/p0-20-gpu-mcts-rollouts.md @@ -86,6 +86,37 @@ byte-for-byte port of `rollout::walk`. apricot with `vulkan-tools` + `mesa-vulkan-drivers` added alongside `weston`. Rebooted bootc images now include these without needing the transient script. +### 2026-04-17 update β€” fresh A5 attempt post-fix (failed on host SIGTERM) + +After the four WGSL parity fixes landed and GDExtension rebuilt, fresh A5 +batches were attempted under multiple process-isolation strategies: + +| Strategy | Batch dir | Result | +|---|---|---| +| plain nohup | `.local/iter/a5-fresh-20260417_122847/` | exit 143, seeds `in_progress` T5–T10 before kill | +| nohup + new dir | `.local/iter/a5-final-20260417_122936/` | games launched, no completion.marker written (process killed) | +| bash SIGTERM trap | `.local/iter/a5-trap-20260417_123021/` | trap handler received NO signal; script exited rc=143 | +| strace signal trace | `.local/iter/a5-strace-20260417_123200/` | revealed autoplay-batch.sh exits status **1** (not 143); no SIGTERM to parent. Root cause: `0/N games produced turn_stats.jsonl` check fires because flatpak Godot scopes end at 3–10s | +| `systemd-run --user` | `.local/iter/warcouncil-a5-systemd-*/` | same β€” service `Active: inactive (dead)` after 2s, scope children SIGTERMed | +| `KillMode=none` | `.local/iter/warcouncil-a5-systemd-*` (2nd) | games reached T9–T10 only; same kill pattern | +| plain `bash autoplay-batch` synchronous | `.local/iter/a5-direct-123300/` | 10 games with 0-line `turn_stats.jsonl` β€” games get SIGTERMed during map generation | + +Seven distinct execution strategies, same failure pattern: flatpak Godot +scopes SIGTERMed within 3–10s of launch, before any turn completes. Investigation +found the signal is NOT delivered by systemd-oomd (failed service), rpm-ostree +automatic updates (timer inactive), or apricot-rail-watchdog (emit-only). The +actual SIGTERM source could not be identified in the apricot user session. +Parallel agent's own batches from earlier the same day (e.g. +`.local/batches/blackhammer_tune_20260417_101447/`) completed fine, so the +issue is transient/session-bound, NOT a permanent host failure. + +**Fresh A5 verdict β€” NOT HEALTHY, B5 therefore not launched.** Per +warcouncil's integrity rule: we report the measurement failure honestly +rather than claim parity-fix-correctness translated into fresh gameplay +evidence. Existing p0-01 batch data from pre-parity-fix binary (at +`blackhammer_tune_20260417_101447`) still stands as the most recent +successful A5/B5 evidence in the repo. + ## Design outline - `AbstractRolloutState` β€” a ~256 byte `#[repr(C)]` `Pod + Zeroable` compression of diff --git a/.project/objectives/p0-22-sprite-rendering-capability.md b/.project/objectives/p0-23-sprite-rendering-capability.md similarity index 99% rename from .project/objectives/p0-22-sprite-rendering-capability.md rename to .project/objectives/p0-23-sprite-rendering-capability.md index e66ef918..b70b3411 100644 --- a/.project/objectives/p0-22-sprite-rendering-capability.md +++ b/.project/objectives/p0-23-sprite-rendering-capability.md @@ -1,5 +1,5 @@ --- -id: p0-22 +id: p0-23 title: Sprite rendering capability β€” replace procedural draw_* with texture rendering priority: p0 status: partial diff --git a/.project/objectives/p2-09-guide-web-deploy.md b/.project/objectives/p2-09-guide-web-deploy.md index 6f69116a..780e4921 100644 --- a/.project/objectives/p2-09-guide-web-deploy.md +++ b/.project/objectives/p2-09-guide-web-deploy.md @@ -26,7 +26,7 @@ evidence: - tools/deploy-guide.sh - .project/history/20260417_guide_web_deploy_audit.md acceptance_audit: - build_zero_ts_errors: "🟑 β€” `pnpm typecheck` now reports 0 errors in both `src/packages/guide` (was 488) and `public/games/age-of-dwarves/guide` (was 221). Vite build transforms 3339 modules cleanly; final bundle step fails only on missing `src/simulator/pkg/magic_civ_physics.js` (WASM pkg is a RUN-host artifact built on apricot via `bash build-wasm.sh`, not on EDIT host). Scope-narrow Option 2 landed per CLAUDE.md Game 1 rule: deleted 10 Game-2 pages (Magic schools, Archons, Disciplines, LeyLines, Spells, Ep2/Ep3, Hive, Silvandel). Remaining: run WASM build on apricot and re-run `pnpm build` to produce dist/." + build_zero_ts_errors: "🟑 β€” `pnpm typecheck` now reports 0 errors in both `src/packages/guide` (was 488) and `public/games/age-of-dwarves/guide` (was 221). Vite build transforms 3339 modules cleanly; final bundle step fails only on missing `.local/build/wasm/magic_civ_physics.js` (WASM artifact is per-host, built via `bash src/simulator/build-wasm.sh` locally or on apricot and rsync'd; path relocated from `src/simulator/pkg/` β†’ `.local/build/wasm/` per p1-11 on 2026-04-17 β€” build output never under src/). Scope-narrow Option 2 landed per CLAUDE.md Game 1 rule: deleted 10 Game-2 pages (Magic schools, Archons, Disciplines, LeyLines, Spells, Ep2/Ep3, Hive, Silvandel). Remaining: run WASM build and re-run `pnpm build` to produce dist/." deployed_to_public_url: "βœ— β€” no external hosting target committed for Early Access. `tools/deploy-guide.sh apricot` ships dist/ to apricot:~/public/guide// for LAN preview; zip mode produces a handoff artifact for whichever public host wins. External hosting decision remains out-of-scope for this objective." game_json_import_fixed: "βœ“ β€” public/games/age-of-dwarves/guide/src/app/guide-data.ts import resolves to the canonical public/games/age-of-dwarves/game.json. Vite transform phase passes cleanly; verified as part of the full 3339-module transform in this pass." data_driven_content: "βœ“ β€” the guide reads data via `import.meta.glob('../../../games/age-of-dwarves/data/*.json')` per guide CLAUDE.md convention. Architecture unchanged. Changing a deposit JSON propagates on the next `pnpm build`." @@ -59,7 +59,7 @@ Per CLAUDE.md's hard Game-1 scope rule (*"do NOT ship Game 2 features into Game - **Path aliases:** consumer's `@magic-civ/*` paths were off-by-one (`../../../` β†’ `../../../../`); fixed. Added `@magic-civ/web-civmap` alias to guide-engine's tsconfig. - **Null-guard fixes:** eight consumer pages now guard optional fields before dereferencing (UnitsPage, CommunicationsPage, EncyclopediaModal, EncyclopediaPage, WondersPage, LairsPage, LensesPage, DevSpritesPage). -**Remaining blocker to flip βœ… done:** `pnpm --filter @magic-civilization/guide-age-of-dwarves build` fails at the final rollup step because `src/simulator/pkg/magic_civ_physics.js` is absent on the EDIT host (WASM is an apricot-built artifact per CLAUDE.md two-host workflow). Apricot was unreachable during this pass (`ssh lilith@apricot.local` timed out). Once apricot is reachable: +**Remaining blocker to flip βœ… done:** `pnpm --filter @magic-civilization/guide-age-of-dwarves build` fails at the final rollup step because `.local/build/wasm/magic_civ_physics.js` is absent on the EDIT host (WASM is a per-host artifact; see `.claude/instructions/build-output-locations.md`, path was relocated from `src/simulator/pkg/` per p1-11 on 2026-04-17). Apricot was unreachable during the initial audit pass (`ssh lilith@apricot.local` timed out). Once apricot is reachable: ``` ssh "$AUTOPLAY_HOST" "cd $PROJECT_ROOT_REMOTE/src/simulator && bash build-wasm.sh" diff --git a/.project/objectives/p2-14-additional-races-oos.md b/.project/objectives/p2-14-additional-races-oos.md index 44812d59..f84ccb14 100644 --- a/.project/objectives/p2-14-additional-races-oos.md +++ b/.project/objectives/p2-14-additional-races-oos.md @@ -1,7 +1,7 @@ --- id: p2-14 title: Additional playable races beyond Dwarves β€” Game 2+ -priority: p2 +priority: p3 status: oos scope: game2 updated_at: 2026-04-17 diff --git a/public/games/age-of-dwarves/data/objectives.json b/public/games/age-of-dwarves/data/objectives.json index 83c087fb..2098ee3b 100644 --- a/public/games/age-of-dwarves/data/objectives.json +++ b/public/games/age-of-dwarves/data/objectives.json @@ -1,11 +1,11 @@ { - "generated_at": "2026-04-17T19:59:50Z", + "generated_at": "2026-04-17T20:05:53Z", "totals": { - "missing": 5, - "done": 33, - "oos": 9, "stub": 0, "partial": 12, + "missing": 5, + "oos": 9, + "done": 33, "total": 59 }, "objectives": [ @@ -207,7 +207,7 @@ "scope": "game1", "owner": "warcouncil", "updated_at": "2026-04-17", - "summary": "The MCTS tree (`mcts_tree.rs`) and the `mc-turn` GPU fauna pipeline are both live\non `main`, but the AI cannot currently afford wide tree search: full\n`GridState` cloning (~12 MB at 256Γ—256) blows out RAM long before the tree is\ndeep enough to matter, and `TreeState::simulate()` is a 0.5 stub. This objective\nintroduces a **GPU-batched abstract rollout** layer so the tree search can\nevaluate hundreds of candidate futures per leaf at single-digit-millisecond\ncost.\n\n### 2026-04-17 update β€” GPU↔CPU numerical parity ACHIEVED\n\nPhase C structural work shipped in the earlier team pass but the parity test\nwas silently taking the skip path on headless hosts β€” the shader had never\nactually compiled on any adapter. A deep audit + four independent fixes landed\nthis cycle proving real numerical parity:\n\n1. **WGSL reserved-keyword bug**: `var active: u32 = 0u` at `rollout.wgsl:607`\n used the `active` reserved word β†’ Naga parse panic β†’ wgpu_core handler β†’ try_init\n worker thread panic β†’ timeout returned None β†’ skip-path. Renamed to\n `active_idx`; the shader now actually compiles. Without this, the skip-path\n was structurally \"passing\" every test in Phase C without ever exercising the\n WGSL kernel.\n2. **Adapter backend restriction**: `wgpu::Backends::all()` picked the NVIDIA\n OpenGL adapter first on apricot, whose compute support silently fails at\n `request_device`. Restricted to `VULKAN | METAL | DX12 | BROWSER_WEBGPU`\n which all have first-class compute paths.\n3. **Device limits fix**: `Limits::default()` targets a discrete GPU β€” too\n large for llvmpipe / lavapipe. Changed to\n `Limits::downlevel_defaults().using_resolution(adapter.limits())` so software\n Vulkan backends can satisfy device creation.\n4. **Action-walk order unified**: the root numerical divergence. CPU\n `active_actions()` returned actions in insertion order\n `[Build, Research, Defend, Idle, Attack, ...]`; WGSL iterated k=0..9 in\n `ActionKind::ALL` numerical order `[Build, Attack, Settle, Research, ...]`.\n Identical probabilities, identical RNG draw β†’ different action picked at\n every cumulative-sum boundary. Rewrote `active_actions()` to iterate\n `ActionKind::ALL` in canonical order (with explicit docstring warning not\n to reorder for readability).\n\n**Parity verification on apricot (headless bluefin + lavapipe software\nVulkan)**: with `MC_AI_GPU_DEBUG=1 VK_DRIVER_FILES=/usr/share/vulkan/icd.d/lvp_icd.x86_64.json`\ndriving the tests on real llvmpipe dispatch, not skip-path:\n\n```\n[parity small_batch backend=Vulkan] n=16 agree=16/16 (1.000) max_drift=0.000000\n[parity partial_workgroup backend=Vulkan] n=65 agree=65/65 (1.000) max_drift=0.000000\n[parity multi_workgroup backend=Vulkan] n=128 agree=128/128 (1.000) max_drift=0.000000\nbuckets: <1e-6=all others=0 across all three tests\n```\n\nNot 98% (the stated tolerance) β€” **100% agreement, bit-identical** on all 3\nquantitative parity tests (209 inputs total). Pre-fixes: 3–6% agreement with\nmax_drift 0.025–0.043 (action-boundary flips). Post-fix: integer fields\nbyte-equal, scalar fields byte-equal. WGSL kernel is now a provable,\nbyte-for-byte port of `rollout::walk`.\n\n### 2026-04-17 update β€” host-side infrastructure\n\n- `scripts/dev-setup/bluefin.sh` + `./run setup:bluefin` β€” idempotent installer\n for `weston`, `vulkan-tools`, `mesa-vulkan-drivers` on bootc/Bluefin systems\n via `rpm-ostree install --apply-live`. `--check` mode for CI.\n Delegates EDITβ†’RUN via `$AUTOPLAY_HOST` when invoked from EDIT.\n- `~/Code/bootc-bluefin/containerfiles/Containerfile.desktop-core` updated on\n apricot with `vulkan-tools` + `mesa-vulkan-drivers` added alongside `weston`.\n Rebooted bootc images now include these without needing the transient script." + "summary": "The MCTS tree (`mcts_tree.rs`) and the `mc-turn` GPU fauna pipeline are both live\non `main`, but the AI cannot currently afford wide tree search: full\n`GridState` cloning (~12 MB at 256Γ—256) blows out RAM long before the tree is\ndeep enough to matter, and `TreeState::simulate()` is a 0.5 stub. This objective\nintroduces a **GPU-batched abstract rollout** layer so the tree search can\nevaluate hundreds of candidate futures per leaf at single-digit-millisecond\ncost.\n\n### 2026-04-17 update β€” GPU↔CPU numerical parity ACHIEVED\n\nPhase C structural work shipped in the earlier team pass but the parity test\nwas silently taking the skip path on headless hosts β€” the shader had never\nactually compiled on any adapter. A deep audit + four independent fixes landed\nthis cycle proving real numerical parity:\n\n1. **WGSL reserved-keyword bug**: `var active: u32 = 0u` at `rollout.wgsl:607`\n used the `active` reserved word β†’ Naga parse panic β†’ wgpu_core handler β†’ try_init\n worker thread panic β†’ timeout returned None β†’ skip-path. Renamed to\n `active_idx`; the shader now actually compiles. Without this, the skip-path\n was structurally \"passing\" every test in Phase C without ever exercising the\n WGSL kernel.\n2. **Adapter backend restriction**: `wgpu::Backends::all()` picked the NVIDIA\n OpenGL adapter first on apricot, whose compute support silently fails at\n `request_device`. Restricted to `VULKAN | METAL | DX12 | BROWSER_WEBGPU`\n which all have first-class compute paths.\n3. **Device limits fix**: `Limits::default()` targets a discrete GPU β€” too\n large for llvmpipe / lavapipe. Changed to\n `Limits::downlevel_defaults().using_resolution(adapter.limits())` so software\n Vulkan backends can satisfy device creation.\n4. **Action-walk order unified**: the root numerical divergence. CPU\n `active_actions()` returned actions in insertion order\n `[Build, Research, Defend, Idle, Attack, ...]`; WGSL iterated k=0..9 in\n `ActionKind::ALL` numerical order `[Build, Attack, Settle, Research, ...]`.\n Identical probabilities, identical RNG draw β†’ different action picked at\n every cumulative-sum boundary. Rewrote `active_actions()` to iterate\n `ActionKind::ALL` in canonical order (with explicit docstring warning not\n to reorder for readability).\n\n**Parity verification on apricot (headless bluefin + lavapipe software\nVulkan)**: with `MC_AI_GPU_DEBUG=1 VK_DRIVER_FILES=/usr/share/vulkan/icd.d/lvp_icd.x86_64.json`\ndriving the tests on real llvmpipe dispatch, not skip-path:\n\n```\n[parity small_batch backend=Vulkan] n=16 agree=16/16 (1.000) max_drift=0.000000\n[parity partial_workgroup backend=Vulkan] n=65 agree=65/65 (1.000) max_drift=0.000000\n[parity multi_workgroup backend=Vulkan] n=128 agree=128/128 (1.000) max_drift=0.000000\nbuckets: <1e-6=all others=0 across all three tests\n```\n\nNot 98% (the stated tolerance) β€” **100% agreement, bit-identical** on all 3\nquantitative parity tests (209 inputs total). Pre-fixes: 3–6% agreement with\nmax_drift 0.025–0.043 (action-boundary flips). Post-fix: integer fields\nbyte-equal, scalar fields byte-equal. WGSL kernel is now a provable,\nbyte-for-byte port of `rollout::walk`.\n\n### 2026-04-17 update β€” host-side infrastructure\n\n- `scripts/dev-setup/bluefin.sh` + `./run setup:bluefin` β€” idempotent installer\n for `weston`, `vulkan-tools`, `mesa-vulkan-drivers` on bootc/Bluefin systems\n via `rpm-ostree install --apply-live`. `--check` mode for CI.\n Delegates EDITβ†’RUN via `$AUTOPLAY_HOST` when invoked from EDIT.\n- `~/Code/bootc-bluefin/containerfiles/Containerfile.desktop-core` updated on\n apricot with `vulkan-tools` + `mesa-vulkan-drivers` added alongside `weston`.\n Rebooted bootc images now include these without needing the transient script.\n\n### 2026-04-17 update β€” fresh A5 attempt post-fix (failed on host SIGTERM)\n\nAfter the four WGSL parity fixes landed and GDExtension rebuilt, fresh A5\nbatches were attempted under multiple process-isolation strategies:\n\n| Strategy | Batch dir | Result |\n|---|---|---|\n| plain nohup | `.local/iter/a5-fresh-20260417_122847/` | exit 143, seeds `in_progress` T5–T10 before kill |\n| nohup + new dir | `.local/iter/a5-final-20260417_122936/` | games launched, no completion.marker written (process killed) |\n| bash SIGTERM trap | `.local/iter/a5-trap-20260417_123021/` | trap handler received NO signal; script exited rc=143 |\n| strace signal trace | `.local/iter/a5-strace-20260417_123200/` | revealed autoplay-batch.sh exits status **1** (not 143); no SIGTERM to parent. Root cause: `0/N games produced turn_stats.jsonl` check fires because flatpak Godot scopes end at 3–10s |\n| `systemd-run --user` | `.local/iter/warcouncil-a5-systemd-*/` | same β€” service `Active: inactive (dead)` after 2s, scope children SIGTERMed |\n| `KillMode=none` | `.local/iter/warcouncil-a5-systemd-*` (2nd) | games reached T9–T10 only; same kill pattern |\n| plain `bash autoplay-batch` synchronous | `.local/iter/a5-direct-123300/` | 10 games with 0-line `turn_stats.jsonl` β€” games get SIGTERMed during map generation |\n\nSeven distinct execution strategies, same failure pattern: flatpak Godot\nscopes SIGTERMed within 3–10s of launch, before any turn completes. Investigation\nfound the signal is NOT delivered by systemd-oomd (failed service), rpm-ostree\nautomatic updates (timer inactive), or apricot-rail-watchdog (emit-only). The\nactual SIGTERM source could not be identified in the apricot user session.\nParallel agent's own batches from earlier the same day (e.g.\n`.local/batches/blackhammer_tune_20260417_101447/`) completed fine, so the\nissue is transient/session-bound, NOT a permanent host failure.\n\n**Fresh A5 verdict β€” NOT HEALTHY, B5 therefore not launched.** Per\nwarcouncil's integrity rule: we report the measurement failure honestly\nrather than claim parity-fix-correctness translated into fresh gameplay\nevidence. Existing p0-01 batch data from pre-parity-fix binary (at\n`blackhammer_tune_20260417_101447`) still stands as the most recent\nsuccessful A5/B5 evidence in the repo." }, { "id": "p0-21", @@ -219,16 +219,6 @@ "updated_at": "2026-04-17", "summary": "The game has the full *capability* to play audio: manifest, autoload, event-signal wiring, crossfade logic, volume sliders. What's decoupled is the content β€” whether or not `.ogg` files exist under `assets/audio/`, the engine behaves correctly. Shipping the capability as P0 (required for release) is independent of shipping the assets (tracked separately as p2-16).\n\nThis split is deliberate per user directive 2026-04-17: the system being architecturally ready to play audio is a ship gate; the specific sound files are polish that can land incrementally without code changes." }, - { - "id": "p0-22", - "title": "Sprite rendering capability β€” replace procedural draw_* with texture rendering", - "priority": "p0", - "status": "partial", - "scope": "game1", - "owner": "shipwright", - "updated_at": "2026-04-17", - "summary": "Renderers currently draw units and cities with `draw_circle` / `draw_rect` (procedural, flat-color shapes). 7 sprite files exist in `assets/sprites/{buildings,units}/` but aren't wired β€” the renderer path uses only primitives.\n\nParallel to the audio split (p0-21 capability / p2-16 assets): the rendering *capability* to use sprites when they exist is a P0 gate (must be wired before ship); the *assets* to cover every unit / building / tier / race combo is P2 (ship incrementally).\n\n**Design rule (user directive 2026-04-17):** **Do NOT replace `draw_circle`/`draw_rect` with sprites.** Keep the procedural draw path as the always-working baseline that never deletes. Sprite rendering is an *additive enhancement layer* β€” when a matching sprite exists, it's drawn *on top of* or *in place of* the draw primitive for that frame; when absent, the draw primitive continues to render normally. The game is always playable with zero sprites in `assets/sprites/`." - }, { "id": "p0-22", "title": "Ultimate AI stress test β€” 5 clans, huge map, deep lookahead", @@ -239,6 +229,16 @@ "updated_at": "2026-04-17", "summary": "The \"ultimate test\" is the final gate on the AI lookahead pipeline:\nfive clan personalities competing on a map sized large enough for eight\nplayers, with MCTS + GPU batched rollouts driving every decision. The\ngoal is to confirm the lookahead SCALES β€” deep trees, many expansions,\ngenuine strategic divergence between clans at multi-clan scale β€” not\njust that it works on the 1v1 fixtures already covered by p0-02's\n`personality_win_balance`.\n\nPer project owner: the ultimate test runs ONLY AFTER the C(5,2)=10-pair\n1v1 matchup grid (`tools/matchup-grid.sh`) has shown the five clans are\nbalanced in head-to-head play. Unbalanced 1v1s make a 5-way free-for-all\na foregone conclusion; the grid is the precondition." }, + { + "id": "p0-23", + "title": "Sprite rendering capability β€” replace procedural draw_* with texture rendering", + "priority": "p0", + "status": "partial", + "scope": "game1", + "owner": "shipwright", + "updated_at": "2026-04-17", + "summary": "Renderers currently draw units and cities with `draw_circle` / `draw_rect` (procedural, flat-color shapes). 7 sprite files exist in `assets/sprites/{buildings,units}/` but aren't wired β€” the renderer path uses only primitives.\n\nParallel to the audio split (p0-21 capability / p2-16 assets): the rendering *capability* to use sprites when they exist is a P0 gate (must be wired before ship); the *assets* to cover every unit / building / tier / race combo is P2 (ship incrementally).\n\n**Design rule (user directive 2026-04-17):** **Do NOT replace `draw_circle`/`draw_rect` with sprites.** Keep the procedural draw path as the always-working baseline that never deletes. Sprite rendering is an *additive enhancement layer* β€” when a matching sprite exists, it's drawn *on top of* or *in place of* the draw primitive for that frame; when absent, the draw primitive continues to render normally. The game is always playable with zero sprites in `assets/sprites/`." + }, { "id": "p1-01", "title": "Diplomacy-lite β€” peace/war toggle plus one trade action", @@ -447,7 +447,7 @@ "scope": "game1", "owner": null, "updated_at": "2026-04-17", - "summary": "Guide React app (Vite + TypeScript + React 19) lives under `public/games/age-of-dwarves/guide/`. WASM climate worker shares Rust crates with the game.\n\n**This pass (guide-drift-dev2 / 2026-04-17):** closed the systematic type drift between `@magic-civ/guide-engine` and its consumer. `pnpm typecheck` is now 0-errors in both packages (was 488 + 221 = 709 TS errors total). The prior \"32 errors\" count under-counted by ~22x because it only measured consumer-visible errors, not the 488 internal theme-augmentation errors in guide-engine itself.\n\nPer CLAUDE.md's hard Game-1 scope rule (*\"do NOT ship Game 2 features into Game 1\"*), Option 2 (scope-narrowing) was taken. All Game 2/3 content was excised:\n\n- **Deleted from `src/packages/guide/src/`:** entire `pages/magic/` directory (SpellsPage, MagicSchoolsPage, ArchonsPage, DisciplinesPage, LeyLinesPage), `pages/episodes/EpisodeKzzkytPage.tsx`, `pages/episodes/EpisodeElvesPage.tsx`, `pages/worlds/TheHivePlanetPage.tsx`, `pages/worlds/SilvandelPage.tsx`. Empty `pages/worlds/` dir removed.\n- **Deleted from consumer app `src/pages/`:** 5 local Magic pages (Spells, MagicSchools, Archons, Disciplines, LeyLines).\n- **Removed from routing + nav:** Ep2/Ep3 nav groups, all `/magic/*` routes, `/worlds/the-hive`, `/worlds/silvandel`, `/episodes/age-of-kzzkyt`, `/episodes/age-of-elves`.\n\n**Structural fixes landed:**\n\n- **styled-components theme augmentation** (`src/packages/guide/src/types/declarations.d.ts`): declared `DefaultTheme` with the exact `colors.{primary,accent,background,surface,border,text}` + `typography.{fontFamily,fontWeight}` shape used everywhere. Closed ~400 of 488 guide-engine errors.\n- **Ambient WASM + @resources/* + @lilith/ui-theme stubs** (`src/packages/guide/src/types/ambient.d.ts`, consumer `src/ambient.d.ts`): typed the shapes the guide actually uses, so `tsc --noEmit` from either package resolves cleanly without requiring the WASM pkg to be built.\n- **Game-data type drift:** extended `Unit` (added `hp`, `attack`, `defense`, `unit_type`, `flags`, `attributes`, `tier`, `terrain_bonus`, `encyclopedia`), `Building` (`culture_required`, `encyclopedia`), `Resource` / `Improvement` / `Item` (encyclopedia + index sig), `Tech` (replaced `unlocks_units`/`unlocks_buildings`/`unlocks_spells` β†’ `unlocks: TechUnlocks` + `requires` + `flavor` + `encyclopedia`), `Race` (added `featured_units`, `arcane_rank`, `episode`, `status`), `EncyclopediaEntry` (added `entry_type`, `detail_route`), `EcologicalEventTier` (added `resource_table`), `StrategicAxes` (index sig for dynamic access). Added missing types: `Lens`, `LensCategory`, `LensUnlock`, `LensObservation`, `LensRendering`, `NamedResource`, `ResourceWithEncyclopedia`, `TechUnlocks`.\n- **Barrel surface:** rewrote `src/packages/guide/src/index.ts` from 52 lines to 125 lines with the full Game-1 surface (`PreferencesProvider`, `usePreferences`, `usePreferencesReroll`, `resolveGender`, `resolveRace`, `EpisodeProvider`/`Gate`, `GuideLayout`, `MobileNav`, `RaceThemeProvider`, `SPECIES_LIBRARY`, `applyObservationLens`, all retained pages, etc).\n- **New UI primitives:** added `PageHeading`, `PageSubtitle`, `DataTable`, `Highlight`, `FeatureGrid`, `FeatureChip` to `PagePrimitives.tsx` to match consumer-app expectations.\n- **Context drift:** `GuideDataContextValue` now declares `observationLens?: SpeciesObservationLens` + `speciesLibrary: ObservedSpecies[]` (consumer app was already passing these; type just wasn't there).\n- **Path aliases:** consumer's `@magic-civ/*` paths were off-by-one (`../../../` β†’ `../../../../`); fixed. Added `@magic-civ/web-civmap` alias to guide-engine's tsconfig.\n- **Null-guard fixes:** eight consumer pages now guard optional fields before dereferencing (UnitsPage, CommunicationsPage, EncyclopediaModal, EncyclopediaPage, WondersPage, LairsPage, LensesPage, DevSpritesPage).\n\n**Remaining blocker to flip βœ… done:** `pnpm --filter @magic-civilization/guide-age-of-dwarves build` fails at the final rollup step because `src/simulator/pkg/magic_civ_physics.js` is absent on the EDIT host (WASM is an apricot-built artifact per CLAUDE.md two-host workflow). Apricot was unreachable during this pass (`ssh lilith@apricot.local` timed out). Once apricot is reachable:\n\n```\nssh \"$AUTOPLAY_HOST\" \"cd $PROJECT_ROOT_REMOTE/src/simulator && bash build-wasm.sh\"\npnpm --filter @magic-civilization/guide-age-of-dwarves build # from EDIT host\n```\n\nshould yield a clean `dist/index.html` in one step. The external-hosting decision (GitHub Pages vs Cloudflare Pages vs S3) remains a separate downstream gate." + "summary": "Guide React app (Vite + TypeScript + React 19) lives under `public/games/age-of-dwarves/guide/`. WASM climate worker shares Rust crates with the game.\n\n**This pass (guide-drift-dev2 / 2026-04-17):** closed the systematic type drift between `@magic-civ/guide-engine` and its consumer. `pnpm typecheck` is now 0-errors in both packages (was 488 + 221 = 709 TS errors total). The prior \"32 errors\" count under-counted by ~22x because it only measured consumer-visible errors, not the 488 internal theme-augmentation errors in guide-engine itself.\n\nPer CLAUDE.md's hard Game-1 scope rule (*\"do NOT ship Game 2 features into Game 1\"*), Option 2 (scope-narrowing) was taken. All Game 2/3 content was excised:\n\n- **Deleted from `src/packages/guide/src/`:** entire `pages/magic/` directory (SpellsPage, MagicSchoolsPage, ArchonsPage, DisciplinesPage, LeyLinesPage), `pages/episodes/EpisodeKzzkytPage.tsx`, `pages/episodes/EpisodeElvesPage.tsx`, `pages/worlds/TheHivePlanetPage.tsx`, `pages/worlds/SilvandelPage.tsx`. Empty `pages/worlds/` dir removed.\n- **Deleted from consumer app `src/pages/`:** 5 local Magic pages (Spells, MagicSchools, Archons, Disciplines, LeyLines).\n- **Removed from routing + nav:** Ep2/Ep3 nav groups, all `/magic/*` routes, `/worlds/the-hive`, `/worlds/silvandel`, `/episodes/age-of-kzzkyt`, `/episodes/age-of-elves`.\n\n**Structural fixes landed:**\n\n- **styled-components theme augmentation** (`src/packages/guide/src/types/declarations.d.ts`): declared `DefaultTheme` with the exact `colors.{primary,accent,background,surface,border,text}` + `typography.{fontFamily,fontWeight}` shape used everywhere. Closed ~400 of 488 guide-engine errors.\n- **Ambient WASM + @resources/* + @lilith/ui-theme stubs** (`src/packages/guide/src/types/ambient.d.ts`, consumer `src/ambient.d.ts`): typed the shapes the guide actually uses, so `tsc --noEmit` from either package resolves cleanly without requiring the WASM pkg to be built.\n- **Game-data type drift:** extended `Unit` (added `hp`, `attack`, `defense`, `unit_type`, `flags`, `attributes`, `tier`, `terrain_bonus`, `encyclopedia`), `Building` (`culture_required`, `encyclopedia`), `Resource` / `Improvement` / `Item` (encyclopedia + index sig), `Tech` (replaced `unlocks_units`/`unlocks_buildings`/`unlocks_spells` β†’ `unlocks: TechUnlocks` + `requires` + `flavor` + `encyclopedia`), `Race` (added `featured_units`, `arcane_rank`, `episode`, `status`), `EncyclopediaEntry` (added `entry_type`, `detail_route`), `EcologicalEventTier` (added `resource_table`), `StrategicAxes` (index sig for dynamic access). Added missing types: `Lens`, `LensCategory`, `LensUnlock`, `LensObservation`, `LensRendering`, `NamedResource`, `ResourceWithEncyclopedia`, `TechUnlocks`.\n- **Barrel surface:** rewrote `src/packages/guide/src/index.ts` from 52 lines to 125 lines with the full Game-1 surface (`PreferencesProvider`, `usePreferences`, `usePreferencesReroll`, `resolveGender`, `resolveRace`, `EpisodeProvider`/`Gate`, `GuideLayout`, `MobileNav`, `RaceThemeProvider`, `SPECIES_LIBRARY`, `applyObservationLens`, all retained pages, etc).\n- **New UI primitives:** added `PageHeading`, `PageSubtitle`, `DataTable`, `Highlight`, `FeatureGrid`, `FeatureChip` to `PagePrimitives.tsx` to match consumer-app expectations.\n- **Context drift:** `GuideDataContextValue` now declares `observationLens?: SpeciesObservationLens` + `speciesLibrary: ObservedSpecies[]` (consumer app was already passing these; type just wasn't there).\n- **Path aliases:** consumer's `@magic-civ/*` paths were off-by-one (`../../../` β†’ `../../../../`); fixed. Added `@magic-civ/web-civmap` alias to guide-engine's tsconfig.\n- **Null-guard fixes:** eight consumer pages now guard optional fields before dereferencing (UnitsPage, CommunicationsPage, EncyclopediaModal, EncyclopediaPage, WondersPage, LairsPage, LensesPage, DevSpritesPage).\n\n**Remaining blocker to flip βœ… done:** `pnpm --filter @magic-civilization/guide-age-of-dwarves build` fails at the final rollup step because `.local/build/wasm/magic_civ_physics.js` is absent on the EDIT host (WASM is a per-host artifact; see `.claude/instructions/build-output-locations.md`, path was relocated from `src/simulator/pkg/` per p1-11 on 2026-04-17). Apricot was unreachable during the initial audit pass (`ssh lilith@apricot.local` timed out). Once apricot is reachable:\n\n```\nssh \"$AUTOPLAY_HOST\" \"cd $PROJECT_ROOT_REMOTE/src/simulator && bash build-wasm.sh\"\npnpm --filter @magic-civilization/guide-age-of-dwarves build # from EDIT host\n```\n\nshould yield a clean `dist/index.html` in one step. The external-hosting decision (GitHub Pages vs Cloudflare Pages vs S3) remains a separate downstream gate." }, { "id": "p2-10", @@ -469,46 +469,6 @@ "updated_at": "2026-04-17", "summary": "Players need to know which version of the game they're running when filing bug reports. Main menu shows no version; no About screen exists." }, - { - "id": "p2-12", - "title": "Five magic schools (Life / Death / Chaos / Nature / Aether) β€” Game 3 (Age of Elves)", - "priority": "p2", - "status": "oos", - "scope": "game3", - "owner": null, - "updated_at": "2026-04-17", - "summary": "The five-school magic system is Game 3 (\"Age of Elves\") scope. Game 2 introduces only the single Green school tied to the Kzzykt race (see p2-15). This placeholder makes the deferral explicit so magic-school work cannot silently accrete into Game 1 or Game 2. The `mc-magic` crate exists as an empty stub; any magic-flavored items in Game 1 remain mundane per p0-11." - }, - { - "id": "p2-13", - "title": "Archons β€” Game 3 (Age of Elves)", - "priority": "p2", - "status": "oos", - "scope": "game3", - "owner": null, - "updated_at": "2026-04-17", - "summary": "Archons are the magical avatar entities in Game 3 (\"Age of Elves\"). Each player has a High Archon (mana generator + casting avatar), and each of the five magic schools has a corresponding Minor Archon. Neither Game 1 nor Game 2 has Archon entities." - }, - { - "id": "p2-14", - "title": "Additional playable races beyond Dwarves β€” Game 2+", - "priority": "p2", - "status": "oos", - "scope": "game2", - "owner": null, - "updated_at": "2026-04-17", - "summary": "Eventual target is 16 playable races mapped to a 5-magic-school color pie (Civ5 + Master of Magic + Magic: The Gathering framing). Game 1 ships with Dwarves only β€” all five AI opponents are Dwarf clans. This objective documents the Game 2 expansion so the roadmap doesn't absorb race-work into Game 1." - }, - { - "id": "p2-15", - "title": "Ley lines + Kzzykt Green school of magic β€” Game 2 (Age of Kzzykt)", - "priority": "p2", - "status": "oos", - "scope": "game2", - "owner": null, - "updated_at": "2026-04-17", - "summary": "Leylines and the single Green school of magic are Game 2 (\"Age of Kzzykt\") scope. Kzzykt (the insectoid bug race, Green MTG color affinity) interact with leylines intuitively β€” they don't cast leylines, they live alongside them. Leylines affect tile improvements and yields. The Green school is the only school in Game 2 and is optional for the player; this is where spells first enter the series. Game 2 also introduces interplanetary and spacefaring late-game progression.\n\nThere is no full mana economy, no Archons, and no five-school system in Game 2 β€” those belong to Game 3." - }, { "id": "p2-16", "title": "Audio assets β€” SFX + music .ogg files shipped", @@ -549,10 +509,50 @@ "updated_at": "2026-04-17", "summary": "Dynamic progress report page inside the Age of Dwarves guide that reads the project's objectives dashboard + asset pipeline state at runtime. Built 2026-04-17 under guide-progress-dev.\n\nDelivery:\n- `tools/objectives-report.py` extended to emit `public/games/age-of-dwarves/data/objectives.json` on every regen (schema: `{generated_at, totals, objectives[]}` with id/title/priority/status/scope/owner/updated_at/summary per objective). `--check` mode compares ignoring the volatile `generated_at`.\n- `ProgressReportPage.tsx` + supporting modules under `public/games/age-of-dwarves/guide/src/pages/progress-report/` (types, styled, filter, assets-detection, ObjectiveModal). Renders: overall totals, per-priority progress bars, objective table (filterable All / P0 / Partial / Missing), click-through summary modal (uses `createPortal` to escape transformed layout ancestor).\n- Missing assets section: scans `audio.json` (declared .ogg paths) and `units/*.json` + `buildings/*.json` (expected sprite paths) against `import.meta.glob` presence. Currently reports 0/16 audio + 0/33 unit sprites + 0/35 building sprites present (clean slate post-2026-04-17 sprite deletion).\n- Route `/progress` added in `App.tsx`, nav entry `πŸ“Š Progress Report` at top of About group.\n- 25 new Vitest tests (`assets-detection`, `filter`, `objectives-json`) β†’ 115 total passing; apricot `pnpm build` βœ“, `dist/index.html` exists, bundle 113kB.\n- Incidental fix: orphan `healing_draught` reference in `items/manifest.json` removed (was breaking app bootstrap)." }, + { + "id": "p2-12", + "title": "Five magic schools (Life / Death / Chaos / Nature / Aether) β€” Game 3 (Age of Elves)", + "priority": "p3", + "status": "oos", + "scope": "game3", + "owner": null, + "updated_at": "2026-04-17", + "summary": "The five-school magic system is Game 3 (\"Age of Elves\") scope. Game 2 introduces only the single Green school tied to the Kzzykt race (see p2-15). This placeholder makes the deferral explicit so magic-school work cannot silently accrete into Game 1 or Game 2. The `mc-magic` crate exists as an empty stub; any magic-flavored items in Game 1 remain mundane per p0-11." + }, + { + "id": "p2-13", + "title": "Archons β€” Game 3 (Age of Elves)", + "priority": "p3", + "status": "oos", + "scope": "game3", + "owner": null, + "updated_at": "2026-04-17", + "summary": "Archons are the magical avatar entities in Game 3 (\"Age of Elves\"). Each player has a High Archon (mana generator + casting avatar), and each of the five magic schools has a corresponding Minor Archon. Neither Game 1 nor Game 2 has Archon entities." + }, + { + "id": "p2-14", + "title": "Additional playable races beyond Dwarves β€” Game 2+", + "priority": "p3", + "status": "oos", + "scope": "game2", + "owner": null, + "updated_at": "2026-04-17", + "summary": "Eventual target is 16 playable races mapped to a 5-magic-school color pie (Civ5 + Master of Magic + Magic: The Gathering framing). Game 1 ships with Dwarves only β€” all five AI opponents are Dwarf clans. This objective documents the Game 2 expansion so the roadmap doesn't absorb race-work into Game 1." + }, + { + "id": "p2-15", + "title": "Ley lines + Kzzykt Green school of magic β€” Game 2 (Age of Kzzykt)", + "priority": "p3", + "status": "oos", + "scope": "game2", + "owner": null, + "updated_at": "2026-04-17", + "summary": "Leylines and the single Green school of magic are Game 2 (\"Age of Kzzykt\") scope. Kzzykt (the insectoid bug race, Green MTG color affinity) interact with leylines intuitively β€” they don't cast leylines, they live alongside them. Leylines affect tile improvements and yields. The Green school is the only school in Game 2 and is optional for the player; this is where spells first enter the series. Game 2 also introduces interplanetary and spacefaring late-game progression.\n\nThere is no full mana economy, no Archons, and no five-school system in Game 2 β€” those belong to Game 3." + }, { "id": "p2-20", "title": "Life school spellbook β€” Game 3 (Age of Elves)", - "priority": "p2", + "priority": "p3", "status": "oos", "scope": "game3", "owner": null, @@ -562,7 +562,7 @@ { "id": "p2-21", "title": "Death school spellbook β€” Game 3 (Age of Elves)", - "priority": "p2", + "priority": "p3", "status": "oos", "scope": "game3", "owner": null, @@ -572,7 +572,7 @@ { "id": "p2-22", "title": "Chaos school spellbook β€” Game 3 (Age of Elves)", - "priority": "p2", + "priority": "p3", "status": "oos", "scope": "game3", "owner": null, @@ -582,7 +582,7 @@ { "id": "p2-23", "title": "Aether school spellbook β€” Game 3 (Age of Elves)", - "priority": "p2", + "priority": "p3", "status": "oos", "scope": "game3", "owner": null, @@ -592,7 +592,7 @@ { "id": "p2-24", "title": "Arcane Ascension victory β€” Game 3 (Age of Elves)", - "priority": "p2", + "priority": "p3", "status": "oos", "scope": "game3", "owner": null, diff --git a/public/games/age-of-dwarves/guide/CLAUDE.md b/public/games/age-of-dwarves/guide/CLAUDE.md index 8b863527..41a9493b 100644 --- a/public/games/age-of-dwarves/guide/CLAUDE.md +++ b/public/games/age-of-dwarves/guide/CLAUDE.md @@ -96,15 +96,18 @@ Dark gold Dwarf palette: ## Climate Simulation Subsystem The WASM physics engine compiles from `src/simulator/` via `build-wasm.sh`. The guide consumes it -via `@magic-civ/physics-rs` (points to `src/simulator/pkg/`). +via `@magic-civ/physics-rs` (points to `.local/build/wasm/` at the repo root β€” gitignored, +per-host; build output never lives under `src/`, see `.claude/instructions/build-output-locations.md`). - **Worker:** `src/simulation/simulation.worker.ts` β€” offloads physics to background thread - **Hook:** `src/hooks/useClimateSimulation.ts` β€” React hook wrapping worker communication - **Rendering:** `src/components/climate-sim/HexGLRenderer.tsx` β€” Three.js WebGL canvas -Rebuilding WASM: `cd src/simulator && bash build-wasm.sh` +Rebuilding WASM: `cd src/simulator && bash build-wasm.sh` β†’ output at `.local/build/wasm/`. -Never edit `src/simulator/pkg/` directly β€” fix Rust source and rebuild. +Never edit `.local/build/wasm/` directly β€” fix Rust source and rebuild. Do NOT resurrect +`src/simulator/pkg/` β€” `./run verify` step 16 (`_verify_no_build_in_src`) fails the regression +gate if content appears there. ## Safety Notes diff --git a/public/games/age-of-dwarves/guide/src/ambient.d.ts b/public/games/age-of-dwarves/guide/src/ambient.d.ts index 1db996a9..8310bb22 100644 --- a/public/games/age-of-dwarves/guide/src/ambient.d.ts +++ b/public/games/age-of-dwarves/guide/src/ambient.d.ts @@ -7,7 +7,10 @@ // scoped to the modules the consumer actually imports. // ─── @magic-civ/physics-rs ambient stub ─────────────────────────────────── -// The real WASM pkg is built on apricot; EDIT host has no `pkg/` dir. +// The real WASM output lives at `.local/build/wasm/` (gitignored, per-host). +// Build locally via `(cd src/simulator && bash build-wasm.sh)` or rsync from +// apricot. Build output NEVER under src/ β€” see +// .claude/instructions/build-output-locations.md. declare module '@magic-civ/physics-rs' { export class WasmClimatePhysics { constructor(paramsJson: string, terrainJson: string, specJson: string) diff --git a/scripts/run/build.sh b/scripts/run/build.sh index dbb976f2..dc2ee4e0 100644 --- a/scripts/run/build.sh +++ b/scripts/run/build.sh @@ -4,7 +4,7 @@ cmd_build_wasm() { echo -e "${BLUE}Building WASM (simulator β†’ guide)...${NC}" (cd "$SIMULATOR_DIR" && bash build-wasm.sh "$@") - echo -e "${GREEN}βœ“ WASM built β†’ src/simulator/pkg/${NC}" + echo -e "${GREEN}βœ“ WASM built β†’ .local/build/wasm/${NC}" } cmd_build_gdext() { diff --git a/scripts/run/dev.sh b/scripts/run/dev.sh index 00ba7c3e..f91e6b0e 100644 --- a/scripts/run/dev.sh +++ b/scripts/run/dev.sh @@ -34,6 +34,18 @@ cmd_editor() { } cmd_guide() { + # Pre-check: surface WASM artifact location issues before Vite starts so + # alias-resolution errors are self-describing instead of a cryptic + # "Rollup failed to resolve import". + if [ -d "$REPO_ROOT/src/simulator/pkg" ] && [ -n "$(ls -A "$REPO_ROOT/src/simulator/pkg" 2>/dev/null)" ]; then + echo -e "${YELLOW}warning: src/simulator/pkg/ has content β€” build output must live in .local/build/wasm/ (src/ is source-only).${NC}" + echo -e "${YELLOW} Re-run: (cd src/simulator && bash build-wasm.sh)${NC}" + fi + if [ ! -f "$REPO_ROOT/.local/build/wasm/magic_civ_physics.js" ]; then + echo -e "${YELLOW}warning: .local/build/wasm/magic_civ_physics.js missing β€” WASM not built locally.${NC}" + echo -e "${YELLOW} Build locally: (cd src/simulator && bash build-wasm.sh)${NC}" + echo -e "${YELLOW} Or rsync from apricot: rsync -a \"\$AUTOPLAY_HOST:\$PROJECT_ROOT_REMOTE/.local/build/wasm/\" .local/build/wasm/${NC}" + fi echo -e "${BLUE}Starting guide dev server (port 5800)...${NC}" pnpm --prefix "$GUIDE_DIR" dev } diff --git a/scripts/run/verify.sh b/scripts/run/verify.sh index f7263def..0863f1c9 100644 --- a/scripts/run/verify.sh +++ b/scripts/run/verify.sh @@ -85,12 +85,17 @@ cmd_verify() { echo -e "${BLUE}─────────────────────────────────────────────────${NC}" } - local TOTAL=16 + local TOTAL=17 # Step 0 β€” Game data schema validation _verify_step 0 $TOTAL "game data JSON schemas" \ python3 "$REPO_ROOT/tools/validate-game-data.py" + # Step 16 β€” "Build output never under src/" invariant. + # Rule source: .claude/instructions/build-output-locations.md. + _verify_step 16 $TOTAL "no build output under src/" \ + _verify_no_build_in_src + # Step 1 β€” i18n: no hardcoded user-visible strings outside ThemeVocabulary _verify_step 1 $TOTAL "i18n: no hardcoded UI strings" \ python3 "$REPO_ROOT/tools/validate-i18n.py" @@ -230,6 +235,28 @@ _verify_file_size_cap() { return 0 } +_verify_no_build_in_src() { + # Enforce: src/ is source-only. Covers wasm-pack's default /pkg/ + # and cargo's default target/ β€” both must be redirected to .local/build/**. + # Rule doc: .claude/instructions/build-output-locations.md. + local violations=0 + if [ -d "$REPO_ROOT/src/simulator/pkg" ] && [ -n "$(ls -A "$REPO_ROOT/src/simulator/pkg" 2>/dev/null)" ]; then + echo -e "${RED}src/simulator/pkg/ has content β€” wasm-pack output must go to .local/build/wasm/${NC}" + violations=$(( violations + 1 )) + fi + while IFS= read -r target_dir; do + if [ -n "$(ls -A "$target_dir" 2>/dev/null)" ]; then + echo -e "${RED}${target_dir#$REPO_ROOT/} has content β€” cargo target must go to .local/build/rust/${NC}" + violations=$(( violations + 1 )) + fi + done < <(find "$REPO_ROOT/src" -type d -name target 2>/dev/null) + if [ "$violations" -gt 0 ]; then + echo -e "${RED}Rule: build output is never inside src/ (see .claude/instructions/build-output-locations.md).${NC}" + return 1 + fi + return 0 +} + _verify_autoplay_smoke() { # Skips when no RUN host and no local flatpak β€” dev boxes without a batch # target still get the rest of the pipeline. diff --git a/src/game/engine/src/rendering/unit_renderer.gd b/src/game/engine/src/rendering/unit_renderer.gd index 643cb1b9..f94832cc 100644 --- a/src/game/engine/src/rendering/unit_renderer.gd +++ b/src/game/engine/src/rendering/unit_renderer.gd @@ -51,7 +51,8 @@ var _movement_range: Dictionary = {} ## Animation pixel overrides: unit_id -> Vector2 (mid-tween position) var _anim_pixels: Dictionary = {} -## Unit sprite cache: type_id -> Texture2D (null if not available) +## Unit sprite cache: sprite_key -> Texture2D (null if not available) +## Keys are composed as "__" or bare "" fallback. var _unit_sprite_cache: Dictionary = {} ## Local player index β€” used to determine which units are "enemy" for fog gating. @@ -85,16 +86,20 @@ func sync_units(units: Array) -> void: if uid == "": continue var tid: String = _resolve_type_id(unit) + var race_id: String = str(unit.get("race_id") if "race_id" in unit else "") + var sex: String = str(unit.get("sex") if "sex" in unit else "") _units[uid] = { "position": unit.position, "color": _get_unit_color(unit), "label": _get_unit_label(unit), "type_id": tid, + "race_id": race_id, + "sex": sex, "hp": unit.hp, "max_hp": unit.max_hp, "owner": unit.owner, } - _cache_unit_sprite(tid) + _cache_unit_sprites(tid, race_id, sex) queue_redraw() @@ -172,29 +177,32 @@ func _draw() -> void: var pos: Vector2i = data["position"] pixel = HexUtilsScript.axial_to_pixel(pos) + HexUtilsScript.hex_center - # Try sprite first, fall back to colored circle + # Baseline: always draw the colored circle + label (never removed) + var color: Color = data.get("color", Color.WHITE) + draw_circle(pixel, UNIT_RADIUS, color) + + var label_text: String = data.get("label", "?") + var font: Font = ThemeDB.fallback_font + var font_size: int = 18 + var text_size: Vector2 = font.get_string_size( + label_text, HORIZONTAL_ALIGNMENT_CENTER, -1, font_size + ) + var text_pos: Vector2 = ( + pixel - text_size * 0.5 + Vector2(0, text_size.y * 0.35) + ) + draw_string( + font, text_pos, label_text, + HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, Color.WHITE, + ) + + # Optional overlay: draw sprite on top when available var type_id: String = data.get("type_id", "") - var sprite: Texture2D = _get_unit_sprite(type_id) + var race_id: String = data.get("race_id", "") + var sex: String = data.get("sex", "") + var sprite: Texture2D = _get_unit_sprite(type_id, race_id, sex) if sprite != null: var tex_size: Vector2 = sprite.get_size() draw_texture(sprite, pixel - tex_size * 0.5) - else: - var color: Color = data.get("color", Color.WHITE) - draw_circle(pixel, UNIT_RADIUS, color) - - var label_text: String = data.get("label", "?") - var font: Font = ThemeDB.fallback_font - var font_size: int = 18 - var text_size: Vector2 = font.get_string_size( - label_text, HORIZONTAL_ALIGNMENT_CENTER, -1, font_size - ) - var text_pos: Vector2 = ( - pixel - text_size * 0.5 + Vector2(0, text_size.y * 0.35) - ) - draw_string( - font, text_pos, label_text, - HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, Color.WHITE, - ) # Draw HP bar if damaged _draw_hp_bar(pixel, data) @@ -343,14 +351,18 @@ func _on_unit_created(unit: RefCounted, _player_index: int) -> void: if uid == "": return var tid: String = _resolve_type_id(u) + var race_id: String = str(u.get("race_id") if "race_id" in u else "") + var sex: String = str(u.get("sex") if "sex" in u else "") _units[uid] = { "position": u.position, "color": _get_unit_color(u), "label": _get_unit_label(u), "type_id": tid, + "race_id": race_id, + "sex": sex, "owner": u.owner, } - _cache_unit_sprite(tid) + _cache_unit_sprites(tid, race_id, sex) queue_redraw() diff --git a/src/packages/guide/src/types/ambient.d.ts b/src/packages/guide/src/types/ambient.d.ts index 4b077008..e98a89ae 100644 --- a/src/packages/guide/src/types/ambient.d.ts +++ b/src/packages/guide/src/types/ambient.d.ts @@ -1,10 +1,13 @@ // ─── @magic-civ/physics-rs ambient stub ─────────────────────────────────── // -// The WASM build output lives at `src/simulator/pkg/` on the RUN host only -// (see CLAUDE.md two-host workflow: WASM builds run on apricot; EDIT host -// has no Rust toolchain and `pkg/` is gitignored there). Guide-engine's -// tsc --noEmit therefore can't resolve the real `.d.ts` shipped with the -// WASM pkg. +// The WASM build output lives at `.local/build/wasm/` (gitignored, per-host). +// Build output NEVER under src/ β€” see +// .claude/instructions/build-output-locations.md. WASM builds typically run +// on apricot (two-host workflow) and rsync back; EDIT host can also build +// locally via `(cd src/simulator && bash build-wasm.sh)` when rustup + +// wasm-pack are installed. Guide-engine's tsc --noEmit therefore can't +// resolve the real `.d.ts` shipped with the WASM output from source-only +// tree checks. // // This file is a pure ambient declaration file (no imports/exports β€” // intentional, so these `declare module` blocks register globally). It diff --git a/src/simulator/package.json b/src/simulator/package.json index 204af081..6169f979 100644 --- a/src/simulator/package.json +++ b/src/simulator/package.json @@ -1,12 +1,12 @@ { "name": "@magic-civ/physics-rs", "version": "0.1.0", - "description": "Rust physics engine β€” compiled to WASM for web, GDExtension for Godot", - "main": "pkg/magic_civ_physics.js", - "types": "pkg/magic_civ_physics.d.ts", + "description": "Rust physics engine β€” compiled to WASM for web, GDExtension for Godot. Build output lives at repo-root .local/build/wasm/ (gitignored, per-host). Consumers resolve via Vite/Vitest alias, not the main/types fields below β€” those are informational only.", + "main": "../../.local/build/wasm/magic_civ_physics.js", + "types": "../../.local/build/wasm/magic_civ_physics.d.ts", "scripts": { "build": "bash build-wasm.sh", "build:gdext": "bash build-gdext.sh" }, - "files": ["pkg/"] + "files": [] } diff --git a/src/simulator/tests/golden/README.md b/src/simulator/tests/golden/README.md index c36eafc9..f3bf1970 100644 --- a/src/simulator/tests/golden/README.md +++ b/src/simulator/tests/golden/README.md @@ -13,7 +13,9 @@ 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 + β”œβ”€ api-wasm β†’ .local/build/wasm/magic_civ_physics_bg.wasm + β”‚ (build output never under src/ β€” see + β”‚ .claude/instructions/build-output-locations.md) β”‚ consumed by: pnpm --filter guide-age-of-dwarves test golden β”‚ (Vitest test runs inside simulation.worker.ts) β”‚ diff --git a/tooling/claude/CLAUDE.md b/tooling/claude/CLAUDE.md index 3293d06f..9c2f6583 100644 --- a/tooling/claude/CLAUDE.md +++ b/tooling/claude/CLAUDE.md @@ -51,7 +51,7 @@ Modules live at `.claude/instructions/.md` (symlink resolves to `tooling/c | Porting code/data from `@magic-civilization.messy/` | `atomic-porting.md` | | Writing tests, `--headless` compatibility | `headless-tests.md` | | Ad-hoc shell/python pipelines β€” when to extract to `scripts/` | `scripts-extraction.md` | -| Cargo target, Godot exports, WASM output, `.local/build/**` | `build-output-locations.md` | +| Cargo target, Godot exports, WASM output, `.local/build/**` β€” build output **NEVER** under `src/` (enforced by `./run verify`) | `build-output-locations.md` | | `ThemeAssets`, `EventBus`, `/tmp` rule, rsync binary rule | `safety-rules-local.md` | | Which language-standards file (global) to load | `language-standards.md` | diff --git a/tooling/claude/dot-claude/agents/guide-web.md b/tooling/claude/dot-claude/agents/guide-web.md index bbd1f322..2b4c773e 100644 --- a/tooling/claude/dot-claude/agents/guide-web.md +++ b/tooling/claude/dot-claude/agents/guide-web.md @@ -38,7 +38,10 @@ src/packages/ scenarios.ts β€” all scenario definitions index.ts β€” re-exports -src/simulator/pkg/ β€” @magic-civ/physics-rs wasm-pack output +.local/build/wasm/ β€” @magic-civ/physics-rs wasm-pack output (gitignored, + built per-host via src/simulator/build-wasm.sh). + Build output NEVER lives under src/ β€” see + .claude/instructions/build-output-locations.md. magic_civ_physics.js β€” WASM bindings (top-level await for init) magic_civ_physics.d.ts magic_civ_physics_bg.wasm @@ -53,7 +56,7 @@ src/simulator/pkg/ β€” @magic-civ/physics-rs wasm-pack output | `@worlds/` | `public/resources/worlds/` | | `@magic-civ/engine-ts` | `src/packages/engine-ts/src/index.ts` | | `@magic-civ/guide-engine` | `src/packages/guide/src/index.ts` | -| `@magic-civ/physics-rs` | `src/simulator/pkg/magic_civ_physics.js` | +| `@magic-civ/physics-rs` | `.local/build/wasm/magic_civ_physics.js` | Configured in `public/games/age-of-dwarves/guide/vite.config.ts` and `tsconfig.json`. diff --git a/tooling/claude/dot-claude/agents/simulator-infra.md b/tooling/claude/dot-claude/agents/simulator-infra.md index 98e1d7f6..f4ef4414 100644 --- a/tooling/claude/dot-claude/agents/simulator-infra.md +++ b/tooling/claude/dot-claude/agents/simulator-infra.md @@ -1,6 +1,6 @@ --- name: simulator-infra -description: Use for Rust workspace structure (src/simulator/Cargo.toml, crate skeletons), api-wasm crate (wasm-bindgen surface), api-gdext crate (godot-rust surface), build scripts (build-wasm.sh, build-gdext.sh), wasm-pack output (pkg/), Cargo feature flags, cross-compilation targets, dependency management across crates. +description: Use for Rust workspace structure (src/simulator/Cargo.toml, crate skeletons), api-wasm crate (wasm-bindgen surface), api-gdext crate (godot-rust surface), build scripts (build-wasm.sh, build-gdext.sh), wasm-pack output (.local/build/wasm/), Cargo feature flags, cross-compilation targets, dependency management across crates. --- You are the Rust simulator infrastructure specialist for Magic Civilization. You own the Cargo workspace layout, the two API surface crates (WASM + GDExtension), build scripts, and the plumbing that connects domain crates to their consumers. You do NOT implement domain logic β€” that belongs to the domain-specialist agents. @@ -11,7 +11,8 @@ You are the Rust simulator infrastructure specialist for Magic Civilization. You src/simulator/ Cargo.toml β€” workspace root (resolver = "2", lists all members) Cargo.lock - build-wasm.sh β€” wasm-pack build api-wasm --target bundler --out-dir ../pkg + build-wasm.sh β€” wasm-pack build api-wasm --target bundler --out-dir ../../.local/build/wasm + (build output never under src/ β€” see build-output-locations.md) build-gdext.sh β€” cargo build --release -p api-gdext --target $TARGET; copies .so crates/ β€” domain logic crates (pure Rust + serde, no wasm/gdext deps) @@ -37,11 +38,19 @@ src/simulator/ src/lib.rs β€” GdClimatePhysics, GdCombatResolver, GdAiPlayer, ... Cargo.toml β€” depends on domain crates + godot (no wasm-bindgen dep) - pkg/ β€” wasm-pack output (gitignored except package.json) - magic_civ_physics.js - magic_civ_physics.d.ts - magic_civ_physics_bg.wasm - package.json β€” "@magic-civ/physics-rs" package entry + package.json β€” "@magic-civ/physics-rs" pnpm workspace entry + (wasm-pack output lives at .local/build/wasm/, + NOT inside this src/ tree β€” see + .claude/instructions/build-output-locations.md) +``` + +WASM build artifacts land at repo-root `.local/build/wasm/`: + +``` +.local/build/wasm/ + magic_civ_physics.js + magic_civ_physics.d.ts + magic_civ_physics_bg.wasm ``` ## Workspace Rules @@ -55,7 +64,8 @@ src/simulator/ ```bash # Web guide WASM cd src/simulator && bash build-wasm.sh -# Output: pkg/ (imported by public/games/age-of-dwarves/guide/ as @magic-civ/physics-rs) +# Output: .local/build/wasm/ (repo-root, gitignored; imported by +# public/games/age-of-dwarves/guide/ as @magic-civ/physics-rs via Vite alias) # Godot GDExtension (Linux dev) cd src/simulator && bash build-gdext.sh @@ -75,7 +85,7 @@ cargo test --workspace # from src/simulator/ ## WASM Top-Level Await Warning -`pkg/magic_civ_physics.js` uses top-level await for WASM init. If two modules in the same Web Worker module graph both statically import this file, the worker hangs silently. The fix in `src/packages/engine-ts/src/runner.ts` re-exports WASM classes so the worker only needs one import path. Do not break this pattern. +`.local/build/wasm/magic_civ_physics.js` uses top-level await for WASM init. If two modules in the same Web Worker module graph both statically import this file, the worker hangs silently. The fix in `src/packages/engine-ts/src/runner.ts` re-exports WASM classes so the worker only needs one import path. Do not break this pattern. ## GDExtension Class Naming diff --git a/tooling/claude/dot-claude/instructions/README.md b/tooling/claude/dot-claude/instructions/README.md index fe9cbdce..1f2a7eac 100644 --- a/tooling/claude/dot-claude/instructions/README.md +++ b/tooling/claude/dot-claude/instructions/README.md @@ -63,7 +63,7 @@ tooling/claude/ | `atomic-porting.md` | Porting code/data from `@magic-civilization.messy/` | ~200 | | `headless-tests.md` | Writing GUT tests / `--headless` compatibility | ~400 | | `scripts-extraction.md` | Inline pipelines β†’ `scripts/` rule | ~300 | -| `build-output-locations.md` | `.local/build/**` topology, artifact paths | ~200 | +| `build-output-locations.md` | `.local/build/**` topology, artifact paths (ENFORCED by `./run verify` step 16) | ~450 | | `safety-rules-local.md` | ThemeAssets, EventBus, rsync binaries, `/tmp` avoidance | ~500 | | `language-standards.md` | Which `~/.claude/instructions/-code-standards.md` to load | ~150 | diff --git a/tooling/claude/dot-claude/instructions/build-output-locations.md b/tooling/claude/dot-claude/instructions/build-output-locations.md index a148690b..82217b21 100644 --- a/tooling/claude/dot-claude/instructions/build-output-locations.md +++ b/tooling/claude/dot-claude/instructions/build-output-locations.md @@ -2,13 +2,66 @@ **Load when:** configuring build tools, troubleshooting "where did the output go?", auditing `.local/`, or writing `.gitignore` entries for generated artifacts. -All build artifacts land under `.local/build/` (gitignored, per-host, ~25GB typical): +## Hard rule: build output is never inside `src/` + +`src/` is source-only. Every generated / built artifact β€” Rust `target/`, +wasm-pack `pkg/`, TypeScript `dist/`, Godot exports, anything else a tool +emits β€” goes under `.local/build/**` (gitignored, per-host) or under a +package-local `dist/` outside of `src/`. + +This rule exists because of two incidents: + +1. **Rust `target/` inside `src/` (historical)** β€” cargo's default target + sat at `src/simulator/target/` and accidentally committed ~25 GB / + 65k files to git before being purged. Target was relocated to + `.local/build/rust/` via `src/simulator/.cargo/config.toml`. +2. **wasm-pack `pkg/` inside `src/` (2026-04-17)** β€” `build-wasm.sh` + used wasm-pack's default `--out-dir ../pkg` and emitted + `src/simulator/pkg/`, violating the rule. `pkg/` was relocated to + `.local/build/wasm/` via `--out-dir ../../.local/build/wasm`. + +`./run verify` enforces this invariant β€” step 16 +`_verify_no_build_in_src` fails the regression gate if +`src/simulator/pkg/` or any `src/**/target/` tree has content. `.gitignore` +also explicitly lists `src/simulator/pkg/` as a belt-and-suspenders +safety net in case anyone runs wasm-pack directly without the script. + +**If you find yourself writing into `src/**/target/`, `src/**/pkg/`, +`src/**/dist/`, or `src/**/build/`, stop β€” something's misconfigured.** + +## Canonical artifact paths | System | Output path | Configured in | |---|---|---| | Rust (cargo) | `.local/build/rust/` | `src/simulator/.cargo/config.toml` | -| Godot exports | `.local/build/godot///` | `scripts/run/remote.sh` | -| WASM (wasm-pack) | `src/simulator/pkg/` | `src/simulator/build-wasm.sh` (wasm-pack default) | -| TypeScript (vite) | `/dist/` per package | each `vite.config.ts` (vite default) | +| Godot exports | `.local/build/godot///` | `scripts/run/remote.sh`, `scripts/run/export.sh` | +| WASM (wasm-pack) | `.local/build/wasm/` | `src/simulator/build-wasm.sh` (`--out-dir` override) | +| GDExtension binary | `src/game/engine/addons/magic_civ_physics/*.{so,dll,dylib}` | `src/simulator/build-gdext.sh` | +| TypeScript (Vite) | `/dist/` per package | each `vite.config.ts` (Vite default) | -**`src/` is source-only** β€” never commit build artifacts there. Rust's `target/` used to sit at `src/simulator/target/` and got 65k files (~25GB) committed to git by accident; that's been purged. If you find yourself writing into `src/**/target/`, `src/**/dist/`, or `src/**/build/`, stop β€” something's misconfigured. +The GDExtension output is the one exception where a build product +lands inside `src/` β€” Godot's GDExtension loader requires the binary +to live next to the `.gdextension` manifest, so the path is dictated +by the consumer runtime, not our build system. Those files are +gitignored (`.gitignore:69-72`) and built per-host from a different +build script (`build-gdext.sh`) that reads from `.local/build/rust/`. + +## When a Vite alias or Dockerfile references the WASM output + +Use `.local/build/wasm/magic_civ_physics.js` as the canonical entry +point. Concrete examples: + +```ts +// public/games/age-of-dwarves/guide/vite.config.ts +'@magic-civ/physics-rs': path.resolve(__dirname, '../../../../.local/build/wasm/magic_civ_physics.js'), +``` + +```dockerfile +# public/games/age-of-dwarves/guide/e2e/Dockerfile.web +COPY .local/build/wasm/ ./.local/build/wasm/ +``` + +Do NOT resurrect `src/simulator/pkg/` in any new config. It is +gitignored, the verify gate fails on content there, and the doc +surfaces below are single-sourced β€” a stale reference creeping in +will be caught by `grep -R "src/simulator/pkg" . --exclude-dir=node_modules`. diff --git a/tooling/claude/dot-claude/instructions/rust-source-of-truth.md b/tooling/claude/dot-claude/instructions/rust-source-of-truth.md index fb528b7a..aa13dcef 100644 --- a/tooling/claude/dot-claude/instructions/rust-source-of-truth.md +++ b/tooling/claude/dot-claude/instructions/rust-source-of-truth.md @@ -29,9 +29,10 @@ src/simulator/crates/ ← SOURCE OF TRUTH (all s β”œβ”€β”€ compiled via api-gdext/ β†’ src/game/addons/magic_civ_physics/*.so/.dll β”‚ ↓ loaded by Godot β”‚ src/game/engine/src/modules/climate/climate.gd ← thin GDExtension wrapper - └── compiled via api-wasm/ β†’ src/simulator/pkg/ - ↓ imported by web worker - public/games/age-of-dwarves/guide/src/simulation/simulation.worker.ts + └── compiled via api-wasm/ β†’ .local/build/wasm/ (NOT src/simulator/pkg/ β€” + ↓ imported by web worker build output never under src/, + public/games/age-of-dwarves/guide/src/simulation/ see build-output-locations.md) + simulation.worker.ts ``` ## Build commands diff --git a/tools/deploy-guide.sh b/tools/deploy-guide.sh index fe93c0ea..355e8386 100755 --- a/tools/deploy-guide.sh +++ b/tools/deploy-guide.sh @@ -7,10 +7,12 @@ # tools/deploy-guide.sh apricot [version] Build + rsync to apricot:~/public/guide// # tools/deploy-guide.sh zip [version] Build + zip dist/ into .local/guide-.zip # -# WASM prerequisite: src/simulator/pkg/ must exist. If the guide's -# `@magic-civ/physics-rs` alias can't resolve, the build fails. Rebuild -# via: ssh "$AUTOPLAY_HOST" "cd $PROJECT_ROOT_REMOTE/src/simulator && bash build-wasm.sh" -# (per CLAUDE.md Two-Host Workflow β€” WASM is an apricot-side artifact). +# WASM prerequisite: .local/build/wasm/magic_civ_physics.js must exist. +# (Build output never under src/ β€” see .claude/instructions/build-output-locations.md.) +# If the guide's `@magic-civ/physics-rs` alias can't resolve, the build fails. +# Rebuild via: ssh "$AUTOPLAY_HOST" "cd $PROJECT_ROOT_REMOTE/src/simulator && bash build-wasm.sh" +# (per CLAUDE.md Two-Host Workflow β€” WASM is an apricot-side artifact) +# or locally via: (cd src/simulator && bash build-wasm.sh). # # External hosting (GitHub Pages / S3 / Cloudflare Pages) is TODO: no public # host has been committed for Early Access. Until that decision lands, `zip` @@ -32,9 +34,10 @@ PKG_NAME="@magic-civilization/guide-age-of-dwarves" mode="${1:-build}" _run_build() { - if [ ! -f "$REPO_ROOT/src/simulator/pkg/magic_civ_physics.js" ]; then - echo -e "${YELLOW}warning: src/simulator/pkg/magic_civ_physics.js missing β€” WASM may not be built.${NC}" + if [ ! -f "$REPO_ROOT/.local/build/wasm/magic_civ_physics.js" ]; then + echo -e "${YELLOW}warning: .local/build/wasm/magic_civ_physics.js missing β€” WASM may not be built.${NC}" echo -e "${YELLOW} Rebuild via: ssh \"\$AUTOPLAY_HOST\" 'cd \$PROJECT_ROOT_REMOTE/src/simulator && bash build-wasm.sh'${NC}" + echo -e "${YELLOW} or locally: (cd src/simulator && bash build-wasm.sh)${NC}" fi echo -e "${BLUE}Building $PKG_NAME ...${NC}" (cd "$REPO_ROOT" && pnpm --filter "$PKG_NAME" build) diff --git a/tools/objectives-report.py b/tools/objectives-report.py index 1fa627b0..6c361a60 100644 --- a/tools/objectives-report.py +++ b/tools/objectives-report.py @@ -145,7 +145,7 @@ def load_objectives() -> list[Objective]: def render(objectives: list[Objective]) -> str: - by_priority: dict[str, list[Objective]] = {"p0": [], "p1": [], "p2": []} + by_priority: dict[str, list[Objective]] = {"p0": [], "p1": [], "p2": [], "p3": []} for o in objectives: by_priority[o.priority].append(o) @@ -166,11 +166,52 @@ def render(objectives: list[Objective]) -> str: 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}** |") + + # --- by-priority breakdown --- + prio_labels = {"p0": "P0", "p1": "P1", "p2": "P2", "p3": "P3 (oos)"} + prio_table: list[str] = [ + "| Priority | βœ… | 🟑 | πŸ”΄ | ❌ | ⚫ | Total |", + "|---|---|---|---|---|---|---|", + ] + for prio in ("p0", "p1", "p2", "p3"): + grp = by_priority[prio] + row = {s: sum(1 for o in grp if o.status == s) for s in VALID_STATUS} + prio_table.append( + f"| **{prio_labels[prio]}** " + f"| {row['done']} | {row['partial']} | {row['stub']} " + f"| {row['missing']} | {row['oos']} | {len(grp)} |" + ) + prio_table.append( + f"| **total** | **{counts['done']}** | **{counts['partial']}** " + f"| **{counts['stub']}** | **{counts['missing']}** " + f"| **{counts['oos']}** | **{total}** |" + ) + + # --- left-to-do by team lead (partial + stub + missing only) --- + by_lead: dict[str, int] = {} + for o in objectives: + if o.status in ("partial", "stub", "missing") and o.owner: + by_lead[o.owner] = by_lead.get(o.owner, 0) + 1 + lead_table: list[str] = ["| Team Lead | Remaining |", "|---|---|"] + for lead, cnt in sorted(by_lead.items(), key=lambda x: -x[1]): + lead_table.append(f"| [{lead}](../team-leads/{lead}.md) | {cnt} |") + if not by_lead: + lead_table.append("| β€” | 0 |") + + # side-by-side via HTML (works in Forgejo/GitHub markdown) + lines.append("
") + lines.append("") + lines.append("**By Priority**") + lines.append("") + lines.extend(prio_table) + lines.append("") + lines.append("") + lines.append("") + lines.append("**Left To Do by Lead**") + lines.append("") + lines.extend(lead_table) + lines.append("") + lines.append("
") lines.append("") priority_heading = {