fix(@projects/@magic-civilization): 🛡️ p3-26 B2 — siege-suppress city healing (besieged cities don't heal)
Regression fix: the B2 healing phase ran end-of-turn BEFORE siege and healed cities unconditionally — so a besieged city healed (e.g. 30→50 hp) before the same turn's siege resolved, defeating captures (last_survivor_via_capture got empty events: a 30-hp city + 3 attackers @15 = 45 dmg no longer captured after healing to 50). Bisected to this session (passed at e926345ad; the healing phase introduced the bug). Fix: process_healing_phase now snapshots all units' tiles and SKIPS healing any city with an enemy unit on its tile (under siege) — real-time siege-suppress matching the live game's intent (its `last_attacked_turn` window). Un-besieged cities still heal. Tests: besieged_city_does_not_heal (new) + last_survivor_via_capture (restored) + healing 11/0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
91095be232
commit
7f4b69eac1
1 changed files with 59 additions and 3 deletions
|
|
@ -65,7 +65,19 @@ const CITY_HEAL_PER_TURN: i32 = 20;
|
||||||
/// This function does **not** call `step()` or any other phase — the parent
|
/// This function does **not** call `step()` or any other phase — the parent
|
||||||
/// wires the call at the correct slot in the turn sequence.
|
/// wires the call at the correct slot in the turn sequence.
|
||||||
pub fn process_healing_phase(state: &mut GameState) {
|
pub fn process_healing_phase(state: &mut GameState) {
|
||||||
for player in &mut state.players {
|
// Snapshot every unit's (owner, tile) up front so city healing can skip
|
||||||
|
// BESIEGED cities — a city with an enemy unit on its tile is under siege and
|
||||||
|
// must NOT heal (else end-of-turn healing would undo the same turn's siege
|
||||||
|
// damage, e.g. heal a 30-hp city to 50 before a 45-dmg siege resolves).
|
||||||
|
// Mirrors the live game's siege-suppress intent via real-time occupancy.
|
||||||
|
let unit_tiles: Vec<(usize, i32, i32)> = state
|
||||||
|
.players
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.flat_map(|(pi, p)| p.units.iter().map(move |u| (pi, u.col, u.row)))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for (pi, player) in state.players.iter_mut().enumerate() {
|
||||||
// Snapshot city positions for garrison detection; `player` is borrowed
|
// Snapshot city positions for garrison detection; `player` is borrowed
|
||||||
// mutably below for units, so we can't hold a reference to
|
// mutably below for units, so we can't hold a reference to
|
||||||
// `player.city_positions` at the same time.
|
// `player.city_positions` at the same time.
|
||||||
|
|
@ -102,9 +114,17 @@ pub fn process_healing_phase(state: &mut GameState) {
|
||||||
unit.hp = (unit.hp + heal_amount).min(unit.max_hp);
|
unit.hp = (unit.hp + heal_amount).min(unit.max_hp);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── City healing ──────────────────────────────────────────────────
|
// ── City healing (siege-suppressed) ───────────────────────────────
|
||||||
for city in &mut player.cities {
|
for (ci, city) in player.cities.iter_mut().enumerate() {
|
||||||
if city.hp > 0 && city.hp < city.max_hp {
|
if city.hp > 0 && city.hp < city.max_hp {
|
||||||
|
let besieged = player.city_positions.get(ci).copied().is_some_and(|(cc, cr)| {
|
||||||
|
unit_tiles
|
||||||
|
.iter()
|
||||||
|
.any(|&(opi, uc, ur)| opi != pi && uc == cc && ur == cr)
|
||||||
|
});
|
||||||
|
if besieged {
|
||||||
|
continue; // under siege → no heal
|
||||||
|
}
|
||||||
city.hp = (city.hp + CITY_HEAL_PER_TURN).min(city.max_hp);
|
city.hp = (city.hp + CITY_HEAL_PER_TURN).min(city.max_hp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -290,6 +310,42 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A city with an enemy unit on its tile is under siege and must NOT heal
|
||||||
|
/// (so end-of-turn healing can't undo the same turn's siege damage).
|
||||||
|
#[test]
|
||||||
|
fn besieged_city_does_not_heal() {
|
||||||
|
let mut p0 = PlayerState {
|
||||||
|
city_positions: vec![(3, 3)],
|
||||||
|
..PlayerState::default()
|
||||||
|
};
|
||||||
|
let mut city = CityState::default();
|
||||||
|
city.hp = 80;
|
||||||
|
city.max_hp = 200;
|
||||||
|
p0.cities.push(city);
|
||||||
|
|
||||||
|
let mut p1 = PlayerState {
|
||||||
|
player_index: 1,
|
||||||
|
..PlayerState::default()
|
||||||
|
};
|
||||||
|
p1.units.push(MapUnit {
|
||||||
|
col: 3,
|
||||||
|
row: 3,
|
||||||
|
hp: 60,
|
||||||
|
max_hp: 60,
|
||||||
|
..MapUnit::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut state = GameState::default();
|
||||||
|
state.players.push(p0);
|
||||||
|
state.players.push(p1);
|
||||||
|
process_healing_phase(&mut state);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
state.players[0].cities[0].hp, 80,
|
||||||
|
"besieged city (enemy unit on tile) must not heal"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// A city at max HP does not overheal.
|
/// A city at max HP does not overheal.
|
||||||
#[test]
|
#[test]
|
||||||
fn full_hp_city_is_not_overhealed() {
|
fn full_hp_city_is_not_overhealed() {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue