Fix missing temporal dimension to dislodge checks

This commit is contained in:
Jaculabilis 2022-03-30 13:00:51 -07:00
parent 9f5ecaa16a
commit d491ea9f64
6 changed files with 132 additions and 10 deletions

View File

@ -84,7 +84,7 @@ public class MovementDecisions
// Create a dislodge decision for this unit. // Create a dislodge decision for this unit.
List<MoveOrder> incoming = orders List<MoveOrder> incoming = orders
.OfType<MoveOrder>() .OfType<MoveOrder>()
.Where(move => move.Province == order.Unit.Province) .Where(order.IsIncoming)
.ToList(); .ToList();
this.IsDislodged[order.Unit] = new(order, incoming); this.IsDislodged[order.Unit] = new(order, incoming);
@ -108,9 +108,7 @@ public class MovementDecisions
// Find competing moves. // Find competing moves.
List<MoveOrder> competing = orders List<MoveOrder> competing = orders
.OfType<MoveOrder>() .OfType<MoveOrder>()
.Where(other .Where(move.IsCompeting)
=> other != move
&& other.Province == move.Province)
.ToList(); .ToList();
// Create the move-related decisions. // Create the move-related decisions.

View File

@ -34,9 +34,20 @@ public class MoveOrder : UnitOrder
this.Location = location; this.Location = location;
} }
/// <summary>
/// Returns whether another move order is in a head-to-head battle with this order.
/// </summary>
public bool IsOpposing(MoveOrder other) public bool IsOpposing(MoveOrder other)
=> this.Season == other.Unit.Season => this.Season == other.Unit.Season
&& other.Season == this.Unit.Season && other.Season == this.Unit.Season
&& this.Province == other.Unit.Province && this.Province == other.Unit.Province
&& other.Province == this.Unit.Province; && other.Province == this.Unit.Province;
/// <summary>
/// Returns whether another move order has the same destination as this order.
/// </summary>
public bool IsCompeting(MoveOrder other)
=> this != other
&& this.Season == other.Season
&& this.Province == other.Province;
} }

View File

@ -16,4 +16,12 @@ public abstract class UnitOrder : Order
{ {
this.Unit = unit; this.Unit = unit;
} }
/// <summary>
/// Returns whether a move order is moving into this order's unit's province.
/// </summary>
public bool IsIncoming(MoveOrder other)
=> this != other
&& other.Season == this.Unit.Season
&& other.Province == this.Unit.Province;
} }

View File

@ -3,23 +3,57 @@ using MultiversalDiplomacy.Adjudicate.Decision;
namespace MultiversalDiplomacyTests; namespace MultiversalDiplomacyTests;
/// <summary>
/// Multiversal Diplomacy assertion constraint extension provider. "NotX" constraints are provided
/// because properties can't be added to Is.Not via extension.
/// </summary>
public class Is : NUnit.Framework.Is public class Is : NUnit.Framework.Is
{ {
/// <summary>
/// Returns a constraint that checks for a positive order validation.
/// </summary>
public static OrderValidationConstraint Valid public static OrderValidationConstraint Valid
=> new(true, ValidationReason.Valid); => new(true, ValidationReason.Valid);
/// <summary>
/// Returns a constraint that checks for a negative order validation.
/// </summary>
public static OrderValidationConstraint Invalid(ValidationReason expected) public static OrderValidationConstraint Invalid(ValidationReason expected)
=> new(false, expected); => new(false, expected);
/// <summary>
/// Returns a constraint that checks for a positive <see cref="IsDislodged"/> decision.
/// </summary>
public static OrderBinaryAdjudicationConstraint<IsDislodged> Dislodged public static OrderBinaryAdjudicationConstraint<IsDislodged> Dislodged
=> new(true); => new(true);
/// <summary>
/// Returns a constraint that checks for a negative <see cref="IsDislodged"/> decision.
/// </summary>
public static OrderBinaryAdjudicationConstraint<IsDislodged> NotDislodged public static OrderBinaryAdjudicationConstraint<IsDislodged> NotDislodged
=> new(false); => new(false);
/// <summary>
/// Returns a constraint that checks for a positive <see cref="DoesMove"/> decision.
/// </summary>
public static OrderBinaryAdjudicationConstraint<DoesMove> Victorious public static OrderBinaryAdjudicationConstraint<DoesMove> Victorious
=> new(true); => new(true);
/// <summary>
/// Returns a constraint that checks for a negative <see cref="DoesMove"/> decision.
/// </summary>
public static OrderBinaryAdjudicationConstraint<DoesMove> Repelled public static OrderBinaryAdjudicationConstraint<DoesMove> Repelled
=> new(false); => new(false);
/// <summary>
/// Returns a constraint that checks for a positive <see cref="GivesSupport"/> decision.
/// </summary>
public static OrderBinaryAdjudicationConstraint<GivesSupport> NotCut
=> new(true);
/// <summary>
/// Returns a constraint that checks for a negative <see cref="GivesSupport"/> decision.
/// </summary>
public static OrderBinaryAdjudicationConstraint<GivesSupport> Cut
=> new(false);
} }

