aboutsummaryrefslogtreecommitdiffziptar.gz
path: root/src/Godosa.Core.Tests/Runs/RouteLegsTests.cs
diff options
context:
space:
mode:
authorgodosa <godosa@godosa.eu>2026-10-06 23:39:36 +0200
committergodosa <godosa@godosa.eu>2026-10-06 23:39:36 +0200
commit39066900773e7857faf2d02e7ed51b71d219d97d (patch)
treeffab8e4ddd971626776c3b1f4ce1da2c186c2187 /src/Godosa.Core.Tests/Runs/RouteLegsTests.cs
downloadgodosa-engine-39066900773e7857faf2d02e7ed51b71d219d97d.tar.gz
godosa-engine-39066900773e7857faf2d02e7ed51b71d219d97d.zip
godosa-engine: initial public history
Diffstat (limited to 'src/Godosa.Core.Tests/Runs/RouteLegsTests.cs')
-rw-r--r--src/Godosa.Core.Tests/Runs/RouteLegsTests.cs116
1 files changed, 116 insertions, 0 deletions
diff --git a/src/Godosa.Core.Tests/Runs/RouteLegsTests.cs b/src/Godosa.Core.Tests/Runs/RouteLegsTests.cs
new file mode 100644
index 0000000..31295ab
--- /dev/null
+++ b/src/Godosa.Core.Tests/Runs/RouteLegsTests.cs
@@ -0,0 +1,116 @@
+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<long, long, bool> Grid(params string[] rows) => (x, y) => rows[y][(int)x] is '#' or 's';
+ private static Func<long, long, bool> 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)!);
+ }
+}