diff --git a/MultiversalDiplomacyTests/TestCaseBuilder.cs b/MultiversalDiplomacyTests/TestCaseBuilder.cs
new file mode 100644
index 0000000..cd42f3d
--- /dev/null
+++ b/MultiversalDiplomacyTests/TestCaseBuilder.cs
@@ -0,0 +1,422 @@
+using System.Collections.ObjectModel;
+
+using MultiversalDiplomacy.Model;
+using MultiversalDiplomacy.Orders;
+
+namespace MultiversalDiplomacyTests;
+
+///
+/// A fluent interface for defining adjudication test cases.
+///
+public class TestCaseBuilder
+{
+ ///
+ /// Context for defining orders given by a power.
+ ///
+ public interface IPowerContext
+ {
+ ///
+ /// Get the context for defining the orders for another power.
+ ///
+ public IPowerContext this[string powerName] { get; }
+
+ ///
+ /// Define an order for an army in a province.
+ ///
+ public IUnitContext Army(string provinceName);
+
+ ///
+ /// Define an order for a fleet in a province, optionally on a specific coast.
+ ///
+ public IUnitContext Fleet(string provinceName, string? coast = null);
+ }
+
+ ///
+ /// Context for defining an order given to a unit.
+ ///
+ public interface IUnitContext
+ {
+ ///
+ /// Ensure the unit exists, but don't create an order for it.
+ ///
+ public IPowerContext Exists();
+
+ ///
+ /// Give the unit a hold order.
+ ///
+ public IPowerContext Holds();
+
+ ///
+ /// Give the unit a move order.
+ ///
+ public IPowerContext MovesTo(string provinceName, string? coast = null);
+
+ ///
+ /// Give the unit a convoy order.
+ ///
+ public IConvoyContext Convoys { get; }
+
+ ///
+ /// Give the unit a support order.
+ ///
+ public ISupportContext Supports { get; }
+ }
+
+ ///
+ /// Context for defining a convoy order.
+ ///
+ public interface IConvoyContext
+ {
+ ///
+ /// Make the convoy order target an army.
+ ///
+ public IConvoyDestinationContext Army(string provinceName, string? powerName = null);
+
+ ///
+ /// Make the convoy order target a fleet.
+ ///
+ public IConvoyDestinationContext Fleet(
+ string provinceName,
+ string? coast = null,
+ string? powerName = null);
+ }
+
+ ///
+ /// Context for defining the destination of a convoy order.
+ ///
+ public interface IConvoyDestinationContext
+ {
+ ///
+ /// Define the destination of the convoy order.
+ ///
+ public IPowerContext To(string provinceName);
+ }
+
+ ///
+ /// Context for defining a support order.
+ ///
+ public interface ISupportContext
+ {
+ ///
+ /// Make the support order target an army.
+ ///
+ public ISupportTypeContext Army(string provinceName, string? powerName = null);
+
+ ///
+ /// Make the support order target a fleet.
+ ///
+ public ISupportTypeContext Fleet(
+ string provinceName,
+ string? coast = null,
+ string? powerName = null);
+ }
+
+ ///
+ /// Context for defining the type of support order.
+ ///
+ public interface ISupportTypeContext
+ {
+ ///
+ /// Give the unit an order to support the target's hold order.
+ ///
+ public IPowerContext Hold();
+
+ ///
+ /// Give the unit an order to support the target's move order.
+ ///
+ public IPowerContext MoveTo(string provinceName, string? coast = null);
+ }
+
+ public World World { get; private set; }
+ public ReadOnlyCollection Orders { get; }
+ private List OrderList;
+ private Season Season;
+
+ ///
+ /// Create a test case builder that will operate on a world.
+ ///
+ public TestCaseBuilder(World world, Season? season = null)
+ {
+ this.World = world;
+ this.OrderList = new List();
+ this.Orders = new(this.OrderList);
+ this.Season = season ?? this.World.Seasons.First();
+ }
+
+ ///
+ /// Get the context for defining the orders for a power.
+ ///
+ public IPowerContext this[string powerName]
+ {
+ get
+ {
+ Power power = this.World.GetPower(powerName);
+ return new PowerContext(this, power);
+ }
+ }
+
+ ///
+ /// Get a unit matching a description. If no such unit exists, one is created and added to the
+ /// .
+ ///
+ ///
+ /// The unit type to create if the unit does not exist.
+ /// Per DATC 4.C.1-2, mismatching unit designations should not invalidate an order, which
+ /// effectively makes the designations superfluous. To support this, the test case builder
+ /// returns a unit that matches the power, location, and season even if the unit found is not
+ /// of this type.
+ ///
+ private Unit GetOrBuildUnit(
+ Power power,
+ Location location,
+ Season season,
+ UnitType type)
+ {
+ foreach (Unit unit in this.World.Units)
+ {
+ if (unit.Power == power
+ && unit.Location == location
+ && unit.Season == season)
+ {
+ return unit;
+ }
+ }
+
+ // Not found
+ Unit newUnit = Unit.Build(location, season, power, type);
+ this.World = this.World.WithUnits(this.World.Units.Append(newUnit));
+ return newUnit;
+ }
+
+ private class PowerContext : IPowerContext
+ {
+ public TestCaseBuilder Builder;
+ public Power Power;
+
+ public PowerContext(TestCaseBuilder Builder, Power Power)
+ {
+ this.Builder = Builder;
+ this.Power = Power;
+ }
+
+ public IPowerContext this[string powerName]
+ => this.Builder[powerName];
+
+ public IUnitContext Army(string provinceName)
+ {
+ Location location = this.Builder.World.GetLand(provinceName);
+ Unit unit = this.Builder.GetOrBuildUnit(
+ this.Power, location, this.Builder.Season, UnitType.Army);
+ return new UnitContext(this, unit);
+ }
+
+ public IUnitContext Fleet(string provinceName, string? coast = null)
+ {
+ Location location = this.Builder.World.GetWater(provinceName, coast);
+ Unit unit = this.Builder.GetOrBuildUnit(
+ this.Power, location, this.Builder.Season, UnitType.Fleet);
+ return new UnitContext(this, unit);
+ }
+ }
+
+ private class UnitContext : IUnitContext
+ {
+ public TestCaseBuilder Builder;
+ public PowerContext PowerContext;
+ public Unit Unit;
+
+ public UnitContext(PowerContext powerContext, Unit unit)
+ {
+ this.Builder = powerContext.Builder;
+ 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 IPowerContext Holds()
+ {
+ HoldOrder order = new HoldOrder(this.PowerContext.Power, this.Unit);
+ this.Builder.OrderList.Add(order);
+ return this.PowerContext;
+ }
+
+ ///
+ /// Order a unit to move to a destination.
+ ///
+ public IPowerContext MovesTo(string provinceName, string? coast = null)
+ {
+ Location destination = this.Unit.Type == UnitType.Army
+ ? this.Builder.World.GetLand(provinceName)
+ : this.Builder.World.GetWater(provinceName, coast);
+ MoveOrder moveOrder = new MoveOrder(
+ this.PowerContext.Power,
+ this.Unit,
+ this.Builder.Season,
+ destination);
+ this.Builder.OrderList.Add(moveOrder);
+ return this.PowerContext;
+ }
+
+ public IConvoyContext Convoys
+ => new ConvoyContext(this);
+
+ public ISupportContext Supports
+ => new SupportContext(this);
+ }
+
+ private class ConvoyContext : IConvoyContext
+ {
+ public TestCaseBuilder Builder;
+ public PowerContext PowerContext;
+ public UnitContext UnitContext;
+
+ public ConvoyContext(UnitContext unitContext)
+ {
+ this.Builder = unitContext.Builder;
+ this.PowerContext = unitContext.PowerContext;
+ this.UnitContext = unitContext;
+ }
+
+ public IConvoyDestinationContext Army(string provinceName, string? powerName = null)
+ {
+ Power power = powerName == null
+ ? this.PowerContext.Power
+ : this.Builder.World.GetPower(powerName);
+ Location location = this.Builder.World.GetLand(provinceName);
+ Unit unit = this.Builder.GetOrBuildUnit(
+ power, location, this.Builder.Season, UnitType.Army);
+ return new ConvoyDestinationContext(this, unit);
+ }
+
+ public IConvoyDestinationContext Fleet(
+ string provinceName,
+ string? coast = null,
+ string? powerName = null)
+ {
+ Power power = powerName == null
+ ? this.PowerContext.Power
+ : 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);
+ return new ConvoyDestinationContext(this, unit);
+ }
+ }
+
+ private class ConvoyDestinationContext : IConvoyDestinationContext
+ {
+ public TestCaseBuilder Builder;
+ public PowerContext PowerContext;
+ public UnitContext UnitContext;
+ public Unit Target;
+
+ public ConvoyDestinationContext(ConvoyContext convoyContext, Unit target)
+ {
+ this.Builder = convoyContext.Builder;
+ this.PowerContext = convoyContext.PowerContext;
+ this.UnitContext = convoyContext.UnitContext;
+ this.Target = target;
+ }
+
+ public IPowerContext To(string provinceName)
+ {
+ Location location = this.Builder.World.GetLand(provinceName);
+ ConvoyOrder order = new ConvoyOrder(
+ this.PowerContext.Power,
+ this.UnitContext.Unit,
+ this.Target,
+ this.Builder.Season,
+ location);
+ this.Builder.OrderList.Add(order);
+ return this.PowerContext;
+ }
+ }
+
+ private class SupportContext : ISupportContext
+ {
+ public TestCaseBuilder Builder;
+ public PowerContext PowerContext;
+ public UnitContext UnitContext;
+
+ public SupportContext(UnitContext unitContext)
+ {
+ this.Builder = unitContext.Builder;
+ this.PowerContext = unitContext.PowerContext;
+ this.UnitContext = unitContext;
+ }
+
+ public ISupportTypeContext Army(string provinceName, string? powerName = null)
+ {
+ Power power = powerName == null
+ ? this.PowerContext.Power
+ : this.Builder.World.GetPower(powerName);
+ Location location = this.Builder.World.GetLand(provinceName);
+ Unit unit = this.Builder.GetOrBuildUnit(
+ power, location, this.Builder.Season, UnitType.Army);
+ return new SupportTypeContext(this, unit);
+ }
+
+ public ISupportTypeContext Fleet(
+ string provinceName,
+ string? coast = null,
+ string? powerName = null)
+ {
+ Power power = powerName == null
+ ? this.PowerContext.Power
+ : 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);
+ return new SupportTypeContext(this, unit);
+ }
+ }
+
+ private class SupportTypeContext : ISupportTypeContext
+ {
+ public TestCaseBuilder Builder;
+ public PowerContext PowerContext;
+ public UnitContext UnitContext;
+ public Unit Target;
+
+ public SupportTypeContext(SupportContext supportContext, Unit target)
+ {
+ this.Builder = supportContext.Builder;
+ this.PowerContext = supportContext.PowerContext;
+ this.UnitContext = supportContext.UnitContext;
+ this.Target = target;
+ }
+
+ public IPowerContext Hold()
+ {
+ SupportHoldOrder order = new SupportHoldOrder(
+ this.PowerContext.Power,
+ this.UnitContext.Unit,
+ this.Target);
+ this.Builder.OrderList.Add(order);
+ return this.PowerContext;
+ }
+
+ public IPowerContext MoveTo(string provinceName, string? coast = null)
+ {
+ Location destination = this.Target.Type == UnitType.Army
+ ? this.Builder.World.GetLand(provinceName)
+ : this.Builder.World.GetWater(provinceName, coast);
+ SupportMoveOrder order = new SupportMoveOrder(
+ this.PowerContext.Power,
+ this.UnitContext.Unit,
+ this.Target,
+ this.Builder.Season,
+ destination);
+ this.Builder.OrderList.Add(order);
+ return this.PowerContext;
+ }
+ }
+}
diff --git a/MultiversalDiplomacyTests/TestCaseBuilderTest.cs b/MultiversalDiplomacyTests/TestCaseBuilderTest.cs
new file mode 100644
index 0000000..26be408
--- /dev/null
+++ b/MultiversalDiplomacyTests/TestCaseBuilderTest.cs
@@ -0,0 +1,115 @@
+using MultiversalDiplomacy.Model;
+using MultiversalDiplomacy.Orders;
+
+using NUnit.Framework;
+
+namespace MultiversalDiplomacyTests;
+
+class TestCaseBuilderTest
+{
+ [Test]
+ public void BuilderCreatesUnits()
+ {
+ TestCaseBuilder setup = new(World.WithStandardMap().WithInitialSeason());
+
+ Assert.That(setup.World.Powers.Count(), Is.EqualTo(7), "Unexpected power count");
+ Assert.That(setup.World.Units, Is.Empty, "Expected no units to be created yet");
+
+ setup
+ ["England"]
+ .Army("London").Exists()
+ .Fleet("Irish Sea").Exists()
+ ["Russia"]
+ .Fleet("Saint Petersburg", "west coast").Exists();
+
+ 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");
+ 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");
+ 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");
+ 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(
+ fleetSTP.Location,
+ Is.EqualTo(setup.World.GetWater("STP", "wc")),
+ "Unit created on wrong coast");
+ }
+
+ [Test]
+ public void BuilderCreatesOrders()
+ {
+ TestCaseBuilder setup = new(World.WithStandardMap().WithInitialSeason());
+
+ Assert.That(setup.World.Powers.Count(), Is.EqualTo(7), "Unexpected power count");
+ Assert.That(setup.World.Units, Is.Empty, "Expected no units to be created yet");
+ Assert.That(setup.Orders, Is.Empty, "Expected no orders to be created yet");
+
+ setup
+ ["Germany"]
+ .Army("Berlin").MovesTo("Kiel")
+ .Army("Prussia").Holds()
+ ["England"]
+ .Fleet("North Sea").Convoys.Army("London").To("Holland")
+ ["France"]
+ .Army("Kiel").Supports.Army("London", powerName: "England").MoveTo("Holland")
+ .Army("Munich").Supports.Army("Kiel").Hold();
+
+ Assert.That(setup.Orders, Is.Not.Empty, "Expected orders to be created");
+ Assert.That(setup.World.Units, Is.Not.Empty, "Expected units to be created");
+ List orders = setup.Orders.OfType().ToList();
+
+ Func OrderForProvince(string name)
+ => order => order.Unit.Location.Province.Name == name;
+
+ UnitOrder orderBer = orders.Single(OrderForProvince("Berlin"));
+ Assert.That(orderBer, Is.InstanceOf(), "Unexpected order type");
+ Assert.That(
+ (orderBer as MoveOrder)?.Location,
+ Is.EqualTo(setup.World.GetLand("Kiel")),
+ "Unexpected move order destination");
+
+ UnitOrder orderPru = orders.Single(OrderForProvince("Prussia"));
+ Assert.That(orderPru, Is.InstanceOf(), "Unexpected order type");
+
+ UnitOrder orderNth = orders.Single(OrderForProvince("North Sea"));
+ Assert.That(orderNth, Is.InstanceOf(), "Unexpected order type");
+ Assert.That(
+ (orderNth as ConvoyOrder)?.Target,
+ Is.EqualTo(setup.World.GetUnitAt("London")),
+ "Unexpected convoy order target");
+ Assert.That(
+ (orderNth as ConvoyOrder)?.Location,
+ Is.EqualTo(setup.World.GetLand("Holland")),
+ "Unexpected convoy order destination");
+
+ UnitOrder orderKie = orders.Single(OrderForProvince("Kiel"));
+ Assert.That(orderKie, Is.InstanceOf(), "Unexpected order type");
+ Assert.That(
+ (orderKie as SupportMoveOrder)?.Target,
+ Is.EqualTo(setup.World.GetUnitAt("London")),
+ "Unexpected convoy order target");
+ Assert.That(
+ (orderKie as SupportMoveOrder)?.Location,
+ Is.EqualTo(setup.World.GetLand("Holland")),
+ "Unexpected convoy order destination");
+
+ UnitOrder orderMun = orders.Single(OrderForProvince("Munich"));
+ Assert.That(orderMun, Is.InstanceOf(), "Unexpected order type");
+ Assert.That(
+ (orderMun as SupportHoldOrder)?.Target,
+ Is.EqualTo(setup.World.GetUnitAt("Kiel")),
+ "Unexpected convoy order target");
+
+ Assert.That(orders.Where(OrderForProvince("London")), Is.Empty, "Unexpected order");
+ }
+}