5dplomacy/MultiversalDiplomacy/Adjudicate/MovementPhaseAdjudicator.cs

263 lines
13 KiB
C#

using MultiversalDiplomacy.Model;
using MultiversalDiplomacy.Orders;
namespace MultiversalDiplomacy.Adjudicate;
/// <summary>
/// Adjudicator for the movement phase.
/// </summary>
public 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.Cast<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;
}
}