diff --git a/MultiversalDiplomacy/Adjudicate/Decision/MovementDecisions.cs b/MultiversalDiplomacy/Adjudicate/Decision/MovementDecisions.cs index 5364ff8..f1067a1 100644 --- a/MultiversalDiplomacy/Adjudicate/Decision/MovementDecisions.cs +++ b/MultiversalDiplomacy/Adjudicate/Decision/MovementDecisions.cs @@ -84,7 +84,7 @@ public class MovementDecisions // Create a dislodge decision for this unit. List incoming = orders .OfType() - .Where(move => move.Province == order.Unit.Province) + .Where(order.IsIncoming) .ToList(); this.IsDislodged[order.Unit] = new(order, incoming); @@ -108,9 +108,7 @@ public class MovementDecisions // Find competing moves. List competing = orders .OfType() - .Where(other - => other != move - && other.Province == move.Province) + .Where(move.IsCompeting) .ToList(); // Create the move-related decisions. diff --git a/MultiversalDiplomacy/Orders/MoveOrder.cs b/MultiversalDiplomacy/Orders/MoveOrder.cs index cb1b6c5..a32e856 100644 --- a/MultiversalDiplomacy/Orders/MoveOrder.cs +++ b/MultiversalDiplomacy/Orders/MoveOrder.cs @@ -34,9 +34,20 @@ public class MoveOrder : UnitOrder this.Location = location; } + /// + /// Returns whether another move order is in a head-to-head battle with this order. + /// public bool IsOpposing(MoveOrder other) => this.Season == other.Unit.Season && other.Season == this.Unit.Season && this.Province == other.Unit.Province && other.Province == this.Unit.Province; + + /// + /// Returns whether another move order has the same destination as this order. + /// + public bool IsCompeting(MoveOrder other) + => this != other + && this.Season == other.Season + && this.Province == other.Province; } diff --git a/MultiversalDiplomacy/Orders/UnitOrder.cs b/MultiversalDiplomacy/Orders/UnitOrder.cs index e6d1f6d..a3aceb1 100644 --- a/MultiversalDiplomacy/Orders/UnitOrder.cs +++ b/MultiversalDiplomacy/Orders/UnitOrder.cs @@ -16,4 +16,12 @@ public abstract class UnitOrder : Order { this.Unit = unit; } + + /// + /// Returns whether a move order is moving into this order's unit's province. + /// + public bool IsIncoming(MoveOrder other) + => this != other + && other.Season == this.Unit.Season + && other.Province == this.Unit.Province; } \ No newline at end of file diff --git a/MultiversalDiplomacyTests/Is.cs b/MultiversalDiplomacyTests/Is.cs index 12d772f..2852517 100644 --- a/MultiversalDiplomacyTests/Is.cs +++ b/MultiversalDiplomacyTests/Is.cs @@ -3,23 +3,57 @@ using MultiversalDiplomacy.Adjudicate.Decision; namespace MultiversalDiplomacyTests; +/// +/// Multiversal Diplomacy assertion constraint extension provider. "NotX" constraints are provided +/// because properties can't be added to Is.Not via extension. +/// public class Is : NUnit.Framework.Is { + /// + /// Returns a constraint that checks for a positive order validation. + /// public static OrderValidationConstraint Valid => new(true, ValidationReason.Valid); + /// + /// Returns a constraint that checks for a negative order validation. + /// public static OrderValidationConstraint Invalid(ValidationReason expected) => new(false, expected); + /// + /// Returns a constraint that checks for a positive decision. + /// public static OrderBinaryAdjudicationConstraint Dislodged => new(true); + /// + /// Returns a constraint that checks for a negative decision. + /// public static OrderBinaryAdjudicationConstraint NotDislodged => new(false); + /// + /// Returns a constraint that checks for a positive decision. + /// public static OrderBinaryAdjudicationConstraint Victorious => new(true); + /// + /// Returns a constraint that checks for a negative decision. + /// public static OrderBinaryAdjudicationConstraint Repelled => new(false); + + /// + /// Returns a constraint that checks for a positive decision. + /// + public static OrderBinaryAdjudicationConstraint NotCut + => new(true); + + /// + /// Returns a constraint that checks for a negative decision. + /// + public static OrderBinaryAdjudicationConstraint Cut + => new(false); } diff --git a/MultiversalDiplomacyTests/TestCaseBuilder.cs b/MultiversalDiplomacyTests/TestCaseBuilder.cs index 57577eb..5d601ad 100644 --- a/MultiversalDiplomacyTests/TestCaseBuilder.cs +++ b/MultiversalDiplomacyTests/TestCaseBuilder.cs @@ -133,7 +133,13 @@ public class TestCaseBuilder /// /// Make the support order target an army. /// - public ISupportTypeContext Army(string provinceName, string? powerName = null); + /// + /// The unit season. If not specified, defaults to the same season as the ordered unit. + /// + public ISupportTypeContext Army( + string provinceName, + Season? season = null, + string? powerName = null); /// /// Make the support order target a fleet. @@ -157,7 +163,14 @@ public class TestCaseBuilder /// /// Give the unit an order to support the target's move order. /// - public IOrderDefinedContext MoveTo(string provinceName, string? coast = null); + /// + /// The target's destination season. If not specified, defaults to the same season as the + /// target (not the ordered unit). + /// + public IOrderDefinedContext MoveTo( + string provinceName, + Season? season = null, + string? coast = null); } /// @@ -506,14 +519,18 @@ public class TestCaseBuilder this.UnitContext = unitContext; } - public ISupportTypeContext Army(string provinceName, string? powerName = null) + public ISupportTypeContext Army( + string provinceName, + Season? season = null, + string? powerName = null) { Power power = powerName == null ? this.PowerContext.Power : this.Builder.World.GetPower(powerName); Location location = this.Builder.World.GetLand(provinceName); + Season destSeason = season ?? this.SeasonContext.Season; Unit unit = this.Builder.GetOrBuildUnit( - power, location, this.SeasonContext.Season, UnitType.Army); + power, location, destSeason, UnitType.Army); return new SupportTypeContext(this, unit); } @@ -559,16 +576,20 @@ public class TestCaseBuilder return new OrderDefinedContext(this.UnitContext, order); } - public IOrderDefinedContext MoveTo(string provinceName, string? coast = null) + public IOrderDefinedContext MoveTo( + string provinceName, + Season? season = null, + string? coast = null) { Location destination = this.Target.Type == UnitType.Army ? this.Builder.World.GetLand(provinceName) : this.Builder.World.GetWater(provinceName, coast); + Season targetDestSeason = season ?? this.Target.Season; SupportMoveOrder order = new SupportMoveOrder( this.PowerContext.Power, this.UnitContext.Unit, this.Target, - this.SeasonContext.Season, + targetDestSeason, destination); this.Builder.OrderList.Add(order); return new OrderDefinedContext(this.UnitContext, order); diff --git a/MultiversalDiplomacyTests/TimeTravelTest.cs b/MultiversalDiplomacyTests/TimeTravelTest.cs index e94b217..c277bd0 100644 --- a/MultiversalDiplomacyTests/TimeTravelTest.cs +++ b/MultiversalDiplomacyTests/TimeTravelTest.cs @@ -50,4 +50,54 @@ public class TimeTravelTest Unit aMun1 = world.GetUnitAt("Mun", fork.Coord); Assert.That(aMun1.Past, Is.EqualTo(originalUnit)); } + + [Test] + public void SupportToRepelledPastMoveForksTimeline() + { + TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance); + + // Fail to dislodge on the first turn, then support the move so it succeeds. + setup[(0, 0)] + .GetReference(out Season s0) + ["Germany"] + .Army("Mun").MovesTo("Tyr").GetReference(out var mun0) + ["Austria"] + .Army("Tyr").Holds().GetReference(out var tyr0); + + setup.ValidateOrders(); + Assert.That(mun0, Is.Valid); + Assert.That(tyr0, Is.Valid); + setup.AdjudicateOrders(); + Assert.That(mun0, Is.Repelled); + Assert.That(tyr0, Is.NotDislodged); + setup.UpdateWorld(); + + setup[(1, 0)] + ["Germany"] + .Army("Mun").Supports.Army("Mun", season: s0).MoveTo("Tyr").GetReference(out var mun1) + ["Austria"] + .Army("Tyr").Holds(); + + // Confirm that history is changed. + setup.ValidateOrders(); + Assert.That(mun1, Is.Valid); + setup.AdjudicateOrders(); + Assert.That(mun1, Is.NotCut); + Assert.That(mun0, Is.Victorious); + Assert.That(tyr0, Is.Dislodged); + + // Confirm that an alternate future is created. + World world = setup.UpdateWorld(); + Season fork = world.GetSeason(1, 1); + Unit tyr1 = world.GetUnitAt("Tyr", fork.Coord); + Assert.That( + tyr1.Past, + Is.EqualTo(mun0.Order.Unit), + "Expected A Mun 0:0 to advance to Tyr 1:1"); + Assert.That( + world.RetreatingUnits.Count, + Is.EqualTo(1), + "Expected A Tyr 0:0 to be in retreat"); + Assert.That(world.RetreatingUnits.First().Unit, Is.EqualTo(tyr0.Order.Unit)); + } }