feat(@projects): close missing-sprite runtime faults

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-06-08 04:14:50 -07:00
parent bc80362327
commit 86ab31afea
2 changed files with 123 additions and 3 deletions

View file

@ -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/<resource_id>.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/<biome_id>.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.

View file

@ -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 {