From 86ab31afeafeeb462f75c8df57025c4c74e9e535 Mon Sep 17 00:00:00 2001 From: Natalie Date: Mon, 8 Jun 2026 04:14:50 -0700 Subject: [PATCH] =?UTF-8?q?feat(@projects):=20=E2=9C=A8=20close=20missing-?= =?UTF-8?q?sprite=20runtime=20faults?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .../p2-22-sprite-generation-pipeline.md | 52 +++++++++++++ .../crates/mc-ai/src/tactical/production.rs | 74 ++++++++++++++++++- 2 files changed, 123 insertions(+), 3 deletions(-) diff --git a/.project/objectives/p2-22-sprite-generation-pipeline.md b/.project/objectives/p2-22-sprite-generation-pipeline.md index e0b8d402..acea24cb 100644 --- a/.project/objectives/p2-22-sprite-generation-pipeline.md +++ b/.project/objectives/p2-22-sprite-generation-pipeline.md @@ -31,6 +31,58 @@ Slate is clean (user deleted 7 pre-existing sprites on 2026-04-17 for quality-ba - ◐ Theater GUI (`server.py` + `gui/`) boot smoke at `http://localhost:5850`. 2026-06-03 — **server side passes, front-end blocked.** `create_app()` builds 35 routes; `uvicorn server:app --port 5850` served `GET /api/stats` 200, `/api/theater` 200, `/api/progress` 200; stopped cleanly by worker PID (never by port). BUT root (`GET /`, the SPA index) returns 404 because `gui/dist` is not built, and **the build cannot complete in this environment**: `pnpm build` fails (`TS2307: Cannot find module 'react'`, ~20 JSX/TS errors) because `pnpm install` does not materialize `gui/node_modules` — the `@lilith/ui-animated` workspace dep hits the known pnpm `workspace:*` resolution bug (MEMORY `project_pnpmfile_workspace_fix`; needs `.pnpmfile.cjs` + Verdaccio). So the `/?spriteTheater=true` Theater UI cannot render until the GUI build is fixed. The Python server boot smoke is genuinely green; the React Theater render is unproven. ESCALATION: GUI build/install needs a separate fix (out of this session's fence focus). - ✓ `docs/PIPELINE.md` post-reset refresh. 2026-06-03 — full rewrite to current code: rembg (U2Net) background removal replacing the pre-reset chroma-key narrative, 9-layer SDXL YAML prompt library inventory, infra-dependency table, category resolution table (units 256², terrain/biome 384×332, buildings/spells 128², resources/improvements/ui 64²), `MAX_REGEN_ATTEMPTS=15`, adaptive guidance, 70/30 seed split, and the XI-v11 approval caveat. +## Missing-sprite fault closure — 2026-06-08 + +A runtime-driven gap-fill pass extended the data-driven stand-in tool +(`tools/standin-sprites/build_standins.py` + `icon_rules.json`) to four renderer +load surfaces the prior pass did not cover, eliminating the `ThemeAssets: FAILED to +load` faults a real session triggers: + +- **Ground truth (unseeded probe):** a 5-turn weston autoplay on apricot + (`RENDER_MODE=weston`, no pinned seed) logged **18** distinct `FAILED to load` + paths — 16× `sprites/throne_room/*`, 1× `sprites/terrain/tower_of_wizardry.png`, + 1× `sprites/units/ancient_hydra_dwarf_male.png`. (AI autoplay never opens the + treasury or resource-overlay paths, so items/resources never faulted at runtime — + they are demonstrably-constructed renderer paths, filled proactively.) +- **Filled (283 new PNGs, all game-icons CC-BY-3.0, one `LICENSES.md` row each):** + - `sprites/throne_room/*` (142) — `throne_room.gd:112` loads each decoration's + literal `sprite` field; 50 distinct icons across 142 slots. *Runtime-faulting.* + - `sprites/items/*` (27) — `treasury_tab.gd:90` loads each item's `sprite` field. + *Proactive (treasury screen not AI-reachable; screenshot-proven only).* + - `sprites/resources/*` (101) — `overlay_renderer.gd:118` constructs + `sprites/resources/.png`. *Proactive (resource overlays not + AI-reachable; screenshot-proven only).* + - `sprites/terrain/*` (13) — special tile-feature biome ids that lack the + biome-SVG fallback; `hex_renderer.gd` preloads `sprites/terrain/.png`. + **SVG-shadow guard:** the terrain branch skips any id with an existing on-disk + `.svg` (load_sprite tries `.png` before `.svg`, so a placeholder PNG would + shadow the real biome art). *Runtime-faulting (map-dependent per seed).* +- **Primary proof — screenshot:** `tools/standin-sprites/standin_sprite_proof.png` + (apricot weston; `standin_sprite_proof.tscn` extended with THRONE ROOM / TREASURY + ITEMS / MAP RESOURCES / TERRAIN FEATURES rows). 24 representatives across the four + new categories all render via the real `ThemeAssets.load_sprite` engine path, zero + MISSING markers. Reviewed in-conversation. This is the load-path proof — note + `ThemeAssets` logs nothing on success, so only the screenshot positively confirms + loads. +- **Corroborating autoplay (NOT a controlled A/B):** a post-fill 5-turn run + (`AUTO_PLAY_SEED=1`) shows the fault list reduced to a single survivor. This is + corroboration, not a before/after delta — the probe was unseeded and the verify run + seeded, so the two render different maps/throne-room states. Because the fill is + comprehensive (all 142/13/27/101 paths a renderer can construct), no reachable load + path faults regardless of seed. The lone survivor, + `units/ancient_hydra_dwarf_male.png`, is EXPECTED and benign: `ancient_hydra` is a + `faction: wild` unit with no gender, the generic `ancient_hydra.png` exists, and the + renderer falls back — the spurious `_dwarf_male` request is a `unit_renderer.gd` + quirk (shipwright fence), deliberately NOT filled (a wild unit must not gain + race/sex variants). +- **Probable Game-2 data leak (flagged, NOT edited — out of fence):** several filled + ids are magic/leyline-flavored despite Game 1 being magic-free — + `terrain/{tower_of_wizardry, ley_nexus, bermuda_anomaly, ancient_temple}` and the + throne_room trophies `trophy_{tower_of_wizardry, mana_node, ley_line_nexus, + bermuda_anomaly}` (source: `public/resources/tiles/water_and_wonders.json`, + `public/resources/throne_rooms/wonders.json`). Placeholders fill them because they + load; the resource JSON was left untouched for the data owner to adjudicate. + ## Depends on - `p0-23` (rendering capability, done) — the pipeline's install path + sprite-key convention is driven by `SPRITE_LOOKUP_RACE_SEX_FORMAT` / `SPRITE_LOOKUP_GENERIC_FORMAT` / `SPRITE_LOOKUP_CITY_FORMAT` in the renderers. diff --git a/src/simulator/crates/mc-ai/src/tactical/production.rs b/src/simulator/crates/mc-ai/src/tactical/production.rs index b8c5c671..ab872727 100644 --- a/src/simulator/crates/mc-ai/src/tactical/production.rs +++ b/src/simulator/crates/mc-ai/src/tactical/production.rs @@ -584,9 +584,15 @@ fn score_building( } } - // Sole-city science uplift (p1-29b): when the AI is the last city standing - // and under threat, science buildings score 1.5× so the AI prioritises - // reaching the next tech tier as its primary escape hatch. + // Sole-city science uplift (p1-29b Fix C): when the AI is the last city + // standing and under threat, science buildings score 1.5× so the AI + // prioritises reaching the next tech tier as its escape hatch. + // REACHABILITY (verified 2026-06-08): live ONLY via `pick_for_city` step 0 + // (the p1-29e economy break-out, `own_mil >= SOLE_CITY_ECON_MIN_DEFENDERS`); + // unreachable via the general scorer (steps 3/7) because a threatened player + // returns melee at step 1 first. Posture-independent — fires even under the + // break-out's forced Production posture. Dormant when own_mil < 2. Regression: + // `sole_city_science_uplift_is_live_and_boosts_only_science`. if sole_city_threatened && spec.yield_science > 0 { mult *= 1.5; } @@ -997,6 +1003,68 @@ mod tests { assert_eq!(PRODUCTION_AXIS_BUILDING_BIAS, 8); } + // ── p1-29b Fix C: sole-city science uplift is LIVE (not dead code) ─── + // + // Reachability (verified 2026-06-08): the `sole_city_threatened && + // yield_science > 0` 1.5× uplift in `score_building` is reached ONLY via + // `pick_for_city` step 0 — the p1-29e sole-city economy break-out, gated + // `own_mil >= SOLE_CITY_ECON_MIN_DEFENDERS` (=2). It is NOT reached via the + // general building scorer (steps 3/7): a threatened player has + // `posture == Threatened`, so step 1 returns a melee unit before those steps + // run while `sole_city_threatened`. The uplift is posture-INDEPENDENT, so it + // fires even under the break-out's forced `Production` posture — which is why + // the earlier "forces Production not science → dead" reasoning was wrong. + // Dormant when `own_mil < 2` (e.g. autoplay seeds with P1 own_mil=0), but + // structurally live. This test pins the function logic; the call path is the + // step-0 break-out above. + fn sci_spec(id: &str, science: i32, category: &str) -> super::super::state::TacticalBuildingSpec { + super::super::state::TacticalBuildingSpec { + id: id.into(), + tier: 1, + category: category.into(), + cost: 60, + tech_required: None, + race_required: None, + wonder_type: None, + requires_resource: None, + requires_existing: None, + yield_food: 0, + yield_production: 0, + yield_gold: 0, + yield_science: science, + yield_culture: 0, + yield_defense: 0, + yield_gpp: 0, + great_work_slots: 0, + yield_happiness: 0, + } + } + + #[test] + fn sole_city_science_uplift_is_live_and_boosts_only_science() { + let w = ScoringWeights::default(); // effect_science = 0.45 > 0 + let axes = RawAxes::NEUTRAL; // production/wealth/aggression = 5 → no category mult + let priors = crate::tactical::state::BuildingPriors::default(); // empty → neutral + + // Science building under the break-out's Production posture: the uplift + // is the only multiplier in play, so the threatened-sole-city score is + // exactly 1.5× the unthreatened score. + let sci = sci_spec("library", 3, "science"); + let up = score_building(&sci, &w, axes, BuildingPosture::Production, true, &priors); + let no = score_building(&sci, &w, axes, BuildingPosture::Production, false, &priors); + assert!(up > no, "sole-city science uplift must boost science buildings ({up} vs {no})"); + assert!( + (up - no * 1.5).abs() < 1e-3, + "uplift must be exactly 1.5× ({up} vs {no})" + ); + + // A non-science building is unaffected by the flag. + let prod = sci_spec("forge", 0, "production"); + let pu = score_building(&prod, &w, axes, BuildingPosture::Production, true, &priors); + let pn = score_building(&prod, &w, axes, BuildingPosture::Production, false, &priors); + assert_eq!(pu, pn, "non-science buildings must be unaffected by sole_city_threatened"); + } + // ── Tier-progression unit selection (p0-39) ───────────────────────── fn unit_spec(id: &str, tier: u32, tech: Option<&str>, unit_type: &str) -> super::super::state::TacticalUnitSpec {