5dplomacy/MultiversalDiplomacy/Adjudicate/PathFinder.cs

160 lines
7.0 KiB
C#
Raw Normal View History

using MultiversalDiplomacy.Model;
using MultiversalDiplomacy.Orders;
namespace MultiversalDiplomacy.Adjudicate;
/// <summary>
/// Helper class encapsulating the convoy pathfindind code.
/// </summary>
public static class PathFinder
{
/// <summary>
/// Determines if a convoy path exists for a move in a convoy order.
/// </summary>
public static bool ConvoyPathExists(World world, ConvoyOrder order)
=> ConvoyPathExists(world, order.Target, order.Location, order.Season);
/// <summary>
/// Determines if a convoy path exists for a move order.
/// </summary>
public static bool ConvoyPathExists(World world, MoveOrder order)
2024-08-15 05:28:56 +00:00
=> ConvoyPathExists(world, order.Unit, order.Location, world.Seasons[order.Season]);
private static bool ConvoyPathExists(
World world,
Unit movingUnit,
Location unitLocation,
Season unitSeason)
{
// A convoy path exists between two locations if both are land locations in provinces that
// also have coasts, and between those coasts there is a path of adjacent sea provinces
// (not coastal) that are occupied by fleets. The move order is valid even if the fleets
// belong to another power or were not given convoy orders; it will simply fail.
IDictionary<(string location, Season season), Unit> fleets = world.Units
.Where(unit => unit.Type == UnitType.Fleet)
2024-08-14 15:20:04 +00:00
.ToDictionary(unit => (unit.Location, unit.Season));
// Verify that the origin is a coastal province.
if (world.Map.GetLocation(movingUnit).Type != LocationType.Land) return false;
IEnumerable<Location> originCoasts = world.Map.GetLocation(movingUnit).Province.Locations
.Where(location => location.Type == LocationType.Water);
if (!originCoasts.Any()) return false;
// Verify that the destination is a coastal province.
if (unitLocation.Type != LocationType.Land) return false;
IEnumerable<Location> destCoasts = unitLocation.Province.Locations
.Where(location => location.Type == LocationType.Water);
if (!destCoasts.Any()) return false;
// Seed the to-visit set with the origin coasts. Coastal locations will be filtered out of
// locations added to the to-visit set, but the logic will still work with these as
// starting points.
Queue<(Location location, Season season)> toVisit = new(
originCoasts.Select(location => (location, unitSeason)));
HashSet<(Location, Season)> visited = new();
// Begin pathfinding.
while (toVisit.Any())
{
// Visit the next point in the queue.
(Location currentLocation, Season currentSeason) = toVisit.Dequeue();
visited.Add((currentLocation, currentSeason));
2024-08-12 21:12:49 +00:00
var adjacents = GetAdjacentPoints(world, currentLocation, currentSeason);
foreach ((Location adjLocation, Season adjSeason) in adjacents)
{
// If the destination is adjacent, then a path exists.
if (destCoasts.Contains(adjLocation) && unitSeason == adjSeason) return true;
// If not, add this location to the to-visit set if it isn't a coast, has a fleet,
// and hasn't already been visited.
if (!adjLocation.Province.Locations.Any(l => l.Type == LocationType.Land)
&& fleets.ContainsKey((adjLocation.Designation, adjSeason))
&& !visited.Contains((adjLocation, adjSeason)))
{
toVisit.Enqueue((adjLocation, adjSeason));
}
}
}
// If the destination was never reached, then no path exists.
return false;
}
2024-08-12 21:12:49 +00:00
private static List<(Location, Season)> GetAdjacentPoints(World world, Location location, Season season)
{
2024-08-12 21:12:49 +00:00
List<(Location, Season)> adjacentPoints = [];
List<Location> adjacentLocations = location.Adjacents.ToList();
2024-08-12 21:12:49 +00:00
List<Season> adjacentSeasons = GetAdjacentSeasons(world, season).ToList();
foreach (Location adjacentLocation in adjacentLocations)
{
adjacentPoints.Add((adjacentLocation, season));
}
foreach (Season adjacentSeason in adjacentSeasons)
{
adjacentPoints.Add((location, adjacentSeason));
}
foreach (Location adjacentLocation in adjacentLocations)
{
foreach (Season adjacentSeason in adjacentSeasons)
{
adjacentPoints.Add((adjacentLocation, adjacentSeason));
}
}
return adjacentPoints;
}
2024-08-12 21:12:49 +00:00
/// <summary>
/// Returns all seasons that are adjacent to a season.
/// </summary>
public static IEnumerable<Season> GetAdjacentSeasons(World world, Season season)
{
List<Season> adjacents = [];
// The immediate past and all immediate futures are adjacent.
2024-08-15 05:03:56 +00:00
if (season.Past != null) adjacents.Add(world.Seasons[season.Past]);
2024-08-12 22:25:23 +00:00
adjacents.AddRange(world.GetFutures(season));
2024-08-12 21:12:49 +00:00
// Find all adjacent timelines by finding all timelines that branched off of this season's
// timeline, i.e. all futures of this season's past that have different timelines. Also
// include any timelines that branched off of the timeline this timeline branched off from.
List<Season> adjacentTimelineRoots = [];
2024-08-12 21:12:49 +00:00
Season? current;
for (current = season;
2024-08-15 05:03:56 +00:00
current?.Past != null && world.Seasons[current.Past].Timeline == current.Timeline;
current = world.Seasons[current.Past])
2024-08-12 21:12:49 +00:00
{
adjacentTimelineRoots.AddRange(
2024-08-12 22:25:23 +00:00
world.GetFutures(current).Where(s => s.Timeline != current.Timeline));
2024-08-12 21:12:49 +00:00
}
// At the end of the for loop, if this season is part of the first timeline, then current
// is the root season (current.past == null); if this season is in a branched timeline,
// then current is the branch timeline's root season (current.past.timeline !=
// current.timeline). There are co-branches if this season is in a branched timeline, since
// the first timeline by definition cannot have co-branches.
2024-08-15 05:03:56 +00:00
if (current?.Past != null && world.Seasons[current.Past] is Season past)
2024-08-12 21:12:49 +00:00
{
2024-08-12 22:25:23 +00:00
IEnumerable<Season> cobranchRoots = world
.GetFutures(past)
.Where(s => s.Timeline != current.Timeline && s.Timeline != past.Timeline);
2024-08-12 21:12:49 +00:00
adjacentTimelineRoots.AddRange(cobranchRoots);
}
// Walk up all alternate timelines to find seasons within one turn of this season.
foreach (Season timelineRoot in adjacentTimelineRoots)
{
for (Season? branchSeason = timelineRoot;
branchSeason != null && branchSeason.Turn <= season.Turn + 1;
2024-08-12 22:25:23 +00:00
branchSeason = world.GetFutures(branchSeason)
2024-08-12 21:12:49 +00:00
.FirstOrDefault(s => s!.Timeline == branchSeason.Timeline, null))
{
if (branchSeason.Turn >= season.Turn - 1) adjacents.Add(branchSeason);
}
}
return adjacents;
}
}