From a91a02898769b84431d91c2e77e62c40f22aa287 Mon Sep 17 00:00:00 2001 From: "@permadeath.com" Date: Sat, 29 Aug 2026 01:41:53 -0400 Subject: [PATCH] fix(candidates): measure a backward step by where the mover stands The gate was already on it - a backward step is priced by the same `step_mp` - but the elevation rule beside it read the hexes' floors, so a hovercraft backing off a lake's shore read as flat ground where MegaMek sees a level change. A corpus case with woods behind a hovercraft holds the seam closed. --- crates/sds-core/src/pathfind.rs | 81 +++++++++++++++++-- crates/sds-core/tests/corpus/pathfind.jsonl | 1 + .../tests/corpus/pathfind_routes.jsonl | 1 + crates/sds-core/tests/pathfind_corpus.rs | 17 ++-- .../sds-core/tests/pathfind_differential.rs | 8 +- plan/candidates.md | 18 +++++ sds/match.py | 6 ++ 7 files changed, 117 insertions(+), 15 deletions(-) diff --git a/crates/sds-core/src/pathfind.rs b/crates/sds-core/src/pathfind.rs index 3e61c0a..ff9c48f 100644 --- a/crates/sds-core/src/pathfind.rs +++ b/crates/sds-core/src/pathfind.rs @@ -955,10 +955,17 @@ fn turn_onto(steps: &mut Vec, facing: &mut i32, onto: i32) { /// disengagement turns its back first, and `positional::RearArcExposure` is a /// weight against a hazard nothing in the candidate set can avoid. /// -/// Every route this emits is one MegaMek accepts: `./sds.sh routes-check` hands -/// each to `isMoveLegal` and finds **37,166 of 37,166** legal. That check is -/// offline and runs when the emitter changes - nothing here happens in a match, -/// where the search is Rust and asks MegaMek nothing. +/// Every route this emits for a Mek is one MegaMek accepts: `./sds.sh +/// routes-check` hands each to `isMoveLegal` and finds every one of the +/// Phoenix Hawk's legal. A mover's are not all accepted yet - 44 of them are +/// priced above the budget by rules of MegaMek's arithmetic the port still +/// charges the Mek's, pinned in `corpus/pathfind_routes.jsonl` and stated in +/// `plan/candidates.md`. The gate is not one of them: a backward step is +/// refused a hex the mover may not enter exactly as a forward one is, which is +/// what `a_barred_hex_is_refused_behind_the_mover_as_well` and the corpus's +/// hovercraft-with-woods-behind-it case are for. That check is offline and runs +/// when the emitter changes - nothing here happens in a match, where the search +/// is Rust and asks MegaMek nothing. pub fn search(board: &MoveBoard, walker: &Walker<'_>, blocked: &[Coord]) -> Search { search_with(board, walker, blocked, true, &[]) } @@ -1151,11 +1158,23 @@ fn search_with( // states and the whole of what `isMoveLegal` objected to on the // water board: 189 refused routes, every one a Mek in water // backing out of it. + // + // **Where the mover stands, not the hex's floor.** The two are the + // same for a Mek and are not for a hovercraft, which is on the + // surface of a pond rather than on the bottom of it: read off the + // floor, backing off the shore of a level-1 lake reads as flat + // ground and MegaMek refuses it. Same reading + // [`MoveBoard::step_mp`] climbs by - see [`Footing::height`] - so + // the backward step and the forward one cannot drift apart. + let mover = walker.mover(); let here = board.hex(node.stand.hex); let there = board.hex(behind); - let flat = here.level - here.depth == there.level - there.depth; + let flat = mover.height(&here) == mover.height(&there); if flat && !blocked.contains(&behind) { - if let Some(cost) = board.step_mp(node.stand.hex, behind, &walker.mover()) { + // The gate is [`MoveBoard::step_mp`]'s, so a hex the mover may + // not enter is refused behind it as well as ahead of it: see + // `a_barred_hex_is_refused_behind_the_mover_as_well`. + if let Some(cost) = board.step_mp(node.stand.hex, behind, &mover) { offer( Node { stand: Stand::new(behind, node.stand.facing), @@ -3059,4 +3078,54 @@ mod tests { let twin = mek.clone(); assert_eq!(MovementProfile::of(&mek), MovementProfile::of(&twin)); } + + /// The seam between the gate and the backward step. + /// + /// Backing up and the terrain gate were built in different worktrees + /// against different bases, and "it is a step, so it must be gated" is the + /// kind of assumption that is false exactly at a seam. It is one here + /// because a backward step is priced by [`MoveBoard::step_mp`] like a + /// forward one - but the elevation rule beside it is its own, and that one + /// did read the hexes' floors until this test's sibling case went into the + /// corpus. + #[test] + fn a_barred_hex_is_refused_behind_the_mover_as_well() { + let board = wooded(); + let shut = answers(0, 1, true); + let hover = Walker { + start: Stand::new(Coord::new(4, 3), 0), + ground_turn_mp: GROUND_TURN_MP, + mp: 6, + walk_mp: 6, + jump_mp: 0, + max_elevation_change: 1, + prone: false, + mp_used: 0, + mode: Mode::Ground, + terrain: &shut, + }; + // Facing north, so the woods at (4, 4) are the hex behind it. + assert_eq!(hover.start.behind(), Coord::new(4, 4)); + let reached: Vec = reachable(&board, &hover, &[]) + .into_iter() + .map(|r| r.stand.hex) + .collect(); + assert!( + !reached.contains(&Coord::new(4, 4)), + "backed into a hex the mover may not enter: {reached:?}" + ); + // And the same board to a mover the woods are open to, so the case is + // about the gate and not about the geometry. + let open_to = answers(0, 1, false); + let mek = Walker { + terrain: &open_to, + max_elevation_change: MEK_MAX_ELEVATION_CHANGE, + ..hover + }; + let reached: Vec = reachable(&board, &mek, &[]) + .into_iter() + .map(|r| r.stand.hex) + .collect(); + assert!(reached.contains(&Coord::new(4, 4)), "{reached:?}"); + } } diff --git a/crates/sds-core/tests/corpus/pathfind.jsonl b/crates/sds-core/tests/corpus/pathfind.jsonl index e5b7c98..e0ef396 100644 --- a/crates/sds-core/tests/corpus/pathfind.jsonl +++ b/crates/sds-core/tests/corpus/pathfind.jsonl @@ -57,6 +57,7 @@ {"type":"case","x":6,"y":5,"facing":0,"mp":4,"mode":"ground","prone":false,"mpUsed":0,"nonGeodesicLegal":0,"considered":76,"legal":31,"settled":19,"states":[[6,5,0,0,0,0,"NONE"],[7,4,0,3,1,0,"MOVE_WALK"],[7,4,1,2,1,0,"MOVE_WALK"],[7,4,2,3,1,0,"MOVE_WALK"],[7,4,3,4,1,0,"MOVE_WALK"],[7,4,4,3,1,0,"MOVE_WALK"],[7,4,5,4,1,0,"MOVE_WALK"]],"board":"forest","specimen":"Pegasus_Scout_Hover_Tank_(Missile)"} {"type":"case","x":6,"y":5,"facing":0,"mp":6,"mode":"ground","prone":false,"mpUsed":0,"nonGeodesicLegal":0,"considered":136,"legal":62,"settled":34,"states":[[5,5,3,6,1,0,"MOVE_WALK"],[5,5,4,5,1,0,"MOVE_WALK"],[5,5,5,6,1,0,"MOVE_WALK"],[6,5,0,0,0,0,"NONE"],[7,4,0,3,1,0,"MOVE_WALK"],[7,4,1,2,1,0,"MOVE_WALK"],[7,4,2,3,1,0,"MOVE_WALK"],[7,4,3,4,1,0,"MOVE_WALK"],[7,4,4,3,1,0,"MOVE_WALK"],[7,4,5,4,1,0,"MOVE_WALK"]],"board":"forest","specimen":"Pegasus_Scout_Hover_Tank_(Missile)"} {"type":"case","x":4,"y":8,"facing":0,"mp":4,"mode":"ground","prone":false,"mpUsed":0,"nonGeodesicLegal":0,"considered":168,"legal":96,"settled":42,"states":[[3,7,0,2,1,0,"MOVE_WALK"],[3,7,1,2,1,0,"MOVE_WALK"],[3,7,2,2,1,0,"MOVE_WALK"],[3,7,3,2,1,0,"MOVE_WALK"],[3,7,4,2,1,0,"MOVE_WALK"],[3,7,5,2,1,0,"MOVE_WALK"],[3,8,0,1,1,0,"MOVE_WALK"],[3,8,1,1,1,0,"MOVE_WALK"],[3,8,2,1,1,0,"MOVE_WALK"],[3,8,3,1,1,0,"MOVE_WALK"],[3,8,4,1,1,0,"MOVE_WALK"],[3,8,5,1,1,0,"MOVE_WALK"],[4,7,0,2,1,0,"MOVE_WALK"],[4,7,1,2,1,0,"MOVE_WALK"],[4,7,2,2,1,0,"MOVE_WALK"],[4,7,3,2,1,0,"MOVE_WALK"],[4,7,4,2,1,0,"MOVE_WALK"],[4,7,5,2,1,0,"MOVE_WALK"],[4,8,0,0,0,0,"NONE"],[4,8,1,0,0,0,"MOVE_NONE"],[4,8,2,0,0,0,"MOVE_NONE"],[4,8,3,0,0,0,"MOVE_NONE"],[4,8,4,0,0,0,"MOVE_NONE"],[4,8,5,0,0,0,"MOVE_NONE"],[4,9,0,2,1,0,"MOVE_WALK"],[4,9,1,2,1,0,"MOVE_WALK"],[4,9,2,2,1,0,"MOVE_WALK"],[4,9,3,2,1,0,"MOVE_WALK"],[4,9,4,2,1,0,"MOVE_WALK"],[4,9,5,2,1,0,"MOVE_WALK"],[5,7,0,2,1,0,"MOVE_WALK"],[5,7,1,2,1,0,"MOVE_WALK"],[5,7,2,2,1,0,"MOVE_WALK"],[5,7,3,2,1,0,"MOVE_WALK"],[5,7,4,2,1,0,"MOVE_WALK"],[5,7,5,2,1,0,"MOVE_WALK"],[5,8,0,2,1,0,"MOVE_WALK"],[5,8,1,2,1,0,"MOVE_WALK"],[5,8,2,2,1,0,"MOVE_WALK"],[5,8,3,2,1,0,"MOVE_WALK"],[5,8,4,2,1,0,"MOVE_WALK"],[5,8,5,2,1,0,"MOVE_WALK"]],"board":"forest","specimen":"Elemental_Battle_Armor_[Laser](Sqd5)"} +{"type":"case","x":11,"y":12,"facing":3,"mp":6,"mode":"ground","prone":false,"mpUsed":0,"nonGeodesicLegal":0,"considered":184,"legal":92,"settled":46,"states":[[11,12,0,3,0,0,"MOVE_WALK"],[11,12,1,2,0,0,"MOVE_WALK"],[11,12,2,1,0,0,"MOVE_WALK"],[11,12,3,0,0,0,"NONE"],[11,12,4,1,0,0,"MOVE_WALK"],[11,12,5,2,0,0,"MOVE_WALK"],[11,13,0,4,1,0,"MOVE_WALK"],[11,13,1,3,1,0,"MOVE_WALK"],[11,13,2,2,1,0,"MOVE_WALK"],[11,13,3,1,1,0,"MOVE_WALK"],[11,13,4,2,1,0,"MOVE_WALK"],[11,13,5,3,1,0,"MOVE_WALK"],[12,12,0,4,1,0,"MOVE_WALK"],[12,12,1,3,1,0,"MOVE_WALK"],[12,12,2,4,1,0,"MOVE_WALK"],[12,12,3,3,1,0,"MOVE_WALK"],[12,12,4,2,1,0,"MOVE_WALK"],[12,12,5,3,1,0,"MOVE_WALK"]],"board":"forest","specimen":"Pegasus_Scout_Hover_Tank_(Missile)"} {"type":"board","width":16,"height":17,"hexes":[{"x":7,"y":0,"level":0,"kind":1,"terrain":{"incline_bottom":1}},{"x":9,"y":0,"level":0,"kind":1,"terrain":{"incline_bottom":1}},{"x":10,"y":0,"level":0,"kind":1,"terrain":{"incline_bottom":1}},{"x":11,"y":0,"level":0,"kind":1,"terrain":{"incline_bottom":1}},{"x":13,"y":0,"level":0,"kind":2,"terrain":{"incline_high_bottom":1}},{"x":14,"y":0,"level":0,"kind":2,"terrain":{"incline_high_bottom":1}},{"x":15,"y":0,"level":6,"kind":3,"terrain":{"incline_high_top":1}},{"x":1,"y":1,"level":0,"kind":4,"terrain":{"woods":1,"foliage_elev":2}},{"x":5,"y":1,"level":0,"kind":1,"terrain":{"incline_bottom":1}},{"x":6,"y":1,"level":0,"kind":5,"terrain":{"woods":1,"foliage_elev":2,"incline_bottom":1}},{"x":7,"y":1,"level":1,"kind":6,"terrain":{"incline_top":1,"incline_bottom":1}},{"x":8,"y":1,"level":0,"kind":1,"terrain":{"incline_bottom":1}},{"x":9,"y":1,"level":2,"kind":6,"terrain":{"incline_top":1,"incline_bottom":1}},{"x":10,"y":1,"level":2,"kind":6,"terrain":{"incline_top":1,"incline_bottom":1}},{"x":11,"y":1,"level":2,"kind":6,"terrain":{"incline_top":1,"incline_bottom":1}},{"x":12,"y":1,"level":0,"kind":7,"terrain":{"incline_high_bottom":1,"incline_bottom":1}},{"x":13,"y":1,"level":6,"kind":3,"terrain":{"incline_high_top":1}},{"x":14,"y":1,"level":6,"kind":3,"terrain":{"incline_high_top":1}},{"x":15,"y":1,"level":6,"kind":0},{"x":3,"y":2,"level":0,"kind":1,"terrain":{"incline_bottom":1}},{"x":4,"y":2,"level":0,"kind":1,"terrain":{"incline_bottom":1}},{"x":5,"y":2,"level":1,"kind":6,"terrain":{"incline_top":1,"incline_bottom":1}},{"x":6,"y":2,"level":1,"kind":8,"terrain":{"woods":1,"incline_top":1,"incline_bottom":1,"foliage_elev":2}},{"x":7,"y":2,"level":2,"kind":9,"terrain":{"incline_high_bottom":1,"incline_top":1}},{"x":8,"y":2,"level":2,"kind":10,"terrain":{"incline_high_bottom":1,"incline_top":1,"incline_bottom":1}},{"x":9,"y":2,"level":4,"kind":6,"terrain":{"incline_top":1,"incline_bottom":1}},{"x":10,"y":2,"level":4,"kind":11,"terrain":{"incline_top":1}},{"x":11,"y":2,"level":4,"kind":6,"terrain":{"incline_top":1,"incline_bottom":1}},{"x":12,"y":2,"level":2,"kind":10,"terrain":{"incline_high_bottom":1,"incline_top":1,"incline_bottom":1}},{"x":13,"y":2,"level":6,"kind":3,"terrain":{"incline_high_top":1}},{"x":14,"y":2,"level":6,"kind":0},{"x":15,"y":2,"level":6,"kind":0},{"x":1,"y":3,"level":0,"kind":1,"terrain":{"incline_bottom":1}},{"x":2,"y":3,"level":0,"kind":1,"terrain":{"incline_bottom":1}},{"x":3,"y":3,"level":1,"kind":6,"terrain":{"incline_top":1,"incline_bottom":1}},{"x":4,"y":3,"level":1,"kind":6,"terrain":{"incline_top":1,"incline_bottom":1}},{"x":5,"y":3,"level":2,"kind":8,"terrain":{"woods":1,"incline_top":1,"incline_bottom":1,"foliage_elev":2}},{"x":6,"y":3,"level":2,"kind":12,"terrain":{"woods":2,"incline_top":1,"incline_bottom":1,"incline_high_bottom":1,"foliage_elev":2}},{"x":7,"y":3,"level":6,"kind":13,"terrain":{"incline_high_top":1,"incline_top":1,"incline_bottom":1}},{"x":8,"y":3,"level":6,"kind":13,"terrain":{"incline_high_top":1,"incline_top":1,"incline_bottom":1}},{"x":9,"y":3,"level":6,"kind":6,"terrain":{"incline_top":1,"incline_bottom":1}},{"x":10,"y":3,"level":4,"kind":1,"terrain":{"incline_bottom":1}},{"x":11,"y":3,"level":6,"kind":11,"terrain":{"incline_top":1}},{"x":12,"y":3,"level":6,"kind":14,"terrain":{"incline_high_top":1,"incline_top":1}},{"x":13,"y":3,"level":6,"kind":0},{"x":14,"y":3,"level":6,"kind":0},{"x":15,"y":3,"level":6,"kind":0},{"x":0,"y":4,"level":0,"kind":1,"terrain":{"incline_bottom":1}},{"x":1,"y":4,"level":1,"kind":6,"terrain":{"incline_top":1,"incline_bottom":1}},{"x":2,"y":4,"level":1,"kind":6,"terrain":{"incline_top":1,"incline_bottom":1}},{"x":3,"y":4,"level":2,"kind":6,"terrain":{"incline_top":1,"incline_bottom":1}},{"x":4,"y":4,"level":2,"kind":6,"terrain":{"incline_top":1,"incline_bottom":1}},{"x":5,"y":4,"level":4,"kind":10,"terrain":{"incline_high_bottom":1,"incline_top":1,"incline_bottom":1}},{"x":6,"y":4,"level":4,"kind":10,"terrain":{"incline_high_bottom":1,"incline_top":1,"incline_bottom":1}},{"x":7,"y":4,"level":7,"kind":13,"terrain":{"incline_high_top":1,"incline_top":1,"incline_bottom":1}},{"x":8,"y":4,"level":7,"kind":11,"terrain":{"incline_top":1}},{"x":9,"y":4,"level":7,"kind":11,"terrain":{"incline_top":1}},{"x":10,"y":4,"level":6,"kind":6,"terrain":{"incline_top":1,"incline_bottom":1}},{"x":11,"y":4,"level":6,"kind":1,"terrain":{"incline_bottom":1}},{"x":12,"y":4,"level":6,"kind":0},{"x":13,"y":4,"level":6,"kind":15,"terrain":{"woods":2,"foliage_elev":2}},{"x":14,"y":4,"level":6,"kind":4,"terrain":{"woods":1,"foliage_elev":2}},{"x":15,"y":4,"level":6,"kind":0},{"x":0,"y":5,"level":0,"kind":1,"terrain":{"incline_bottom":1}},{"x":1,"y":5,"level":1,"kind":10,"terrain":{"incline_high_bottom":1,"incline_top":1,"incline_bottom":1}},{"x":2,"y":5,"level":2,"kind":6,"terrain":{"incline_top":1,"incline_bottom":1}},{"x":3,"y":5,"level":4,"kind":6,"terrain":{"incline_top":1,"incline_bottom":1}},{"x":4,"y":5,"level":4,"kind":6,"terrain":{"incline_top":1,"incline_bottom":1}},{"x":5,"y":5,"level":5,"kind":10,"terrain":{"incline_high_bottom":1,"incline_top":1,"incline_bottom":1}},{"x":6,"y":5,"level":7,"kind":13,"terrain":{"incline_high_top":1,"incline_top":1,"incline_bottom":1}},{"x":7,"y":5,"level":8,"kind":6,"terrain":{"incline_top":1,"incline_bottom":1}},{"x":8,"y":5,"level":7,"kind":1,"terrain":{"incline_bottom":1}},{"x":9,"y":5,"level":7,"kind":1,"terrain":{"incline_bottom":1}},{"x":10,"y":5,"level":7,"kind":11,"terrain":{"incline_top":1}},{"x":11,"y":5,"level":7,"kind":11,"terrain":{"incline_top":1}},{"x":12,"y":5,"level":6,"kind":1,"terrain":{"incline_bottom":1}},{"x":13,"y":5,"level":6,"kind":1,"terrain":{"incline_bottom":1}},{"x":14,"y":5,"level":6,"kind":4,"terrain":{"woods":1,"foliage_elev":2}},{"x":15,"y":5,"level":6,"kind":0},{"x":0,"y":6,"level":0,"kind":1,"terrain":{"incline_bottom":1}},{"x":1,"y":6,"level":1,"kind":9,"terrain":{"incline_high_bottom":1,"incline_top":1}},{"x":2,"y":6,"level":4,"kind":13,"terrain":{"incline_high_top":1,"incline_top":1,"incline_bottom":1}},{"x":3,"y":6,"level":6,"kind":11,"terrain":{"incline_top":1}},{"x":4,"y":6,"level":5,"kind":10,"terrain":{"incline_high_bottom":1,"incline_top":1,"incline_bottom":1}},{"x":5,"y":6,"level":8,"kind":16,"terrain":{"incline_high_top":1,"incline_bottom":1}},{"x":6,"y":6,"level":8,"kind":13,"terrain":{"incline_high_top":1,"incline_top":1,"incline_bottom":1}},{"x":7,"y":6,"level":10,"kind":6,"terrain":{"incline_top":1,"incline_bottom":1}},{"x":8,"y":6,"level":8,"kind":6,"terrain":{"incline_top":1,"incline_bottom":1}},{"x":9,"y":6,"level":8,"kind":6,"terrain":{"incline_top":1,"incline_bottom":1}},{"x":10,"y":6,"level":7,"kind":1,"terrain":{"incline_bottom":1}},{"x":11,"y":6,"level":7,"kind":1,"terrain":{"incline_bottom":1}},{"x":12,"y":6,"level":7,"kind":11,"terrain":{"incline_top":1}},{"x":13,"y":6,"level":6,"kind":1,"terrain":{"incline_bottom":1}},{"x":14,"y":6,"level":6,"kind":0},{"x":15,"y":6,"level":6,"kind":0},{"x":0,"y":7,"level":0,"kind":1,"terrain":{"incline_bottom":1}},{"x":1,"y":7,"level":1,"kind":9,"terrain":{"incline_high_bottom":1,"incline_top":1}},{"x":2,"y":7,"level":4,"kind":17,"terrain":{"woods":1,"incline_bottom":1,"incline_high_top":1,"foliage_elev":2}},{"x":3,"y":7,"level":6,"kind":11,"terrain":{"incline_top":1}},{"x":4,"y":7,"level":5,"kind":7,"terrain":{"incline_high_bottom":1,"incline_bottom":1}},{"x":5,"y":7,"level":8,"kind":16,"terrain":{"incline_high_top":1,"incline_bottom":1}},{"x":6,"y":7,"level":10,"kind":6,"terrain":{"incline_top":1,"incline_bottom":1}},{"x":7,"y":7,"level":12,"kind":11,"terrain":{"incline_top":1}},{"x":8,"y":7,"level":10,"kind":6,"terrain":{"incline_top":1,"incline_bottom":1}},{"x":9,"y":7,"level":10,"kind":11,"terrain":{"incline_top":1}},{"x":10,"y":7,"level":8,"kind":6,"terrain":{"incline_top":1,"incline_bottom":1}},{"x":11,"y":7,"level":7,"kind":1,"terrain":{"incline_bottom":1}},{"x":12,"y":7,"level":7,"kind":11,"terrain":{"incline_top":1}},{"x":13,"y":7,"level":6,"kind":1,"terrain":{"incline_bottom":1}},{"x":14,"y":7,"level":6,"kind":0},{"x":15,"y":7,"level":6,"kind":0},{"x":0,"y":8,"level":0,"kind":1,"terrain":{"incline_bottom":1}},{"x":1,"y":8,"level":1,"kind":10,"terrain":{"incline_high_bottom":1,"incline_top":1,"incline_bottom":1}},{"x":2,"y":8,"level":4,"kind":18,"terrain":{"woods":1,"incline_top":1,"incline_bottom":1,"incline_high_top":1,"foliage_elev":2}},{"x":3,"y":8,"level":6,"kind":14,"terrain":{"incline_high_top":1,"incline_top":1}},{"x":4,"y":8,"level":5,"kind":7,"terrain":{"incline_high_bottom":1,"incline_bottom":1}},{"x":5,"y":8,"level":8,"kind":16,"terrain":{"incline_high_top":1,"incline_bottom":1}},{"x":6,"y":8,"level":10,"kind":19,"terrain":{"water":1}},{"x":7,"y":8,"level":10,"kind":6,"terrain":{"incline_top":1,"incline_bottom":1}},{"x":8,"y":8,"level":10,"kind":6,"terrain":{"incline_top":1,"incline_bottom":1}},{"x":9,"y":8,"level":8,"kind":1,"terrain":{"incline_bottom":1}},{"x":10,"y":8,"level":8,"kind":6,"terrain":{"incline_top":1,"incline_bottom":1}},{"x":11,"y":8,"level":7,"kind":1,"terrain":{"incline_bottom":1}},{"x":12,"y":8,"level":7,"kind":11,"terrain":{"incline_top":1}},{"x":13,"y":8,"level":6,"kind":1,"terrain":{"incline_bottom":1}},{"x":14,"y":8,"level":6,"kind":0},{"x":15,"y":8,"level":6,"kind":0},{"x":0,"y":9,"level":0,"kind":1,"terrain":{"incline_bottom":1}},{"x":1,"y":9,"level":1,"kind":6,"terrain":{"incline_top":1,"incline_bottom":1}},{"x":2,"y":9,"level":3,"kind":10,"terrain":{"incline_high_bottom":1,"incline_top":1,"incline_bottom":1}},{"x":3,"y":9,"level":6,"kind":14,"terrain":{"incline_high_top":1,"incline_top":1}},{"x":4,"y":9,"level":5,"kind":7,"terrain":{"incline_high_bottom":1,"incline_bottom":1}},{"x":5,"y":9,"level":8,"kind":14,"terrain":{"incline_high_top":1,"incline_top":1}},{"x":6,"y":9,"level":8,"kind":19,"terrain":{"water":1}},{"x":7,"y":9,"level":8,"kind":19,"terrain":{"water":1}},{"x":8,"y":9,"level":8,"kind":1,"terrain":{"incline_bottom":1}},{"x":9,"y":9,"level":8,"kind":4,"terrain":{"woods":1,"foliage_elev":2}},{"x":10,"y":9,"level":8,"kind":11,"terrain":{"incline_top":1}},{"x":11,"y":9,"level":7,"kind":1,"terrain":{"incline_bottom":1}},{"x":12,"y":9,"level":7,"kind":11,"terrain":{"incline_top":1}},{"x":13,"y":9,"level":6,"kind":1,"terrain":{"incline_bottom":1}},{"x":14,"y":9,"level":6,"kind":0},{"x":15,"y":9,"level":6,"kind":0},{"x":0,"y":10,"level":0,"kind":1,"terrain":{"incline_bottom":1}},{"x":1,"y":10,"level":0,"kind":7,"terrain":{"incline_high_bottom":1,"incline_bottom":1}},{"x":2,"y":10,"level":3,"kind":20,"terrain":{"incline_high_top":1,"incline_high_bottom":1,"incline_top":1}},{"x":3,"y":10,"level":3,"kind":2,"terrain":{"incline_high_bottom":1}},{"x":4,"y":10,"level":6,"kind":13,"terrain":{"incline_high_top":1,"incline_top":1,"incline_bottom":1}},{"x":5,"y":10,"level":7,"kind":6,"terrain":{"incline_top":1,"incline_bottom":1}},{"x":6,"y":10,"level":8,"kind":11,"terrain":{"incline_top":1}},{"x":7,"y":10,"level":8,"kind":11,"terrain":{"incline_top":1}},{"x":8,"y":10,"level":8,"kind":19,"terrain":{"water":1}},{"x":9,"y":10,"level":8,"kind":19,"terrain":{"water":1}},{"x":10,"y":10,"level":8,"kind":11,"terrain":{"incline_top":1}},{"x":11,"y":10,"level":7,"kind":8,"terrain":{"woods":1,"incline_top":1,"incline_bottom":1,"foliage_elev":2}},{"x":12,"y":10,"level":7,"kind":11,"terrain":{"incline_top":1}},{"x":13,"y":10,"level":6,"kind":1,"terrain":{"incline_bottom":1}},{"x":14,"y":10,"level":6,"kind":0},{"x":15,"y":10,"level":6,"kind":0},{"x":1,"y":11,"level":0,"kind":2,"terrain":{"incline_high_bottom":1}},{"x":2,"y":11,"level":3,"kind":3,"terrain":{"incline_high_top":1}},{"x":3,"y":11,"level":3,"kind":21,"terrain":{"incline_high_top":1,"incline_high_bottom":1}},{"x":4,"y":11,"level":6,"kind":16,"terrain":{"incline_high_top":1,"incline_bottom":1}},{"x":5,"y":11,"level":7,"kind":14,"terrain":{"incline_high_top":1,"incline_top":1}},{"x":6,"y":11,"level":7,"kind":1,"terrain":{"incline_bottom":1}},{"x":7,"y":11,"level":7,"kind":1,"terrain":{"incline_bottom":1}},{"x":8,"y":11,"level":8,"kind":11,"terrain":{"incline_top":1}},{"x":9,"y":11,"level":7,"kind":19,"terrain":{"water":1}},{"x":10,"y":11,"level":7,"kind":6,"terrain":{"incline_top":1,"incline_bottom":1}},{"x":11,"y":11,"level":6,"kind":1,"terrain":{"incline_bottom":1}},{"x":12,"y":11,"level":6,"kind":5,"terrain":{"woods":1,"foliage_elev":2,"incline_bottom":1}},{"x":13,"y":11,"level":6,"kind":15,"terrain":{"woods":2,"foliage_elev":2}},{"x":14,"y":11,"level":6,"kind":0},{"x":15,"y":11,"level":6,"kind":0},{"x":2,"y":12,"level":0,"kind":2,"terrain":{"incline_high_bottom":1}},{"x":3,"y":12,"level":0,"kind":2,"terrain":{"incline_high_bottom":1}},{"x":4,"y":12,"level":3,"kind":21,"terrain":{"incline_high_top":1,"incline_high_bottom":1}},{"x":5,"y":12,"level":3,"kind":22,"terrain":{"incline_high_top":1,"incline_high_bottom":1,"incline_bottom":1}},{"x":6,"y":12,"level":7,"kind":14,"terrain":{"incline_high_top":1,"incline_top":1}},{"x":7,"y":12,"level":7,"kind":11,"terrain":{"incline_top":1}},{"x":8,"y":12,"level":7,"kind":6,"terrain":{"incline_top":1,"incline_bottom":1}},{"x":9,"y":12,"level":6,"kind":19,"terrain":{"water":1}},{"x":10,"y":12,"level":6,"kind":1,"terrain":{"incline_bottom":1}},{"x":11,"y":12,"level":6,"kind":0},{"x":12,"y":12,"level":6,"kind":4,"terrain":{"woods":1,"foliage_elev":2}},{"x":13,"y":12,"level":6,"kind":0},{"x":14,"y":12,"level":6,"kind":0},{"x":15,"y":12,"level":6,"kind":0},{"x":1,"y":13,"level":0,"kind":4,"terrain":{"woods":1,"foliage_elev":2}},{"x":4,"y":13,"level":0,"kind":2,"terrain":{"incline_high_bottom":1}},{"x":5,"y":13,"level":0,"kind":2,"terrain":{"incline_high_bottom":1}},{"x":6,"y":13,"level":5,"kind":13,"terrain":{"incline_high_top":1,"incline_top":1,"incline_bottom":1}},{"x":7,"y":13,"level":5,"kind":16,"terrain":{"incline_high_top":1,"incline_bottom":1}},{"x":8,"y":13,"level":5,"kind":16,"terrain":{"incline_high_top":1,"incline_bottom":1}},{"x":9,"y":13,"level":0,"kind":19,"terrain":{"water":1}},{"x":10,"y":13,"level":6,"kind":3,"terrain":{"incline_high_top":1}},{"x":11,"y":13,"level":6,"kind":3,"terrain":{"incline_high_top":1}},{"x":12,"y":13,"level":6,"kind":0},{"x":13,"y":13,"level":6,"kind":0},{"x":14,"y":13,"level":6,"kind":0},{"x":15,"y":13,"level":6,"kind":0},{"x":5,"y":14,"level":0,"kind":4,"terrain":{"woods":1,"foliage_elev":2}},{"x":6,"y":14,"level":0,"kind":2,"terrain":{"incline_high_bottom":1}},{"x":7,"y":14,"level":0,"kind":2,"terrain":{"incline_high_bottom":1}},{"x":8,"y":14,"level":0,"kind":2,"terrain":{"incline_high_bottom":1}},{"x":9,"y":14,"level":0,"kind":19,"terrain":{"water":1}},{"x":10,"y":14,"level":0,"kind":2,"terrain":{"incline_high_bottom":1}},{"x":11,"y":14,"level":6,"kind":3,"terrain":{"incline_high_top":1}},{"x":12,"y":14,"level":6,"kind":0},{"x":13,"y":14,"level":6,"kind":4,"terrain":{"woods":1,"foliage_elev":2}},{"x":14,"y":14,"level":6,"kind":0},{"x":15,"y":14,"level":6,"kind":0},{"x":6,"y":15,"level":0,"kind":4,"terrain":{"woods":1,"foliage_elev":2}},{"x":7,"y":15,"level":0,"kind":19,"terrain":{"water":1}},{"x":8,"y":15,"level":0,"kind":19,"terrain":{"water":1}},{"x":10,"y":15,"level":0,"kind":2,"terrain":{"incline_high_bottom":1}},{"x":11,"y":15,"level":0,"kind":2,"terrain":{"incline_high_bottom":1}},{"x":12,"y":15,"level":6,"kind":3,"terrain":{"incline_high_top":1}},{"x":13,"y":15,"level":6,"kind":3,"terrain":{"incline_high_top":1}},{"x":14,"y":15,"level":6,"kind":0},{"x":15,"y":15,"level":6,"kind":3,"terrain":{"incline_high_top":1}},{"x":7,"y":16,"level":0,"kind":19,"terrain":{"water":1}},{"x":12,"y":16,"level":0,"kind":2,"terrain":{"incline_high_bottom":1}},{"x":13,"y":16,"level":0,"kind":2,"terrain":{"incline_high_bottom":1}},{"x":14,"y":16,"level":6,"kind":3,"terrain":{"incline_high_top":1}},{"x":15,"y":16,"level":0,"kind":2,"terrain":{"incline_high_bottom":1}}],"kinds":23,"board":"mountain","scenario":"pathfind-mountain.mms","unit":"Phoenix Hawk PXH-1","specimen":"Phoenix_Hawk_PXH-1","walkMp":6,"runMp":9,"jumpMp":6} {"type":"unit","board":"mountain","specimen":"Manticore_Heavy_Tank","unit":{"id":3,"name":"Manticore Heavy Tank","ownerId":0,"team":1,"friendly":true,"x":-1,"y":-1,"facing":0,"secondaryFacing":0,"twists":[0,1,2,3,4,5],"elevation":0,"terrain":{"mp":[0,0,0,0,1,1,0,0,1,0,0,0,2,0,0,2,0,1,1,0,0,0,0],"barred":[false,false,false,false,false,false,false,false,false,false,false,false,true,false,false,true,false,false,false,true,false,false,false],"stands":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-1,0,0,0]},"weight":60.0,"bv":993,"bvOriginal":993,"walkMp":4,"runMp":6,"jumpMp":0,"jumpMpTerrain":0,"movementMode":"TRACKED","maxElevationChange":1,"armor":176,"armorMax":176,"internal":30,"internalMax":30,"heat":0,"heatCapacity":999,"heatBuildup":0,"explodableAmmo":true,"locations":[{"name":"FR","armor":42,"armorMax":42,"internal":6,"internalMax":6,"weapons":1,"weaponDamage":5.0},{"name":"RS","armor":33,"armorMax":33,"internal":6,"internalMax":6},{"name":"LS","armor":33,"armorMax":33,"internal":6,"internalMax":6},{"name":"RR","armor":26,"armorMax":26,"internal":6,"internalMax":6},{"name":"TU","armor":42,"armorMax":42,"internal":6,"internalMax":6,"weapons":3,"weaponDamage":24.0}],"prone":false,"mpUsed":0,"shutDown":false,"immobile":false,"stuck":false,"canUnstickByJumping":false,"elevationValid":true,"destroyed":false,"crippled":false,"done":false,"gunnery":4,"piloting":5,"role":"BRAWLER","forcePath":[],"weapons":[{"id":0,"name":"Medium Laser","location":"FR","heat":3,"short":3,"medium":6,"long":9,"minimum":0,"avgDamageShort":5.0,"avgDamageMedium":5.0,"avgDamageLong":5.0,"rackSize":0,"damagePerPacket":5.0,"usable":true},{"id":1,"name":"LRM 10","location":"TU","heat":4,"short":7,"medium":14,"long":21,"minimum":6,"avgDamageShort":6.0,"avgDamageMedium":6.0,"avgDamageLong":6.0,"rackSize":10,"damagePerPacket":1.0,"usable":true,"ammo":{"type":"LRM","munition":"LRM 10","damagePerShot":1,"rackSize":10,"shotsLeft":12}},{"id":2,"name":"SRM 6","location":"TU","heat":4,"short":3,"medium":6,"long":9,"minimum":0,"avgDamageShort":8.0,"avgDamageMedium":8.0,"avgDamageLong":8.0,"rackSize":6,"damagePerPacket":2.0,"usable":true,"ammo":{"type":"SRM","munition":"SRM 6","damagePerShot":2,"rackSize":6,"shotsLeft":15}},{"id":3,"name":"PPC","location":"TU","heat":10,"short":6,"medium":12,"long":18,"minimum":3,"avgDamageShort":10.0,"avgDamageMedium":10.0,"avgDamageLong":10.0,"rackSize":0,"damagePerPacket":10.0,"usable":true}]}} {"type":"unit","board":"mountain","specimen":"Phoenix_Hawk_PXH-1","unit":{"id":2,"name":"Phoenix Hawk PXH-1","ownerId":0,"team":1,"friendly":true,"x":-1,"y":-1,"facing":0,"secondaryFacing":0,"twists":[0,1,5],"elevation":0,"terrain":{"mp":[0,0,0,0,1,1,0,0,1,0,0,0,2,0,0,2,0,1,1,0,0,0,0],"barred":[false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false,false],"stands":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,-1,0,0,0]},"weight":45.0,"bv":1041,"bvOriginal":1041,"walkMp":6,"runMp":9,"jumpMp":6,"jumpMpTerrain":6,"movementMode":"BIPED","maxElevationChange":2,"armor":128,"armorMax":128,"internal":75,"internalMax":75,"heat":0,"heatCapacity":10,"heatBuildup":0,"explodableAmmo":true,"locations":[{"name":"HD","armor":6,"armorMax":6,"internal":3,"internalMax":3,"cockpit":true},{"name":"CT","armor":23,"armorMax":23,"rearArmor":5,"rearArmorMax":5,"internal":14,"internalMax":14,"ammo":true,"ammoCased":false,"engine":true,"gyro":true},{"name":"RT","armor":18,"armorMax":18,"rearArmor":4,"rearArmorMax":4,"internal":11,"internalMax":11},{"name":"LT","armor":18,"armorMax":18,"rearArmor":4,"rearArmorMax":4,"internal":11,"internalMax":11},{"name":"RA","armor":10,"armorMax":10,"internal":7,"internalMax":7,"weapons":3,"weaponDamage":15.0,"actuators":4},{"name":"LA","armor":10,"armorMax":10,"internal":7,"internalMax":7,"weapons":2,"weaponDamage":7.0,"actuators":4},{"name":"RL","armor":15,"armorMax":15,"internal":11,"internalMax":11,"actuators":4},{"name":"LL","armor":15,"armorMax":15,"internal":11,"internalMax":11,"actuators":4}],"prone":false,"mpUsed":0,"shutDown":false,"immobile":false,"stuck":false,"canUnstickByJumping":false,"elevationValid":true,"destroyed":false,"crippled":false,"done":false,"gunnery":4,"piloting":5,"role":"SKIRMISHER","forcePath":[],"weapons":[{"id":2,"name":"Medium Laser","location":"LA","heat":3,"short":3,"medium":6,"long":9,"minimum":0,"avgDamageShort":5.0,"avgDamageMedium":5.0,"avgDamageLong":5.0,"rackSize":0,"damagePerPacket":5.0,"usable":true},{"id":3,"name":"Machine Gun","location":"LA","heat":0,"short":1,"medium":2,"long":3,"minimum":0,"avgDamageShort":2.0,"avgDamageMedium":2.0,"avgDamageLong":2.0,"rackSize":0,"damagePerPacket":2.0,"usable":true,"ammo":{"type":"MG","munition":"MG Ammo","damagePerShot":1,"rackSize":2,"shotsLeft":200}},{"id":4,"name":"Large Laser","location":"RA","heat":8,"short":5,"medium":10,"long":15,"minimum":0,"avgDamageShort":8.0,"avgDamageMedium":8.0,"avgDamageLong":8.0,"rackSize":0,"damagePerPacket":8.0,"usable":true},{"id":5,"name":"Medium Laser","location":"RA","heat":3,"short":3,"medium":6,"long":9,"minimum":0,"avgDamageShort":5.0,"avgDamageMedium":5.0,"avgDamageLong":5.0,"rackSize":0,"damagePerPacket":5.0,"usable":true},{"id":6,"name":"Machine Gun","location":"RA","heat":0,"short":1,"medium":2,"long":3,"minimum":0,"avgDamageShort":2.0,"avgDamageMedium":2.0,"avgDamageLong":2.0,"rackSize":0,"damagePerPacket":2.0,"usable":true,"ammo":{"type":"MG","munition":"MG Ammo","damagePerShot":1,"rackSize":2,"shotsLeft":200}}]}} diff --git a/crates/sds-core/tests/corpus/pathfind_routes.jsonl b/crates/sds-core/tests/corpus/pathfind_routes.jsonl index 63cd1d7..52005dd 100644 --- a/crates/sds-core/tests/corpus/pathfind_routes.jsonl +++ b/crates/sds-core/tests/corpus/pathfind_routes.jsonl @@ -52,6 +52,7 @@ {"board":"forest","specimen":"Pegasus_Scout_Hover_Tank_(Missile)","x":6,"y":5,"facing":0,"mp":4,"mode":"ground","prone":false,"mpUsed":0,"routes":13,"digest":"4662a939fa7f982e","illegal":[]} {"board":"forest","specimen":"Pegasus_Scout_Hover_Tank_(Missile)","x":6,"y":5,"facing":0,"mp":6,"mode":"ground","prone":false,"mpUsed":0,"routes":17,"digest":"1eac04c6dc9f96a7","illegal":[]} {"board":"forest","specimen":"Elemental_Battle_Armor_[Laser](Sqd5)","x":4,"y":8,"facing":0,"mp":4,"mode":"ground","prone":false,"mpUsed":0,"routes":12,"digest":"b7e82dff319762db","illegal":[]} +{"board":"forest","specimen":"Pegasus_Scout_Hover_Tank_(Missile)","x":11,"y":12,"facing":3,"mp":6,"mode":"ground","prone":false,"mpUsed":0,"routes":18,"digest":"35fe4fa43fad5753","illegal":[]} {"board":"mountain","specimen":"Phoenix_Hawk_PXH-1","x":2,"y":6,"facing":0,"mp":4,"mode":"ground","prone":false,"mpUsed":0,"routes":26,"digest":"bb70f414440f0d11","illegal":[]} {"board":"mountain","specimen":"Phoenix_Hawk_PXH-1","x":2,"y":6,"facing":0,"mp":6,"mode":"ground","prone":false,"mpUsed":0,"routes":61,"digest":"52642e93bb5da48c","illegal":[]} {"board":"mountain","specimen":"Phoenix_Hawk_PXH-1","x":2,"y":6,"facing":0,"mp":9,"mode":"ground","prone":false,"mpUsed":0,"routes":152,"digest":"81b7f836274dec17","illegal":[]} diff --git a/crates/sds-core/tests/pathfind_corpus.rs b/crates/sds-core/tests/pathfind_corpus.rs index 8bfe4c6..9bca378 100644 --- a/crates/sds-core/tests/pathfind_corpus.rs +++ b/crates/sds-core/tests/pathfind_corpus.rs @@ -152,9 +152,9 @@ fn the_corpus_covers_the_cases_worth_testing() { ); assert_eq!( cases.len(), - 205, + 206, "four boards, three starts, two facings, three ground budgets, two jump \ - ones and three from the floor, three carrying spent points, and ten \ + ones and three from the floor, three carrying spent points, and eleven \ about a mover that is not a Mek" ); assert_eq!( @@ -202,10 +202,15 @@ fn the_corpus_covers_the_cases_worth_testing() { boards["forest"].jump_mp ); + // The sweep is the Mek's: three starts, both column parities, both facings + // and every budget, on each of four boards. A specimen has one start and + // one facing of its own - what it is there to say is what MegaMek permits + // a different movement mode, not more geometry - so counting its cases + // here would read as a sweep nobody recorded. for board in ["open", "forest", "water", "mountain"] { let starts: BTreeSet<(i32, i32)> = cases .iter() - .filter(|case| case.board == board) + .filter(|case| case.board == board && case.specimen == PATHFIND_MEK) .map(|case| (case.x, case.y)) .collect(); assert_eq!(starts.len(), 3, "{board}: three starts"); @@ -214,7 +219,7 @@ fn the_corpus_covers_the_cases_worth_testing() { let facings: BTreeSet = cases .iter() - .filter(|case| case.board == board) + .filter(|case| case.board == board && case.specimen == PATHFIND_MEK) .map(|case| case.facing) .collect(); assert_eq!(facings, BTreeSet::from([0, 1])); @@ -222,7 +227,9 @@ fn the_corpus_covers_the_cases_worth_testing() { let budgets = |mode: &str| -> BTreeSet { cases .iter() - .filter(|case| case.board == board && case.mode == mode) + .filter(|case| { + case.board == board && case.mode == mode && case.specimen == PATHFIND_MEK + }) .map(|case| case.mp) .collect() }; diff --git a/crates/sds-core/tests/pathfind_differential.rs b/crates/sds-core/tests/pathfind_differential.rs index d78641f..cccbf0d 100644 --- a/crates/sds-core/tests/pathfind_differential.rs +++ b/crates/sds-core/tests/pathfind_differential.rs @@ -354,7 +354,7 @@ fn our_reachable_set_is_megameks() { compared += 1; } - assert_eq!(compared, 202, "every case compared"); + assert_eq!(compared, 203, "every case compared"); assert_eq!(afoot, 3, "and the infantry cases recorded but not compared"); assert!(states > 0); @@ -500,7 +500,7 @@ fn hexes_moved_is_the_most_at_the_same_price() { // it lists that we reach *more cheaply* are skipped above and counted in // `our_reachable_set_is_megameks`, which is where a price difference // belongs. - assert_eq!(compared, 37296, "every comparable state compared"); + assert_eq!(compared, 37314, "every comparable state compared"); // Where the two agree on the price and disagree on the ground covered, and // ours is always the one covering more - the assertion above is the half // that matters, because more hexes is a better target movement modifier @@ -898,8 +898,8 @@ fn every_route_we_emit_is_one_megamek_accepts() { mispriced, 44, "routes a mover is offered that MegaMek prices above the budget" ); - assert_eq!(verdicts.len(), 205, "every case has a verdict"); - assert_eq!(checked, 37621, "every route checked"); + assert_eq!(verdicts.len(), 206, "every case has a verdict"); + assert_eq!(checked, 37639, "every route checked"); } /// MegaMek's answer for one case's routes. diff --git a/plan/candidates.md b/plan/candidates.md index 272567c..606b243 100644 --- a/plan/candidates.md +++ b/plan/candidates.md @@ -2727,6 +2727,24 @@ Two things the recording turned up beyond the pricing above: know about; the Rust side is given no occupancy at all. The recorder parks them, which is what makes the two questions the same question. +### The seam with the backward step + +Backing up and this gate were built in different worktrees against different +bases, and "it is a step, so it must be gated" is the assumption that is false +exactly at a seam. Checked rather than reasoned about, and it was half true: a +backward step is priced by `MoveBoard::step_mp` like a forward one, so the gate +was already on it - but the elevation rule beside it was its own, and it read +the hexes' floors. A hovercraft backing off the shore of a level-1 lake reads as +flat ground on the floors and as a level change to MegaMek, which is the +over-reach direction. It reads `Footing::height` now, the same number the +forward step climbs by. + +Two things hold the seam closed: `a_barred_hex_is_refused_behind_the_mover_as_well` +in `pathfind.rs`, and a corpus case at `forest 11,12 f3` - a hovercraft on clear +level-1 ground with light woods directly behind it at the same level, so nothing +but the gate can refuse the step. MegaMek reaches 18 states from there and the +woods hex is not one of them. + ### Not yet - [ ] The two pricing rules above, once the rules questions are answered. Each diff --git a/sds/match.py b/sds/match.py index 78508a0..9ff3554 100644 --- a/sds/match.py +++ b/sds/match.py @@ -608,6 +608,12 @@ PATHFIND_MOVER_CASES = ( ("water", "Elemental_Battle_Armor_[Laser](Sqd5)", 12, 12, 0, 4), ("water", "Foot_Platoon_(Rifle_Light)", 12, 12, 0, 4), ("water", "Manticore_Heavy_Tank", 12, 12, 0, 6), + # The seam between the gate and the backward step. A hovercraft on clear + # level-1 ground with light woods directly behind it, at the same level, so + # the elevation rule beside the backward step cannot be what refuses it: + # only the gate can. Backing up and the gate were built in different + # worktrees, and this is the case that says they met. + ("forest", "Pegasus_Scout_Hover_Tank_(Missile)", 11, 12, 3, 6), ("open", "Pegasus_Scout_Hover_Tank_(Missile)", 8, 8, 0, 6), ("mountain", "Manticore_Heavy_Tank", 2, 6, 0, 6), ) -- 2.54.0