From d2c9539d499a4c28f4724ece923b5dabc4d09eae Mon Sep 17 00:00:00 2001 From: Natalie Date: Thu, 30 Apr 2026 00:50:14 -0400 Subject: [PATCH] =?UTF-8?q?feat(@projects/@magic-civilization):=20?= =?UTF-8?q?=E2=9C=A8=20add=20combat=20stack=20limits=20and=20occupation=20?= =?UTF-8?q?penalties?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .project/objectives/p1-29.md | 15 ++++++ .../p1-43-building-stacking-upgrade.md | 49 +++++++++++++---- .../games/age-of-dwarves/data/objectives.json | 8 +-- .../crates/mc-core/src/grid/terrain_blend.rs | 53 +++++++++++++++++++ .../instructions/agents-task-map.md | 6 ++- 5 files changed, 116 insertions(+), 15 deletions(-) diff --git a/.project/objectives/p1-29.md b/.project/objectives/p1-29.md index 0d92c081..c0db2c6b 100644 --- a/.project/objectives/p1-29.md +++ b/.project/objectives/p1-29.md @@ -50,3 +50,18 @@ Three-round hypothesis tree: - ❌ Cross-team handoff exists in `.project/handoffs/` documenting which team-lead owns the capture/balance change - ❌ p0-01's evidence updated to cite this objective's closure as the source of v1-style symmetry/unit-tier gate satisfaction - ❌ Per-difficulty validation: `AI_DIFFICULTY=hard tools/autoplay-batch.sh 10 500` shows median winner `tier_peak ≥ 10` reached by T200 (per user directive). `AI_DIFFICULTY=insane` same or stronger. `AI_DIFFICULTY=easy` shows clearly weaker progression. Use `tools/time-to-peak-unit.py` and a new `tools/time-to-tier-peak.py` (analogous metric for `tier_peak` not just unit) to measure. + +## combat-dev work log (2026-04-29) + +**R10 canonical baseline** (`p1-39-r10-canonical-hard-20260428_151849`): +- median `tier_peak_gap = 5.0` FAIL (target ≤4) +- median `winner_tier_peak = 4.5` PASS +- Levers 1+2 (city HP / walls) already at aggressive R5a values: `BASE_CITY_HP=500`, `melee_wall_penalty` tier1=0.40/tier2=0.25. Further bumps risk stalemate (documented in siege.rs code comments). + +**Levers implemented (batch `p1-29-stack-occ-20260430_004304` running on apricot):** +- **Lever 3 — Stack-of-doom cap**: `MAX_CITY_ATTACKS_PER_TURN=3` in `auto_play.gd`. Limits how many times a player's units can attack the same city in one turn. Counter reset each player turn, keyed by city position. + - File: `src/game/engine/scenes/tests/auto_play.gd` (vars at line ~49, reset at ~952, check at ~2054) +- **Lever 4 — Occupation penalty**: Captured cities produce at 50% for 20 turns. `captured_turn` field added to `city.gd`, set in `combat_utils.gd::capture_city`, multiplier applied in `turn_processor.gd::_process_production`. + - Files: `src/game/engine/src/entities/city.gd`, `src/game/engine/src/modules/combat/combat_utils.gd`, `src/game/engine/src/modules/management/turn_processor.gd` + +**Batch status**: Running. Will update with results when complete. diff --git a/.project/objectives/p1-43-building-stacking-upgrade.md b/.project/objectives/p1-43-building-stacking-upgrade.md index 3c615662..f95f1bf6 100644 --- a/.project/objectives/p1-43-building-stacking-upgrade.md +++ b/.project/objectives/p1-43-building-stacking-upgrade.md @@ -44,16 +44,45 @@ Recommendation: option **(a) Replacement** with declaration on the upper tier (` - ✗ Design pass + sign-off from user on the three questions above. - ✗ `building.schema.json` extends with: `requires_existing: `, `consumes_existing: `. Both default null/false. Validator confirms `requires_existing` resolves to a real building id. - ✗ For each declared stack-pair: the upper-tier building is NEW data (or repurposed existing) authored under `resources/buildings/.json` with the new fields populated. -- ✗ Initial 3-step ladders (suggested, pending user confirmation — each is ``): - - **Military**: `barracks` → `infantry` (NEW) → `armory` (existing t3) - - **Science**: `library` → `scriptorium` (NEW) → `university` (existing t3) - - **Culture**: `monument` → `bardic_circle` (existing wonder t4, repurposed?) → `great_hall` (existing t3) - - **Production**: `forge` → `iron_forge` (NEW) → `dwarf_deep_forge` (existing t3) — note race-specific successor - - **Defense**: `walls` → `watchtower` (existing t1, demote?) → `castle` (existing t3) — already a clean ladder - - **Food**: `granary` → `mill` (existing t2) → `watermill` (existing t2) — needs a Lv3 (no candidate) - - **Wealth**: `marketplace` → `market` (existing duplicate, reconcile) → `guild_hall` (existing t4) - - **Religion**: `temple` → ??? (no Lv2/3 candidate, needs authoring) - - Final ladder list user-decided. New buildings authored where needed; existing ones get the `requires_existing` field added. +- ✗ Initial 3-step ladders (suggested, pending user confirmation — each is ``). User clarifications 2026-04-29: **no religion**, **medical chain replaces it**, **military decomposes into many weapon-class chains** (not one). + +### Civilian + economic chains (one per category) + + - **Science**: `library` → `scriptorium` (NEW) → `university` → produces sages / cartographers / engineers + - **Culture**: `monument` → `gathering_hall` → `great_hall` → produces bards / loremasters + - **Production**: `forge` → `iron_forge` (NEW) → `dwarf_deep_forge` → produces smiths / engineers + - **Defense**: `walls` → `watchtower` → `castle` → produces garrison/sentry units + - **Food**: `granary` → `mill` → `watermill` → yield-only, no unit + - **Wealth**: `marketplace` → `market` (reconcile duplicate) → `guild_hall` → produces merchants + - **Medical** (NEW): `barber` (NEW) → `clinic` (NEW) → `hospital` (NEW) → produces battle medics / field surgeons + +### Military — fourteen parallel weapon-class chains + +Military is NOT one ladder. Each weapon class is its own stack with its own producer building(s). The existing data already supplies most of the building IDs — they just need the `produces:` list and explicit stack relationships. Counts in parens are the units that exist today in `resources/units/` for that class: + +| Weapon class | Producer chain | Notable units | +|---|---|---| +| **Melee infantry** (12 units) | `barracks` → `drill_yard` (existing t2) → `armory` (existing t3) | warrior → pikeman → defender → ironwarden → hammerguard → mithril_vanguard → mountain_king | +| **Heavy melee** (4 units) | `sword_hall` (existing t2) → `dwarf_deep_forge` | berserker → hearth_raider → goretooth → war_ram | +| **Bow / Crossbow** (3+4 dwarf) | `bolt_range` (existing t1) → ? (Lv2 needed) | quarrelman → archer → bolt_thrower_crew | +| **Marksman / Sniper** (3 units) | `marksman_lodge` (existing t6) → ? | marksman → anti_tank_rifleman → deep_eye | +| **Riflery** (7 units) | `rifle_range` (existing t6) → `gun_works` (existing t7) → `coil_foundry` (existing t9) | rifleman → light_field_gun → machine_gunner → storm_trooper → steam_howitzer | +| **Explosive / Grenade** (4 units) | `powder_works` (existing t5) → `assault_school` (existing t8) | cannon_crew → powder_sapper → trench_raider → bombard | +| **Cavalry / Mounted** (4 units) | `stable` (existing t1) → `boar_pen` (existing t2) | boar_scout → ram_rider → cavalry → tusker_knight | +| **Siege** (5 units) | `siege_workshop` (existing t2) → `siege_works` (existing t8) | ballista_crew → catapult_crew → trebuchet_crew | +| **Mechanized / Walker** (8 units) | `walker_yard` (existing t7) → `tank_yard` (existing t9) → `armour_yard` (existing t8) | steam_walker → iron_strider → riveted_trooper → adamantine_tank | +| **Artillery** (6 units) | `rocket_pad` (existing t9) | motorized_artillery → rocket_battery → rail_cannon → apex_artillery | +| **Stealth / Special** (3 units) | `shadow_school` (existing t9) | commando → soulbolt → doomsoul | +| **Magical / Runic** (6 units) | `runesmith_hall` (existing t6) | runesmith → rune_spear → emp_trooper → stormbolt_trooper | +| **Air** (6 units) | `airfield` → `zeppelin_dock` (existing t8) | gyrocopter → iron_hawk → war_zeppelin → mithril_hawk → sky_fortress | +| **Naval** (13 units) | `harbor` → `deep_harbor` (existing t5) → `naval_fortress` (existing t8) | river_galley → war_galley → dreadnought → fortress_ship | + +### Implications + +- ~14 producer building chains for military alone, plus 7 civilian/economic chains. Total ~21 producer slots. +- Most chains already have building IDs; only **a handful of NEW buildings** needed to fill ladder gaps (`infantry` Lv2 melee, `iron_forge` Lv2 production, `scriptorium` Lv2 science, `barber`/`clinic`/`hospital` for medical). +- Each producer building gets a `produces: [unit_id, ...]` field declaring its unit roster. Stacking the building expands the roster to higher-tier units. +- The dwarf-prefixed units (`dwarf_warrior`, `dwarf_river_galley`, etc., tier=null) are race-flavored variants — should reconcile with the generic-tier roster as a separate audit. - ✗ Engine: `city.can_build(bid)` returns true for upper-tier IF `bid::requires_existing` is in `city.buildings[]`. City build dispatch (`ai_turn_bridge_dispatch.gd::dispatch_set_production` and the GDScript city UI) honors `consumes_existing` by removing the prerequisite from `city.buildings[]` when the upgrade completes. - ✗ AI integration (depends on `p1-42`): the AI catalog scoring treats stack-upgrades as a single ladder — barracks → infantry scored as a 2-step build path with combined cost. - ✗ City UI surfaces stack relationships: when looking at `barracks` in the encyclopedia / build menu, the upgrade target is shown ("Can be upgraded to: Infantry"). diff --git a/public/games/age-of-dwarves/data/objectives.json b/public/games/age-of-dwarves/data/objectives.json index a7bcf1a4..68d98c7c 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-30T04:44:03Z", + "generated_at": "2026-04-30T04:50:00Z", "totals": { - "partial": 10, - "oos": 20, "stub": 1, - "done": 111, "missing": 15, + "oos": 20, + "done": 111, "in_progress": 1, + "partial": 10, "total": 158 }, "objectives": [ diff --git a/src/simulator/crates/mc-core/src/grid/terrain_blend.rs b/src/simulator/crates/mc-core/src/grid/terrain_blend.rs index eb1a5cda..892475ee 100644 --- a/src/simulator/crates/mc-core/src/grid/terrain_blend.rs +++ b/src/simulator/crates/mc-core/src/grid/terrain_blend.rs @@ -223,6 +223,59 @@ mod tests { assert_eq!(collected.len(), 6); } + /// Cross-file schema guard: every blend's `edge_terrain` id must + /// exist as a defined terrain in `land_blends.json`. Without this, + /// a typo in `terrain_blends.json` (e.g., `foothils` instead of + /// `foothills`) would silently produce edges referencing an + /// undefined terrain id at runtime. + #[test] + fn every_blend_edge_terrain_exists_in_land_blends_json() { + const BLENDS_JSON: &str = include_str!( + "../../../../../../public/resources/tiles/terrain_blends.json" + ); + const LAND_BLENDS_JSON: &str = include_str!( + "../../../../../../public/resources/tiles/land_blends.json" + ); + + // Parse the blend table. + let blend_file: TerrainBlendFile = serde_json::from_str(BLENDS_JSON) + .expect("terrain_blends.json must parse"); + + // Parse land_blends.json and collect its terrain ids. The schema + // matches existing terrain JSON files: a `terrains` array of + // objects with an `id` field. + let land_blends: serde_json::Value = serde_json::from_str(LAND_BLENDS_JSON) + .expect("land_blends.json must parse"); + let defined_ids: std::collections::HashSet = land_blends + .get("terrains") + .and_then(|t| t.as_array()) + .expect("land_blends.json must have a `terrains` array") + .iter() + .filter_map(|entry| { + entry + .get("id") + .and_then(|i| i.as_str()) + .map(|s| s.to_string()) + }) + .collect(); + + // Every blend must reference a defined terrain id. + let mut missing = Vec::new(); + for blend in &blend_file.blends { + if !defined_ids.contains(&blend.edge_terrain) { + missing.push(format!( + "blend {:?}+{:?} → {} (not in land_blends.json)", + blend.pair[0], blend.pair[1], blend.edge_terrain + )); + } + } + assert!( + missing.is_empty(), + "blend table references undefined terrain ids:\n {}", + missing.join("\n ") + ); + } + /// End-to-end: parse the actual production `terrain_blends.json` and /// verify the canonical Game 1 blend entries resolve. This protects /// against the data file drifting from the schema. diff --git a/tooling/claude/dot-claude/instructions/agents-task-map.md b/tooling/claude/dot-claude/instructions/agents-task-map.md index de56fa7c..83e0a315 100644 --- a/tooling/claude/dot-claude/instructions/agents-task-map.md +++ b/tooling/claude/dot-claude/instructions/agents-task-map.md @@ -2,7 +2,7 @@ **Load when:** deciding which specialist agent to dispatch for a concrete task. Specialists live in `.claude/agents/` and are task-level executors, separate from team-leads (see `team-leads.md`). -## The 11 specialists +## The 13 specialists | Agent | Use for | |-------|---------| @@ -17,6 +17,8 @@ | `godot-renderer` | TileMap, sprites, camera, fog of war, hex visuals, animation | | `guide-web` | Player guide web app: React pages, components, climate sim, Vitest | | `simulator-infra` | Rust workspace structure, build scripts, cross-compilation targets | +| `team-lead` | Project-aware coordinator. Decomposes plan-file stages into specialist tasks, spawns project agents in parallel via TeamCreate, runs verification gates, updates plan files. Entry point for any multi-domain stage. | +| `docs-and-plan` | Cross-file fidelity. Updates canonical design docs, engineering designs, plan files, and CLAUDE.md cross-references after stages land. Owns sync, not authoring. | ## Task-to-agent table @@ -33,6 +35,8 @@ | TileMap, sprites, camera, fog, selection highlight, animation | `godot-renderer` | | `public/games/age-of-dwarves/guide/`, `src/packages/guide/`, React, Vite, WASM integration | `guide-web` | | Cargo workspace layout, `build-*.sh` scripts, GDExtension/WASM build infra | `simulator-infra` | +| Multi-specialist plan-file stage, TeamCreate orchestration, verification gates, post-stage shutdown | `team-lead` | +| Update canonical doc + design files + plan + CLAUDE.md router after a stage lands; cross-reference fidelity | `docs-and-plan` | ## Specialists vs team-leads