1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
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)!);
}
}
|