fix(simulator): 🐛 project real unit movement into the tactical AI state

project_tactical_player hardcoded moves_left: 2 for every unit (a stale 'bench
MapUnit doesn't model moves_left' comment) while MapUnit::movement_remaining is
the field the move dispatch actually decrements and the player view gates legal
moves on. The AI therefore believed every unit always had movement, planned
moves for already-exhausted units, and the dispatch rejected them.

Measured over a 200-turn hotseat self-play (seed 42): 'no movement points
remaining' move rejections dropped 10,862 → 0 (total controller misfires
10,972 → 110, the rest legitimate path/stacking edge cases). The AI's
world-model now matches enforcement; turns no longer churn through ~54 dead
move attempts each.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Natalie 2026-06-23 18:57:15 -04:00
parent 60c8ce0ef6
commit 5eed0bb579

View file

@ -1169,10 +1169,14 @@ fn project_tactical_player(
hex: (u.col, u.row),
hp: u.hp.max(0) as u32,
hp_max: u.max_hp.max(0) as u32,
// Bench `MapUnit` doesn't model moves_left — the turn processor
// refreshes it implicitly. v1 reports full (= 2) so the AI
// can plan a single move per turn.
moves_left: 2,
// Real remaining movement so the AI's world-model matches the
// dispatch's enforcement. Hardcoding 2 made the controller plan
// moves for already-exhausted units, so ~99% of its move
// suggestions were rejected "no movement points remaining" and
// armies barely maneuvered. `MapUnit::movement_remaining` is the
// same field the move dispatch decrements and the player view
// gates legal moves on (projection.rs ~444).
moves_left: u.movement_remaining.max(0) as u32,
fortified: u.is_fortified,
// p2-71b — flip the founder flag for the canonical Game-1
// founder so `TacticalUnit::is_founder()` returns true and the
@ -1377,6 +1381,7 @@ mod tests {
u.unit_id = "dwarf_warrior".into();
u.hp = 100;
u.max_hp = 100;
u.movement_remaining = 2;
ps.units.push(u);
}
} else {