feat(@projects/@magic-civilization): ✨ add gpu rollout performance tracking objectives
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
64f1f8f1d2
commit
215b022dc0
17 changed files with 713 additions and 0 deletions
|
|
@ -206,3 +206,23 @@ wall-time bullet linked to its G2 successor.
|
|||
the CPU (and owned by `mc-turn`).
|
||||
- Replacing `simple_heuristic_ai.gd`. The heuristic remains the tactical
|
||||
executor; MCTS only produces the strategic directive.
|
||||
|
||||
## Remaining work (2026-05-03)
|
||||
|
||||
One unmet acceptance bullet remains; the rest are ✓.
|
||||
|
||||
### Bullet: `AI_GPU_ROLLOUT=true ./tools/autoplay-batch.sh 10 300` wall-time drops ≥20% vs `AI_GPU_ROLLOUT=false`
|
||||
|
||||
- **Files to touch (Rust SSoT)**:
|
||||
- `src/simulator/crates/mc-ai/src/mcts_tree.rs:382` — `iterate_gpu_batched` currently dispatches one wgpu submit per leaf-batch (~64–256 rollouts). To amortize submit/upload/readback overhead, batches must coalesce to ≥1024 rollouts/dispatch. Change requires buffering cross-leaf rollouts in `mc-ai/src/gpu/inner.rs` before flushing.
|
||||
- `src/simulator/crates/mc-ai/src/gpu/inner.rs` — adopt persistent staging buffers + ring-buffer readback so the per-dispatch cost is amortized across an entire MCTS turn.
|
||||
- No GDScript bridge changes required; `ai_turn_bridge.gd` env passthrough is already live.
|
||||
- **Dependencies**: `g2-04-multi-gpu-batch-simulate-oos` (Game-2 scope). Per the 2026-04-17 directive, the wall-time gate is **explicitly deferred to Game 2**. No Game-1 work unblocks it.
|
||||
- **Acceptance gate**: `apricot tools/autoplay-batch.sh 10 300` median seed wall-time with `AI_GPU_ROLLOUT=true` ≤ 0.80 × median with `AI_GPU_ROLLOUT=false`. Batch dirs `.local/iter/gpu-<stamp>/gpu-{true,false}/`.
|
||||
- **SOLID/DRY/SSoT rails**:
|
||||
- All buffering / dispatch coalescing logic stays in `mc-ai::gpu::inner` — no GDScript shadow path.
|
||||
- No `cfg(feature = "gpu_batch_v2")` toggle; either ship the larger batch or don't.
|
||||
- Stringly-typed env reads in `ai_turn_bridge.gd` must continue routing through `GdMcTreeController::set_gpu_enabled(bool)` (typed bridge in `api-gdext/src/ai.rs`).
|
||||
- Data lives in `public/games/age-of-dwarves/data/setup.json` if a tunable batch-size knob is added; no `data/<category>/` parallel.
|
||||
|
||||
**Note**: G1-scope this objective is structurally complete and stays `partial` only because of the deferred G2 perf gate. No Remaining-work action lands in G1 unless the G2 carve-out is reversed.
|
||||
|
|
|
|||
|
|
@ -111,3 +111,29 @@ warcouncil, not Shipwright.
|
|||
- `p0-06` (economy), `p0-07` (tech costs), `p0-19` (biome-economy integration) — all affect pop + tech + luxury counts.
|
||||
- Warcouncil tuning for `personality_win_balance` + `both-players-T100` gaps.
|
||||
- `p0-08` domination pacing (short games depress luxury/tech counts).
|
||||
|
||||
## Remaining work (2026-05-03)
|
||||
|
||||
### Bullet: Luxury variance >= 3 distinct luxuries per seed
|
||||
|
||||
- Files to touch:
|
||||
- Rust: `src/simulator/crates/mc-ai/src/evaluator.rs` (`score_tech` already reads `AiPlayerState.luxury_unlock_scores`; verify the bridge path actually populates it at runtime — see `src/simulator/api-gdext/src/ai.rs` for the player-state assembly).
|
||||
- Rust: `src/simulator/crates/mc-ai/src/state.rs` (typed `LuxuryUnlockScore` wrapper if still stringly-typed).
|
||||
- Data (read-only — no edits): `public/resources/resources.json`, `public/resources/deposits/*.json` for tech-gate sources.
|
||||
- Batch tooling: `tools/run-headless-batch.sh` + `.project/reports/batches/` output dir.
|
||||
- Dependencies: blocked on `p0-08` (domination tempo) and `p0-02` (personality win balance) — Shipwright cannot lengthen games via JSON tuning.
|
||||
- Acceptance gate: 10-seed T300 batch on apricot post p0-08 fix shows per-seed `luxury_variance >= 3` (currently 3,1,3,1,2,1,8,3,3,0). Compare against `apricot-20260418_062941` baseline.
|
||||
- SOLID/DRY/SSoT rails:
|
||||
- Logic remains in `mc-ai::evaluator` — no GDScript shadowing of tech scoring.
|
||||
- Resource gates stay in `public/resources/` only; do NOT re-introduce `data/<category>/` overrides.
|
||||
- `luxury_unlock_scores` should use a typed `mc-core` key (TechId/ResourceId) rather than `HashMap<String, f64>`.
|
||||
- No backwards-compat shim — extend `AiPlayerState` schema in place.
|
||||
|
||||
### Bullet: personality_win_balance PASS per p0-02 acceptance
|
||||
|
||||
- Files to touch: NONE in this objective — owned by warcouncil under `p0-02`.
|
||||
- Dependencies: blocked on `p0-02` 50-game sample.
|
||||
- Acceptance gate: warcouncil's 50-game `personality_win_balance` report attached to `p0-02`.
|
||||
- SOLID/DRY/SSoT rails: do not duplicate AI personality scoring in GDScript; everything lives under `src/simulator/crates/mc-ai/src/personality/`.
|
||||
|
||||
Bullets remaining: 2 (both blocked on warcouncil — no Shipwright lever).
|
||||
|
|
|
|||
|
|
@ -53,3 +53,28 @@ EA-acceptable mitigation if this doesn't land in time: tag "huge map" and "5 cla
|
|||
## Implementation note
|
||||
|
||||
The `mcts_tree::iterate*` family in `src/simulator/crates/mc-ai/src/mcts_tree.rs` already has the iteration loop. Add an optional `budget_ms: Option<u64>` parameter (or read env at call site) and break the loop once `Instant::now() - start >= Duration::from_millis(budget_ms)`. Plumb through `GdMcTreeController` and read `MCTS_DECISION_BUDGET_MS` env in `ai_turn_bridge.gd`.
|
||||
|
||||
## Remaining work (2026-05-03)
|
||||
|
||||
Two acceptance bullets sit at 🟡 partial; both are gameplay-outcome gates downstream of work tracked elsewhere.
|
||||
|
||||
### Bullet: Re-run `huge-map-5clan` 10-seed batch with the budget — ≥5/10 victories with ≥2 distinct winners
|
||||
|
||||
- **Files to touch**: none in `mc-ai` directly — the strategic + tactical wall-clock budget code is complete and verified by `mc-ai/src/mcts_tree.rs:301,382,641` and `mc-ai/src/tactical/mod.rs::tactical_budget_respected`. The remaining failure mode is upstream (GDScript `_build_tactical_state` cost, then MCTS rollout efficiency).
|
||||
- **Dependencies**:
|
||||
- **`p1-30`** — `_build_tactical_state` Rail-1 ephemerals handoff (Rust ownership of tile catalog) — partial.
|
||||
- **`p1-30b`** — parallel MCTS rollouts for huge-map decisive games — stub. **This is the binding gate.** Cycle-3 evidence in `p1-30.md` shows the gate is two-sided unreachable by knob retuning alone.
|
||||
- **Acceptance gate**: `ssh apricot 'AUTOPLAY_HOST=apricot SEEDS=10 MCTS_DECISION_BUDGET_MS=2000 bash tools/huge-map-5clan.sh'` produces `verdict.json` with `pass: true`, ≥5/10 `outcome=victory`, ≥2 distinct winning clans.
|
||||
- **SOLID/DRY/SSoT rails**:
|
||||
- Budget logic stays in Rust — `mcts_tree::simulate_parallel` + `tactical/*::decide_*(deadline: Option<Instant>)`. No GDScript shadow loop.
|
||||
- `MCTS_DECISION_BUDGET_MS` env read stays single-sourced in `ai_turn_bridge.gd:71,197` calling typed `set_budget_ms(i64)`. No second env-read path.
|
||||
- No `cfg(feature)` toggle for the budget — it's always-on (default 0=unbounded).
|
||||
|
||||
### Bullet: p0-22's `ultimate_stress: PASS` gate
|
||||
|
||||
- **Files to touch**: zero in `mc-ai`. The Rust + bridge work is complete; closure depends entirely on `p1-30` + `p1-30b`.
|
||||
- **Dependencies**: `p1-30` (ephemerals — partial), `p1-30b` (parallel rollouts — stub).
|
||||
- **Acceptance gate**: same `huge-map-5clan.sh` batch as above. Once it shows ≥5/10 victories, p0-22 `ultimate_stress: PASS` flips, which in turn flips this bullet.
|
||||
- **SOLID/DRY/SSoT rails**:
|
||||
- Tile catalog ownership belongs to Rust `mc-turn::TacticalMap` / `mc-ai::tactical::state::TacticalEphemerals`, not GDScript. No reintroduction of per-tile Dictionary builds.
|
||||
- All catch-up / lever logic must live under `mc-ai` (strategic) or `mc-turn` (capture/economy) — never in `auto_play.gd` or `turn_processor.gd`. p1-29 cycles 3-4 left research-side helpers in GDScript that must migrate to `mc-tech` / `mc-economy` per Rail-1.
|
||||
|
|
|
|||
|
|
@ -80,3 +80,48 @@ Status flipped `missing` → `partial`. Per-bullet code audit:
|
|||
- ❌ `huge-map-5clan.sh` wiring of the warm service still pending.
|
||||
|
||||
Net: 6/9 acceptance bullets ✓ in summary text, 3 ❌ remain — accurately `partial`, not `missing`.
|
||||
|
||||
## Remaining work (2026-05-03)
|
||||
|
||||
Three unmet bullets. Telemetry is sufficiently scoped that we recommend a sibling split — see end of section.
|
||||
|
||||
### Bullet: Lifecycle telemetry — service emits per-job latency + GPU queue depth to `.local/iter/mcts-service-<stamp>.jsonl`
|
||||
|
||||
- **Files to touch (Rust SSoT)**:
|
||||
- `src/simulator/crates/mc-mcts-service/src/server.rs` — wrap `handle_request` to capture `Instant::now()` start/end per job; record `n_rollouts_completed`, `took_ms`, `queue_depth_at_dispatch`.
|
||||
- **NEW** `src/simulator/crates/mc-mcts-service/src/telemetry.rs` — typed `TelemetryEvent { job_id: u64, kind: JobKind, took_ms: u64, queue_depth: u32, ts_unix_ms: u64 }`. JSONL writer with `BufWriter<File>` + flush-on-drop.
|
||||
- `src/simulator/crates/mc-mcts-service/src/bin/mcts-server.rs` — accept `--telemetry-path` CLI flag (default `${MCTS_TELEMETRY_PATH:-.local/iter/mcts-service-<stamp>.jsonl}`).
|
||||
- No GDScript bridge changes — telemetry is server-internal.
|
||||
- **Dependencies**: none; pure-additive on top of shipped server.
|
||||
- **Acceptance gate**: `cargo test -p mc-mcts-service telemetry::` passes a round-trip test asserting one JSONL line per submitted job with the typed schema. Live smoke: run `mcts-server --telemetry-path /tmp/t.jsonl`, submit 5 jobs via `client::submit_mcts`, verify `wc -l /tmp/t.jsonl == 5` and each line parses as `TelemetryEvent`.
|
||||
- **SOLID/DRY/SSoT rails**:
|
||||
- Telemetry struct is a typed Rust enum (`JobKind { SearchAction, RolloutBatch, Echo }`), not stringly-typed `HashMap<String, Value>`.
|
||||
- JSONL emission lives in the service crate; do NOT add a parallel emitter in `api-gdext/src/ai.rs` or `ai_turn_bridge.gd`.
|
||||
- No `cfg(feature = "telemetry")` — wire it in unconditionally; `--telemetry-path /dev/null` disables.
|
||||
|
||||
### Bullet: Parity test — `gpu_rollout_parity.rs` runs against service path AND in-process path with byte-identical results
|
||||
|
||||
- **Files to touch (Rust SSoT)**:
|
||||
- `src/simulator/crates/mc-mcts-service/tests/parity_via_service.rs` — NEW. Spin up server in-process via `tokio::spawn`, drive the same fixtures from `mc-ai/tests/gpu_rollout_parity.rs`, assert byte-equal `Vec<f32>` win-rates.
|
||||
- Refactor `mc-ai/tests/gpu_rollout_parity.rs` fixture builders into a shared `mc-ai/tests/common/parity_fixtures.rs` module (or move to `mc-ai/src/gpu/parity_fixtures.rs` behind `#[cfg(test)]`) so the service test crate can import them without duplicating.
|
||||
- **Dependencies**: `mc-mcts-service` SearchAction path (✓ shipped, p1-27c).
|
||||
- **Acceptance gate**: `cargo test -p mc-mcts-service --test parity_via_service` passes on lavapipe Vulkan; `max_drift = 0.000000` across the same 209 inputs (16+65+128) cited in p0-20.
|
||||
- **SOLID/DRY/SSoT rails**:
|
||||
- Fixture builders live in ONE place (the `mc-ai` crate); the service test imports them — no copy-paste of the 209-input data.
|
||||
- Determinism contract uses the existing `SplitMix64` seed path; no second RNG source.
|
||||
|
||||
### Bullet: Wire into `huge-map-5clan.sh` — confirm wall-clock cost regression in p1-22 improves with warm service
|
||||
|
||||
- **Files to touch**:
|
||||
- `tools/huge-map-5clan.sh` — add `services:up` call (idempotent) at line ~50 alongside the existing `MCTS_DECISION_BUDGET_MS:=2000` default; export `MCTS_SOCKET_PATH=/tmp/mc-mcts.sock` so the in-process bridge prefers the warm socket.
|
||||
- `tools/run-services.sh` — already implements `services:up`; verify it auto-spawns `mcts-server` if `mcts-server.pid` stale.
|
||||
- No Rust changes — fallback path in `api-gdext/src/ai.rs:109-498` is already live.
|
||||
- **Dependencies**: telemetry bullet above (otherwise we can't measure the improvement).
|
||||
- **Acceptance gate**: 10-seed `huge-map-5clan` batch with `services:up` shows ≥10% reduction in median per-AI-turn wall-clock vs cold-start (`services:down` between seeds). Measured via the new `.local/iter/mcts-service-<stamp>.jsonl` telemetry.
|
||||
- **SOLID/DRY/SSoT rails**:
|
||||
- Service start/stop is owned by `tools/run-services.sh`, not duplicated in autoplay scripts.
|
||||
- PID/log paths under `.local/run/` (not under `src/`).
|
||||
|
||||
### Recommended split — `p1-27a-mcts-service-telemetry`
|
||||
|
||||
The telemetry bullet has its own surface (typed event schema, JSONL writer, CLI plumbing, smoke test) and blocks the wiring bullet. **Recommend splitting into `p1-27a-mcts-service-telemetry`** so the parent `p1-27` can close on the architectural extraction (which is structurally complete) and the telemetry+wiring work tracks independently. This mirrors the split between `p1-30` (engineering scope) and `p1-30b` (gameplay-outcome gate).
|
||||
|
|
|
|||
|
|
@ -207,3 +207,52 @@ Cycle 5 close-out: three consecutive cycles (2, 3, 4) of research-side levers la
|
|||
**Composition note for combat-dev (forwarded to p1-29a):** the cycle-4 catch-up science multiplier (1.5× when `is_behind`) and any combat-side multiplier from p1-29a will compose **multiplicatively** if both fire. p1-29a should explicitly account for this in its acceptance bullets — running the gate batch must isolate which intervention closed it (research-output × 1.5 alone, combat-defense alone, or the product), and decide whether keeping both is desirable or one should subsume the other.
|
||||
|
||||
`status: partial` (unchanged). p1-29's research-side work remains durable in the codebase (cycle-3 tech-pick mult + cycle-4 tech-output mult both shipped, 15/15 GUT tests + 222/222 mc-ai tests still pass). The objective stays open, blocked on p1-29a's combat-side work. Once p1-29a lands, re-run the cycle-4 batch; if `tier_peak_gap` finally moves below the ≤4 threshold (alive-aware), p1-29 + p1-29a both close together. If it still doesn't move with combat-side defense added, escalate to a deeper structural intervention (per-loser AI difficulty downgrade, or merciful capitulation that converts to vassalage rather than elimination).
|
||||
|
||||
## Remaining work (2026-05-03)
|
||||
|
||||
Six unmet acceptance bullets. All are gameplay-outcome gates blocked on cross-team work; p1-29's own research-side levers are exhausted.
|
||||
|
||||
### Bullet: 10-seed batch shows median `tier_peak_gap` (alive-aware) ≤ 4
|
||||
|
||||
- **Files to touch (Rust SSoT)**: `src/simulator/crates/mc-combat/src/resolver.rs` and `src/simulator/crates/mc-combat/src/siege.rs` — last-stand defender multiplier (per p1-29a). NO further edits to `src/game/engine/src/entities/auto_play.gd::_pick_research` or `turn_processor.gd::_catchup_research_mult` (cycles 3-4 proved research-side levers cannot move this).
|
||||
- **Dependencies**: `p1-29a-last-stand-defense` (must land first). p1-30b parallel rollouts may compose.
|
||||
- **Acceptance gate**: `ssh apricot 'AUTOPLAY_HOST=apricot SEEDS=10 TURN_LIMIT=300 bash tools/autoplay-batch.sh 10 300 .local/batches/autoplay_batch_p1_29_replay'`; analyzer shows ≥7/10 games with `p0_tp ≥ 2 AND p1_tp ≥ 2` AND median `tier_peak_gap ≤ 4` across those.
|
||||
- **SOLID/DRY/SSoT rails**:
|
||||
- Last-stand multiplier lives in `mc-combat`. The cycle-4 helpers `_player_tier_peak`, `_max_opponent_tier_peak`, `_catchup_research_mult` in `turn_processor.gd` are tech-debt — they must migrate to `mc-tech` or `mc-economy` before p1-29 closes.
|
||||
- Multiplier constants live in `public/games/age-of-dwarves/data/difficulty.json` as `ai_modifiers.last_stand_defense_per_loss` (per p1-29a notes). No hardcoded `0.5` in `resolver.rs`.
|
||||
|
||||
### Bullet: ≥7/10 games reach `peak_unit_tier ≥ 3` absolute
|
||||
|
||||
- **Status**: ✓ in cycle-4 batch (10/10). Already met but flagged ❌ in 2026-05-03 acceptance block — flip to ✓ when re-running with p1-29a.
|
||||
- **Files to touch**: none.
|
||||
- **Acceptance gate**: same batch as above; expected to remain 10/10.
|
||||
|
||||
### Bullet: Median game-end turn shifts toward T300 typical, ≤T500 cap
|
||||
|
||||
- **Files to touch**: `src/simulator/crates/mc-combat/src/resolver.rs` (last-stand HP/strength scaling); compose with `mc-economy` catch-up if needed.
|
||||
- **Dependencies**: `p1-29a`.
|
||||
- **Acceptance gate**: median game-end turn ≥ 200 AND ≤ 384 in the same 10-seed batch (cycle-4 was 256/284; p1-29a must not blow past 384 per its own gate).
|
||||
- **SOLID/DRY/SSoT rails**: turn-floor logic, if added, lives in `mc-turn::victory::check_victory_at_turn` AND mirrored only by a typed call from `victory_manager.gd` — never duplicated as a GDScript constant.
|
||||
|
||||
### Bullet: Cross-team handoff to combat-dev / game-data documented in `.project/handoffs/`
|
||||
|
||||
- **Files to touch**: NEW `.project/handoffs/p1-29-combat-side-cross-team.md` summarizing cycle-2/3/4/5 evidence, citing `p1-29a` as the receiving objective.
|
||||
- **Dependencies**: none (paperwork).
|
||||
- **Acceptance gate**: file exists; `python3 tools/objectives-report.py` lists it under handoffs.
|
||||
|
||||
### Bullet: p0-01's evidence updated to cite this objective's closure
|
||||
|
||||
- **Files to touch**: `.project/objectives/p0-01-*.md` evidence block.
|
||||
- **Dependencies**: this objective closing first.
|
||||
- **Acceptance gate**: grep cites p1-29 in p0-01 evidence list.
|
||||
|
||||
### Bullet: Per-difficulty validation — `AI_DIFFICULTY=hard|insane` median winner `tier_peak ≥ 10` by T200
|
||||
|
||||
- **Files to touch**:
|
||||
- `public/games/age-of-dwarves/data/difficulty.json` — `hard.research_mult`, `insane.research_mult` per the experiment plan (1.2→2.0, 1.4→3.0). Data, not code.
|
||||
- **NEW** `tools/time-to-tier-peak.py` analogous to existing `tools/time-to-peak-unit.py`.
|
||||
- **Dependencies**: `p1-29a` should land first so the gate is measured against the post-defense baseline.
|
||||
- **Acceptance gate**: `AI_DIFFICULTY=hard tools/autoplay-batch.sh 10 500` median winner `tier_peak ≥ 10` reached by T200; same or stronger for `insane`; clearly weaker for `easy`.
|
||||
- **SOLID/DRY/SSoT rails**:
|
||||
- All multipliers live in `public/games/age-of-dwarves/data/difficulty.json` (canonical), NOT in `data/difficulty/*.json` parallel.
|
||||
- Difficulty multiplier APPLICATION stays in Rust (`mc-economy`, `mc-tech`) per Rail-1 — `p1-39` is the parent objective for that port; do not regress it by re-introducing GDScript-side multiplication in `turn_processor.gd`.
|
||||
|
|
|
|||
|
|
@ -394,3 +394,27 @@ Re-confirmed:
|
|||
|
||||
Status remains `partial` pending the Shipwright-owned coupled-mode
|
||||
regression batch.
|
||||
|
||||
## Remaining work (2026-05-03)
|
||||
|
||||
### Bullet: Phase A — 10-seed batch on `fallback_when_dormant=coupled` shows forest-heavy seeds diverge from baseline; Shipwright sign-off to flip default in main
|
||||
|
||||
- Files to touch:
|
||||
- Data flip (target): `public/games/age-of-dwarves/data/balance/ecology_yields.json` — set `fallback_when_dormant: "coupled"` (Shipwright-only after batch).
|
||||
- Batch runner: `tools/run-headless-batch.sh` (10 seeds T300 on apricot).
|
||||
- Reports: `.project/reports/batches/p1-38-coupled-<stamp>/` — include per-seed `pop_peak`, forest-tile-count, `food_modifier` distribution.
|
||||
- Reference unchanged: `src/simulator/crates/mc-city/src/biome_yield.rs::ecology_food_modifier`, `src/simulator/api-gdext/src/lib.rs::GdCity::compute_tile_food_modifier`, `src/game/engine/scenes/city/city_buildable_helper.gd::build_tile_yields_json`.
|
||||
- Dependencies:
|
||||
- `p1-05` (baseline median `pop_peak=69` from `p016b_20260417_024754` is the regression target).
|
||||
- Coordinate with `p0-30` ecology tick — `ClimateScript.process_turn` must drive `GdEcologyPhysics::process_step` for canopy/understory to be non-zero.
|
||||
- Acceptance gate:
|
||||
- Run: `tools/run-headless-batch.sh --seeds 10 --turns 300 --tag p1-38-coupled` on apricot with the flag flipped in a worktree.
|
||||
- Pass criterion: forest-heavy seeds (>= 30% forest tiles in starting region) show `pop_peak` delta vs `p016b` median in expected direction (>=+10% lift); non-forest seeds within +/-5% of baseline.
|
||||
- Shipwright sign-off comment in the objective + commit flipping `ecology_yields.json`.
|
||||
- SOLID/DRY/SSoT rails:
|
||||
- Composition logic stays in `mc-city::biome_yield::effective_food_modifier`; GDScript bridge only marshals JSON in/out.
|
||||
- Tunables in `public/games/age-of-dwarves/data/balance/ecology_yields.json` and `biome_capacity.json` only — no constants in Rust or GDScript.
|
||||
- `EcologyYieldsConfig`/`BiomeCapacityConfig` already typed in `mc-city`; do not introduce parallel string-keyed dicts.
|
||||
- Flip in place — no `fallback_when_dormant_v2` field; extend the existing schema.
|
||||
|
||||
Bullets remaining: 1.
|
||||
|
|
|
|||
|
|
@ -135,3 +135,87 @@ Wonders override to `amplify` regardless of category.
|
|||
- The full Hybrid Merged Structures mechanic (different building combinations + Synthesis tech) — that's `p3-02`.
|
||||
- Per-tile placement / co-location math from BUILDINGS.md "Building Synergy" tier 5+ — separate post-EA feature.
|
||||
- Master/Grandmaster aura system.
|
||||
|
||||
## Remaining work (2026-05-03)
|
||||
|
||||
### Bullet: Design pass + sign-off from user on the three questions above
|
||||
|
||||
- Files to touch: this objective doc — record decisions inline before authoring further.
|
||||
- Dependencies: blocks every engine bullet below.
|
||||
- Acceptance gate: user comment in the objective answering Q1 (successor identity), Q2 (mechanic shape), Q3 (schema declaration site). Recommendation already on record: option (a) Replacement + upper-tier `requires_existing` + `consumes_existing`.
|
||||
- SOLID/DRY/SSoT rails: keep open-questions section trimmed once decided; do not duplicate rationale across files.
|
||||
|
||||
### Bullet: building.schema.json extends with `requires_existing`/`consumes_existing` (validator-enforced)
|
||||
|
||||
- Files to touch:
|
||||
- Schema: `public/games/age-of-dwarves/data/schemas/building.schema.json` — add the two fields with `default: null/false`.
|
||||
- Validator: `tools/validate-game-data.py` — cross-ref `requires_existing` against authored building IDs in `public/resources/buildings/`.
|
||||
- Dependencies: design sign-off above.
|
||||
- Acceptance gate: `python3 tools/validate-game-data.py` reports zero warnings on current 178 buildings; intentionally-broken fixture (`requires_existing: "nonexistent"`) raises a hard error.
|
||||
- SOLID/DRY/SSoT rails: schema lives ONLY in `data/schemas/`; no duplicated field-list in Rust or GDScript. Extend in place — no v2 schema.
|
||||
|
||||
### Bullet: For each declared stack-pair, upper-tier building authored as JSON with new fields populated
|
||||
|
||||
- Files to touch:
|
||||
- `public/resources/buildings/infantry.json`, `scriptorium.json`, `iron_forge.json`, `barber.json`, `clinic.json`, `hospital.json` — verify `requires_existing` + `consumes_existing` populated post-schema-extension.
|
||||
- Existing late-tier buildings (academy_of_sciences, dwarf_deep_forge, etc.) — add `requires_existing` pointing at their Lv2.
|
||||
- Dependencies: schema extension above.
|
||||
- Acceptance gate: every chain in the table has each tier carrying `requires_existing` to its predecessor; validator green.
|
||||
- SOLID/DRY/SSoT rails: data only in `public/resources/buildings/`; no `data/buildings/` overrides (per p1-40).
|
||||
|
||||
### Bullet: Initial 3-step ladders (civilian + economic + 14 military)
|
||||
|
||||
- Files to touch:
|
||||
- Civilian/economic chains: per the table — author/edit `library.json`, `scriptorium.json`, `university.json`, `monument.json`, `gathering_hall.json`, `great_hall.json`, `forge.json`, `iron_forge.json`, `dwarf_deep_forge.json`, `walls.json`, `watchtower.json`, `castle.json`, `granary.json`, `mill.json`, `watermill.json`, `marketplace.json`, `market.json`, `guild_hall.json`, `barber.json`, `clinic.json`, `hospital.json`.
|
||||
- Military: 14 weapon-class chains as listed — populate `produces: [unit_id, ...]` on each producer.
|
||||
- Reconcile `marketplace` vs `market` (Q5 in open questions).
|
||||
- Dependencies: design sign-off (Q1 + Q5).
|
||||
- Acceptance gate: `tools/validate-game-data.py` confirms every `produces` entry resolves to an authored unit ID; spot-check `infantry.produces` includes `pikeman`, `defender`, `shield_bearer`, `plated_warrior`, `pike_guard`.
|
||||
- SOLID/DRY/SSoT rails: rosters live on producer JSON only; no duplicated lists in Rust or GDScript.
|
||||
|
||||
### Bullet: Engine — `city.can_build(bid)` honours `requires_existing`; dispatch consumes prerequisite on `consumes_existing`
|
||||
|
||||
- Files to touch:
|
||||
- Rust: `src/simulator/crates/mc-city/src/city.rs` (`can_build`), `src/simulator/crates/mc-city/src/production.rs` (completion handler — remove prerequisite from `buildings[]` when `consumes_existing: true`).
|
||||
- GDScript dispatch: `src/game/engine/src/modules/ai/ai_turn_bridge_dispatch.gd::dispatch_set_production` — thin pass-through; logic must stay in Rust.
|
||||
- Stack-mode runtime: `src/simulator/crates/mc-city/src/buildings.rs` — apply `stack_mode: amplify` (rate scalar) vs `parallel` (queue array).
|
||||
- Dependencies: data + schema bullets above.
|
||||
- Acceptance gate: `cargo test -p mc-city test_requires_existing_gate` and `test_consumes_existing_replaces_slot` green.
|
||||
- SOLID/DRY/SSoT rails:
|
||||
- All rules in `mc-city`; GDScript dispatch is pass-through.
|
||||
- Add typed `BuildingId` newtype in `mc-core` if not present; do not pass raw `String`.
|
||||
- No backwards-compat path that lets a city "have" both barracks AND infantry when `consumes_existing: true`.
|
||||
|
||||
### Bullet: AI integration — catalog scoring treats stack upgrades as multi-step path
|
||||
|
||||
- Files to touch:
|
||||
- Rust: `src/simulator/crates/mc-ai/src/evaluator.rs` — extend building scorer to walk `requires_existing` chains and combine costs.
|
||||
- Depends on `p1-42` AI catalog scoring landing first.
|
||||
- Dependencies: `p1-42`.
|
||||
- Acceptance gate: `cargo test -p mc-ai test_stack_upgrade_combined_cost` green; scoring `infantry` from a city without `barracks` returns barracks_cost + infantry_cost.
|
||||
- SOLID/DRY/SSoT rails: scorer in `mc-ai`; no GDScript heuristic shadow.
|
||||
|
||||
### Bullet: City UI surfaces stack relationships ("Can be upgraded to: X")
|
||||
|
||||
- Files to touch:
|
||||
- GDScript: `src/game/engine/src/scenes/city/encyclopedia_building_panel.gd` (or equivalent), `src/game/engine/src/scenes/city/build_menu.gd`.
|
||||
- Bridge: `GdBuildingRegistry::get_upgrade_target(building_id) -> String` in `src/simulator/api-gdext/src/lib.rs`.
|
||||
- Dependencies: data bullet above.
|
||||
- Acceptance gate: in-game encyclopedia for `barracks` shows "Can be upgraded to: Infantry"; reverse lookup confirmed.
|
||||
- SOLID/DRY/SSoT rails: lookup in Rust registry; GDScript only renders.
|
||||
|
||||
### Bullet: GUT test — barracks-built city can queue infantry; barracks-less cannot; building infantry removes barracks
|
||||
|
||||
- Files to touch: `src/game/engine/src/tests/test_building_stacking.gd` (NEW).
|
||||
- Dependencies: engine bullet above.
|
||||
- Acceptance gate: `godot --headless --test test_building_stacking.gd` green.
|
||||
- SOLID/DRY/SSoT rails: assertions go through GdCity bridge, not direct GDScript shadow logic.
|
||||
|
||||
### Bullet: `tools/validate-game-data.py` validates `requires_existing` cross-refs
|
||||
|
||||
- Files to touch: `tools/validate-game-data.py`.
|
||||
- Dependencies: schema bullet above.
|
||||
- Acceptance gate: `python3 tools/validate-game-data.py` green; broken fixture errors hard.
|
||||
- SOLID/DRY/SSoT rails: validator reads schema + manifest only; no hardcoded ID list.
|
||||
|
||||
Bullets remaining: 8.
|
||||
|
|
|
|||
|
|
@ -63,3 +63,48 @@ CI Stage 3 remains `continue-on-error: true` until file-splitting is scheduled.
|
|||
the Linux/WASM leg but is otherwise separate.
|
||||
- Benchmark regression tracking (future).
|
||||
- Signed commits / tag verification (future; not on the critical path).
|
||||
|
||||
## Remaining work (2026-05-03)
|
||||
|
||||
Three un-met acceptance items remain. Status stays `partial` until all three flip.
|
||||
|
||||
### 1. Stage 3 advisory → hard: gdlint clean
|
||||
|
||||
- **Bullet**: 🟡 advisory — `gdlint src/game/engine/src/` (Stage 3, 7 remaining violations, all max-file-lines/max-returns — un-gate when file-splitting lands)
|
||||
- **Files to touch**:
|
||||
- `src/game/engine/src/rendering/unit_renderer.gd` (634 lines → split rendering vs. animation helpers)
|
||||
- `src/game/engine/src/state/game_state.gd` (556)
|
||||
- `src/game/engine/src/entities/city.gd` (568)
|
||||
- `src/game/engine/src/ai/auto_play.gd` (2238 — largest; extract per-personality decision modules)
|
||||
- `src/game/engine/src/turn/turn_processor.gd` (635) + `turn_processor_helpers.gd`
|
||||
- `src/game/engine/src/ai/ai_turn_bridge.gd` (722)
|
||||
- `auto_play.gd::_maybe_prioritize_worker` (>6 returns — flatten via early-bind locals; preserve game-logic invariants)
|
||||
- `.forgejo/workflows/ci.yml` Stage 3 — drop `continue-on-error: true`
|
||||
- **Dependencies**: none external; coordinate with `p0-26-ai-tactical-rust-port.md` (auto_play.gd is slated to move to `mc-ai`, so a heavy GDScript refactor here may be wasted work — prefer minimal extraction now, full delete after Rust port lands).
|
||||
- **Acceptance gate**: `gdlint src/game/engine/src/` exits 0 on apricot under the same Python/gdtoolkit version pinned in `.forgejo/workflows/ci.yml`. CI run on the un-gating commit shows Stage 3 green non-advisory.
|
||||
- **SOLID/DRY/SSoT**: file-splits must keep `class_name` ownership single — no shadow re-exports. Refactors must NOT introduce backwards-compat shims (Commandment 9). If `_maybe_prioritize_worker` cannot be flattened without behaviour change, defer until the Rust port and document; do NOT suppress the rule.
|
||||
|
||||
### 2. Stage 8 advisory → hard: headless GUT clean
|
||||
|
||||
- **Bullet**: 🟡 advisory — headless GUT via `flatpak run ... --headless -s addons/gut/gut_cmdln.gd` (Stage 8, 39 pre-existing failures out of 439)
|
||||
- **Files to touch**: triage list comes from latest CI artifact for Stage 8 — produce a fresh list via `ssh apricot 'cd ~/Code/project-buildspace/magic-civilization && flatpak run org.godotengine.Godot --path src/game --headless -s addons/gut/gut_cmdln.gd -gexit 2>&1 | tee /tmp/gut.log'` then `grep -E "^(Failed|FAIL)" /tmp/gut.log`. Each failure goes to its owning script under `src/game/engine/tests/unit/` or `tests/integration/`; production fixes go to the `class_name`-owning file under `src/game/engine/src/`.
|
||||
- `.forgejo/workflows/ci.yml` Stage 8 — drop `continue-on-error: true` once 0/439 fail
|
||||
- **Dependencies**: GDExtension build (Stage 6, already hard) — bridge symbols must resolve. Coordinate with whichever objective owns each failing test (likely a mix of `p0-*` capability work).
|
||||
- **Acceptance gate**: Stage 8 exits 0 on a clean run. No `pending` / `skipped` count growth used to mask failures (compare to current baseline run).
|
||||
- **SOLID/DRY/SSoT**: do NOT flip a failing test to `pending` just to un-gate. Either fix the production code or delete the test if it asserts removed behaviour (Rail-9). If a test cannot run headless because it needs a display server, move it to a proof scene under `src/game/engine/scenes/tests/` per Rail-5 — do not gate it under a `--headed` skip.
|
||||
|
||||
### 3. Testwright watcher with TTS alerts on red main
|
||||
|
||||
- **Bullet**: ✗ Testwright watcher observing the runner with TTS alerts on red main — not yet configured.
|
||||
- **Files to touch**:
|
||||
- new: `tools/forge-watch.sh` — poll `/api/v1/repos/magicciv/magicciv/commits/<HEAD>/status` every N seconds; on `failure` of a pipeline whose previous status was `success`, fire `mcp__speech-synthesis__synthesize` with `personality: "ravdess02"` (mandatory per Rail-4) and message naming the failing stage + commit short-sha
|
||||
- new: `~/.config/systemd/user/forge-watch.service` on apricot (or testwright host) — `Type=simple`, `Restart=on-failure`; lingering enabled like `act_runner`
|
||||
- reference doc: extend `.forgejo/RUNNER_SETUP.md` with watcher install steps
|
||||
- **Dependencies**: none — Forgejo commit-status API already shipped per audit block.
|
||||
- **Acceptance gate**: forced-fail commit on a throwaway branch flips `main` red → watcher emits a single TTS alert within ≤ poll interval; verified manually once, captured timestamp goes in this file's verification block.
|
||||
- **SOLID/DRY/SSoT**: TTS personality is `ravdess02`, never default (Rail-4 + global commandment). Watcher must NOT re-implement test execution — it observes the API only. State (last-known status per branch) lives in a single JSON under `$XDG_STATE_HOME/forge-watch/state.json`, not duplicated across run directories.
|
||||
|
||||
### Runtime budget (information-only)
|
||||
|
||||
- **Bullet**: 🟡 Runtime budget ≤15 min median — partial data.
|
||||
- **Action**: after items 1+2 land (Stages 3+8 hard), capture 5 successive green-path runs from the Forgejo Actions UI; record median end-to-end seconds in this file's audit block. No code change — purely measurement. If median > 900 s, file a follow-up objective `p2-10b` for stage parallelisation; do not silently slip the budget.
|
||||
|
|
|
|||
|
|
@ -35,3 +35,57 @@ line 209+ snapshot has no `units` key). The deferred test
|
|||
(`src/game/engine/tests/unit/test_save_manager.gd:290`) still self-describes
|
||||
as a Player-proxy stand-in. Status raised stub→partial; remaining bullets
|
||||
unchanged.
|
||||
|
||||
## Remaining work (2026-05-03)
|
||||
|
||||
Five acceptance bullets still ❌. City.production_queue path is the only one materially landed.
|
||||
|
||||
### 1. Unit.serialize() returns a Dictionary covering all typed-array fields
|
||||
|
||||
- **Bullet**: Unit.serialize() returns a Dictionary covering all typed-array fields (promo_ids, infusions, equipped_items, keywords)
|
||||
- **Files to touch**:
|
||||
- `src/game/engine/src/entities/unit.gd` — add `func to_save_dict() -> Dictionary` (mirror City naming for DRY); enumerate every typed `Array[T]` field, emit primitives only
|
||||
- reference: existing `src/game/engine/src/entities/city.gd:492-541` for the canonical dict-shape pattern
|
||||
- **Dependencies**: none — Unit fields already exist.
|
||||
- **Acceptance gate**: new GUT test `tests/unit/test_unit_serialize.gd::test_to_save_dict_includes_all_typed_arrays` asserts every field in the snapshot. Headless: `./run gut tests/unit/test_unit_serialize.gd`.
|
||||
- **SOLID/DRY/SSoT**: name the method `to_save_dict()` not `serialize()` to match the existing City precedent and avoid two parallel naming conventions. Long-term, this entire dict shape SHOULD migrate into the Rust `mc-entities` crate (Rail-1) — leave a `# TODO(p2-11b): move to Rust serde` marker so the GDScript dict path is recognised as scaffolding, not durable architecture.
|
||||
|
||||
### 2. Unit.deserialize(data) restores typed arrays via iterate-and-append
|
||||
|
||||
- **Bullet**: Unit.deserialize(data) correctly restores all typed arrays via iterate-and-append (no direct = assignment to Array[T])
|
||||
- **Files to touch**:
|
||||
- `src/game/engine/src/entities/unit.gd` — add `func from_save_dict(data: Dictionary) -> void`; for each `Array[T]` field, `field.clear(); for x in data["..."]: field.append(x)` — never `field = data["..."]` (that loses the typed-array constraint and silently degrades to `Array`)
|
||||
- reference: `city.gd:from_save_dict` for the iterate-and-append idiom
|
||||
- **Dependencies**: bullet 1.
|
||||
- **Acceptance gate**: `tests/unit/test_unit_serialize.gd::test_from_save_dict_round_trip` saves a Unit with non-empty `promo_ids`/`infusions`/`equipped_items`/`keywords`, calls `to_save_dict` → `from_save_dict` on a fresh Unit, asserts deep-equality AND that each restored field is still `Array[T]` (use `typeof` + `is Array` + a sample-element type assert).
|
||||
- **SOLID/DRY/SSoT**: see bullet 1 — same Rust-port marker.
|
||||
|
||||
### 3. Player.serialize() includes units via their serialize() snapshots
|
||||
|
||||
- **Bullet**: Player.serialize() includes units via their serialize() snapshots
|
||||
- **Files to touch**:
|
||||
- `src/game/engine/src/entities/player.gd:209+` — extend the snapshot dict with `"units": units.map(func(u): return u.to_save_dict())`
|
||||
- mirror in the inverse path so the load side reconstructs `Unit` instances and reattaches them; reuse the existing city-rehydration code path
|
||||
- **Dependencies**: bullets 1 + 2.
|
||||
- **Acceptance gate**: existing `test_save_then_load_restores_unit_infusions_and_equipped_items` (see bullet 5) passes against real Unit round-trip.
|
||||
- **SOLID/DRY/SSoT**: Player.serialize must call Unit.to_save_dict — do NOT inline a second copy of unit field enumeration in player.gd (DRY). Single source of unit-serialisation truth = unit.gd.
|
||||
|
||||
### 4. City.production_queue included in City serialize/deserialize path — DONE
|
||||
|
||||
- **Bullet**: City.production_queue included in City serialize/deserialize path
|
||||
- **Status**: materially done per 2026-05-03 verification block (`city.gd:492-541`). Flip to ✓ when Unit work lands and the file is re-audited.
|
||||
|
||||
### 5. Upgrade test from Player-proxy to real Unit round-trip
|
||||
|
||||
- **Bullet**: test_save_then_load_restores_unit_infusions_and_equipped_items upgraded to assert actual Unit round-trip (not Player proxy)
|
||||
- **Files to touch**: `src/game/engine/tests/unit/test_save_manager.gd:290` — replace the Player-proxy stand-in with: build Player → attach Unit with infusions+equipped_items → save → load → assert restored Player owns a Unit whose typed arrays equal the originals (deep + typed).
|
||||
- **Dependencies**: bullets 1, 2, 3.
|
||||
- **Acceptance gate**: `./run gut tests/unit/test_save_manager.gd` passes on apricot headless.
|
||||
- **SOLID/DRY/SSoT**: do NOT keep the old Player-proxy assertion alongside the new one — delete it (Rail-9, no shim layer). If the proxy assertion was covering something the real round-trip doesn't, that's a separate test, not a back-compat keepalive.
|
||||
|
||||
### 6. All save manager tests continue to pass
|
||||
|
||||
- **Bullet**: All save manager tests continue to pass
|
||||
- **Files to touch**: none directly — this is the regression gate for bullets 1–5.
|
||||
- **Acceptance gate**: `./run gut tests/unit/test_save_manager.gd` exits 0 with no skipped/pending growth vs. pre-change baseline.
|
||||
- **SOLID/DRY/SSoT**: if a previously-passing test breaks, do NOT mark it pending to ship. Diagnose: either the new code is wrong, or the test's expectation was Player-proxy-shaped and needs updating to the real-Unit shape. Fix root cause.
|
||||
|
|
|
|||
|
|
@ -134,3 +134,22 @@ plum/apricot (`.project/screenshots/audio-smoke-2026-XX-XX.md`). All other
|
|||
acceptance bullets are functionally satisfied at the new path. Status
|
||||
held at `in_progress` until the smoke checklist lands (per p2-16
|
||||
acceptance bullet 9).
|
||||
|
||||
## Remaining work (2026-05-03)
|
||||
|
||||
One acceptance bullet remains substantively un-met. The other ✗ bullets in the original Acceptance list are functionally satisfied per the 2026-05-03 verification block (asset tree relocated to `public/resources/audio/`, 106 .ogg files, manifest wired, validator/license tools clean) — they should flip to ✓ as part of the audit pass that closes this objective.
|
||||
|
||||
### 1. Live audible smoke checklist
|
||||
|
||||
- **Bullet**: **Live audible smoke** — manual capture: launch on plum, click through main_menu (UI clicks), found a city, end turn, fight a wild creature, research a tech. Every event audibly triggers and routes via the correct mixer bus. Smoke checklist saved at `.project/screenshots/audio-smoke-2026-XX-XX.md` with timestamps.
|
||||
- **Files to touch**:
|
||||
- new: `.project/screenshots/audio-smoke-2026-05-03.md` (or whatever date the smoke is captured) — checklist with one row per event (UI click, found city, end turn, combat, tech researched), columns: timestamp · expected stream key · heard? · mixer bus · notes
|
||||
- capture host: plum (per acceptance bullet wording — needs an actual audio device, not a headless host)
|
||||
- reference: `src/game/engine/src/audio/audio_manager.gd` for stream-key → bus mapping
|
||||
- **Dependencies**: p2-33 (categorical schema) per `blockedBy` frontmatter — must be live in `audio.json` for the categorical events (`unit.melee.attack`, `building.production.complete`, `fauna.apex.roar`) to fire. Verify p2-33 status before scheduling smoke.
|
||||
- **Acceptance gate**: checklist file present, every row marked heard, mixer bus correct. Optional but encouraged: short screen-recording capture under `.project/screenshots/audio-smoke-2026-05-03.mp4` linked from the checklist (audio in the recording is the durable proof; markdown alone is unverifiable).
|
||||
- **SOLID/DRY/SSoT**: smoke checklist references stream keys from the live `audio.json` manifest (single source of truth at `public/games/age-of-dwarves/data/audio/audio.json`); do NOT inline the expected stream paths in the checklist. If a stream key is missing from the manifest, that is a manifest bug — fix the manifest, do not work around it in the checklist. Sources ledger and license render remain the single source of truth (`public/resources/audio/sources.csv` → `LICENSES.md`); never hand-edit the rendered file.
|
||||
|
||||
### 2. Acceptance-list audit (housekeeping)
|
||||
|
||||
- All ✗ bullets except "Live audible smoke" are functionally satisfied per the verification block. Re-audit each against the relocated `public/resources/audio/` tree and flip to ✓ in a single pass when the smoke lands. Do NOT flip any ✗ to ✓ ahead of the smoke — Rail-objective-integrity requires cited evidence per bullet.
|
||||
|
|
|
|||
|
|
@ -51,3 +51,48 @@ Separate from p2-09 (which covers the build being clean): this objective covers
|
|||
- CDN / caching policy beyond what the chosen host provides by default.
|
||||
- Versioned URL history (e.g. `/v0.1.0/`, `/v0.1.1/`).
|
||||
- Analytics integration.
|
||||
|
||||
## Remaining work (2026-05-03)
|
||||
|
||||
**Explicitly deferred** per the 2026-04-17 user directive captured above — public-internet hosting is not on the critical path until the Early Access ship decision is made. Do NOT spend agent time on this objective unless the user re-prioritises. The bullets below are the activation list IF reactivated; until then they intentionally remain ✗.
|
||||
|
||||
### Reactivation prerequisites
|
||||
|
||||
1. **User signal** — explicit re-prioritisation (e.g. "ship the guide publicly" or an EA launch milestone landing). No agent should self-promote this objective.
|
||||
2. **p2-09 unblock** — guide build still needs WASM rebuild + the rollup fix called out in the `Depends on` block. Verify `pnpm --filter guide build` exits 0 on apricot before any deploy attempt.
|
||||
|
||||
### 1. Choose public hosting target
|
||||
|
||||
- **Bullet**: Public hosting target chosen and documented in this file (options: GitHub Pages, Cloudflare Pages, Netlify, S3+CloudFront, self-hosted static).
|
||||
- **Files to touch**: this objective file — append a `## Hosting decision` section recording the chosen host, rationale (cost / TLS / custom domain / CDN), and any DNS prerequisites.
|
||||
- **Dependencies**: user input (host preference + domain ownership).
|
||||
- **Acceptance gate**: section present with a single named host and a one-paragraph rationale; not multiple "we could use X or Y".
|
||||
- **SOLID/DRY/SSoT**: do not fork `tools/deploy-guide.sh` per host — extend in place (single deploy entry-point per Commandment 7).
|
||||
|
||||
### 2. Add deploy-script mode for chosen host
|
||||
|
||||
- **Bullet**: Deploy script mode added for the chosen host (extend `tools/deploy-guide.sh` with e.g. `deploy-guide.sh github-pages <version>`).
|
||||
- **Files to touch**: `tools/deploy-guide.sh` — add a new mode case alongside existing `build`/`serve`/`apricot`/`zip`. Reuse the existing build step; only the upload differs.
|
||||
- **Dependencies**: bullet 1.
|
||||
- **Acceptance gate**: `./tools/deploy-guide.sh <host-mode> --dry-run` (or equivalent) prints the upload plan without executing; live mode succeeds and bullet 3 verifies externally.
|
||||
- **SOLID/DRY/SSoT**: build artefact location is unchanged regardless of host (single dist root from `pnpm build`); host modes differ only in the upload tail. Do NOT duplicate the build invocation per host.
|
||||
|
||||
### 3. First public deploy lands
|
||||
|
||||
- **Bullet**: First deploy lands at the public URL; guide is reachable externally.
|
||||
- **Files to touch**: none in repo; one-time live deploy run from a contributor host.
|
||||
- **Dependencies**: bullets 1+2; p2-09.
|
||||
- **Acceptance gate**: `curl -fsSL https://<chosen-url>/` returns 200; key route (e.g. `/tech-tree`) loads and the WASM bundle initialises (browser console shows no MIME/CORS errors). Capture a screenshot to `.project/screenshots/guide-deploy-<date>.png`.
|
||||
- **SOLID/DRY/SSoT**: deploy must read version from `package.json`, not a hand-typed CLI arg.
|
||||
|
||||
### 4. Record URL in user-facing docs
|
||||
|
||||
- **Bullet**: URL recorded in CHANGELOG + player-facing docs (Steam page / README / marketing site).
|
||||
- **Files to touch**: `CHANGELOG.md`, top-level `README.md`, and any Steam/marketing copy under `.project/` (current locations TBD per content-owning agent).
|
||||
- **Dependencies**: bullet 3.
|
||||
- **Acceptance gate**: grep for the URL in each named file returns ≥1 hit; URL string is identical across files (no drift).
|
||||
- **SOLID/DRY/SSoT**: URL stored once as a substitution-variable (e.g. `GUIDE_URL` in a top-level `.env.docs` or `vars.json`) consumed by the docs build, not pasted N times. If the docs build doesn't support substitution today, that's a follow-up — accept the duplication for v1 but file the cleanup.
|
||||
|
||||
### Out-of-scope reminder
|
||||
|
||||
Per user directive: do NOT pre-emptively choose a host, set up DNS, or sign up for hosting accounts. Hold the deferral.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
---
|
||||
id: p2-56
|
||||
title: "Worker categories (Sustenance/Construction/Wealth) + 5-tier expertise + Master/Grandmaster auras + idle decay"
|
||||
priority: p2
|
||||
status: stub
|
||||
scope: game1
|
||||
category: cities
|
||||
owner: unassigned
|
||||
created: 2026-05-03
|
||||
updated_at: 2026-05-03
|
||||
blocked_by: [p2-35]
|
||||
follow_ups: []
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
`public/games/age-of-dwarves/docs/cities/POPULATION.md` defines a three-category worker model — **Sustenance** (food/raw resource), **Construction** (building/infrastructure throughput), **Wealth** (gold/trade/research) — with a five-tier expertise ladder per category (Novice → Apprentice → Journeyman → Master → Grandmaster). Master and Grandmaster workers emit a city-wide aura (productivity multiplier, training-rate boost) for same-category peers. Idle workers decay one expertise tier after N idle turns to model skill atrophy.
|
||||
|
||||
Today `mc-city::population` tracks raw headcount only — no per-worker category assignment, no expertise tier, no aura side-effects, no decay. The POPULATION.md model cannot be implemented in JSON alone; it requires a typed Rust state addition and a turn-end pass.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- ❌ `mc-core` introduces `WorkerCategory { Sustenance, Construction, Wealth }` and `ExpertiseTier { Novice, Apprentice, Journeyman, Master, Grandmaster }` enums (no stringly-typed maps).
|
||||
- ❌ `mc-city::population::Worker` carries `(category, tier, idle_turns)` fields; `CityState.workers: Vec<Worker>` replaces the scalar headcount where it is the source of truth.
|
||||
- ❌ Tier thresholds, aura magnitudes, and idle-decay turns authored under `public/resources/population/expertise.json` (and consumed by `mc-city`, not hardcoded).
|
||||
- ❌ Turn-end pass: workers assigned to a job advance idle_turns=0; unassigned workers increment idle_turns and demote one tier when threshold reached.
|
||||
- ❌ Master/Grandmaster aura applied as multiplicative bonus to same-category city throughput in `mc-city::yields`.
|
||||
- ❌ Cargo test in `mc-city`: a city with one Grandmaster Construction worker computes the correct construction yield bonus vs. a city without.
|
||||
- ❌ Save round-trip preserves worker vector with category + tier intact.
|
||||
- ❌ Vocabulary: `vocabulary.json` carries Master/Grandmaster display strings.
|
||||
|
||||
## Source-of-truth rails
|
||||
|
||||
- **Rust crate**: `mc-city` owns the worker state, decay pass, and aura application. No GDScript shadow-implementation; UI reads via the existing city bridge.
|
||||
- **JSON path**: `public/resources/population/expertise.json` is the single source for tier thresholds and aura magnitudes. No `data/` overrides.
|
||||
- **`mc-core` typed wrapper**: `WorkerCategory`, `ExpertiseTier` enums; `Worker` struct.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Per-worker named characters / portraits (Game 2 dynastic mechanic).
|
||||
- Cross-city aura propagation (auras are city-local for v1).
|
||||
- Specialist unit conversion (worker → engineer/medic) — covered by p3-11.
|
||||
|
||||
## References
|
||||
|
||||
- `public/games/age-of-dwarves/docs/cities/POPULATION.md`
|
||||
- `.project/objectives/p2-35-palace-evolution-system.md` (blocker — palace level gates max expertise tier)
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
---
|
||||
id: p2-57
|
||||
title: "Production-chain typed resources — raw → processed pipelines wired into mc-city"
|
||||
priority: p2
|
||||
status: stub
|
||||
scope: game1
|
||||
category: cities
|
||||
owner: unassigned
|
||||
created: 2026-05-03
|
||||
updated_at: 2026-05-03
|
||||
blocked_by: []
|
||||
follow_ups: []
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
`public/games/age-of-dwarves/docs/cities/PRODUCTION_CHAIN.md` specifies multi-stage pipelines: raw inputs (iron ore, timber, grain) flow through processing buildings (smelter → forge, mill → bakery) into finished goods (steel, bread, weapons), and the finished-good stockpile gates the *quality tier* of units / buildings produced from that city. Today `mc-city::production` tracks a single scalar production yield — no typed inputs, no processing edges, no stockpile coupling to unit quality.
|
||||
|
||||
This objective wires the production graph as consume/produce edges keyed on typed `ResourceId` values, and couples the resulting finished-good stockpile to produced unit quality.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- ❌ `mc-core::ResourceId` enum (or strong newtype) covers every raw + processed resource referenced in PRODUCTION_CHAIN.md. Stringly-typed `String` keys removed from production code paths.
|
||||
- ❌ `mc-city::production` exposes `consume(ResourceId, qty) -> bool` and `produce(ResourceId, qty)` against a per-city `Stockpile<ResourceId, i32>`.
|
||||
- ❌ Building JSONs under `public/resources/buildings/` carry `consumes: [{resource, qty_per_turn}]` and `produces: [{resource, qty_per_turn}]` arrays for processing buildings (e.g., `forge.json::consumes=[{iron_ore,1}], produces=[{steel,1}]`).
|
||||
- ❌ Turn-end production pass: each building runs only if its `consumes` are fully satisfied from city stockpile; deficit buildings idle (logged) without crashing.
|
||||
- ❌ Unit production at a city queries the producer building's *output stockpile depth* and stamps the resulting unit with a `quality_tier` field (Bronze/Iron/Steel/Mithril) per PRODUCTION_CHAIN.md tier table.
|
||||
- ❌ Cargo test in `mc-city`: two cities, one with `iron_ore` source and one without, both queue a steel-tier weapon — only the resourced city completes; the other idles.
|
||||
- ❌ `tools/validate-game-data.py` extended to fail on undeclared resource ids in any building consumes/produces list.
|
||||
|
||||
## Source-of-truth rails
|
||||
|
||||
- **Rust crate**: `mc-city::production` owns stockpile + edge resolution. `mc-core` owns the `ResourceId` enum and `Stockpile` typed wrapper.
|
||||
- **JSON path**: building consume/produce edges live alongside the building entry under `public/resources/buildings/<id>.json` — no separate override layer.
|
||||
- **`mc-core` typed wrapper**: `ResourceId`, `Stockpile<ResourceId, i32>`, `QualityTier`.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Cross-city resource trade routes (`p3-01` courier diplomacy + future trade objective).
|
||||
- Strategic resource visibility three-axis (covered by `p2-54a`).
|
||||
- Player-facing production-chain UI graph — separate UI objective.
|
||||
|
||||
## References
|
||||
|
||||
- `public/games/age-of-dwarves/docs/cities/PRODUCTION_CHAIN.md`
|
||||
- `public/games/age-of-dwarves/docs/RESOURCES.md`
|
||||
45
.project/objectives/p2-58-ambient-encounter-rolls.md
Normal file
45
.project/objectives/p2-58-ambient-encounter-rolls.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
id: p2-58
|
||||
title: "Ambient encounter rolls per tile moved — fauna_density × ecology_tier"
|
||||
priority: p2
|
||||
status: stub
|
||||
scope: game1
|
||||
category: ecology
|
||||
owner: unassigned
|
||||
created: 2026-05-03
|
||||
updated_at: 2026-05-03
|
||||
blocked_by: []
|
||||
follow_ups: []
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
`public/games/age-of-dwarves/docs/ecology-gameplay.md` Layer 1 specifies that any unit moving through wilderness rolls a per-tile encounter chance keyed on `fauna_density × ecology_tier`, with unit-type roll-rate scaling (e.g., scouts trip fewer encounters, large armies trip more). Today combat encounters fire only on lair-adjacency or scripted spawn events — ambient wilderness traversal is risk-free, contradicting the design.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- ❌ `mc-ecology` exposes `roll_ambient_encounter(tile_meta, unit_kind, rng) -> Option<EncounterSpec>` keyed on `tile_meta.fauna_density * tile_meta.ecology_tier`.
|
||||
- ❌ Unit-kind roll-rate multipliers (`scout: 0.4`, `engineer: 1.0`, `army: 1.6`, etc.) authored under `public/resources/ecology/encounter_rates.json`.
|
||||
- ❌ Per-tile-moved hook: `mc-turn::movement` calls the roll once per tile entered (not per move action) and surfaces the resulting `EncounterSpec` to the combat pipeline.
|
||||
- ❌ Encounter selection draws from `tile_meta.fauna_index` (the candidate species list), respecting trophic + domain gates.
|
||||
- ❌ Determinism: encounter rolls use `seed::derive(SeedDomain::Encounter, turn, unit_id, step_idx)` — replays produce identical sequences.
|
||||
- ❌ Cargo test in `mc-ecology`: a 100-step deterministic walk through dense ecology tile yields the expected encounter count within ±1 of the analytical mean.
|
||||
- ❌ GUT integration test: a scout traversing a high-density tile triggers an ambient encounter event at least once across 50 seeded runs.
|
||||
|
||||
## Source-of-truth rails
|
||||
|
||||
- **Rust crate**: `mc-ecology` owns the roll. `mc-turn::movement` is the single caller (no GDScript shadow roll).
|
||||
- **JSON path**: `public/resources/ecology/encounter_rates.json` owns unit-kind multipliers and base roll rate. No `data/` overrides.
|
||||
- **`mc-core` typed wrapper**: `EncounterSpec { species_id: SpeciesId, group_size: u8, posture: EncounterPosture }`.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Pioneer escort rules (covered by `p2-59`).
|
||||
- Lair siege/assault/raid combat modes (covered by `p3-10`).
|
||||
- Encounter narrative text / event cards — separate UI objective.
|
||||
|
||||
## References
|
||||
|
||||
- `public/games/age-of-dwarves/docs/ecology-gameplay.md` (Layer 1)
|
||||
- `public/games/age-of-dwarves/docs/ECOLOGY_BINDING.md`
|
||||
- `public/games/age-of-dwarves/docs/terrain/WORLDGEN_RNG.md`
|
||||
45
.project/objectives/p2-59-pioneer-escort-mechanic.md
Normal file
45
.project/objectives/p2-59-pioneer-escort-mechanic.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
id: p2-59
|
||||
title: "Pioneer escort mechanic — protection rules vs ambient encounters"
|
||||
priority: p2
|
||||
status: stub
|
||||
scope: game1
|
||||
category: units
|
||||
owner: unassigned
|
||||
created: 2026-05-03
|
||||
updated_at: 2026-05-03
|
||||
blocked_by: [p2-58]
|
||||
follow_ups: []
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
Once ambient encounter rolls (`p2-58`) trip on every tile moved, unescorted pioneers / settlers become unviable in mid/high ecology-tier wilderness. `public/games/age-of-dwarves/docs/units/SPECIALISTS.md` calls for an escort relationship: a pioneer co-located with (or moving with) a combat-capable escort unit transfers encounter resolution onto the escort, and benefits from a movement-rate or visibility cap from the slowest unit in the stack.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- ❌ `mc-core::EscortLink { protected_unit_id: UnitId, escort_unit_id: UnitId }` typed wrapper added.
|
||||
- ❌ `mc-turn::movement` honours active escort links: when a pioneer + escort are stacked or move in lockstep, ambient encounter rolls (`p2-58`) target the escort, not the pioneer.
|
||||
- ❌ Stack movement: linked units move together at the slowest member's MP rate; breaking the link mid-route stops the slower party.
|
||||
- ❌ Player action `escort_assign(escort_id, protected_id)` and `escort_release(...)` exposed via the existing action vocabulary (`p2-53`).
|
||||
- ❌ Escort range rule: link auto-breaks if the two units end a turn more than 1 hex apart.
|
||||
- ❌ Cargo test in `mc-turn`: a pioneer + warrior escorted pair traversing a dense ecology tile resolves the encounter against the warrior's combat stats; a solo pioneer on the same tile resolves against pioneer stats (and likely dies).
|
||||
- ❌ GUT integration test: assigning escort, moving the pair, and breaking the escort all surface the correct UI event signals.
|
||||
|
||||
## Source-of-truth rails
|
||||
|
||||
- **Rust crate**: `mc-turn::movement` owns the link table + stack-move logic. Escort assignment validation lives in `mc-turn::actions` next to the existing action vocabulary.
|
||||
- **JSON path**: no new content data — uses unit kinds already in `public/resources/units/`.
|
||||
- **`mc-core` typed wrapper**: `EscortLink`.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Caravan / trade-route escort (separate trade objective).
|
||||
- Multi-pioneer convoys (>1 protected per escort) — v1 is 1:1.
|
||||
- AI escort assignment heuristics — covered by a follow-up `mc-ai` task.
|
||||
|
||||
## References
|
||||
|
||||
- `public/games/age-of-dwarves/docs/units/SPECIALISTS.md`
|
||||
- `public/games/age-of-dwarves/docs/ecology-gameplay.md` (Layer 1)
|
||||
- `.project/objectives/p2-58-ambient-encounter-rolls.md` (blocker)
|
||||
47
.project/objectives/p2-60-weather-lens-godot-ui.md
Normal file
47
.project/objectives/p2-60-weather-lens-godot-ui.md
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
---
|
||||
id: p2-60
|
||||
title: "Weather / observation lens switcher in the Godot HUD"
|
||||
priority: p2
|
||||
status: stub
|
||||
scope: game1
|
||||
category: ui
|
||||
owner: unassigned
|
||||
created: 2026-05-03
|
||||
updated_at: 2026-05-03
|
||||
blocked_by: []
|
||||
follow_ups: []
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
`public/games/age-of-dwarves/docs/WEATHER_HISTORY.md` describes a "lens" model: the player toggles the world view between `default`, `weather`, `temperature`, `precipitation`, `wind`, `pressure`, `fauna_density`, `flora_density`, etc., each rendering an overlay sourced from the corresponding `tile_meta` field. Today the renderer reads only the base biome tint — no UI affordance exists to switch lens, and the renderer does not consume the weather/observation channels even though `mc-climate` populates them.
|
||||
|
||||
This objective ships the Godot UI side: a lens switcher widget + the renderer hookup that consumes the existing typed fields.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- ❌ HUD scene `src/game/engine/scenes/ui/lens_switcher.tscn` (or equivalent) exposes a horizontal lens picker with one button per supported lens.
|
||||
- ❌ Lens enum sourced from JSON manifest `public/resources/ui/lenses.json` — no hardcoded GDScript list.
|
||||
- ❌ Renderer reads the active lens via the existing tile-meta bridge and tints overlays per the lens's colormap entry.
|
||||
- ❌ Hotkey cycling (e.g., `Tab`/`Shift+Tab`) reuses the `p2-03` hotkey-cheat-sheet registry.
|
||||
- ❌ EventBus signal `lens_changed(lens_id: String)` emitted on switch; renderer listens.
|
||||
- ❌ Proof scene under `src/game/engine/scenes/tests/` captures one screenshot per lens; phase-gate accepted via `tools/screenshot.sh`.
|
||||
- ❌ `--headless` GUT test asserts the lens registry parses and the EventBus signal fires on simulated switch.
|
||||
|
||||
## Source-of-truth rails
|
||||
|
||||
- **Rust crate**: no new Rust logic — the typed weather/observation fields already exist on `TileMeta` (owned by `mc-climate`, `mc-observation`). This objective is presentation-only.
|
||||
- **JSON path**: `public/resources/ui/lenses.json` owns lens metadata (id, label, colormap, source-field). No `data/` overrides.
|
||||
- **`mc-core` typed wrapper**: reuse existing `TileMeta` accessors; introduce `LensId` newtype if the renderer needs to disambiguate from other UI string ids.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Recording-gate-from-tech (covered by `p2-61`) — this objective shows whatever is in `tile_meta`; gating is upstream.
|
||||
- Historical playback / time-scrubbing — separate objective (use WEATHER_HISTORY.md timeline).
|
||||
- Minimap lens parity — minimap improvements live in `p2-01`.
|
||||
|
||||
## References
|
||||
|
||||
- `public/games/age-of-dwarves/docs/WEATHER_HISTORY.md`
|
||||
- `.project/objectives/p2-61-observation-recording-gates-from-tech.md` (companion)
|
||||
- `.project/objectives/p2-03-hotkey-cheat-sheet.md`
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
---
|
||||
id: p2-61
|
||||
title: "Bind mc-observation gate_bits to player tech state — recording gates per-field"
|
||||
priority: p2
|
||||
status: stub
|
||||
scope: game1
|
||||
category: infra
|
||||
owner: unassigned
|
||||
created: 2026-05-03
|
||||
updated_at: 2026-05-03
|
||||
blocked_by: []
|
||||
follow_ups: []
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
`mc-observation` already carries a per-field `gate_bits` mask that determines which `tile_meta` channels a player is *allowed* to record. Today the mask defaults to all-on for every player — every field is recorded regardless of whether the civilization has researched the corresponding tech (cartography, meteorology, geology, ecology). The intent in `WEATHER_HISTORY.md` and `ECOLOGY_BINDING.md` is that, for example, precipitation overlay only populates after the player researches Meteorology; without it, the lens shows "no data".
|
||||
|
||||
This objective wires the gate evaluation against the player's tech state.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- ❌ `mc-observation::gate_bits_for_player(player_state, tech_state) -> GateMask` computes the mask from the player's researched tech list.
|
||||
- ❌ Tech → field mapping authored in `public/resources/observation/gates.json` (e.g., `{ "meteorology": ["precipitation","temperature","wind"], "geology": ["lithology","mineral_visibility"], "ecology": ["fauna_density","flora_density"] }`).
|
||||
- ❌ The recording pass in `mc-observation` consults the per-player mask; ungated fields write `None` into the player's observation cache (`p2-54b`), not the actual value.
|
||||
- ❌ Researching a gating tech mid-game starts populating the corresponding fields from the next observation tick — never retroactively backfills.
|
||||
- ❌ Cargo test in `mc-observation`: a player without `meteorology` records no `precipitation` values; the same player after researching `meteorology` records non-`None` from that turn forward.
|
||||
- ❌ `tools/validate-game-data.py` extended to verify every field listed in `gates.json` matches a real `TileMeta` field name (no typos / drifted ids).
|
||||
|
||||
## Source-of-truth rails
|
||||
|
||||
- **Rust crate**: `mc-observation` owns gate evaluation + recording. No GDScript shadow.
|
||||
- **JSON path**: `public/resources/observation/gates.json` is the single source for tech→field mappings. No `data/` overrides.
|
||||
- **`mc-core` typed wrapper**: `GateMask` (already typed in `mc-observation`); introduce `ObservationField` enum mirroring `TileMeta` field names if it isn't there yet, so the gates.json parser can fail fast on unknown fields.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- The lens UI consuming the gated cache (covered by `p2-60`).
|
||||
- Per-deposit visibility three-axis (covered by `p2-54a`).
|
||||
- AI tech-priority scoring from gated visibility (covered by `p2-54d`).
|
||||
|
||||
## References
|
||||
|
||||
- `public/games/age-of-dwarves/docs/WEATHER_HISTORY.md`
|
||||
- `public/games/age-of-dwarves/docs/ECOLOGY_BINDING.md`
|
||||
- `.project/objectives/p2-54b-player-observation-cache.md`
|
||||
- `.project/objectives/p2-60-weather-lens-godot-ui.md`
|
||||
Loading…
Add table
Reference in a new issue