Add movement phase order validation logic
This commit is contained in:
parent
3ad0dbc086
commit
8a7e90b949
|
@ -0,0 +1,93 @@
|
||||||
|
using MultiversalDiplomacy.Orders;
|
||||||
|
|
||||||
|
namespace MultiversalDiplomacy.Adjudicate;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper class for common operations shared between adjudicators.
|
||||||
|
/// </summary>
|
||||||
|
internal static class AdjudicatorHelpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Invalidate all orders that do not match a predicate.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="predicate">A predicate that invalid orders will fail to match.</param>
|
||||||
|
/// <param name="reason">The reason to be given for order invalidation.</param>
|
||||||
|
/// <param name="orders">The set of orders to check.</param>
|
||||||
|
/// <param name="validOrders">The list of orders that passed the predicate.</param>
|
||||||
|
/// <param name="invalidOrders">
|
||||||
|
/// A list of order validation results. Orders invalidated by the predicate will be appended
|
||||||
|
/// to this list.
|
||||||
|
/// </param>
|
||||||
|
public static void InvalidateIfNotMatching<OrderType>(
|
||||||
|
Func<OrderType, bool> predicate,
|
||||||
|
ValidationReason reason,
|
||||||
|
ref List<OrderType> orders,
|
||||||
|
ref List<OrderValidation> invalidOrders)
|
||||||
|
where OrderType : Order
|
||||||
|
{
|
||||||
|
ILookup<bool, OrderType> results = orders.ToLookup<OrderType, bool>(predicate);
|
||||||
|
invalidOrders = invalidOrders
|
||||||
|
.Concat(results[false].Select(order => order.Invalidate(reason)))
|
||||||
|
.ToList();
|
||||||
|
orders = results[true].ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invalidate all orders that are not of an allowed order type.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="orders">The set of orders to check.</param>
|
||||||
|
/// <param name="validOrderTypes">A list of <see cref="Order"/> types that are allowed.</param>
|
||||||
|
/// <param name="validOrders">The list of orders of allowed types.</param>
|
||||||
|
/// <param name="invalidOrders">
|
||||||
|
/// A list of order validation results. Orders of invalid types will be appended to this list.
|
||||||
|
/// </param>
|
||||||
|
public static void InvalidateWrongTypes(
|
||||||
|
List<Type> validOrderTypes,
|
||||||
|
ref List<Order> orders,
|
||||||
|
ref List<OrderValidation> invalidOrders)
|
||||||
|
{
|
||||||
|
List<Type> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invalidate all orders for units not owned by the ordering power.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="orders">The set of orders to check.</param>
|
||||||
|
/// <param name="validOrders">The list of orders with valid targets.</param>
|
||||||
|
/// <param name="invalidOrders">
|
||||||
|
/// A list of order validation results. Orders by the wrong powers will be appended to this list.
|
||||||
|
/// </param>
|
||||||
|
public static void InvalidateWrongPower(
|
||||||
|
List<Order> orders,
|
||||||
|
ref List<Order> validOrders,
|
||||||
|
ref List<OrderValidation> 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
using MultiversalDiplomacy.Orders;
|
using MultiversalDiplomacy.Orders;
|
||||||
|
using MultiversalDiplomacy.Model;
|
||||||
|
|
||||||
namespace MultiversalDiplomacy.Adjudicate;
|
namespace MultiversalDiplomacy.Adjudicate;
|
||||||
|
|
||||||
|
@ -12,11 +13,12 @@ public interface IPhaseAdjudicator
|
||||||
/// which should be rejected before adjudication. Adjudication should be performed on
|
/// which should be rejected before adjudication. Adjudication should be performed on
|
||||||
/// all orders in the output for which <see cref="OrderValidation.Valid"/> is true.
|
/// all orders in the output for which <see cref="OrderValidation.Valid"/> is true.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="world">The global game state.</param>
|
||||||
/// <param name="orders">Orders to validate for adjudication.</param>
|
/// <param name="orders">Orders to validate for adjudication.</param>
|
||||||
/// <returns>
|
/// <returns>
|
||||||
/// A list of order validation results. Note that this list may be longer than the input
|
/// 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
|
/// 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.
|
/// result for the illegal order and a valid result for the replacement order.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
public IEnumerable<OrderValidation> ValidateOrders(IEnumerable<Order> orders);
|
public List<OrderValidation> ValidateOrders(World world, List<Order> orders);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,262 @@
|
||||||
|
using MultiversalDiplomacy.Model;
|
||||||
|
using MultiversalDiplomacy.Orders;
|
||||||
|
|
||||||
|
namespace MultiversalDiplomacy.Adjudicate;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adjudicator for the movement phase.
|
||||||
|
/// </summary>
|
||||||
|
internal class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||||
|
{
|
||||||
|
public List<OrderValidation> ValidateOrders(World world, List<Order> 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<OrderValidation> validationResults = new List<OrderValidation>();
|
||||||
|
|
||||||
|
// Invalidate any orders that aren't a legal type for this phase and remove them from the
|
||||||
|
// working set.
|
||||||
|
AdjudicatorHelpers.InvalidateWrongTypes(
|
||||||
|
new List<Type>
|
||||||
|
{
|
||||||
|
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<UnitOrder> unitOrders = orders.OfType<UnitOrder>().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<HoldOrder> holdOrders = unitOrders.OfType<HoldOrder>().ToList();
|
||||||
|
|
||||||
|
/***************
|
||||||
|
* MOVE ORDERS *
|
||||||
|
***************/
|
||||||
|
// Move order validity is far more complicated, due to multiversal time travel and convoys.
|
||||||
|
List<MoveOrder> moveOrders = unitOrders.OfType<MoveOrder>().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<bool, MoveOrder> 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<MoveOrder> adjacentMoveOrders = moveOrdersByAdjacency[true].ToList();
|
||||||
|
List<MoveOrder> 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<ConvoyOrder> convoyOrders = unitOrders.OfType<ConvoyOrder>().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<SupportHoldOrder> supportHoldOrders = unitOrders.OfType<SupportHoldOrder>().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<SupportMoveOrder> supportMoveOrders = unitOrders.OfType<SupportMoveOrder>().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<UnitOrder>()
|
||||||
|
.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<Unit> duplicateOrderedUnits = unitOrders
|
||||||
|
.GroupBy(o => o.Unit)
|
||||||
|
.Where(orderGroup => orderGroup.Count() > 1)
|
||||||
|
.Select(orderGroup => orderGroup.Key)
|
||||||
|
.ToList();
|
||||||
|
List<UnitOrder> duplicateOrders = unitOrders
|
||||||
|
.Where(o => duplicateOrderedUnits.Contains(o.Unit))
|
||||||
|
.ToList();
|
||||||
|
List<UnitOrder> 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<Unit> allOrderableUnits = world.Units
|
||||||
|
.Where(unit => !unit.Season.Futures.Any())
|
||||||
|
.ToList();
|
||||||
|
HashSet<Unit> orderedUnits = validOrders.Select(order => order.Unit).ToHashSet();
|
||||||
|
List<Unit> unorderedUnits = allOrderableUnits
|
||||||
|
.Where(unit => !orderedUnits.Contains(unit))
|
||||||
|
.ToList();
|
||||||
|
List<HoldOrder> implicitHolds = unorderedUnits
|
||||||
|
.Select(unit => new HoldOrder(unit.Power, unit))
|
||||||
|
.ToList();
|
||||||
|
validationResults = validationResults
|
||||||
|
.Concat(implicitHolds.Select(o => o.Validate(ValidationReason.Valid)))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return validationResults;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
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)
|
||||||
|
=> 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<Location> 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<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));
|
||||||
|
|
||||||
|
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<Location> adjacentLocations = location.Adjacents.ToList();
|
||||||
|
List<Season> 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,7 +18,52 @@ public enum ValidationReason
|
||||||
IllegalOrderReplacedWithHold = 2,
|
IllegalOrderReplacedWithHold = 2,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Another order was submitted that replaced this order.
|
/// The power that issued this order does not control the ordered unit.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
SupersededByLaterOrder = 3,
|
InvalidUnitForPower = 3,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ordered unit received conflicting orders and all orders were invalidated.
|
||||||
|
/// </summary>
|
||||||
|
DuplicateOrders = 4,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An army was ordered into the sea or a fleet was ordered onto the land.
|
||||||
|
/// </summary>
|
||||||
|
IllegalDestinationType = 5,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A unit was ordered to move to where it already is.
|
||||||
|
/// </summary>
|
||||||
|
DestinationMatchesOrigin = 6,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The destination of the move is not reachable from the origin.
|
||||||
|
/// </summary>
|
||||||
|
UnreachableDestination = 7,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The order type is not valid for the unit.
|
||||||
|
/// </summary>
|
||||||
|
InvalidOrderTypeForUnit = 8,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A unit was ordered to support itself.
|
||||||
|
/// </summary>
|
||||||
|
NoSelfSupport = 9,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A unit was ordered to support a location it could not reach.
|
||||||
|
/// </summary>
|
||||||
|
UnreachableSupport = 10,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A unit was ordered to support a move to its own province.
|
||||||
|
/// </summary>
|
||||||
|
NoSupportMoveAgainstSelf = 11,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A unit was ordered that is not currently eligible to receive orders.
|
||||||
|
/// </summary>
|
||||||
|
IneligibleForOrder = 12,
|
||||||
}
|
}
|
|
@ -44,12 +44,24 @@ public class Season
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private TimelineFactory Timelines { get; }
|
private TimelineFactory Timelines { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Future seasons created directly from this season.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<Season> Futures => this.FutureList;
|
||||||
|
private List<Season> FutureList { get; }
|
||||||
|
|
||||||
private Season(Season? past, int turn, int timeline, TimelineFactory factory)
|
private Season(Season? past, int turn, int timeline, TimelineFactory factory)
|
||||||
{
|
{
|
||||||
this.Past = past;
|
this.Past = past;
|
||||||
this.Turn = turn;
|
this.Turn = turn;
|
||||||
this.Timeline = timeline;
|
this.Timeline = timeline;
|
||||||
this.Timelines = factory;
|
this.Timelines = factory;
|
||||||
|
this.FutureList = new();
|
||||||
|
|
||||||
|
if (past != null)
|
||||||
|
{
|
||||||
|
past.FutureList.Add(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -89,6 +101,9 @@ public class Season
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns whether this season is in an adjacent timeline to another 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool InAdjacentTimeline(Season other)
|
public bool InAdjacentTimeline(Season other)
|
||||||
{
|
{
|
||||||
|
@ -106,4 +121,55 @@ public class Season
|
||||||
// Both branched off of the same point
|
// Both branched off of the same point
|
||||||
|| thisRoot.Past == otherRoot.Past;
|
|| thisRoot.Past == otherRoot.Past;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns all seasons that are adjacent to this season.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<Season> GetAdjacentSeasons()
|
||||||
|
{
|
||||||
|
List<Season> 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<Season> 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<Season> 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
namespace MultiversalDiplomacy.Model;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The global game state.
|
||||||
|
/// </summary>
|
||||||
|
public class World
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The game map.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<Province> Provinces { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The game powers.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<Power> Powers { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The state of the multiverse.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<Season> Seasons { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// All units in the multiverse.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<Unit> Units { get; }
|
||||||
|
|
||||||
|
public World(
|
||||||
|
IEnumerable<Province> provinces,
|
||||||
|
IEnumerable<Power> powers,
|
||||||
|
IEnumerable<Season> seasons,
|
||||||
|
IEnumerable<Unit> units)
|
||||||
|
{
|
||||||
|
this.Provinces = provinces;
|
||||||
|
this.Powers = powers;
|
||||||
|
this.Seasons = seasons;
|
||||||
|
this.Units = units;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue