From 6b1b9dce105fd0ff9aa6a5c80c96bfb834c2c4f9 Mon Sep 17 00:00:00 2001 From: Jaculabilis Date: Sun, 27 Mar 2022 14:36:49 -0700 Subject: [PATCH] Refactor adjudication into separate adjudication and update steps This makes it easier to unit test adjudication decisions directly. --- .../{Decision => }/AdjudicationDecision.cs | 2 +- .../Adjudicate/Decision/MovementDecisions.cs | 113 +++++++++ .../Adjudicate/IPhaseAdjudicator.cs | 33 ++- .../Adjudicate/MovementPhaseAdjudicator.cs | 235 ++++++------------ .../Adjudicate/OrderAdjudication.cs | 40 --- MultiversalDiplomacyTests/DATC_A.cs | 16 +- .../MovementAdjudicatorTest.cs | 10 +- MultiversalDiplomacyTests/OrderReference.cs | 41 +++ MultiversalDiplomacyTests/TestAdjudicator.cs | 76 +++++- MultiversalDiplomacyTests/TestCaseBuilder.cs | 28 +++ .../TestCaseBuilderTest.cs | 67 ++++- 11 files changed, 422 insertions(+), 239 deletions(-) rename MultiversalDiplomacy/Adjudicate/{Decision => }/AdjudicationDecision.cs (89%) create mode 100644 MultiversalDiplomacy/Adjudicate/Decision/MovementDecisions.cs delete mode 100644 MultiversalDiplomacy/Adjudicate/OrderAdjudication.cs diff --git a/MultiversalDiplomacy/Adjudicate/Decision/AdjudicationDecision.cs b/MultiversalDiplomacy/Adjudicate/AdjudicationDecision.cs similarity index 89% rename from MultiversalDiplomacy/Adjudicate/Decision/AdjudicationDecision.cs rename to MultiversalDiplomacy/Adjudicate/AdjudicationDecision.cs index 2e365e1..15ea90d 100644 --- a/MultiversalDiplomacy/Adjudicate/Decision/AdjudicationDecision.cs +++ b/MultiversalDiplomacy/Adjudicate/AdjudicationDecision.cs @@ -1,4 +1,4 @@ -namespace MultiversalDiplomacy.Adjudicate.Decision; +namespace MultiversalDiplomacy.Adjudicate; /// /// Base class for adjudication decisions. The decision-based adjudication algorithm is based diff --git a/MultiversalDiplomacy/Adjudicate/Decision/MovementDecisions.cs b/MultiversalDiplomacy/Adjudicate/Decision/MovementDecisions.cs new file mode 100644 index 0000000..dc79957 --- /dev/null +++ b/MultiversalDiplomacy/Adjudicate/Decision/MovementDecisions.cs @@ -0,0 +1,113 @@ +using MultiversalDiplomacy.Model; +using MultiversalDiplomacy.Orders; + +namespace MultiversalDiplomacy.Adjudicate.Decision; + +public class MovementDecisions +{ + public Dictionary IsDislodged { get; } + public Dictionary HasPath { get; } + public Dictionary GivesSupport { get; } + public Dictionary HoldStrength { get; } + public Dictionary AttackStrength { get; } + public Dictionary DefendStrength { get; } + public Dictionary PreventStrength { get; } + public Dictionary DoesMove { get; } + + public IEnumerable Values => + this.IsDislodged.Values.Cast() + .Concat(this.HasPath.Values) + .Concat(this.GivesSupport.Values) + .Concat(this.HoldStrength.Values) + .Concat(this.AttackStrength.Values) + .Concat(this.DefendStrength.Values) + .Concat(this.PreventStrength.Values) + .Concat(this.DoesMove.Values); + + public MovementDecisions(List orders) + { + this.IsDislodged = new(); + this.HasPath = new(); + this.GivesSupport = new(); + this.HoldStrength = new(); + this.AttackStrength = new(); + this.DefendStrength = new(); + this.PreventStrength = new(); + this.DoesMove = new(); + + foreach (UnitOrder order in orders.Cast()) + { + // Create a dislodge decision for this unit. + List incoming = orders + .OfType() + .Where(move => move.Location.Province == order.Unit.Location.Province) + .ToList(); + this.IsDislodged[order.Unit] = new(order, incoming); + + // Ensure a hold strength decision exists. Overwrite any previous once, since it may + // have been created without an order by a previous move or support. + Province province = order.Unit.Location.Province; + this.HoldStrength[province] = new(province, order); + + if (order is MoveOrder move) + { + // Find supports corresponding to this move. + List supports = orders + .OfType() + .Where(support => support.IsSupportFor(move)) + .ToList(); + + // Determine if this move is a head-to-head battle. + MoveOrder? opposingMove = orders + .OfType() + .FirstOrDefault(other => other != null && other.IsOpposing(move), null); + + // Find competing moves. + List competing = orders + .OfType() + .Where(other => other.Location.Province == move.Location.Province) + .ToList(); + + // Create the move-related decisions. + this.HasPath[move] = new(move); + this.AttackStrength[move] = new(move, supports, opposingMove); + this.DefendStrength[move] = new(move, supports); + this.PreventStrength[move] = new(move, supports, opposingMove); + this.DoesMove[move] = new(move, opposingMove, competing); + + // Ensure a hold strength decision exists for the destination. + Province dest = move.Location.Province; + if (!this.HoldStrength.ContainsKey(dest)) + { + this.HoldStrength[dest] = new(dest); + } + } + else if (order is SupportOrder support) + { + // Create the support decision. + this.GivesSupport[support] = new(support, incoming); + + // Ensure a hold strength decision exists for the target's province. + Province target = support.Target.Location.Province; + if (!this.HoldStrength.ContainsKey(target)) + { + this.HoldStrength[target] = new(target); + } + + if (support is SupportHoldOrder supportHold) + { + this.HoldStrength[target].Supports.Add(supportHold); + } + else if (support is SupportMoveOrder supportMove) + { + // Ensure a hold strength decision exists for the target's destination. + Province dest = supportMove.Location.Province; + if (!this.HoldStrength.ContainsKey(dest)) + { + this.HoldStrength[dest] = new(dest); + } + } + } + } + } +} \ No newline at end of file diff --git a/MultiversalDiplomacy/Adjudicate/IPhaseAdjudicator.cs b/MultiversalDiplomacy/Adjudicate/IPhaseAdjudicator.cs index 5869f3d..5ab155a 100644 --- a/MultiversalDiplomacy/Adjudicate/IPhaseAdjudicator.cs +++ b/MultiversalDiplomacy/Adjudicate/IPhaseAdjudicator.cs @@ -23,11 +23,32 @@ public interface IPhaseAdjudicator public List ValidateOrders(World world, List orders); /// - /// Given a list of valid orders, adjudicate the success and failure of the orders. The world - /// will be updated with new seasons and unit positions and returned alongside the adjudication - /// results. + /// Given a list of valid orders, adjudicate the success and failure of the orders. The kinds + /// of adjudication decisions returned depends on the phase adjudicator. /// - public (List results, World updated) AdjudicateOrders( - World world, - List orders); + /// The global game state. + /// + /// Orders to adjudicate. The order list should contain only valid orders, as validated by + /// , and should contain exactly one order for every unit able to + /// be ordered. + /// + /// + /// A list of adjudication decicions. The decision types will be specific to the phase + /// adjudicator and should be comprehensible to that adjudicator's method. + /// + public List AdjudicateOrders(World world, List orders); + + /// + /// Given a list of adjudications, update the world according to the adjudication results. + /// + /// The global game state. + /// + /// The results of adjudication. Like , all objects to be updated + /// should have a relevant adjudication. The adjudication types will be specific to the phase + /// adjudicator. + /// + /// + /// A new copy of the world, updated according to the adjudication. + /// + public World UpdateWorld(World world, List decisions); } diff --git a/MultiversalDiplomacy/Adjudicate/MovementPhaseAdjudicator.cs b/MultiversalDiplomacy/Adjudicate/MovementPhaseAdjudicator.cs index 0799d0a..e8f43da 100644 --- a/MultiversalDiplomacy/Adjudicate/MovementPhaseAdjudicator.cs +++ b/MultiversalDiplomacy/Adjudicate/MovementPhaseAdjudicator.cs @@ -9,120 +9,6 @@ namespace MultiversalDiplomacy.Adjudicate; /// public class MovementPhaseAdjudicator : IPhaseAdjudicator { - private class Decisions - { - public Dictionary IsDislodged { get; } - public Dictionary HasPath { get; } - public Dictionary GivesSupport { get; } - public Dictionary HoldStrength { get; } - public Dictionary AttackStrength { get; } - public Dictionary DefendStrength { get; } - public Dictionary PreventStrength { get; } - public Dictionary DoesMove { get; } - - public List UnresolvedDecisions { get; } - - public Decisions(List orders) - { - this.IsDislodged = new(); - this.HasPath = new(); - this.GivesSupport = new(); - this.HoldStrength = new(); - this.AttackStrength = new(); - this.DefendStrength = new(); - this.PreventStrength = new(); - this.DoesMove = new(); - - foreach (UnitOrder order in orders.Cast()) - { - // Create a dislodge decision for this unit. - List incoming = orders - .OfType() - .Where(move => move.Location.Province == order.Unit.Location.Province) - .ToList(); - this.IsDislodged[order.Unit] = new(order, incoming); - - // Ensure a hold strength decision exists. - Province province = order.Unit.Location.Province; - if (!this.HoldStrength.ContainsKey(province)) - { - this.HoldStrength[province] = new(province, order); - } - - if (order is MoveOrder move) - { - // Find supports corresponding to this move. - List supports = orders - .OfType() - .Where(support => support.IsSupportFor(move)) - .ToList(); - - // Determine if this move is a head-to-head battle. - MoveOrder? opposingMove = orders - .OfType() - .FirstOrDefault(other => other != null && other.IsOpposing(move), null); - - // Find competing moves. - List competing = orders - .OfType() - .Where(other => other.Location.Province == move.Location.Province) - .ToList(); - - // Create the move-related decisions. - this.HasPath[move] = new(move); - this.AttackStrength[move] = new(move, supports, opposingMove); - this.DefendStrength[move] = new(move, supports); - this.PreventStrength[move] = new(move, supports, opposingMove); - this.DoesMove[move] = new(move, opposingMove, competing); - - // Ensure a hold strength decision exists for the destination. - Province dest = move.Location.Province; - if (!this.HoldStrength.ContainsKey(dest)) - { - this.HoldStrength[dest] = new(dest); - } - } - else if (order is SupportOrder support) - { - // Create the support decision. - this.GivesSupport[support] = new(support, incoming); - - // Ensure a hold strength decision exists for the target's province. - Province target = support.Target.Location.Province; - if (!this.HoldStrength.ContainsKey(target)) - { - this.HoldStrength[target] = new(target); - } - - if (support is SupportHoldOrder supportHold) - { - this.HoldStrength[target].Supports.Add(supportHold); - } - else if (support is SupportMoveOrder supportMove) - { - // Ensure a hold strength decision exists for the target's destination. - Province dest = supportMove.Location.Province; - if (!this.HoldStrength.ContainsKey(dest)) - { - this.HoldStrength[dest] = new(dest); - } - } - } - } - - this.UnresolvedDecisions = new List() - .Concat(this.IsDislodged.Values) - .Concat(this.HasPath.Values) - .Concat(this.GivesSupport.Values) - .Concat(this.HoldStrength.Values) - .Concat(this.AttackStrength.Values) - .Concat(this.DefendStrength.Values) - .Concat(this.PreventStrength.Values) - .Concat(this.DoesMove.Values) - .ToList(); - } - } - public static IPhaseAdjudicator Instance { get; } = new MovementPhaseAdjudicator(); public List ValidateOrders(World world, List orders) @@ -377,35 +263,38 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator return validationResults; } - public (List results, World updated) AdjudicateOrders( - World world, - List orders) + public List AdjudicateOrders(World world, List orders) { // Define all adjudication decisions to be made. - Decisions decisions = new Decisions(orders); + MovementDecisions decisions = new(orders); + + List unresolvedDecisions = decisions.Values.ToList(); // Adjudicate all decisions. bool progress = false; do { progress = false; - foreach (AdjudicationDecision decision in decisions.UnresolvedDecisions.ToList()) + foreach (AdjudicationDecision decision in unresolvedDecisions.ToList()) { progress |= ResolveDecision(decision, world, decisions); - if (decision.Resolved) decisions.UnresolvedDecisions.Remove(decision); + if (decision.Resolved) unresolvedDecisions.Remove(decision); } } while (progress); - if (decisions.UnresolvedDecisions.Any()) + if (unresolvedDecisions.Any()) { throw new ApplicationException("Some orders not resolved!"); } - List adjudications = new(); + return decisions.Values.ToList(); + } - // All orders other than move orders are hold orders with extra steps. - ILookup moveOrders = orders.ToLookup(order => order is MoveOrder); - List nonMoveOrders = moveOrders[false].ToList(); + public World UpdateWorld(World world, List decisions) + { + Dictionary moves = decisions + .OfType() + .ToDictionary(dm => dm.Order); // All moves to a particular season in a single phase result in the same future. Keep a // record of when a future season has been created. @@ -413,34 +302,35 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator List createdUnits = new(); List retreats = new(); - // For each move order with a successful does-move decision, ensure the future exists and - // progress the unit to the future. - foreach (MoveOrder move in moveOrders[true].Cast()) + // Successful move orders result in the unit moving to the destination and creating a new + // future, while unsuccessful move orders are processed the same way as non-move orders. + foreach (DoesMove doesMove in moves.Values) { - DoesMove doesMove = decisions.DoesMove[move]; if (doesMove.Outcome == true) { - if (!createdFutures.TryGetValue(move.Season, out Season? future)) + if (!createdFutures.TryGetValue(doesMove.Order.Season, out Season? future)) { - // A timeline doesn't fork unless it already has a continuation. - future = move.Season.Futures.Any() - ? move.Season.MakeNext() - : move.Season.MakeFork(); - createdFutures[move.Season] = future; + // A timeline that doesn't have a future yet simply continues. Otherwise, it forks. + future = !doesMove.Order.Season.Futures.Any() + ? doesMove.Order.Season.MakeNext() + : doesMove.Order.Season.MakeFork(); + createdFutures[doesMove.Order.Season] = future; } - createdUnits.Add(move.Unit.Next(move.Location, future)); + createdUnits.Add(doesMove.Order.Unit.Next(doesMove.Order.Location, future)); } - else - { - // If the move order failed, the moving unit will stay put, which puts it in the - // same bucket as the hold orders. - nonMoveOrders.Add(move); - } - adjudications.Add(new(move, doesMove.Outcome == true)); } - foreach (UnitOrder order in nonMoveOrders.Cast()) + // Process unsuccessful moves, all holds, and all supports. + foreach (IsDislodged isDislodged in decisions.OfType()) { + UnitOrder order = isDislodged.Order; + + // Skip the move orders that were processed above. + if (order is MoveOrder move && moves[move].Outcome == true) + { + continue; + } + if (!createdFutures.TryGetValue(order.Unit.Season, out Season? future)) { // Any unit given an order is, by definition, at the front of a timeline. @@ -449,7 +339,6 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator } // For each stationary unit that wasn't dislodged, continue it into the future. - IsDislodged isDislodged = decisions.IsDislodged[order.Unit]; if (isDislodged.Outcome == false) { createdUnits.Add(order.Unit.Next(order.Unit.Location, future)); @@ -464,15 +353,6 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator RetreatingUnit retreat = new(order.Unit, validRetreats); retreats.Add(retreat); } - - if (order is SupportOrder support) - { - adjudications.Add(new(support, decisions.GivesSupport[support].Outcome == true)); - } - else - { - adjudications.Add(new(order, isDislodged.Outcome == false)); - } } // TODO provide more structured information about order outcomes @@ -482,10 +362,13 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator .WithUnits(world.Units.Concat(createdUnits)) .WithRetreats(retreats); - return (adjudications, updated); + return updated; } - private bool ResolveDecision(AdjudicationDecision decision, World world, Decisions decisions) + private bool ResolveDecision( + AdjudicationDecision decision, + World world, + MovementDecisions decisions) => decision.Resolved ? false : decision switch { IsDislodged d => ResolveIsUnitDislodged(d, world, decisions), @@ -499,7 +382,10 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator _ => throw new NotSupportedException($"Unknown decision type: {decision.GetType()}") }; - private bool ResolveIsUnitDislodged(IsDislodged decision, World world, Decisions decisions) + private bool ResolveIsUnitDislodged( + IsDislodged decision, + World world, + MovementDecisions decisions) { bool progress = false; @@ -557,7 +443,10 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator return progress; } - private bool ResolveDoesMoveHavePath(HasPath decision, World world, Decisions decisions) + private bool ResolveDoesMoveHavePath( + HasPath decision, + World world, + MovementDecisions decisions) { bool progress= false; @@ -583,7 +472,10 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator throw new NotImplementedException(); // TODO } - private bool ResolveIsSupportGiven(GivesSupport decision, World world, Decisions decisions) + private bool ResolveIsSupportGiven( + GivesSupport decision, + World world, + MovementDecisions decisions) { bool progress = false; @@ -632,7 +524,10 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator return progress; } - private bool ResolveHoldStrength(HoldStrength decision, World world, Decisions decisions) + private bool ResolveHoldStrength( + HoldStrength decision, + World world, + MovementDecisions decisions) { bool progress = false; @@ -670,7 +565,10 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator } } - private bool ResolveAttackStrength(AttackStrength decision, World world, Decisions decisions) + private bool ResolveAttackStrength( + AttackStrength decision, + World world, + MovementDecisions decisions) { bool progress = false; @@ -768,7 +666,10 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator } } - private bool ResolveDefendStrength(DefendStrength decision, World world, Decisions decisions) + private bool ResolveDefendStrength( + DefendStrength decision, + World world, + MovementDecisions decisions) { bool progress = false; @@ -788,7 +689,10 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator return progress; } - private bool ResolvePreventStrength(PreventStrength decision, World world, Decisions decisions) + private bool ResolvePreventStrength( + PreventStrength decision, + World world, + MovementDecisions decisions) { bool progress = false; @@ -837,7 +741,10 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator return progress; } - private bool ResolveDoesUnitMove(DoesMove decision, World world, Decisions decisions) + private bool ResolveDoesUnitMove( + DoesMove decision, + World world, + MovementDecisions decisions) { bool progress = false; diff --git a/MultiversalDiplomacy/Adjudicate/OrderAdjudication.cs b/MultiversalDiplomacy/Adjudicate/OrderAdjudication.cs deleted file mode 100644 index c6ee08d..0000000 --- a/MultiversalDiplomacy/Adjudicate/OrderAdjudication.cs +++ /dev/null @@ -1,40 +0,0 @@ -using MultiversalDiplomacy.Orders; - -namespace MultiversalDiplomacy.Adjudicate; - -/// -/// Represents the result of adjudicating an order. -/// -public class OrderAdjudication -{ - /// - /// The order that was adjudicated. - /// - public Order Order { get; } - - /// - /// Whether the order succeeded or failed. - /// - public bool Success { get; } - - // /// - // /// The reason for the order's outcome. - // /// - // public string Reason { get; } - - public OrderAdjudication(Order order, bool success/*, string reason*/) - { - this.Order = order; - this.Success = success; - // this.Reason = reason; - } -} - -public static class OrderAdjudicationExtensions -{ - /// - /// Create an accepting this order. - /// - public static OrderAdjudication Succeed(this Order order) - => new OrderAdjudication(order, true); -} \ No newline at end of file diff --git a/MultiversalDiplomacyTests/DATC_A.cs b/MultiversalDiplomacyTests/DATC_A.cs index b7809b6..55dd429 100644 --- a/MultiversalDiplomacyTests/DATC_A.cs +++ b/MultiversalDiplomacyTests/DATC_A.cs @@ -17,7 +17,7 @@ public class DATC_A setup["England"] .Fleet("North Sea").MovesTo("Picardy").GetReference(out var order); - setup.ValidateOrders(new MovementPhaseAdjudicator()); + setup.ValidateOrders(MovementPhaseAdjudicator.Instance); Assert.That(order.Validation, Is.Invalid(ValidationReason.UnreachableDestination)); } @@ -57,7 +57,7 @@ public class DATC_A setup["Germany"] .Fleet("Kiel").MovesTo("Kiel").GetReference(out var order); - setup.ValidateOrders(new MovementPhaseAdjudicator()); + setup.ValidateOrders(MovementPhaseAdjudicator.Instance); Assert.That(order.Validation, Is.Invalid(ValidationReason.DestinationMatchesOrigin)); } @@ -75,7 +75,7 @@ public class DATC_A .Fleet("London").MovesTo("Yorkshire") .Army("Wales").Supports.Fleet("London").MoveTo("Yorkshire"); - setup.ValidateOrders(new MovementPhaseAdjudicator()); + setup.ValidateOrders(MovementPhaseAdjudicator.Instance); Assert.That(orderNth.Validation, Is.Invalid(ValidationReason.DestinationMatchesOrigin)); Assert.That(orderYor.Validation, Is.Invalid(ValidationReason.DestinationMatchesOrigin)); @@ -91,7 +91,7 @@ public class DATC_A ["Germany"] .Fleet("London", powerName: "England").MovesTo("North Sea").GetReference(out var order); - setup.ValidateOrders(new MovementPhaseAdjudicator()); + setup.ValidateOrders(MovementPhaseAdjudicator.Instance); Assert.That(order.Validation, Is.Invalid(ValidationReason.InvalidUnitForPower)); } @@ -105,7 +105,7 @@ public class DATC_A .Fleet("London").MovesTo("Belgium") .Fleet("North Sea").Convoys.Army("London").To("Belgium").GetReference(out var order); - setup.ValidateOrders(new MovementPhaseAdjudicator()); + setup.ValidateOrders(MovementPhaseAdjudicator.Instance); Assert.That(order.Validation, Is.Invalid(ValidationReason.InvalidOrderTypeForUnit)); } @@ -121,7 +121,7 @@ public class DATC_A ["Austria"] .Fleet("Trieste").Supports.Fleet("Trieste").Hold().GetReference(out var order); - setup.ValidateOrders(new MovementPhaseAdjudicator()); + setup.ValidateOrders(MovementPhaseAdjudicator.Instance); Assert.That(order.Validation, Is.Invalid(ValidationReason.NoSelfSupport)); @@ -136,7 +136,7 @@ public class DATC_A ["Italy"] .Fleet("Rome").MovesTo("Venice").GetReference(out var order); - setup.ValidateOrders(new MovementPhaseAdjudicator()); + setup.ValidateOrders(MovementPhaseAdjudicator.Instance); Assert.That(order.Validation, Is.Invalid(ValidationReason.UnreachableDestination)); } @@ -152,7 +152,7 @@ public class DATC_A .Army("Apulia").MovesTo("Venice") .Fleet("Rome").Supports.Army("Apulia").MoveTo("Venice").GetReference(out var order); - setup.ValidateOrders(new MovementPhaseAdjudicator()); + setup.ValidateOrders(MovementPhaseAdjudicator.Instance); Assert.That(order.Validation, Is.Invalid(ValidationReason.UnreachableSupport)); diff --git a/MultiversalDiplomacyTests/MovementAdjudicatorTest.cs b/MultiversalDiplomacyTests/MovementAdjudicatorTest.cs index ec23950..11374e8 100644 --- a/MultiversalDiplomacyTests/MovementAdjudicatorTest.cs +++ b/MultiversalDiplomacyTests/MovementAdjudicatorTest.cs @@ -15,7 +15,7 @@ public class MovementAdjudicatorTest setup["Germany"] .Army("Mun").Holds().GetReference(out var order); - setup.ValidateOrders(new MovementPhaseAdjudicator()); + setup.ValidateOrders(MovementPhaseAdjudicator.Instance); Assert.That(order.Validation, Is.Valid, "Unexpected validation result"); Assert.That(order.Replacement, Is.Null, "Unexpected order replacement"); @@ -28,7 +28,7 @@ public class MovementAdjudicatorTest setup["Germany"] .Army("Mun").MovesTo("Tyr").GetReference(out var order); - setup.ValidateOrders(new MovementPhaseAdjudicator()); + setup.ValidateOrders(MovementPhaseAdjudicator.Instance); Assert.That(order.Validation, Is.Valid, "Unexpected validation result"); Assert.That(order.Replacement, Is.Null, "Unexpected order replacement"); @@ -41,7 +41,7 @@ public class MovementAdjudicatorTest setup["Germany"] .Fleet("Nth").Convoys.Army("Hol").To("Lon").GetReference(out var order); - setup.ValidateOrders(new MovementPhaseAdjudicator()); + setup.ValidateOrders(MovementPhaseAdjudicator.Instance); Assert.That(order.Validation, Is.Valid, "Unexpected validation result"); Assert.That(order.Replacement, Is.Null, "Unexpected order replacement"); @@ -54,7 +54,7 @@ public class MovementAdjudicatorTest setup["Germany"] .Army("Mun").Supports.Army("Kie").Hold().GetReference(out var order); - setup.ValidateOrders(new MovementPhaseAdjudicator()); + setup.ValidateOrders(MovementPhaseAdjudicator.Instance); Assert.That(order.Validation, Is.Valid, "Unexpected validation result"); Assert.That(order.Replacement, Is.Null, "Unexpected order replacement"); @@ -67,7 +67,7 @@ public class MovementAdjudicatorTest setup["Germany"] .Army("Mun").Supports.Army("Kie").MoveTo("Ber").GetReference(out var order); - setup.ValidateOrders(new MovementPhaseAdjudicator()); + setup.ValidateOrders(MovementPhaseAdjudicator.Instance); Assert.That(order.Validation, Is.Valid, "Unexpected validation result"); Assert.That(order.Replacement, Is.Null, "Unexpected order replacement"); diff --git a/MultiversalDiplomacyTests/OrderReference.cs b/MultiversalDiplomacyTests/OrderReference.cs index ca18d3c..5dea7a3 100644 --- a/MultiversalDiplomacyTests/OrderReference.cs +++ b/MultiversalDiplomacyTests/OrderReference.cs @@ -1,4 +1,6 @@ using MultiversalDiplomacy.Adjudicate; +using MultiversalDiplomacy.Adjudicate.Decision; +using MultiversalDiplomacy.Model; using MultiversalDiplomacy.Orders; using NUnit.Framework; @@ -61,6 +63,45 @@ public class OrderReference where OrderType : Order } } + public List Adjudications + { + get + { + if (this.Builder.AdjudicationResults == null) + { + throw new InvalidOperationException("Adjudication has not been done yet"); + } + var adjudications = this.Builder.AdjudicationResults.Where(ad => ad switch + { + IsDislodged dislodged => dislodged.Order == this.Order, + DoesMove moves => moves.Order == this.Order, + _ => false, + }).ToList(); + return adjudications; + } + } + + public RetreatingUnit? Retreat + { + get + { + if (this.Builder.AdjudicationResults == null) + { + throw new InvalidOperationException("Adjudication has not been done yet"); + } + if (this.Order is UnitOrder unitOrder) + { + var retreat = this.Builder.World.RetreatingUnits.Where( + ru => ru.Unit == unitOrder.Unit); + if (retreat.Any()) + { + return retreat.Single(); + } + } + return null; + } + } + public OrderReference(TestCaseBuilder builder, OrderType order) { this.Builder = builder; diff --git a/MultiversalDiplomacyTests/TestAdjudicator.cs b/MultiversalDiplomacyTests/TestAdjudicator.cs index b5b5751..b85f8d1 100644 --- a/MultiversalDiplomacyTests/TestAdjudicator.cs +++ b/MultiversalDiplomacyTests/TestAdjudicator.cs @@ -1,4 +1,5 @@ using MultiversalDiplomacy.Adjudicate; +using MultiversalDiplomacy.Adjudicate.Decision; using MultiversalDiplomacy.Model; using MultiversalDiplomacy.Orders; @@ -6,24 +7,79 @@ namespace MultiversalDiplomacyTests; public class TestAdjudicator : IPhaseAdjudicator { - public static Func, List> RubberStamp = - (world, orders) => orders.Select(o => o.Validate(ValidationReason.Valid)).ToList(); + public static List RubberStamp(World world, List orders) + { + return orders.Select(o => o.Validate(ValidationReason.Valid)).ToList(); + } + + public static List NoMoves( + World world, + List orders) + { + List results = new(); + foreach (Order order in orders) + { + switch (order) + { + case MoveOrder move: + { + var doesMove = new DoesMove(move, null, new List()); + doesMove.Update(false); + results.Add(doesMove); + var dislodged = new IsDislodged(move, new List()); + dislodged.Update(false); + results.Add(dislodged); + break; + } + + default: + { + if (order is not UnitOrder unitOrder) + { + throw new ArgumentException(order.GetType().Name); + } + var dislodged = new IsDislodged(unitOrder, new List()); + dislodged.Update(false); + results.Add(dislodged); + break; + } + } + } + return results; + } + + public static World Noop(World world, List decisions) + => world; + + private static List NoValidate(World world, List orders) + => throw new NotImplementedException(); + + private static List NoAdjudicate(World world, List orders) + => throw new NotImplementedException(); + + private static World NoUpdate(World world, List decisions) + => throw new NotImplementedException(); private Func, List> ValidateOrdersCallback; + private Func, List> AdjudicateOrdersCallback; + private Func, World> UpdateWorldCallback; public TestAdjudicator( - Func, List> validateOrdersCallback) + Func, List>? validate = null, + Func, List>? adjudicate = null, + Func, World>? update = null) { - this.ValidateOrdersCallback = validateOrdersCallback; + this.ValidateOrdersCallback = validate ?? NoValidate; + this.AdjudicateOrdersCallback = adjudicate ?? NoAdjudicate; + this.UpdateWorldCallback = update ?? NoUpdate; } public List ValidateOrders(World world, List orders) => this.ValidateOrdersCallback.Invoke(world, orders); - public (List results, World updated) AdjudicateOrders( - World world, - List orders) - { - throw new NotImplementedException(); - } + public List AdjudicateOrders(World world, List orders) + => this.AdjudicateOrdersCallback(world, orders); + + public World UpdateWorld(World world, List decisions) + => this.UpdateWorldCallback(world, decisions); } \ No newline at end of file diff --git a/MultiversalDiplomacyTests/TestCaseBuilder.cs b/MultiversalDiplomacyTests/TestCaseBuilder.cs index 6767f7d..9812085 100644 --- a/MultiversalDiplomacyTests/TestCaseBuilder.cs +++ b/MultiversalDiplomacyTests/TestCaseBuilder.cs @@ -161,6 +161,7 @@ public class TestCaseBuilder private List OrderList; private Season Season; public List? ValidationResults { get; private set; } + public List? AdjudicationResults { get; private set; } /// /// Create a test case builder that will operate on a world. @@ -172,6 +173,7 @@ public class TestCaseBuilder this.Orders = new(this.OrderList); this.Season = season ?? this.World.Seasons.First(); this.ValidationResults = null; + this.AdjudicationResults = null; } /// @@ -225,6 +227,32 @@ public class TestCaseBuilder return this.ValidationResults; } + public List AdjudicateOrders(IPhaseAdjudicator adjudicator) + { + if (this.ValidationResults == null) + { + throw new InvalidOperationException("Cannot adjudicate before validation"); + } + + List orders = this.ValidationResults + .Where(validation => validation.Valid) + .Select(validation => validation.Order) + .ToList(); + this.AdjudicationResults = adjudicator.AdjudicateOrders(this.World, orders); + return this.AdjudicationResults; + } + + public World UpdateWorld(IPhaseAdjudicator adjudicator) + { + if (this.AdjudicationResults == null) + { + throw new InvalidOperationException("Cannot update before adjudication"); + } + + this.World = adjudicator.UpdateWorld(this.World, this.AdjudicationResults); + return this.World; + } + private class PowerContext : IPowerContext { public TestCaseBuilder Builder; diff --git a/MultiversalDiplomacyTests/TestCaseBuilderTest.cs b/MultiversalDiplomacyTests/TestCaseBuilderTest.cs index 267acbd..f116e0b 100644 --- a/MultiversalDiplomacyTests/TestCaseBuilderTest.cs +++ b/MultiversalDiplomacyTests/TestCaseBuilderTest.cs @@ -1,4 +1,5 @@ using MultiversalDiplomacy.Adjudicate; +using MultiversalDiplomacy.Adjudicate.Decision; using MultiversalDiplomacy.Model; using MultiversalDiplomacy.Orders; @@ -117,7 +118,7 @@ class TestCaseBuilderTest [Test] public void BuilderProvidesReferencesForValidation() { - IPhaseAdjudicator rubberStamp = new TestAdjudicator(TestAdjudicator.RubberStamp); + IPhaseAdjudicator rubberStamp = new TestAdjudicator(validate: TestAdjudicator.RubberStamp); TestCaseBuilder setup = new TestCaseBuilder(World.WithStandardMap().WithInitialSeason()); setup["Germany"] @@ -133,13 +134,13 @@ class TestCaseBuilderTest Is.EqualTo(setup.World.GetLand("Mun")), "Wrong unit"); - Assert.That( - () => orderMun.Validation, + Assert.That( + code: () => _ = orderMun.Validation, Throws.Exception, "Validation property should be inaccessible before validation actually happens"); setup.ValidateOrders(rubberStamp); - Assert.That( - () => orderMun.Validation, + Assert.That( + code: () => _ = orderMun.Validation, Throws.Nothing, "Validation property should be accessible after validation"); @@ -156,4 +157,60 @@ class TestCaseBuilderTest Is.EqualTo(ValidationReason.Valid), "Unexpected validation reason"); } + + public void BuilderProvidesReferencesForAdjudication() + { + IPhaseAdjudicator rubberStamp = new TestAdjudicator( + validate: TestAdjudicator.RubberStamp, + adjudicate: TestAdjudicator.NoMoves); + + TestCaseBuilder setup = new TestCaseBuilder(World.WithStandardMap().WithInitialSeason()); + setup["Germany"] + .Army("Mun").Holds().GetReference(out var orderMun); + + Assert.That( + code: () => _ = orderMun.Adjudications, + Throws.Exception, + "Adjudication property should be inaccessible before validation"); + Assert.That( + code: () => _ = orderMun.Retreat, + Throws.Exception, + "Retreat property should be inaccessible before validation"); + + setup.ValidateOrders(rubberStamp); + Assert.That( + code: () => _ = orderMun.Adjudications, + Throws.Exception, + "Adjudication property should be inaccessible before adjudication"); + Assert.That( + code: () => _ = orderMun.Retreat, + Throws.Exception, + "Retreat property should be inaccessible before adjudication"); + + var decisions = setup.AdjudicateOrders(rubberStamp); + Assert.That( + code: () => _ = orderMun.Adjudications, + Throws.Nothing, + "Adjudication property should be accessible after adjudication"); + Assert.That( + code: () => _ = orderMun.Retreat, + Throws.Nothing, + "Retreat property should be accessible after validation"); + + Assert.That(orderMun.Retreat, Is.Null, "Noop adjudicator shouldn't cause retreats"); + Assert.That( + orderMun.Adjudications.Count, + Is.EqualTo(1), + "Unexpected number of adjudications"); + AdjudicationDecision decision = orderMun.Adjudications.First(); + Assert.That(decision.Resolved, Is.True, "Unexpected unresolved decision"); + Assert.That( + decision, + Is.AssignableTo(), + "Noop adjudicator should provide a dislodge decision for a hold"); + CollectionAssert.Contains( + decisions, + decision, + "Expected the adjudicated decision to be provided by the order reference"); + } }