feat(@projects/@magic-civilization): add combat stack limits and occupation penalties

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-04-30 00:50:14 -04:00
parent 868a4b6bd2
commit d2c9539d49
5 changed files with 116 additions and 15 deletions

View file

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

View file

@ -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: <id|null>`, `consumes_existing: <bool>`. 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/<id>.json` with the new fields populated.
- ✗ Initial 3-step ladders (suggested, pending user confirmation — each is `<existing Lv1> → <new or repurposed Lv2> → <existing Lv3+>`):
- **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 `<existing Lv1> → <Lv2> → <existing Lv3+>`). 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").

View file

@ -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": [

View file

@ -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<String> = 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.

View file

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