View File

@ -133,7 +133,13 @@ public class TestCaseBuilder
/// <summary> /// <summary>
/// Make the support order target an army. /// Make the support order target an army.
/// </summary> /// </summary>
public ISupportTypeContext Army(string provinceName, string? powerName = null); /// <param name="season">
/// The unit season. If not specified, defaults to the same season as the ordered unit.
/// </param>
public ISupportTypeContext Army(
string provinceName,
Season? season = null,
string? powerName = null);
/// <summary> /// <summary>
/// Make the support order target a fleet. /// Make the support order target a fleet.
@ -157,7 +163,14 @@ public class TestCaseBuilder
/// <summary> /// <summary>
/// Give the unit an order to support the target's move order. /// Give the unit an order to support the target's move order.
/// </summary> /// </summary>
public IOrderDefinedContext<SupportMoveOrder> MoveTo(string provinceName, string? coast = null); /// <param name="season">
/// The target's destination season. If not specified, defaults to the same season as the
/// target (not the ordered unit).
/// </param>
public IOrderDefinedContext<SupportMoveOrder> MoveTo(
string provinceName,
Season? season = null,
string? coast = null);
} }
/// <summary> /// <summary>
@ -506,14 +519,18 @@ public class TestCaseBuilder
this.UnitContext = unitContext; 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 Power power = powerName == null
? this.PowerContext.Power ? this.PowerContext.Power
: this.Builder.World.GetPower(powerName); : this.Builder.World.GetPower(powerName);
Location location = this.Builder.World.GetLand(provinceName); Location location = this.Builder.World.GetLand(provinceName);
Season destSeason = season ?? this.SeasonContext.Season;
Unit unit = this.Builder.GetOrBuildUnit( Unit unit = this.Builder.GetOrBuildUnit(
power, location, this.SeasonContext.Season, UnitType.Army); power, location, destSeason, UnitType.Army);
return new SupportTypeContext(this, unit); return new SupportTypeContext(this, unit);
} }
@ -559,16 +576,20 @@ public class TestCaseBuilder
return new OrderDefinedContext<SupportHoldOrder>(this.UnitContext, order); return new OrderDefinedContext<SupportHoldOrder>(this.UnitContext, order);
} }
public IOrderDefinedContext<SupportMoveOrder> MoveTo(string provinceName, string? coast = null) public IOrderDefinedContext<SupportMoveOrder> MoveTo(
string provinceName,
Season? season = null,
string? coast = null)
{ {
Location destination = this.Target.Type == UnitType.Army Location destination = this.Target.Type == UnitType.Army
? this.Builder.World.GetLand(provinceName) ? this.Builder.World.GetLand(provinceName)
: this.Builder.World.GetWater(provinceName, coast); : this.Builder.World.GetWater(provinceName, coast);
Season targetDestSeason = season ?? this.Target.Season;
SupportMoveOrder order = new SupportMoveOrder( SupportMoveOrder order = new SupportMoveOrder(
this.PowerContext.Power, this.PowerContext.Power,
this.UnitContext.Unit, this.UnitContext.Unit,
this.Target, this.Target,
this.SeasonContext.Season, targetDestSeason,
destination); destination);
this.Builder.OrderList.Add(order); this.Builder.OrderList.Add(order);
return new OrderDefinedContext<SupportMoveOrder>(this.UnitContext, order); return new OrderDefinedContext<SupportMoveOrder>(this.UnitContext, order);

View File

@ -50,4 +50,54 @@ public class TimeTravelTest
Unit aMun1 = world.GetUnitAt("Mun", fork.Coord); Unit aMun1 = world.GetUnitAt("Mun", fork.Coord);
Assert.That(aMun1.Past, Is.EqualTo(originalUnit)); 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));
}
} }