diff --git a/MultiversalDiplomacy/Adjudicate/AdjudicatorHelpers.cs b/MultiversalDiplomacy/Adjudicate/AdjudicatorHelpers.cs
new file mode 100644
index 0000000..e613539
--- /dev/null
+++ b/MultiversalDiplomacy/Adjudicate/AdjudicatorHelpers.cs
@@ -0,0 +1,93 @@
+using MultiversalDiplomacy.Orders;
+
+namespace MultiversalDiplomacy.Adjudicate;
+
+///
+/// Helper class for common operations shared between adjudicators.
+///
+internal static class AdjudicatorHelpers
+{
+ ///
+ /// Invalidate all orders that do not match a predicate.
+ ///
+ /// A predicate that invalid orders will fail to match.
+ /// The reason to be given for order invalidation.
+ /// The set of orders to check.
+ /// The list of orders that passed the predicate.
+ ///
+ /// A list of order validation results. Orders invalidated by the predicate will be appended
+ /// to this list.
+ ///
+ public static void InvalidateIfNotMatching(
+ Func predicate,
+ ValidationReason reason,
+ ref List orders,
+ ref List invalidOrders)
+ where OrderType : Order
+ {
+ ILookup results = orders.ToLookup(predicate);
+ invalidOrders = invalidOrders
+ .Concat(results[false].Select(order => order.Invalidate(reason)))
+ .ToList();
+ orders = results[true].ToList();
+ }
+
+ ///
+ /// Invalidate all orders that are not of an allowed order type.
+ ///
+ /// The set of orders to check.
+ /// A list of types that are allowed.
+ /// The list of orders of allowed types.
+ ///
+ /// A list of order validation results. Orders of invalid types will be appended to this list.
+ ///
+ public static void InvalidateWrongTypes(
+ List validOrderTypes,
+ ref List orders,
+ ref List invalidOrders)
+ {
+ List nonOrderTypes = validOrderTypes
+ .Where(t => !t.IsSubclassOf(typeof(Order)))
+ .ToList();
+ if (nonOrderTypes.Any())
+ {
+ throw new ArgumentException($"Unknown order type: {nonOrderTypes.Select(t => t.FullName).First()}");
+ }
+
+ InvalidateIfNotMatching(
+ order => !validOrderTypes.Contains(order.GetType()),
+ ValidationReason.InvalidOrderTypeForPhase,
+ ref orders,
+ ref invalidOrders);
+ }
+
+ ///
+ /// Invalidate all orders for units not owned by the ordering power.
+ ///
+ /// The set of orders to check.
+ /// The list of orders with valid targets.
+ ///
+ /// A list of order validation results. Orders by the wrong powers will be appended to this list.
+ ///
+ public static void InvalidateWrongPower(
+ List orders,
+ ref List validOrders,
+ ref List invalidOrders)
+ {
+ InvalidateIfNotMatching(
+ order => order switch {
+ ConvoyOrder convoy => convoy.Power == convoy.Unit.Power,
+ DisbandOrder disband => disband.Power == disband.Unit.Power,
+ HoldOrder hold => hold.Power == hold.Unit.Power,
+ MoveOrder move => move.Power == move.Unit.Power,
+ RetreatOrder retreat => retreat.Power == retreat.Unit.Power,
+ SupportHoldOrder support => support.Power == support.Unit.Power,
+ SupportMoveOrder support => support.Power == support.Unit.Power,
+ // Any order not given to a unit by definition cannot be given to a unit of the wrong power
+ _ => true,
+ },
+ ValidationReason.InvalidUnitForPower,
+ ref orders,
+ ref invalidOrders);
+ }
+}
diff --git a/MultiversalDiplomacy/Adjudicate/IPhaseAdjudicator.cs b/MultiversalDiplomacy/Adjudicate/IPhaseAdjudicator.cs
index 3bdfbf9..e14fb8a 100644
--- a/MultiversalDiplomacy/Adjudicate/IPhaseAdjudicator.cs
+++ b/MultiversalDiplomacy/Adjudicate/IPhaseAdjudicator.cs
@@ -1,4 +1,5 @@
using MultiversalDiplomacy.Orders;
+using MultiversalDiplomacy.Model;
namespace MultiversalDiplomacy.Adjudicate;
@@ -12,11 +13,12 @@ public interface IPhaseAdjudicator
/// which should be rejected before adjudication. Adjudication should be performed on
/// all orders in the output for which is true.
///
+ /// The global game state.
/// Orders to validate for adjudication.
///
/// A list of order validation results. Note that this list may be longer than the input
/// list if illegal orders were replaced with hold orders, as there will be an invalid
/// result for the illegal order and a valid result for the replacement order.
///
- public IEnumerable ValidateOrders(IEnumerable orders);
+ public List ValidateOrders(World world, List orders);
}
diff --git a/MultiversalDiplomacy/Adjudicate/MovementPhaseAdjudicator.cs b/MultiversalDiplomacy/Adjudicate/MovementPhaseAdjudicator.cs
new file mode 100644
index 0000000..9011f70
--- /dev/null
+++ b/MultiversalDiplomacy/Adjudicate/MovementPhaseAdjudicator.cs
@@ -0,0 +1,262 @@
+using MultiversalDiplomacy.Model;
+using MultiversalDiplomacy.Orders;
+
+namespace MultiversalDiplomacy.Adjudicate;
+
+///
+/// Adjudicator for the movement phase.
+///
+internal class MovementPhaseAdjudicator : IPhaseAdjudicator
+{
+ public List ValidateOrders(World world, List orders)
+ {
+ // The basic workflow of this function will be to look for invalid orders, remove these
+ // from the working set of orders, and then perform one final check for duplicate orders
+ // at the end. This is to comply with DATC 4.D.3's requirement that a unit that receives
+ // a legal and an illegal order follows the legal order rather than holding.
+ List validationResults = new List();
+
+ // Invalidate any orders that aren't a legal type for this phase and remove them from the
+ // working set.
+ AdjudicatorHelpers.InvalidateWrongTypes(
+ new List
+ {
+ typeof(HoldOrder),
+ typeof(MoveOrder),
+ typeof(ConvoyOrder),
+ typeof(SupportHoldOrder),
+ typeof(SupportMoveOrder)
+ },
+ ref orders,
+ ref validationResults);
+
+ // Invalidate any orders by a power that were given to another power's units and remove
+ // them from the working set.
+ AdjudicatorHelpers.InvalidateWrongPower(orders, ref orders, ref validationResults);
+
+ // Since all the order types in this phase are UnitOrders, downcast to get the Unit.
+ List unitOrders = orders.OfType().ToList();
+
+ // Invalidate any order given to a unit in the past.
+ AdjudicatorHelpers.InvalidateIfNotMatching(
+ order => !order.Unit.Season.Futures.Any(),
+ ValidationReason.IneligibleForOrder,
+ ref unitOrders,
+ ref validationResults);
+
+ /***************
+ * HOLD ORDERS *
+ ***************/
+ // Hold orders are always valid.
+ List holdOrders = unitOrders.OfType().ToList();
+
+ /***************
+ * MOVE ORDERS *
+ ***************/
+ // Move order validity is far more complicated, due to multiversal time travel and convoys.
+ List moveOrders = unitOrders.OfType().ToList();
+
+ // Trivial check: armies cannot move to water and fleets cannot move to land.
+ AdjudicatorHelpers.InvalidateIfNotMatching(
+ order => (order.Unit.Type == UnitType.Army && order.Location.Type == LocationType.Land)
+ || (order.Unit.Type == UnitType.Fleet && order.Location.Type == LocationType.Water),
+ ValidationReason.IllegalDestinationType,
+ ref moveOrders,
+ ref validationResults);
+
+ // Trivial check: a unit cannot move to where it already is.
+ AdjudicatorHelpers.InvalidateIfNotMatching(
+ order => !(order.Location == order.Unit.Location && order.Season == order.Unit.Season),
+ ValidationReason.DestinationMatchesOrigin,
+ ref moveOrders,
+ ref validationResults);
+
+ // If the unit is moving to a valid destination that isn't where it already is, then the
+ // move order is valid if there is a path from the origin to the destination. In the easy
+ // case, the destination is directly adjacent to the origin with respect to the map, the
+ // turn, and the timeline. These moves are valid. Any other move must be checked for
+ // potential validity as a convoy move.
+ ILookup moveOrdersByAdjacency = moveOrders
+ .ToLookup(order =>
+ // Map adjacency
+ order.Unit.Location.Adjacents.Contains(order.Location)
+ // Turn adjacency
+ && Math.Abs(order.Unit.Season.Turn - order.Season.Turn) <= 1
+ // Timeline adjacency
+ && order.Unit.Season.InAdjacentTimeline(order.Season));
+ List adjacentMoveOrders = moveOrdersByAdjacency[true].ToList();
+ List nonAdjacentMoveOrders = moveOrdersByAdjacency[false].ToList();
+
+ // Only armies can move to non-adjacent destinations, since fleets cannot be convoyed.
+ AdjudicatorHelpers.InvalidateIfNotMatching(
+ order => order.Unit.Type == UnitType.Army,
+ ValidationReason.UnreachableDestination,
+ ref nonAdjacentMoveOrders,
+ ref validationResults);
+
+ // For all remaining convoyable move orders, check if there is a path between the origin
+ // and the destination.
+ AdjudicatorHelpers.InvalidateIfNotMatching(
+ order => PathFinder.ConvoyPathExists(world, order),
+ ValidationReason.UnreachableDestination,
+ ref nonAdjacentMoveOrders,
+ ref validationResults);
+
+ /*****************
+ * CONVOY ORDERS *
+ *****************/
+ // A convoy order must be to a fleet and target an army.
+ List convoyOrders = unitOrders.OfType().ToList();
+ AdjudicatorHelpers.InvalidateIfNotMatching(
+ order => order.Unit.Type == UnitType.Fleet && order.Target.Type == UnitType.Army,
+ ValidationReason.InvalidOrderTypeForUnit,
+ ref convoyOrders,
+ ref validationResults);
+
+ // A convoy for an illegal move is illegal, which means all the move validity checks
+ // now need to be repeated for the convoy target.
+
+ // Trivial check: cannot convoy to non-coastal province.
+ AdjudicatorHelpers.InvalidateIfNotMatching(
+ order => order.Location.Type == LocationType.Land
+ && order.Location.Province.Locations.Any(loc => loc.Type == LocationType.Water),
+ ValidationReason.IllegalDestinationType,
+ ref convoyOrders,
+ ref validationResults);
+
+ // Trivial check: cannot convoy a unit to its own location
+ AdjudicatorHelpers.InvalidateIfNotMatching(
+ order => !(
+ order.Location == order.Target.Location
+ && order.Season == order.Target.Season),
+ ValidationReason.DestinationMatchesOrigin,
+ ref convoyOrders,
+ ref validationResults);
+
+ // By definition, the move enabled by a convoy order is a convoyable move order, so it
+ // should be checked for a convoy path.
+ AdjudicatorHelpers.InvalidateIfNotMatching(
+ order => PathFinder.ConvoyPathExists(world, order),
+ ValidationReason.UnreachableDestination,
+ ref convoyOrders,
+ ref validationResults);
+
+ /***********************
+ * SUPPORT-HOLD ORDERS *
+ ***********************/
+ // Support-hold orders are typically valid if the supporting unit can move to the
+ // destination.
+ List supportHoldOrders = unitOrders.OfType().ToList();
+
+ // Support-hold orders are invalid if the unit supports itself.
+ AdjudicatorHelpers.InvalidateIfNotMatching(
+ order => order.Unit != order.Target,
+ ValidationReason.NoSelfSupport,
+ ref supportHoldOrders,
+ ref validationResults);
+
+ // Support-hold orders are invalid if the supporting unit couldn't move to the destination
+ // without a convoy. This is the same direct adjacency calculation as above, except that
+ // the supporting unit only needs to be able to move to the *province*, even if the target
+ // is holding in a location within that province that the supporting unit couldn't move to.
+ // The reverse is not true: a unit cannot support another province if that province is only
+ // reachable from a different location in the unit's province.
+ AdjudicatorHelpers.InvalidateIfNotMatching(
+ order =>
+ // Map adjacency with respect to province
+ order.Unit.Location.Adjacents.Any(
+ adjLocation => adjLocation.Province == order.Target.Location.Province)
+ // Turn adjacency
+ && Math.Abs(order.Unit.Season.Turn - order.Target.Season.Turn) <= 1
+ // Timeline adjacency
+ && order.Unit.Season.InAdjacentTimeline(order.Target.Season),
+ ValidationReason.UnreachableSupport,
+ ref supportHoldOrders,
+ ref validationResults);
+
+ /***********************
+ * SUPPORT-MOVE ORDERS *
+ ***********************/
+ // Support-move orders, like support-hold orders, are typically valid if the supporting
+ // unit can move to the destination.
+ List supportMoveOrders = unitOrders.OfType().ToList();
+
+ // Support-move orders are invalid if the unit supports a move to any location in its own
+ // province.
+ AdjudicatorHelpers.InvalidateIfNotMatching(
+ order => order.Unit.Location.Province != order.Location.Province,
+ ValidationReason.NoSupportMoveAgainstSelf,
+ ref supportMoveOrders,
+ ref validationResults);
+
+ // Support-move orders, like support-hold orders, are valid only if the supporting unit
+ // can reach the destination *province* of the move, even if the destination *location*
+ // is unreachable (DATC 6.B.4). The same is not true of reachability from another location
+ // in the supporting unit's province (DATC 6.B.5).
+ AdjudicatorHelpers.InvalidateIfNotMatching(
+ order =>
+ // Map adjacency with respect to province
+ order.Unit.Location.Adjacents.Any(
+ adjLocation => adjLocation.Province == order.Location.Province)
+ // Turn adjacency
+ && Math.Abs(order.Unit.Season.Turn - order.Season.Turn) <= 1
+ // Timeline adjacency
+ && order.Unit.Season.InAdjacentTimeline(order.Season),
+ ValidationReason.UnreachableSupport,
+ ref supportMoveOrders,
+ ref validationResults);
+
+ // One more edge case: support-move orders by a fleet for an army are illegal if that army
+ // requires a convoy and the supporting fleet is a part of the only convoy path (DATC
+ // 6.D.31).
+ // TODO: support convoy path check with "as if this fleet were missing"
+
+ // Collect the valid orders together
+ unitOrders =
+ holdOrders.Cast()
+ .Concat(adjacentMoveOrders)
+ .Concat(nonAdjacentMoveOrders)
+ .Concat(convoyOrders)
+ .Concat(supportHoldOrders)
+ .Concat(supportMoveOrders)
+ .ToList();
+
+ // DATC 4.D.3 prefers that multiple orders to the same unit in the same order set be
+ // replaced by a hold order. Since this function only takes one combined list of orders,
+ // it is assumed that the caller has combined the order sets from all powers in a way that
+ // is compliant with DATC 4.D.1-2. If there are still duplicate orders in the input, they
+ // were not addressed by 4.D.1-2 and will be handled according to 4.D.3, i.e. replaced with
+ // hold orders. Note that this happens last, after all other invalidations have been
+ // applied in order to comply with what 4.D.3 specifies about illegal orders.
+ List duplicateOrderedUnits = unitOrders
+ .GroupBy(o => o.Unit)
+ .Where(orderGroup => orderGroup.Count() > 1)
+ .Select(orderGroup => orderGroup.Key)
+ .ToList();
+ List duplicateOrders = unitOrders
+ .Where(o => duplicateOrderedUnits.Contains(o.Unit))
+ .ToList();
+ List validOrders = unitOrders.Except(duplicateOrders).ToList();
+ validationResults = validationResults
+ .Concat(duplicateOrders.Select(o => o.Invalidate(ValidationReason.DuplicateOrders)))
+ .Concat(validOrders.Select(o => o.Validate(ValidationReason.Valid)))
+ .ToList();
+
+ // Finally, add implicit hold orders for units without legal orders.
+ List allOrderableUnits = world.Units
+ .Where(unit => !unit.Season.Futures.Any())
+ .ToList();
+ HashSet orderedUnits = validOrders.Select(order => order.Unit).ToHashSet();
+ List unorderedUnits = allOrderableUnits
+ .Where(unit => !orderedUnits.Contains(unit))
+ .ToList();
+ List implicitHolds = unorderedUnits
+ .Select(unit => new HoldOrder(unit.Power, unit))
+ .ToList();
+ validationResults = validationResults
+ .Concat(implicitHolds.Select(o => o.Validate(ValidationReason.Valid)))
+ .ToList();
+
+ return validationResults;
+ }
+}
diff --git a/MultiversalDiplomacy/Adjudicate/PathFinder.cs b/MultiversalDiplomacy/Adjudicate/PathFinder.cs
new file mode 100644
index 0000000..c9a8375
--- /dev/null
+++ b/MultiversalDiplomacy/Adjudicate/PathFinder.cs
@@ -0,0 +1,108 @@
+using MultiversalDiplomacy.Model;
+using MultiversalDiplomacy.Orders;
+
+namespace MultiversalDiplomacy.Adjudicate;
+
+///
+/// Helper class encapsulating the convoy pathfindind code.
+///
+public static class PathFinder
+{
+ ///
+ /// Determines if a convoy path exists for a move in a convoy order.
+ ///
+ public static bool ConvoyPathExists(World world, ConvoyOrder order)
+ => ConvoyPathExists(world, order.Target, order.Location, order.Season);
+
+ ///
+ /// Determines if a convoy path exists for a move order.
+ ///
+ public static bool ConvoyPathExists(World world, MoveOrder order)
+ => ConvoyPathExists(world, order.Unit, order.Location, 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<(Location location, Season season), Unit> fleets = world.Units
+ .Where(unit => unit.Type == UnitType.Fleet)
+ .ToDictionary(unit => (unit.Location, unit.Season));
+
+ // Verify that the origin is a coastal province.
+ if (movingUnit.Location.Type != LocationType.Land) return false;
+ IEnumerable originCoasts = movingUnit.Location.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 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));
+
+ var adjacents = GetAdjacentPoints(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, adjSeason))
+ && !visited.Contains((adjLocation, adjSeason)))
+ {
+ toVisit.Enqueue((adjLocation, adjSeason));
+ }
+ }
+ }
+
+ // If the destination was never reached, then no path exists.
+ return false;
+ }
+
+ private static List<(Location, Season)> GetAdjacentPoints(Location location, Season season)
+ {
+ List<(Location, Season)> adjacentPoints = new();
+ List adjacentLocations = location.Adjacents.ToList();
+ List adjacentSeasons = season.GetAdjacentSeasons().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;
+ }
+}
\ No newline at end of file
diff --git a/MultiversalDiplomacy/Adjudicate/ValidationReason.cs b/MultiversalDiplomacy/Adjudicate/ValidationReason.cs
index 50b65d3..fd757c3 100644
--- a/MultiversalDiplomacy/Adjudicate/ValidationReason.cs
+++ b/MultiversalDiplomacy/Adjudicate/ValidationReason.cs
@@ -18,7 +18,52 @@ public enum ValidationReason
IllegalOrderReplacedWithHold = 2,
///
- /// Another order was submitted that replaced this order.
+ /// The power that issued this order does not control the ordered unit.
///
- SupersededByLaterOrder = 3,
+ InvalidUnitForPower = 3,
+
+ ///
+ /// The ordered unit received conflicting orders and all orders were invalidated.
+ ///
+ DuplicateOrders = 4,
+
+ ///
+ /// An army was ordered into the sea or a fleet was ordered onto the land.
+ ///
+ IllegalDestinationType = 5,
+
+ ///
+ /// A unit was ordered to move to where it already is.
+ ///
+ DestinationMatchesOrigin = 6,
+
+ ///
+ /// The destination of the move is not reachable from the origin.
+ ///
+ UnreachableDestination = 7,
+
+ ///
+ /// The order type is not valid for the unit.
+ ///
+ InvalidOrderTypeForUnit = 8,
+
+ ///
+ /// A unit was ordered to support itself.
+ ///
+ NoSelfSupport = 9,
+
+ ///
+ /// A unit was ordered to support a location it could not reach.
+ ///
+ UnreachableSupport = 10,
+
+ ///
+ /// A unit was ordered to support a move to its own province.
+ ///
+ NoSupportMoveAgainstSelf = 11,
+
+ ///
+ /// A unit was ordered that is not currently eligible to receive orders.
+ ///
+ IneligibleForOrder = 12,
}
\ No newline at end of file
diff --git a/MultiversalDiplomacy/Model/Season.cs b/MultiversalDiplomacy/Model/Season.cs
index 50721bc..2c2132a 100644
--- a/MultiversalDiplomacy/Model/Season.cs
+++ b/MultiversalDiplomacy/Model/Season.cs
@@ -44,12 +44,24 @@ public class Season
///
private TimelineFactory Timelines { get; }
+ ///
+ /// Future seasons created directly from this season.
+ ///
+ public IEnumerable Futures => this.FutureList;
+ private List FutureList { get; }
+
private Season(Season? past, int turn, int timeline, TimelineFactory factory)
{
this.Past = past;
this.Turn = turn;
this.Timeline = timeline;
this.Timelines = factory;
+ this.FutureList = new();
+
+ if (past != null)
+ {
+ past.FutureList.Add(this);
+ }
}
///
@@ -89,6 +101,9 @@ public class Season
///
/// Returns whether this season is in an adjacent timeline to another season.
+ /// Seasons are considered to be in adjacent timelines if they are in the same timeline,
+ /// one is in a timeline that branched from the other's timeline, or both are in timelines
+ /// that branched from the same point.
///
public bool InAdjacentTimeline(Season other)
{
@@ -106,4 +121,55 @@ public class Season
// Both branched off of the same point
|| thisRoot.Past == otherRoot.Past;
}
+
+ ///
+ /// Returns all seasons that are adjacent to this season.
+ ///
+ public IEnumerable GetAdjacentSeasons()
+ {
+ List adjacents = new();
+
+ // The immediate past and all immediate futures are adjacent.
+ if (this.Past != null) adjacents.Add(this.Past);
+ adjacents.AddRange(this.FutureList);
+
+ // 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 adjacentTimelineRoots = new();
+ Season? current;
+ for (current = this;
+ current?.Past?.Timeline != null && current.Past.Timeline == current.Timeline;
+ current = current.Past)
+ {
+ adjacentTimelineRoots.AddRange(
+ current.FutureList.Where(s => s.Timeline != current.Timeline));
+ }
+
+ // 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.
+ if (current?.Past != null)
+ {
+ IEnumerable cobranchRoots = current.Past.FutureList
+ .Where(s => s.Timeline != current.Timeline && s.Timeline != current.Past.Timeline);
+ 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 <= this.Turn + 1;
+ branchSeason = branchSeason.FutureList
+ .FirstOrDefault(s => s?.Timeline == branchSeason.Timeline, null))
+ {
+ if (branchSeason.Turn >= this.Turn - 1) adjacents.Add(branchSeason);
+ }
+ }
+
+ return adjacents;
+ }
}
diff --git a/MultiversalDiplomacy/Model/World.cs b/MultiversalDiplomacy/Model/World.cs
new file mode 100644
index 0000000..54cb12a
--- /dev/null
+++ b/MultiversalDiplomacy/Model/World.cs
@@ -0,0 +1,39 @@
+namespace MultiversalDiplomacy.Model;
+
+///
+/// The global game state.
+///
+public class World
+{
+ ///
+ /// The game map.
+ ///
+ public IEnumerable Provinces { get; }
+
+ ///
+ /// The game powers.
+ ///
+ public IEnumerable Powers { get; }
+
+ ///
+ /// The state of the multiverse.
+ ///
+ public IEnumerable Seasons { get; }
+
+ ///
+ /// All units in the multiverse.
+ ///
+ public IEnumerable Units { get; }
+
+ public World(
+ IEnumerable provinces,
+ IEnumerable powers,
+ IEnumerable seasons,
+ IEnumerable units)
+ {
+ this.Provinces = provinces;
+ this.Powers = powers;
+ this.Seasons = seasons;
+ this.Units = units;
+ }
+}
\ No newline at end of file