using Godosa.Core.Runs; using Xunit; namespace Godosa.Core.Tests.Runs; public class RouteLegsTests { // 5×5 grid, rows top to bottom; '#' blocked, 's' soft (sealed by the game's scripts, opened later). private static Func Grid(params string[] rows) => (x, y) => rows[y][(int)x] is '#' or 's'; private static Func Soft(params string[] rows) => (x, y) => rows[y][(int)x] == 's'; [Fact] public void Path_GoesRoundAWall_StartExcluded() { var blocked = Grid( ".#...", ".#...", ".#...", ".....", "....."); // From 0,0 the only way past the wall is under it: down the left column to row 3, then up-right diagonally. var path = RouteLegs.Path((0, 0), (2, 0), 5, 5, blocked); Assert.NotNull(path); Assert.Equal((2, 0), path![^1]); Assert.DoesNotContain((0L, 0L), path); Assert.Equal(6, path.Count); // 0,1 0,2 1,3 2,2 2,1 2,0 Assert.All(path, c => Assert.False(blocked(c.X, c.Y))); } [Fact] public void Path_NoWay_Null() { var blocked = Grid( "..#..", "..#..", "..#..", "..#..", "..#.."); Assert.Null(RouteLegs.Path((0, 0), (4, 0), 5, 5, blocked)); } // The game's router takes a leg only within 2 cells (Chebyshev): from each stop the farthest accepted path cell. [Fact] public void Legs_FarthestAcceptedStopEachTime() { var path = new List<(long X, long Y)> { (1, 0), (2, 0), (3, 0), (4, 0), (5, 0), (6, 0), (7, 0) }; var legs = RouteLegs.Legs((0, 0), path, (a, b) => Math.Max(Math.Abs(a.X - b.X), Math.Abs(a.Y - b.Y)) <= 2); Assert.Equal([(2L, 0L), (4L, 0L), (6L, 0L), (7L, 0L)], legs); } [Fact] public void Legs_RouterAcceptsNothing_NullWithTheStuckStop() { var path = new List<(long X, long Y)> { (1, 0), (2, 0) }; Assert.Null(RouteLegs.Legs((0, 0), path, (a, b) => b == (1, 0), out var stuck)); Assert.Equal((1, 0), stuck); } // Closed off only by a sealed (soft) cell: the seals a way would cross once open, start to goal. [Fact] public void Seals_ListedInOrder() { string[] rows = [ "..s..", "#####", ".....", ".....", ".....", ]; Assert.Null(RouteLegs.Path((0, 0), (4, 0), 5, 5, Grid(rows))); Assert.Equal([(2L, 0L)], RouteLegs.Seals((0, 0), (4, 0), 5, 5, Grid(rows), Soft(rows))); Assert.Empty(RouteLegs.Seals((0, 0), (4, 4), 5, 5, Grid(rows), Soft(rows))); // walled, not sealed } [Fact] public void Seals_CappedAndNoneWhenOpenAlready() { string[] twice = [ ".s.s.", "#####", ".....", ".....", ".....", ]; Assert.Equal([(1L, 0L), (3L, 0L)], RouteLegs.Seals((0, 0), (4, 0), 5, 5, Grid(twice), Soft(twice))); Assert.Equal([(1L, 0L)], RouteLegs.Seals((0, 0), (4, 0), 5, 5, Grid(twice), Soft(twice), max: 1)); // Open the long way round (row 2): a seal on the short way is no reason to wait. string[] detour = [ "..s..", "#.#.#", ".....", ".....", ".....", ]; Assert.Empty(RouteLegs.Seals((0, 0), (4, 0), 5, 5, Grid(detour), Soft(detour))); } // Hops: walk to a portal, click it, arrive at its exit; BFS over portal exits. [Fact] public void Hops_ThroughTwoPortals() { // Three rooms on a line, x 0–9 / 10–19 / 20–29 (walking stays inside a room); portal A in room 1 at 5 → 12, B in // room 2 at 18 → 25; a dead-end portal C in room 1 at 2 → 40. static int Room(long x) => (int)(x / 10); bool Reaches(long from, long to) => Room(from) == Room(to); var portals = new[] { ("A", 5L, 12L), ("B", 18L, 25L), ("C", 2L, 40L) }; var hops = RouteLegs.Hops(1L, 27L, portals, p => p.Item2, p => p.Item3, Reaches); Assert.NotNull(hops); Assert.Equal(["A", "B"], hops!.Select(p => p.Item1)); Assert.Null(RouteLegs.Hops(1L, 35L, portals, p => p.Item2, p => p.Item3, Reaches)); Assert.Empty(RouteLegs.Hops(1L, 7L, portals, p => p.Item2, p => p.Item3, Reaches)!); } }