namespace Godosa.Core.Runs;
/// Route planning for run scripts (docs/notes/run-scripts-routing.md): a way over a grid (8 neighbours) cut into
/// the legs the game's own router accepts, the script seals that close a way, and portal hops between walkable areas. Pure;
/// the game passes its grid, its router's acceptance test and its portals.
public static class RouteLegs
{
/// Breadth-first over [0,width)×[0,height), 8 neighbours, until : each reached
/// cell → the one it was reached from (start → itself).
public static Dictionary<(long X, long Y), (long X, long Y)> Reach((long X, long Y) start, (long X, long Y) goal, long width, long height,
Func blocked)
{
var from = new Dictionary<(long X, long Y), (long X, long Y)> { [start] = start };
var queue = new Queue<(long X, long Y)>([start]);
while (queue.Count > 0 && !from.ContainsKey(goal))
{
var at = queue.Dequeue();
for (var dx = -1; dx <= 1; dx++)
for (var dy = -1; dy <= 1; dy++)
{
var next = (at.X + dx, at.Y + dy);
if (from.ContainsKey(next) || next.Item1 < 0 || next.Item2 < 0 || next.Item1 >= width || next.Item2 >= height
|| blocked(next.Item1, next.Item2))
continue;
from[next] = at;
queue.Enqueue(next);
}
}
return from;
}
/// The cells from (excluded) to (included); null when walled
/// off.
public static List<(long X, long Y)>? Path((long X, long Y) start, (long X, long Y) goal, long width, long height, Func blocked)
{
var from = Reach(start, goal, width, height, blocked);
return from.ContainsKey(goal) ? Trace(from, start, goal) : null;
}
/// Legs along : from each stop the farthest path cell (the
/// game's router takes the trip), until the goal; null when the router takes no further cell from a stop.
public static List<(long X, long Y)>? Legs((long X, long Y) start, IReadOnlyList<(long X, long Y)> path,
Func<(long X, long Y), (long X, long Y), bool> accepts) => Legs(start, path, accepts, out _);
///
/// The stop no leg leaves (refusals name it).
public static List<(long X, long Y)>? Legs((long X, long Y) start, IReadOnlyList<(long X, long Y)> path,
Func<(long X, long Y), (long X, long Y), bool> accepts, out (long X, long Y) stuck)
{
var legs = new List<(long X, long Y)>();
var (stop, index) = (start, 0);
while (index < path.Count)
{
var best = path.Count - 1;
while (best >= index && !accepts(stop, path[best]))
best--;
if (best < index)
{
stuck = stop;
return null;
}
(stop, index) = (path[best], best + 1);
legs.Add(stop);
}
stuck = default;
return legs;
}
/// The cells (blocked by the game's scripts, opened later) the way would cross once
/// open, start to goal, at most ; empty when walled off anyway or open already.
public static List<(long X, long Y)> Seals((long X, long Y) start, (long X, long Y) goal, long width, long height,
Func blocked, Func soft, int max = 8)
{
if (Path(start, goal, width, height, blocked) != null)
return [];
var way = Path(start, goal, width, height, (x, y) => blocked(x, y) && !soft(x, y));
return way == null ? [] : way.Where(c => soft(c.X, c.Y)).Take(max).ToList();
}
/// Portal hops: breadth-first over portal exits from ; the portals to take, in order, so
/// that (a walk inside one area) gets to ; empty when it walks there;
/// null when no chain does.
public static List? Hops(TPos start, TPos goal, IReadOnlyList portals, Func at,
Func exit, Func reaches) where TPos : notnull
{
var via = new Dictionary { [start] = (start, default) };
var queue = new Queue([start]);
while (queue.Count > 0)
{
var here = queue.Dequeue();
if (reaches(here, goal))
{
var hops = new List();
for (var p = here; !EqualityComparer.Default.Equals(p, start); p = via[p].From)
hops.Add(via[p].Portal!);
hops.Reverse();
return hops;
}
foreach (var portal in portals)
if (!via.ContainsKey(exit(portal)) && reaches(here, at(portal)))
{
via[exit(portal)] = (here, portal);
queue.Enqueue(exit(portal));
}
}
return null;
}
private static List<(long X, long Y)> Trace(Dictionary<(long X, long Y), (long X, long Y)> from, (long X, long Y) start, (long X, long Y) goal)
{
var path = new List<(long X, long Y)>();
for (var c = goal; c != start; c = from[c])
path.Add(c);
path.Reverse();
return path;
}
}