Add basic movement phase update tests

This commit is contained in:
Jaculabilis 2022-03-29 17:16:00 -07:00
parent d4e68844c6
commit 6a6810ef07
7 changed files with 273 additions and 44 deletions

View File

@ -39,6 +39,11 @@ public class Season
/// </summary>
public int Timeline { get; }
/// <summary>
/// The season's spatial location as a turn-timeline tuple.
/// </summary>
public (int Turn, int Timeline) Coord => (this.Turn, this.Timeline);
/// <summary>
/// The shared timeline number generator.
/// </summary>

View File

@ -232,6 +232,19 @@ public class World
? GetLocation(provinceName, l => l.Type == LocationType.Water)
: GetLocation(provinceName, l => l.Name == coastName || l.Abbreviation == coastName);
/// <summary>
/// Get a season by coordinate. Throws if the season is not found.
/// </summary>
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;
}
/// <summary>
/// Get a power by name. Throws if there is not exactly one such power.
/// </summary>
@ -248,14 +261,18 @@ public class World
}
/// <summary>
/// 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.
/// </summary>
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;
}

View File

@ -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));
}
}

View File

@ -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));
}
}

View File

@ -11,11 +11,37 @@ namespace MultiversalDiplomacyTests;
/// </summary>
public class TestCaseBuilder
{
/// <summary>
/// Context for choosing a season to define orders for.
/// </summary>
public interface ISeasonContext
{
/// <summary>
/// Choose a new season to define orders for.
/// </summary>
public ISeasonContext this[(int turn, int timeline) seasonCoord] { get; }
/// <summary>
/// Get the context for defining the orders for a power.
/// </summary>
public IPowerContext this[string powerName] { get; }
/// <summary>
/// Save a reference to this season.
/// </summary>
public ISeasonContext GetReference(out Season season);
}
/// <summary>
/// Context for defining orders given by a power.
/// </summary>
public interface IPowerContext
{
/// <summary>
/// Choose a new season to define orders for.
/// </summary>
public ISeasonContext this[(int turn, int timeline) seasonCoord] { get; }
/// <summary>
/// Get the context for defining the orders for another power.
/// </summary>
@ -135,6 +161,11 @@ public class TestCaseBuilder
/// </summary>
public interface IOrderDefinedContext<OrderType> where OrderType : Order
{
/// <summary>
/// Choose a new season to define orders for.
/// </summary>
public ISeasonContext this[(int turn, int timeline) seasonCoord] { get; }
/// <summary>
/// Get the context for defining the orders for another power.
/// </summary>
@ -159,34 +190,31 @@ public class TestCaseBuilder
public World World { get; private set; }
public ReadOnlyCollection<Order> Orders { get; }
private List<Order> OrderList;
private Season Season;
public List<OrderValidation>? ValidationResults { get; private set; }
public List<AdjudicationDecision>? AdjudicationResults { get; private set; }
/// <summary>
/// Create a test case builder that will operate on a world.
/// </summary>
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;
}
/// <summary>
/// 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.
/// </summary>
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];
/// <summary>
/// Get the context for defining the orders for a season.
/// </summary>
public ISeasonContext this[(int turn, int timeline) seasonCoord]
=> new SeasonContext(this, this.World.GetSeason(seasonCoord.turn, seasonCoord.timeline));
/// <summary>
/// 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;
}
/// <summary>
/// Declare that a unit exists without giving it an order.
/// </summary>
public IPowerContext Exists()
=> this.PowerContext;
/// <summary>
/// Order a unit to hold.
/// </summary>
public IOrderDefinedContext<HoldOrder> Holds()
{
HoldOrder order = new HoldOrder(this.PowerContext.Power, this.Unit);
@ -319,9 +372,6 @@ public class TestCaseBuilder
return new OrderDefinedContext<HoldOrder>(this, order);
}
/// <summary>
/// Order a unit to move to a destination.
/// </summary>
public IOrderDefinedContext<MoveOrder> 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<MoveOrder>(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<ConvoyOrder>(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<SupportMoveOrder>(this.UnitContext, order);
@ -494,6 +552,7 @@ public class TestCaseBuilder
private class OrderDefinedContext<OrderType> : IOrderDefinedContext<OrderType> 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);

View File

@ -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(

View File

@ -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();