From 71cc742490644429f7f03c953cb5d57a83b7aa6d Mon Sep 17 00:00:00 2001 From: Natalie Date: Fri, 17 Apr 2026 23:55:14 -0700 Subject: [PATCH] =?UTF-8?q?feat(@projects/@magic-civilization):=20?= =?UTF-8?q?=E2=9C=A8=20add=20welcome=20guide=20theme=20alignment=20objecti?= =?UTF-8?q?ve?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .project/objectives/README.md | 7 +- .../p1-15-guide-next-deploy-infra.md | 8 ++ ...-guide-welcome-homepage-theme-alignment.md | 93 +++++++++++++++++++ .project/team-leads/README.md | 2 +- .project/team-leads/tourguide.md | 1 + .../games/age-of-dwarves/data/objectives.json | 18 +++- scripts/apricot-run.sh | 49 ++++++++-- tools/autoplay-batch.sh | 9 ++ 8 files changed, 170 insertions(+), 17 deletions(-) create mode 100644 .project/objectives/p2-29-guide-welcome-homepage-theme-alignment.md diff --git a/.project/objectives/README.md b/.project/objectives/README.md index c5d4b40d..0a1920b8 100644 --- a/.project/objectives/README.md +++ b/.project/objectives/README.md @@ -16,9 +16,9 @@ |---|---|---|---|---|---|---| | **P0** | 26 | 6 | 3 | 0 | 0 | 35 | | **P1** | 13 | 3 | 2 | 0 | 1 | 19 | -| **P2** | 9 | 6 | 0 | 8 | 0 | 23 | +| **P2** | 9 | 6 | 0 | 9 | 0 | 24 | | **P3 (oos)** | 0 | 0 | 0 | 0 | 17 | 17 | -| **total** | **48** | **15** | **5** | **8** | **18** | **94** | +| **total** | **48** | **15** | **5** | **9** | **18** | **95** | @@ -30,8 +30,8 @@ | [warcouncil](../team-leads/warcouncil.md) | 6 | | [wireguard](../team-leads/wireguard.md) | 4 | | [shipwright](../team-leads/shipwright.md) | 3 | +| [tourguide](../team-leads/tourguide.md) | 3 | | [testwright](../team-leads/testwright.md) | 2 | -| [tourguide](../team-leads/tourguide.md) | 2 | | [asset-audio](../team-leads/asset-audio.md) | 1 | @@ -126,6 +126,7 @@ | [p2-26](p2-26-mundane-wonder-sprites.md) | ❌ missing | Mundane-wonder sprites — 24 distinct, higher-fidelity art | [asset-sprite](../team-leads/asset-sprite.md) | 2026-04-17 | | [p2-27](p2-27-city-population-tier-sprites.md) | ❌ missing | City population-tier sprites — city_q1 through city_q5 | [asset-sprite](../team-leads/asset-sprite.md) | 2026-04-17 | | [p2-28](p2-28-sprite-provenance-ledger.md) | ❌ missing | Sprite provenance ledger — LICENSES.md per-file attribution | [asset-sprite](../team-leads/asset-sprite.md) | 2026-04-17 | +| [p2-29](p2-29-guide-welcome-homepage-theme-alignment.md) | ❌ missing | Welcome modal + HomePage lore + guide theme align to the player's chosen race/gender | [tourguide](../team-leads/tourguide.md) | 2026-04-17 | ## Out of Scope (Game 2 / Game 3) diff --git a/.project/objectives/p1-15-guide-next-deploy-infra.md b/.project/objectives/p1-15-guide-next-deploy-infra.md index 89bbccfe..f9ed4691 100644 --- a/.project/objectives/p1-15-guide-next-deploy-infra.md +++ b/.project/objectives/p1-15-guide-next-deploy-infra.md @@ -46,6 +46,14 @@ All acceptance bullets verifiable: fresh deploy is picked up on the next visit; (f) passes through `/__sim-cache/` for future p2-21 baked frames. `docker exec host-nginx nginx -t` validates before the reload. + **Gotcha (2026-04-17):** the outer `http {}` block must contain + `include /etc/nginx/mime.types; default_type application/octet-stream;`. + Without it nginx serves every file as `text/plain`, which browsers + refuse to execute as ES modules — the guide renders blank with + "disallowed MIME type" console errors on every `/assets/*.js`. + `cmd_deploy_guide_next`'s step-5 probe now asserts + `Content-Type: application/javascript` on a hashed JS asset to + catch this regression at deploy time. - ✓ **Bind mount** — `/bigdisk/nginx/docker-compose.yml` adds `/bigdisk/next/:/var/www/next/:ro`. Container recreated via `docker compose up -d`; `docker exec host-nginx ls /var/www/next/mc/` diff --git a/.project/objectives/p2-29-guide-welcome-homepage-theme-alignment.md b/.project/objectives/p2-29-guide-welcome-homepage-theme-alignment.md new file mode 100644 index 00000000..d9b592ff --- /dev/null +++ b/.project/objectives/p2-29-guide-welcome-homepage-theme-alignment.md @@ -0,0 +1,93 @@ +--- +id: p2-29 +title: Welcome modal + HomePage lore + guide theme align to the player's chosen race/gender +priority: p2 +status: missing +scope: game1 +owner: tourguide +updated_at: 2026-04-17 +evidence: + - public/games/age-of-dwarves/guide/src/components/welcome/WelcomeModal.tsx + - public/games/age-of-dwarves/guide/src/pages/HomePage.tsx + - public/games/age-of-dwarves/guide/src/theme/fantasy-theme.ts + - src/packages/guide/src/theme/RaceThemeProvider.tsx + - public/games/age-of-dwarves/guide/src/contexts/PreferencesContext.tsx +--- + +## Summary + +The guide already exposes a welcome modal that lets the player pick a race +(Dwarf in Game 1 — `CONCRETE_RACES = ['dwarf']`) and a gender, plus a +`RaceThemeProvider` that merges a per-race/per-gender palette into the +styled-components theme. But the three surfaces don't line up: + +- **WelcomeModal copy** reads like a settings dialog ("Settings", field + labels) rather than an invitation into the story, so the player's first + impression is admin-UI-shaped. +- **HomePage ``** *does* pull the player's race + name via + `usePreferences()`, but the surrounding `` / `` / `` + hardcode out-of-scope narrative ("16 asymmetric races, 5 magic schools") + that is Game 2/3 territory and does NOT update when the player picks a + dwarf leader. +- **Theme application** — the palette change from `RaceThemeProvider` fires + on confirm, but a browser already on the HomePage may not re-derive the + Hero/Pitch colors in a visually coherent way (Cinzel serif, Dwarf copper + `#c07040`, etc.). The "align with welcome" contract is not exercised. + +When the player picks **Dwarf + Female** in the modal and clicks Begin, +all three surfaces should read as one piece: a dwarf-themed guide, +referring to the named dwarf leader, in Dwarf scope language (no "5 +magic schools" pitch, no generic cross-race framing). + +## Acceptance + +- **Race-aware Hero / Tagline / Pitch**: the top-of-HomePage block no + longer hardcodes cross-race copy. For Game 1 Dwarf scope, the pitch is + Dwarf-first (craft, industry, mundane tech, fortress cities). When a + future episode adds a second concrete race, the prose comes from a + race-keyed data source (e.g. `resources/races//guide_pitch.md`) + — not another `{race.name}` sprinkle. +- **"Your story begins" block** renders race-specific lore for the + chosen race + gender. Dwarf-female reads coherently ("Your band of + free Dwarves have chosen you, , as their leader" in a voice + consistent with the Dwarf palette). The current ley-line / + magic-schools paragraphs are gated behind `` + (Game 2 content) so a default Game 1 bundle shows Dwarf narrative + only; `VITE_DEV_GUIDE=1` keeps them visible per p1-15. +- **Theme alignment**: after confirming Dwarf + Female in the + WelcomeModal, the `RaceThemeProvider` palette is live — + `theme.colors.primary.main === '#c07040'` (dwarf copper), + `theme.typography.fontFamily.serif === "Cinzel, …"`, + `theme.colors.background.primary === '#1a1510'`. HomePage's + `` / `` / `` accents derive from + those tokens and visibly shift from the default palette to the Dwarf + palette within one render cycle. +- **WelcomeModal voice**: the modal's copy is rewritten in the same + voice as HomePage. Instead of "Settings", the modal frames the + choice as the start of the story ("Choose your leader" / "Your band + of free Dwarves choose…" etc.). `BeginButton` label reinforces the + story beat rather than "Save". +- **Smoke test**: `pnpm --prefix public/games/age-of-dwarves/guide + test:e2e --grep welcome` (new spec) walks (1) load page fresh, (2) + pick Dwarf + Female + type a name, (3) click Begin, (4) assert the + HomePage `` reads "Your Story Begins" with the chosen + name rendered in `` + the body prose uses Dwarf + vocabulary (no "magic schools" / "archons" strings). Console + remains zero-error. + +## Non-goals + +- Additional races beyond Dwarf — Game 2/3 race content stays gated. +- Multi-language / i18n for the prose — copy is authored in English, + matching the rest of the guide. +- In-modal preview of HomePage changes — `onPreview` already flashes + the palette; no need for a live HomePage inset. +- A new WelcomeModal design — this is a copy + theme-alignment pass, + not a visual redesign. + +## Why this belongs to tourguide + +Closes the "clone → click Begin → first-page impression" path that the +dev-guide deploy (`mc.next.black.local`, p1-15) is meant to showcase. +Same owner that authored the tabbed Progress Report + the Markdown +component + the deploy pipeline. diff --git a/.project/team-leads/README.md b/.project/team-leads/README.md index eace7320..3559a827 100644 --- a/.project/team-leads/README.md +++ b/.project/team-leads/README.md @@ -69,4 +69,4 @@ specialist does not own any objective. | [warcouncil](warcouncil.md) | Warcouncil | AI action generation, MCTS, GPU look-ahead, clan personality differentiation | p0-01, p0-02, p0-20 | | [shipwright](shipwright.md) | Shipwright | Drive Game 1 to release via /experts-team cron loop until every P0 is done | p0-05, p0-14, p0-15, p0-16, p0-17 | | [testwright](testwright.md) | Testwright | Regression-test coverage across Rust + GDScript + data validators — seeds the evidence substrate for the Objective Status Integrity rule | p1-09, p2-10 | -| [tourguide](tourguide.md) | Tourguide | Developer experience of the guide web app — dev server boots on plum, route coverage e2e, dev-tier deploy at mc.next.black.local, sim-cache baked on apricot, and the "no build output in src" rule stays enforced | p1-11, p1-12, p1-13, p1-15, p1-17, p2-20, p2-21 | +| [tourguide](tourguide.md) | Tourguide | Developer experience of the guide web app — dev server boots on plum, route coverage e2e, dev-tier deploy at mc.next.black.local, sim-cache baked on apricot, welcome→HomePage→theme alignment, and the "no build output in src" rule stays enforced | p1-11, p1-12, p1-13, p1-15, p1-17, p2-20, p2-21, p2-29 | diff --git a/.project/team-leads/tourguide.md b/.project/team-leads/tourguide.md index edc31706..e8770abd 100644 --- a/.project/team-leads/tourguide.md +++ b/.project/team-leads/tourguide.md @@ -10,6 +10,7 @@ objectives: - p1-17 - p2-20 - p2-21 + - p2-29 --- ## Mandate diff --git a/public/games/age-of-dwarves/data/objectives.json b/public/games/age-of-dwarves/data/objectives.json index 72140371..52063a56 100644 --- a/public/games/age-of-dwarves/data/objectives.json +++ b/public/games/age-of-dwarves/data/objectives.json @@ -1,12 +1,12 @@ { - "generated_at": "2026-04-18T06:48:23Z", + "generated_at": "2026-04-18T06:53:01Z", "totals": { "stub": 5, "partial": 15, - "missing": 8, - "done": 48, "oos": 18, - "total": 94 + "missing": 9, + "done": 48, + "total": 95 }, "objectives": [ { @@ -779,6 +779,16 @@ "updated_at": "2026-04-17", "summary": "Every sprite PNG that ships in `public/games/age-of-dwarves/assets/sprites/` must have a corresponding row in `public/games/age-of-dwarves/assets/sprites/LICENSES.md` recording source, license, author, URL, and SHA256. This is a cross-cutting compliance objective that runs continuously alongside the delivery children (`p2-23` … `p2-27`) — the ledger is complete exactly when every on-disk sprite has a matching row and every row points at an on-disk file.\n\nCommercial-use compatibility is non-negotiable. AI-generated output must come from a model on the approved list (`juggernaut-xl-v9`, `epicrealism-xl`, `illustrious-xl-v2`, or current equivalent per CLAUDE.md). Commissioned art must have assigned commercial rights in writing." }, + { + "id": "p2-29", + "title": "Welcome modal + HomePage lore + guide theme align to the player's chosen race/gender", + "priority": "p2", + "status": "missing", + "scope": "game1", + "owner": "tourguide", + "updated_at": "2026-04-17", + "summary": "The guide already exposes a welcome modal that lets the player pick a race\n(Dwarf in Game 1 — `CONCRETE_RACES = ['dwarf']`) and a gender, plus a\n`RaceThemeProvider` that merges a per-race/per-gender palette into the\nstyled-components theme. But the three surfaces don't line up:\n\n- **WelcomeModal copy** reads like a settings dialog (\"Settings\", field\n labels) rather than an invitation into the story, so the player's first\n impression is admin-UI-shaped.\n- **HomePage ``** *does* pull the player's race + name via\n `usePreferences()`, but the surrounding `` / `` / ``\n hardcode out-of-scope narrative (\"16 asymmetric races, 5 magic schools\")\n that is Game 2/3 territory and does NOT update when the player picks a\n dwarf leader.\n- **Theme application** — the palette change from `RaceThemeProvider` fires\n on confirm, but a browser already on the HomePage may not re-derive the\n Hero/Pitch colors in a visually coherent way (Cinzel serif, Dwarf copper\n `#c07040`, etc.). The \"align with welcome\" contract is not exercised.\n\nWhen the player picks **Dwarf + Female** in the modal and clicks Begin,\nall three surfaces should read as one piece: a dwarf-themed guide,\nreferring to the named dwarf leader, in Dwarf scope language (no \"5\nmagic schools\" pitch, no generic cross-race framing)." + }, { "id": "g2-01", "title": "Ley lines — Game 2 (Age of Kzzykt)", diff --git a/scripts/apricot-run.sh b/scripts/apricot-run.sh index 92ba399e..723ee250 100755 --- a/scripts/apricot-run.sh +++ b/scripts/apricot-run.sh @@ -35,21 +35,53 @@ for envfile in "${PROJECT_DIR}/.env" "${PROJECT_DIR}/.env.local"; do fi done -# ── Resource policy for PARALLEL ───────────────────────────────────── -# Precedence: explicit PARALLEL env > USE_MAX_CORES=true (nproc on RUN host) -# > MIN_CORES from .env (default 4). Games are single-core each; -# this controls how many run concurrently. +# MODE + positional args resolved early so the resource-policy block can +# peek at the seed count (which differs per mode — for `clan` it's $2 +# because $1 is the clan_id; for smoke/gpu-walltime it's $1). +MODE="${1:?usage: apricot-run.sh [args]}" +shift || true + +# ── Resource policy for PARALLEL + RAYON_NUM_THREADS ───────────────── +# Each Godot instance spawns its own rayon thread pool for MCTS rollouts; +# rayon defaults to nproc unless RAYON_NUM_THREADS is set. If PARALLEL +# instances each claim all nproc threads, we get PARALLEL*nproc threads +# fighting over nproc cores → thrashing, each process effectively single +# core. Better: PARALLEL = number of seeds (one instance each), and +# RAYON_NUM_THREADS = nproc / PARALLEL so the box is saturated evenly. +case "${MODE}" in + clan) _seed_count_peek="${2:-10}" ;; # $1 is clan_id, $2 is seeds + *) _seed_count_peek="${1:-10}" ;; # smoke, gpu-walltime +esac + +NPROC="$(ssh "${APRICOT}" nproc 2>/dev/null || echo 8)" + if [[ -n "${PARALLEL:-}" ]]; then PARALLEL_EFFECTIVE="${PARALLEL}" PARALLEL_SOURCE="env override" elif [[ "${USE_MAX_CORES:-false}" == "true" ]]; then - PARALLEL_EFFECTIVE="$(ssh "${APRICOT}" nproc 2>/dev/null || echo "${MIN_CORES:-4}")" - PARALLEL_SOURCE="USE_MAX_CORES=true → nproc" + # One instance per seed — up to NPROC. More instances than that + # would queue serially anyway (NPROC concurrent Godots max). + PARALLEL_EFFECTIVE="$(( _seed_count_peek < NPROC ? _seed_count_peek : NPROC ))" + PARALLEL_SOURCE="USE_MAX_CORES=true → min(seeds=${_seed_count_peek}, nproc=${NPROC})" else PARALLEL_EFFECTIVE="${MIN_CORES:-4}" PARALLEL_SOURCE="MIN_CORES default" fi export PARALLEL="${PARALLEL_EFFECTIVE}" + +# RAYON_NUM_THREADS per Godot instance = fair share of cores. +if [[ -n "${RAYON_NUM_THREADS:-}" ]]; then + RAYON_SOURCE="env override" +else + if [[ "${PARALLEL_EFFECTIVE}" -gt 0 ]]; then + RAYON_NUM_THREADS="$(( NPROC / PARALLEL_EFFECTIVE ))" + else + RAYON_NUM_THREADS=1 + fi + [[ "${RAYON_NUM_THREADS}" -lt 1 ]] && RAYON_NUM_THREADS=1 + RAYON_SOURCE="nproc(${NPROC}) / PARALLEL(${PARALLEL_EFFECTIVE})" +fi +export RAYON_NUM_THREADS # Source + build scratch lives under $HOME/.cache (flatpak-visible via # --filesystem=home). /tmp was tried first but flatpak's sandbox can't see # /tmp, so Godot rejected the --path argument with "Invalid project path". @@ -58,9 +90,6 @@ export PARALLEL="${PARALLEL_EFFECTIVE}" SCRATCH="\$HOME/.cache/mc-src-${STAMP}" # expanded on apricot RESULTS="\$HOME/.cache/mc-batches/${STAMP}" # expanded on apricot -MODE="${1:?usage: apricot-run.sh [args]}" -shift || true - # Resolve $HOME on apricot so SCRATCH / RESULTS are fully-qualified paths on that host. SCRATCH_ABS="$(ssh "${APRICOT}" "echo \$HOME/.cache/mc-src-${STAMP}")" RESULTS_ABS="$(ssh "${APRICOT}" "echo \$HOME/.cache/mc-batches/${STAMP}")" @@ -72,6 +101,8 @@ echo " RUN host: ${APRICOT}" echo " SCRATCH: ${SCRATCH_ABS} (per-run source + build scratch)" echo " RESULTS: ${RESULTS_ABS} (persistent batch output)" echo " PARALLEL: ${PARALLEL_EFFECTIVE} (source: ${PARALLEL_SOURCE})" +echo " RAYON_NUM_THREADS/instance: ${RAYON_NUM_THREADS} (source: ${RAYON_SOURCE})" +echo " Total CPU saturation: ${PARALLEL_EFFECTIVE} × ${RAYON_NUM_THREADS} = $((PARALLEL_EFFECTIVE * RAYON_NUM_THREADS))/${NPROC} cores" echo " AI_GPU_ROLLOUT: ${AI_GPU_ROLLOUT:-true (default on for smoke/clan)}" echo "============================================================" diff --git a/tools/autoplay-batch.sh b/tools/autoplay-batch.sh index 2c5efdfe..b1db26c3 100755 --- a/tools/autoplay-batch.sh +++ b/tools/autoplay-batch.sh @@ -161,6 +161,15 @@ _run_local() { "--env=AI_PIN_PERSONALITY=${AI_PIN_PERSONALITY:-}" "--env=MAP_SIZE=${MAP_SIZE:-}" "--env=NUM_PLAYERS=${NUM_PLAYERS:-}" + "--env=AI_USE_MCTS=${AI_USE_MCTS:-}" + "--env=AI_GPU_ROLLOUT=${AI_GPU_ROLLOUT:-}" + # Rayon thread cap per Godot instance. Without this, rayon + # defaults to nproc (e.g. 64 on apricot); with PARALLEL=N + # instances each claiming 64 threads, the box has N*64 threads + # fighting over 64 cores → thrashing, each process effectively + # single-core. Caller (apricot-run.sh) computes nproc/PARALLEL + # so cores are divided fairly across instances. + "--env=RAYON_NUM_THREADS=${RAYON_NUM_THREADS:-}" ) local GODOT_ARGS=("--path" "$GAME_DIR" "--rendering-method" "gl_compatibility")