From 8a7e90b949526aefec860a4da512b4c5519e4eb0 Mon Sep 17 00:00:00 2001 From: Jaculabilis Date: Sat, 12 Mar 2022 20:29:00 -0800 Subject: [PATCH] Add movement phase order validation logic --- .../Adjudicate/AdjudicatorHelpers.cs | 93 +++++++ .../Adjudicate/IPhaseAdjudicator.cs | 4 +- .../Adjudicate/MovementPhaseAdjudicator.cs | 262 ++++++++++++++++++ MultiversalDiplomacy/Adjudicate/PathFinder.cs | 108 ++++++++ .../Adjudicate/ValidationReason.cs | 49 +++- MultiversalDiplomacy/Model/Season.cs | 66 +++++ MultiversalDiplomacy/Model/World.cs | 39 +++ 7 files changed, 618 insertions(+), 3 deletions(-) create mode 100644 MultiversalDiplomacy/Adjudicate/AdjudicatorHelpers.cs create mode 100644 MultiversalDiplomacy/Adjudicate/MovementPhaseAdjudicator.cs create mode 100644 MultiversalDiplomacy/Adjudicate/PathFinder.cs create mode 100644 MultiversalDiplomacy/Model/World.cs 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