diff --git a/src/simulator/crates/mc-turn/src/processor.rs b/src/simulator/crates/mc-turn/src/processor.rs index ab3a563d..72e0e076 100644 --- a/src/simulator/crates/mc-turn/src/processor.rs +++ b/src/simulator/crates/mc-turn/src/processor.rs @@ -5285,6 +5285,81 @@ mod move_request_tests { ); } + #[test] + fn teched_army_fords_water_then_attacks_enemy_on_far_landmass() { + // The conquest payoff end-to-end: landmass A (col 0) | ocean wall (col 1) + // | landmass B (cols 2-3). A teched army on A fords the ocean to B and + // strikes an enemy waiting there — embark turns an unreachable rival into + // an attackable one. + let mut grid = GridState::new(4, 3); + for r in 0..3 { + for c in 0..4 { + let idx = grid.idx(c, r); + grid.tiles[idx].biome_label_id = + if c == 1 { "ocean" } else { "plains" }.to_string(); + } + } + let mut state = GameState::default(); + state.grid = Some(grid); + state + .units_catalog + .load_json_str( + r#"[{"id":"warrior","movement":10,"domain":"land","hp":60,"attack":20,"defense":2}]"#, + ) + .expect("catalog loads"); + state.players.push(PlayerState { + player_index: 0, + embark_level: mc_core::EmbarkLevel::Ocean, + units: vec![MapUnit { + id: 1, col: 0, row: 0, unit_id: "warrior".into(), + hp: 60, max_hp: 60, attack: 20, defense: 2, ..MapUnit::default() + } + .with_moves(10)], + ..PlayerState::default() + }); + state.players.push(PlayerState { + player_index: 1, + units: vec![MapUnit { + id: 2, col: 3, row: 0, unit_id: "warrior".into(), + hp: 60, max_hp: 60, attack: 20, defense: 2, ..MapUnit::default() + } + .with_moves(0)], + ..PlayerState::default() + }); + + // 1) FORD: army (0,0) -> (2,0), crossing the ocean column to landmass B, + // landing adjacent to the enemy at (3,0). + state.pending_move_requests.push(move_req(0, (2, 0))); + process_move_requests(&mut state); + assert_eq!( + (state.players[0].units[0].col, state.players[0].units[0].row), + (2, 0), + "teched army forded the ocean to the far landmass" + ); + + // 2) ATTACK: strike the enemy now within reach across the water. + let enemy_hp_before = state.players[1].units[0].hp; + state.pending_pvp_attacks.push(crate::game_state::AttackRequest { + attacker_player: 0, + attacker_unit: 0, + defender_player: 1, + defender_unit: 0, + }); + let processor = TurnProcessor::new(500); + let mut result = TurnResult::default(); + processor.process_pvp_combat(&mut state, &mut result); + + assert!(result.pvp_battles >= 1, "a cross-water battle resolved"); + let enemy_dead = !state.players[1].units.iter().any(|u| u.id == 2); + let enemy_hurt = enemy_dead + || state.players[1].units.iter().find(|u| u.id == 2).map(|u| u.hp).unwrap_or(0) + < enemy_hp_before; + assert!( + enemy_hurt, + "the army that forded the ocean damaged the enemy on the far landmass" + ); + } + #[test] fn zero_budget_rejects() { let mut state = build_state_with_unit(7, (0, 0), 0, |_, _| "plains");