From 6a6810ef07e61d9292b85bedc8c1d6cdbfe5a374 Mon Sep 17 00:00:00 2001 From: Jaculabilis Date: Tue, 29 Mar 2022 17:16:00 -0700 Subject: [PATCH] Add basic movement phase update tests --- MultiversalDiplomacy/Model/Season.cs | 5 + MultiversalDiplomacy/Model/World.cs | 23 ++- .../MovementAdjudicatorTest.cs | 131 +++++++++++++++++ MultiversalDiplomacyTests/SeasonTests.cs | 15 ++ MultiversalDiplomacyTests/TestCaseBuilder.cs | 132 +++++++++++++----- .../TestCaseBuilderTest.cs | 9 +- MultiversalDiplomacyTests/UnitTests.cs | 2 +- 7 files changed, 273 insertions(+), 44 deletions(-) diff --git a/MultiversalDiplomacy/Model/Season.cs b/MultiversalDiplomacy/Model/Season.cs index 33b6208..adc860f 100644 --- a/MultiversalDiplomacy/Model/Season.cs +++ b/MultiversalDiplomacy/Model/Season.cs @@ -39,6 +39,11 @@ public class Season /// public int Timeline { get; } + /// + /// The season's spatial location as a turn-timeline tuple. + /// + public (int Turn, int Timeline) Coord => (this.Turn, this.Timeline); + /// /// The shared timeline number generator. /// diff --git a/MultiversalDiplomacy/Model/World.cs b/MultiversalDiplomacy/Model/World.cs index 69cb500..42161b5 100644 --- a/MultiversalDiplomacy/Model/World.cs +++ b/MultiversalDiplomacy/Model/World.cs @@ -232,6 +232,19 @@ public class World ? GetLocation(provinceName, l => l.Type == LocationType.Water) : GetLocation(provinceName, l => l.Name == coastName || l.Abbreviation == coastName); + /// + /// Get a season by coordinate. Throws if the season is not found. + /// + public Season GetSeason(int turn, int timeline) + { + Season? foundSeason = this.Seasons.SingleOrDefault( + s => s != null && s.Turn == turn && s.Timeline == timeline, + null); + if (foundSeason == null) throw new KeyNotFoundException( + $"Season {turn}:{timeline} not found"); + return foundSeason; + } + /// /// Get a power by name. Throws if there is not exactly one such power. /// @@ -248,14 +261,18 @@ public class World } /// - /// Returns a unit in a province. Throws if there is not exactly one such unit. + /// Returns a unit in a province. Throws if there are duplicate units. /// - public Unit? GetUnitAt(string provinceName) + public Unit GetUnitAt(string provinceName, (int turn, int timeline)? seasonCoord = null) { Province province = GetProvince(provinceName); + seasonCoord ??= (this.RootSeason.Turn, this.RootSeason.Timeline); + Season season = GetSeason(seasonCoord.Value.turn, seasonCoord.Value.timeline); Unit? foundUnit = this.Units.SingleOrDefault( - u => u != null && u.Location.Province == province, + u => u != null && u.Location.Province == province && u.Season == season, null); + if (foundUnit == null) throw new KeyNotFoundException( + $"Unit at {province} at {season} not found"); return foundUnit; } diff --git a/MultiversalDiplomacyTests/MovementAdjudicatorTest.cs b/MultiversalDiplomacyTests/MovementAdjudicatorTest.cs index a54d2eb..5ffaadb 100644 --- a/MultiversalDiplomacyTests/MovementAdjudicatorTest.cs +++ b/MultiversalDiplomacyTests/MovementAdjudicatorTest.cs @@ -152,4 +152,135 @@ public class MovementAdjudicatorTest Assert.That(attackMun.MinValue, Is.EqualTo(2)); Assert.That(attackMun.MaxValue, Is.EqualTo(2)); } + + [Test] + public void Update_SingleHold() + { + TestCaseBuilder setup = new(World.WithStandardMap()); + setup["Germany"] + .Army("Mun").Holds().GetReference(out var mun); + + setup.ValidateOrders(MovementPhaseAdjudicator.Instance); + Assert.That(mun, Is.Valid); + + setup.AdjudicateOrders(MovementPhaseAdjudicator.Instance); + Assert.That(mun, Is.NotDislodged); + + World updated = setup.UpdateWorld(MovementPhaseAdjudicator.Instance); + + // Confirm the future was created + Assert.That(updated.Seasons.Count, Is.EqualTo(2)); + Season future = updated.Seasons.Single(s => s != updated.RootSeason); + Assert.That(future.Past, Is.EqualTo(updated.RootSeason)); + Assert.That(future.Futures, Is.Empty); + Assert.That(future.Timeline, Is.EqualTo(updated.RootSeason.Timeline)); + Assert.That(future.Turn, Is.EqualTo(Season.FIRST_TURN + 1)); + + // Confirm the unit was created + Assert.That(updated.Units.Count, Is.EqualTo(2)); + Unit second = updated.Units.Single(u => u.Past != null); + Assert.That(second.Location, Is.EqualTo(mun.Order.Unit.Location)); + } + + [Test] + public void Update_DoubleHold() + { + TestCaseBuilder setup = new(World.WithStandardMap()); + setup[(0, 0)] + .GetReference(out Season s1) + ["Germany"] + .Army("Mun").Holds().GetReference(out var mun1); + + Assert.That(mun1.Order.Unit.Season, Is.EqualTo(s1)); + setup.ValidateOrders(MovementPhaseAdjudicator.Instance); + Assert.That(mun1, Is.Valid); + setup.AdjudicateOrders(MovementPhaseAdjudicator.Instance); + Assert.That(mun1, Is.NotDislodged); + World updated = setup.UpdateWorld(MovementPhaseAdjudicator.Instance); + + // Confirm the future was created + Season s2 = updated.GetSeason(1, 0); + Assert.That(s2.Past, Is.EqualTo(s1)); + Assert.That(s2.Futures, Is.Empty); + Assert.That(s2.Timeline, Is.EqualTo(s1.Timeline)); + Assert.That(s2.Turn, Is.EqualTo(s1.Turn + 1)); + + // Confirm the unit was created in the future + Unit u2 = updated.GetUnitAt("Mun", s2.Coord); + Assert.That(updated.Units.Count, Is.EqualTo(2)); + Assert.That(u2, Is.Not.EqualTo(mun1.Order.Unit)); + Assert.That(u2.Past, Is.EqualTo(mun1.Order.Unit)); + Assert.That(u2.Season, Is.EqualTo(s2)); + + setup = new(updated); + setup[(1, 0)] + ["Germany"] + .Army("Mun").Holds().GetReference(out var mun2); + + // Validate the second set of orders + var validations = setup.ValidateOrders(MovementPhaseAdjudicator.Instance); + Assert.That(validations.Count, Is.EqualTo(1)); + Assert.That(mun2, Is.Valid); + + // Adjudicate the second set of orders + var adjudications = setup.AdjudicateOrders(MovementPhaseAdjudicator.Instance); + Assert.That(mun2, Is.NotDislodged); + + // Update the world again + updated = setup.UpdateWorld(MovementPhaseAdjudicator.Instance); + Season s3 = updated.GetSeason(s2.Turn + 1, s2.Timeline); + Unit u3 = updated.GetUnitAt("Mun", s3.Coord); + Assert.That(u3.Past, Is.EqualTo(mun2.Order.Unit)); + } + + [Test] + public void Update_DoubleMove() + { + TestCaseBuilder setup = new(World.WithStandardMap()); + setup[(0, 0)] + .GetReference(out Season s1) + ["Germany"] + .Army("Mun").MovesTo("Tyr").GetReference(out var mun1); + + Assert.That(mun1.Order.Unit.Season, Is.EqualTo(s1)); + setup.ValidateOrders(MovementPhaseAdjudicator.Instance); + Assert.That(mun1, Is.Valid); + setup.AdjudicateOrders(MovementPhaseAdjudicator.Instance); + Assert.That(mun1, Is.Victorious); + World updated = setup.UpdateWorld(MovementPhaseAdjudicator.Instance); + + // Confirm the future was created + Season s2 = updated.GetSeason(s1.Turn + 1, s1.Timeline); + Assert.That(s2.Past, Is.EqualTo(s1)); + Assert.That(s2.Futures, Is.Empty); + Assert.That(s2.Timeline, Is.EqualTo(s1.Timeline)); + Assert.That(s2.Turn, Is.EqualTo(s1.Turn + 1)); + + // Confirm the unit was created in the future + Unit u2 = updated.GetUnitAt("Tyr", s2.Coord); + Assert.That(updated.Units.Count, Is.EqualTo(2)); + Assert.That(u2, Is.Not.EqualTo(mun1.Order.Unit)); + Assert.That(u2.Past, Is.EqualTo(mun1.Order.Unit)); + Assert.That(u2.Season, Is.EqualTo(s2)); + + setup = new(updated); + setup[(1, 0)] + ["Germany"] + .Army("Tyr").MovesTo("Mun").GetReference(out var tyr2); + + // Validate the second set of orders + var validations = setup.ValidateOrders(MovementPhaseAdjudicator.Instance); + Assert.That(validations.Count, Is.EqualTo(1)); + Assert.That(tyr2, Is.Valid); + + // Adjudicate the second set of orders + var adjudications = setup.AdjudicateOrders(MovementPhaseAdjudicator.Instance); + Assert.That(tyr2, Is.Victorious); + + // Update the world again + updated = setup.UpdateWorld(MovementPhaseAdjudicator.Instance); + Season s3 = updated.GetSeason(s2.Turn + 1, s2.Timeline); + Unit u3 = updated.GetUnitAt("Mun", s3.Coord); + Assert.That(u3.Past, Is.EqualTo(u2)); + } } diff --git a/MultiversalDiplomacyTests/SeasonTests.cs b/MultiversalDiplomacyTests/SeasonTests.cs index 0bb2556..7462cef 100644 --- a/MultiversalDiplomacyTests/SeasonTests.cs +++ b/MultiversalDiplomacyTests/SeasonTests.cs @@ -47,4 +47,19 @@ public class SeasonTests Assert.That(b2.InAdjacentTimeline(c1), Is.True, "Expected alts with common origin to be adjacent"); Assert.That(b2.InAdjacentTimeline(d1), Is.False, "Expected alts from different origins not to be adjacent"); } + + [Test] + public void LookupTest() + { + World world = World.WithStandardMap(); + Season s2 = world.RootSeason.MakeNext(); + Season s3 = s2.MakeNext(); + Season s4 = s2.MakeFork(); + World updated = world.Update(seasons: world.Seasons.Append(s2).Append(s3).Append(s4)); + + Assert.That(updated.GetSeason(Season.FIRST_TURN, 0), Is.EqualTo(updated.RootSeason)); + Assert.That(updated.GetSeason(Season.FIRST_TURN + 1, 0), Is.EqualTo(s2)); + Assert.That(updated.GetSeason(Season.FIRST_TURN + 2, 0), Is.EqualTo(s3)); + Assert.That(updated.GetSeason(Season.FIRST_TURN + 2, 1), Is.EqualTo(s4)); + } } \ No newline at end of file diff --git a/MultiversalDiplomacyTests/TestCaseBuilder.cs b/MultiversalDiplomacyTests/TestCaseBuilder.cs index 75aa36c..8e019b6 100644 --- a/MultiversalDiplomacyTests/TestCaseBuilder.cs +++ b/MultiversalDiplomacyTests/TestCaseBuilder.cs @@ -11,11 +11,37 @@ namespace MultiversalDiplomacyTests; /// public class TestCaseBuilder { + /// + /// Context for choosing a season to define orders for. + /// + public interface ISeasonContext + { + /// + /// Choose a new season to define orders for. + /// + public ISeasonContext this[(int turn, int timeline) seasonCoord] { get; } + + /// + /// Get the context for defining the orders for a power. + /// + public IPowerContext this[string powerName] { get; } + + /// + /// Save a reference to this season. + /// + public ISeasonContext GetReference(out Season season); + } + /// /// Context for defining orders given by a power. /// public interface IPowerContext { + /// + /// Choose a new season to define orders for. + /// + public ISeasonContext this[(int turn, int timeline) seasonCoord] { get; } + /// /// Get the context for defining the orders for another power. /// @@ -135,6 +161,11 @@ public class TestCaseBuilder /// public interface IOrderDefinedContext where OrderType : Order { + /// + /// Choose a new season to define orders for. + /// + public ISeasonContext this[(int turn, int timeline) seasonCoord] { get; } + /// /// Get the context for defining the orders for another power. /// @@ -159,34 +190,31 @@ public class TestCaseBuilder public World World { get; private set; } public ReadOnlyCollection Orders { get; } 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. /// - public TestCaseBuilder(World world, Season? season = null) + public TestCaseBuilder(World world) { this.World = world; this.OrderList = new(); this.Orders = new(this.OrderList); - this.Season = season ?? this.World.Seasons.First(); this.ValidationResults = null; this.AdjudicationResults = null; } /// - /// Get the context for defining the orders for a power. + /// Get the context for defining the orders for a power. Defaults to the root season. /// - public IPowerContext this[string powerName] - { - get - { - Power power = this.World.GetPower(powerName); - return new PowerContext(this, power); - } - } + public IPowerContext this[string powerName] => this[(0, 0)][powerName]; + + /// + /// Get the context for defining the orders for a season. + /// + public ISeasonContext this[(int turn, int timeline) seasonCoord] + => new SeasonContext(this, this.World.GetSeason(seasonCoord.turn, seasonCoord.timeline)); /// /// Get a unit matching a description. If no such unit exists, one is created and added to the @@ -253,19 +281,48 @@ public class TestCaseBuilder return this.World; } + private class SeasonContext : ISeasonContext + { + public TestCaseBuilder Builder; + public Season Season; + + public SeasonContext(TestCaseBuilder Builder, Season season) + { + this.Builder = Builder; + this.Season = season; + } + + public ISeasonContext this[(int turn, int timeline) seasonCoord] + => this.Builder[(seasonCoord.turn, seasonCoord.timeline)]; + + public IPowerContext this[string powerName] + => new PowerContext(this, this.Builder.World.GetPower(powerName)); + + public ISeasonContext GetReference(out Season season) + { + season = this.Season; + return this; + } + } + private class PowerContext : IPowerContext { public TestCaseBuilder Builder; + public SeasonContext SeasonContext; public Power Power; - public PowerContext(TestCaseBuilder Builder, Power Power) + public PowerContext(SeasonContext seasonContext, Power Power) { - this.Builder = Builder; + this.Builder = seasonContext.Builder; + this.SeasonContext = seasonContext; this.Power = Power; } + public ISeasonContext this[(int turn, int timeline) seasonCoord] + => this.SeasonContext[seasonCoord]; + public IPowerContext this[string powerName] - => this.Builder[powerName]; + => this.SeasonContext[powerName]; public IUnitContext Army(string provinceName, string? powerName = null) { @@ -274,7 +331,7 @@ public class TestCaseBuilder : this.Builder.World.GetPower(powerName); Location location = this.Builder.World.GetLand(provinceName); Unit unit = this.Builder.GetOrBuildUnit( - power, location, this.Builder.Season, UnitType.Army); + power, location, this.SeasonContext.Season, UnitType.Army); return new UnitContext(this, unit); } @@ -285,7 +342,7 @@ public class TestCaseBuilder : this.Builder.World.GetPower(powerName); Location location = this.Builder.World.GetWater(provinceName, coast); Unit unit = this.Builder.GetOrBuildUnit( - power, location, this.Builder.Season, UnitType.Fleet); + power, location, this.SeasonContext.Season, UnitType.Fleet); return new UnitContext(this, unit); } } @@ -293,25 +350,21 @@ public class TestCaseBuilder private class UnitContext : IUnitContext { public TestCaseBuilder Builder; + public SeasonContext SeasonContext; public PowerContext PowerContext; public Unit Unit; public UnitContext(PowerContext powerContext, Unit unit) { this.Builder = powerContext.Builder; + this.SeasonContext = powerContext.SeasonContext; this.PowerContext = powerContext; this.Unit = unit; } - /// - /// Declare that a unit exists without giving it an order. - /// public IPowerContext Exists() => this.PowerContext; - /// - /// Order a unit to hold. - /// public IOrderDefinedContext Holds() { HoldOrder order = new HoldOrder(this.PowerContext.Power, this.Unit); @@ -319,9 +372,6 @@ public class TestCaseBuilder return new OrderDefinedContext(this, order); } - /// - /// Order a unit to move to a destination. - /// public IOrderDefinedContext MovesTo(string provinceName, string? coast = null) { Location destination = this.Unit.Type == UnitType.Army @@ -330,7 +380,7 @@ public class TestCaseBuilder MoveOrder moveOrder = new MoveOrder( this.PowerContext.Power, this.Unit, - this.Builder.Season, + this.SeasonContext.Season, destination); this.Builder.OrderList.Add(moveOrder); return new OrderDefinedContext(this, moveOrder); @@ -346,12 +396,14 @@ public class TestCaseBuilder private class ConvoyContext : IConvoyContext { public TestCaseBuilder Builder; + public SeasonContext SeasonContext; public PowerContext PowerContext; public UnitContext UnitContext; public ConvoyContext(UnitContext unitContext) { this.Builder = unitContext.Builder; + this.SeasonContext = unitContext.SeasonContext; this.PowerContext = unitContext.PowerContext; this.UnitContext = unitContext; } @@ -363,7 +415,7 @@ public class TestCaseBuilder : this.Builder.World.GetPower(powerName); Location location = this.Builder.World.GetLand(provinceName); Unit unit = this.Builder.GetOrBuildUnit( - power, location, this.Builder.Season, UnitType.Army); + power, location, this.SeasonContext.Season, UnitType.Army); return new ConvoyDestinationContext(this, unit); } @@ -377,7 +429,7 @@ public class TestCaseBuilder : this.Builder.World.GetPower(powerName); Location location = this.Builder.World.GetWater(provinceName, coast); Unit unit = this.Builder.GetOrBuildUnit( - power, location, this.Builder.Season, UnitType.Fleet); + power, location, this.SeasonContext.Season, UnitType.Fleet); return new ConvoyDestinationContext(this, unit); } } @@ -385,6 +437,7 @@ public class TestCaseBuilder private class ConvoyDestinationContext : IConvoyDestinationContext { public TestCaseBuilder Builder; + public SeasonContext SeasonContext; public PowerContext PowerContext; public UnitContext UnitContext; public Unit Target; @@ -392,6 +445,7 @@ public class TestCaseBuilder public ConvoyDestinationContext(ConvoyContext convoyContext, Unit target) { this.Builder = convoyContext.Builder; + this.SeasonContext = convoyContext.SeasonContext; this.PowerContext = convoyContext.PowerContext; this.UnitContext = convoyContext.UnitContext; this.Target = target; @@ -404,7 +458,7 @@ public class TestCaseBuilder this.PowerContext.Power, this.UnitContext.Unit, this.Target, - this.Builder.Season, + this.SeasonContext.Season, location); this.Builder.OrderList.Add(order); return new OrderDefinedContext(this.UnitContext, order); @@ -414,12 +468,14 @@ public class TestCaseBuilder private class SupportContext : ISupportContext { public TestCaseBuilder Builder; + public SeasonContext SeasonContext; public PowerContext PowerContext; public UnitContext UnitContext; public SupportContext(UnitContext unitContext) { this.Builder = unitContext.Builder; + this.SeasonContext = unitContext.SeasonContext; this.PowerContext = unitContext.PowerContext; this.UnitContext = unitContext; } @@ -431,7 +487,7 @@ public class TestCaseBuilder : this.Builder.World.GetPower(powerName); Location location = this.Builder.World.GetLand(provinceName); Unit unit = this.Builder.GetOrBuildUnit( - power, location, this.Builder.Season, UnitType.Army); + power, location, this.SeasonContext.Season, UnitType.Army); return new SupportTypeContext(this, unit); } @@ -445,7 +501,7 @@ public class TestCaseBuilder : this.Builder.World.GetPower(powerName); Location location = this.Builder.World.GetWater(provinceName, coast); Unit unit = this.Builder.GetOrBuildUnit( - power, location, this.Builder.Season, UnitType.Fleet); + power, location, this.SeasonContext.Season, UnitType.Fleet); return new SupportTypeContext(this, unit); } } @@ -453,6 +509,7 @@ public class TestCaseBuilder private class SupportTypeContext : ISupportTypeContext { public TestCaseBuilder Builder; + public SeasonContext SeasonContext; public PowerContext PowerContext; public UnitContext UnitContext; public Unit Target; @@ -460,6 +517,7 @@ public class TestCaseBuilder public SupportTypeContext(SupportContext supportContext, Unit target) { this.Builder = supportContext.Builder; + this.SeasonContext = supportContext.SeasonContext; this.PowerContext = supportContext.PowerContext; this.UnitContext = supportContext.UnitContext; this.Target = target; @@ -484,7 +542,7 @@ public class TestCaseBuilder this.PowerContext.Power, this.UnitContext.Unit, this.Target, - this.Builder.Season, + this.SeasonContext.Season, destination); this.Builder.OrderList.Add(order); return new OrderDefinedContext(this.UnitContext, order); @@ -494,6 +552,7 @@ public class TestCaseBuilder private class OrderDefinedContext : IOrderDefinedContext where OrderType : Order { public TestCaseBuilder Builder; + public SeasonContext SeasonContext; public PowerContext PowerContext; public UnitContext UnitContext; public OrderType Order; @@ -501,12 +560,17 @@ public class TestCaseBuilder public OrderDefinedContext(UnitContext unitContext, OrderType order) { this.Builder = unitContext.Builder; + this.SeasonContext = unitContext.SeasonContext; this.PowerContext = unitContext.PowerContext; this.UnitContext = unitContext; this.Order = order; } - public IPowerContext this[string powerName] => this.PowerContext[powerName]; + public ISeasonContext this[(int turn, int timeline) seasonCoord] + => this.SeasonContext[seasonCoord]; + + public IPowerContext this[string powerName] + => this.SeasonContext[powerName]; public IUnitContext Army(string provinceName, string? powerName = null) => this.PowerContext.Army(provinceName, powerName); diff --git a/MultiversalDiplomacyTests/TestCaseBuilderTest.cs b/MultiversalDiplomacyTests/TestCaseBuilderTest.cs index 539b141..41c82dd 100644 --- a/MultiversalDiplomacyTests/TestCaseBuilderTest.cs +++ b/MultiversalDiplomacyTests/TestCaseBuilderTest.cs @@ -27,18 +27,15 @@ class TestCaseBuilderTest Assert.That(setup.Orders, Is.Empty, "Expected no orders to be created yet"); Assert.That(setup.World.Units, Is.Not.Empty, "Expected units to be created"); - Unit armyLON = setup.World.GetUnitAt("London") - ?? throw new AssertionException("Expected a unit in London"); + Unit armyLON = setup.World.GetUnitAt("London"); Assert.That(armyLON.Power.Name, Is.EqualTo("England"), "Unit created with wrong power"); Assert.That(armyLON.Type, Is.EqualTo(UnitType.Army), "Unit created with wrong type"); - Unit fleetIRI = setup.World.GetUnitAt("Irish Sea") - ?? throw new AssertionException("Expected a unit in Irish Sea"); + Unit fleetIRI = setup.World.GetUnitAt("Irish Sea"); Assert.That(fleetIRI.Power.Name, Is.EqualTo("England"), "Unit created with wrong power"); Assert.That(fleetIRI.Type, Is.EqualTo(UnitType.Fleet), "Unit created with wrong type"); - Unit fleetSTP = setup.World.GetUnitAt("Saint Petersburg") - ?? throw new AssertionException("Expected a unit in Saint Petersburg"); + Unit fleetSTP = setup.World.GetUnitAt("Saint Petersburg"); Assert.That(fleetSTP.Power.Name, Is.EqualTo("Russia"), "Unit created with wrong power"); Assert.That(fleetSTP.Type, Is.EqualTo(UnitType.Fleet), "Unit created with wrong type"); Assert.That( diff --git a/MultiversalDiplomacyTests/UnitTests.cs b/MultiversalDiplomacyTests/UnitTests.cs index 09a427f..c73bb12 100644 --- a/MultiversalDiplomacyTests/UnitTests.cs +++ b/MultiversalDiplomacyTests/UnitTests.cs @@ -14,7 +14,7 @@ public class UnitTests Boh = world.GetLand("Boh"), Tyr = world.GetLand("Tyr"); Power pw1 = world.GetPower("Austria"); - Season s1 = world.Seasons.First(); + Season s1 = world.RootSeason; Unit u1 = Unit.Build(Mun, s1, pw1, UnitType.Army); Season s2 = s1.MakeNext();