feat(@projects/@magic-civilization): ✨ add combat stack limits and occupation penalties
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
868a4b6bd2
commit
d2c9539d49
5 changed files with 116 additions and 15 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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").
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue