using System.Collections.ObjectModel; using MultiversalDiplomacy.Adjudicate; using MultiversalDiplomacy.Model; using MultiversalDiplomacy.Orders; using NUnit.Framework; namespace MultiversalDiplomacyTests; /// /// A fluent interface for defining adjudication test cases. /// 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[(string timeline, int turn) 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[(string timeline, int turn) seasonCoord] { get; } /// /// 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, string? powerName = null); /// /// Define an order for a fleet in a province, optionally on a specific coast. /// public IUnitContext Fleet(string provinceName, string? coast = null, string? powerName = 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 IOrderDefinedContext Holds(); /// /// Give the unit a move order. /// /// /// The destination season. If not specified, defaults to the same season as the unit. /// public IOrderDefinedContext MovesTo( string provinceName, Season? season = null, 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 IOrderDefinedContext To(string provinceName); } /// /// Context for defining a support order. /// public interface ISupportContext { /// /// Make the support order target an army. /// /// /// 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. /// 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 IOrderDefinedContext Hold(); /// /// Give the unit an order to support the target's move order. /// /// /// 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); } /// /// Context for additional operations on a defined order or defining another order. This /// context mimics the with additional functionality related to /// the order that was just defined. /// public interface IOrderDefinedContext where OrderType : Order { /// /// Perform validation, adjudication, and update using the defined orders. /// public TestCaseBuilder Execute(IPhaseAdjudicator? adjudicator = null); /// /// Choose a new season to define orders for. /// public ISeasonContext this[(string timeline, int turn) seasonCoord] { get; } /// /// Get the context for defining the orders for another power. /// public IPowerContext this[string powerName] { get; } /// /// Define an order for a new army in a province. /// public IUnitContext Army(string provinceName, string? powerName = null); /// /// Define an order for a new fleet in a province, optionally on a specific coast. /// public IUnitContext Fleet(string provinceName, string? coast = null, string? powerName = null); /// /// Save a reference to the order just defined. /// public IOrderDefinedContext GetReference(out OrderReference order); } public World World { get; private set; } private IPhaseAdjudicator LastUsedAdjudicator { get; set; } public ReadOnlyCollection Orders { get; } private List OrderList; 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, IPhaseAdjudicator? adjudicator = null) { this.World = world; this.LastUsedAdjudicator = adjudicator ?? new TestAdjudicator(); this.OrderList = new(); this.Orders = new(this.OrderList); this.ValidationResults = null; this.AdjudicationResults = null; } /// /// Get the context for defining the orders for a power. Defaults to the root season. /// public IPowerContext this[string powerName] => this[("a", 0)][powerName]; /// /// Get the context for defining the orders for a season. /// public ISeasonContext this[(string timeline, int turn) seasonCoord] => new SeasonContext(this, new(seasonCoord)); /// /// 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( string power, Location location, Season season, UnitType type) { foreach (Unit unit in this.World.Units) { if (unit.Power == power && World.Map.GetLocation(unit).Province == location.Province && unit.Season == season) { return unit; } } // Not found Unit newUnit = Unit.Build(location.Key, season, power, type); this.World = this.World.Update(units: this.World.Units.Append(newUnit)); return newUnit; } public List ValidateOrders(IPhaseAdjudicator? adjudicator = null) { adjudicator ??= this.LastUsedAdjudicator; this.LastUsedAdjudicator = adjudicator; this.ValidationResults = adjudicator.ValidateOrders(this.World, this.Orders.ToList()); this.OrderList.Clear(); return this.ValidationResults; } public List AdjudicateOrders(IPhaseAdjudicator? adjudicator = null) { if (this.ValidationResults == null) { throw new InvalidOperationException("Cannot adjudicate before validation"); } adjudicator ??= this.LastUsedAdjudicator; this.LastUsedAdjudicator = adjudicator; List orders = this.ValidationResults .Where(validation => validation.Valid) .Select(validation => validation.Order) .ToList(); this.AdjudicationResults = adjudicator.AdjudicateOrders(this.World, orders); this.ValidationResults = null; return this.AdjudicationResults; } public World UpdateWorld(IPhaseAdjudicator? adjudicator = null) { if (this.AdjudicationResults == null) { throw new InvalidOperationException("Cannot update before adjudication"); } adjudicator ??= this.LastUsedAdjudicator; this.LastUsedAdjudicator = adjudicator; this.World = adjudicator.UpdateWorld(this.World, this.AdjudicationResults); this.AdjudicationResults = null; 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[(string timeline, int turn) seasonCoord] => this.Builder[(seasonCoord.timeline, seasonCoord.turn)]; public IPowerContext this[string powerName] => new PowerContext(this, this.Builder.World.Map.GetPower(powerName)); public ISeasonContext GetReference(out Season season) { season = this.Season; return this; } } private class PowerContext : IPowerContext { public TestCaseBuilder Builder; public SeasonContext SeasonContext; public string Power; public PowerContext(SeasonContext seasonContext, string power) { Assert.That(power, Is.AnyOf([.. seasonContext.Builder.World.Map.Powers]), "Invalid power"); this.Builder = seasonContext.Builder; this.SeasonContext = seasonContext; this.Power = power; } public ISeasonContext this[(string timeline, int turn) seasonCoord] => this.SeasonContext[seasonCoord]; public IPowerContext this[string powerName] => this.SeasonContext[powerName]; public IUnitContext Army(string provinceName, string? powerName = null) { string power = powerName == null ? this.Power : this.Builder.World.Map.GetPower(powerName); Location location = this.Builder.World.Map.GetLand(provinceName); Unit unit = this.Builder.GetOrBuildUnit( power, location, this.SeasonContext.Season, UnitType.Army); return new UnitContext(this, unit); } public IUnitContext Fleet(string provinceName, string? coast = null, string? powerName = null) { string power = powerName == null ? this.Power : this.Builder.World.Map.GetPower(powerName); Location location = this.Builder.World.Map.GetWater(provinceName, coast); Unit unit = this.Builder.GetOrBuildUnit( power, location, this.SeasonContext.Season, UnitType.Fleet); return new UnitContext(this, unit); } } 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; } public IPowerContext Exists() => this.PowerContext; public IOrderDefinedContext Holds() { HoldOrder order = new HoldOrder(this.PowerContext.Power, this.Unit); this.Builder.OrderList.Add(order); return new OrderDefinedContext(this, order); } public IOrderDefinedContext MovesTo( string provinceName, Season? season = null, string? coast = null) { Location destination = this.Unit.Type == UnitType.Army ? this.Builder.World.Map.GetLand(provinceName) : this.Builder.World.Map.GetWater(provinceName, coast); Season destSeason = season ?? this.SeasonContext.Season; MoveOrder moveOrder = new MoveOrder( this.PowerContext.Power, this.Unit, destSeason, destination.Key); this.Builder.OrderList.Add(moveOrder); return new OrderDefinedContext(this, moveOrder); } public IConvoyContext Convoys => new ConvoyContext(this); public ISupportContext Supports => new SupportContext(this); } 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; } public IConvoyDestinationContext Army(string provinceName, string? powerName = null) { string power = powerName == null ? this.PowerContext.Power : this.Builder.World.Map.GetPower(powerName); Location location = this.Builder.World.Map.GetLand(provinceName); Unit unit = this.Builder.GetOrBuildUnit( power, location, this.SeasonContext.Season, UnitType.Army); return new ConvoyDestinationContext(this, unit); } public IConvoyDestinationContext Fleet( string provinceName, string? coast = null, string? powerName = null) { string power = powerName == null ? this.PowerContext.Power : this.Builder.World.Map.GetPower(powerName); Location location = this.Builder.World.Map.GetWater(provinceName, coast); Unit unit = this.Builder.GetOrBuildUnit( power, location, this.SeasonContext.Season, UnitType.Fleet); return new ConvoyDestinationContext(this, unit); } } private class ConvoyDestinationContext : IConvoyDestinationContext { public TestCaseBuilder Builder; public SeasonContext SeasonContext; public PowerContext PowerContext; public UnitContext UnitContext; public Unit Target; 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; } public IOrderDefinedContext To(string provinceName) { Location location = this.Builder.World.Map.GetLand(provinceName); ConvoyOrder order = new ConvoyOrder( this.PowerContext.Power, this.UnitContext.Unit, this.Target, this.SeasonContext.Season, location); this.Builder.OrderList.Add(order); return new OrderDefinedContext(this.UnitContext, order); } } 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; } public ISupportTypeContext Army( string provinceName, Season? season = null, string? powerName = null) { string power = powerName == null ? this.PowerContext.Power : this.Builder.World.Map.GetPower(powerName); Location location = this.Builder.World.Map.GetLand(provinceName); Season destSeason = season ?? this.SeasonContext.Season; Unit unit = this.Builder.GetOrBuildUnit( power, location, destSeason, UnitType.Army); return new SupportTypeContext(this, unit); } public ISupportTypeContext Fleet( string provinceName, string? coast = null, string? powerName = null) { string power = powerName == null ? this.PowerContext.Power : this.Builder.World.Map.GetPower(powerName); Location location = this.Builder.World.Map.GetWater(provinceName, coast); Unit unit = this.Builder.GetOrBuildUnit( power, location, this.SeasonContext.Season, UnitType.Fleet); return new SupportTypeContext(this, unit); } } private class SupportTypeContext : ISupportTypeContext { public TestCaseBuilder Builder; public SeasonContext SeasonContext; public PowerContext PowerContext; public UnitContext UnitContext; public Unit Target; 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; } public IOrderDefinedContext Hold() { SupportHoldOrder order = new SupportHoldOrder( this.PowerContext.Power, this.UnitContext.Unit, this.Target); this.Builder.OrderList.Add(order); return new OrderDefinedContext(this.UnitContext, order); } public IOrderDefinedContext MoveTo( string provinceName, Season? season = null, string? coast = null) { Location destination = this.Target.Type == UnitType.Army ? this.Builder.World.Map.GetLand(provinceName) : this.Builder.World.Map.GetWater(provinceName, coast); Season targetDestSeason = season ?? this.Target.Season; SupportMoveOrder order = new SupportMoveOrder( this.PowerContext.Power, this.UnitContext.Unit, this.Target, targetDestSeason, destination); this.Builder.OrderList.Add(order); return new OrderDefinedContext(this.UnitContext, order); } } private class OrderDefinedContext : IOrderDefinedContext where OrderType : Order { public TestCaseBuilder Builder; public SeasonContext SeasonContext; public PowerContext PowerContext; public UnitContext UnitContext; public OrderType Order; 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 TestCaseBuilder Execute(IPhaseAdjudicator? adjudicator = null) { adjudicator ??= this.Builder.LastUsedAdjudicator; this.Builder.LastUsedAdjudicator = adjudicator; this.Builder.ValidateOrders(adjudicator); this.Builder.AdjudicateOrders(adjudicator); this.Builder.UpdateWorld(adjudicator); return this.Builder; } public ISeasonContext this[(string timeline, int turn) 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); public IUnitContext Fleet(string provinceName, string? coast = null, string? powerName = null) => this.PowerContext.Fleet(provinceName, coast, powerName); public IOrderDefinedContext GetReference(out OrderReference order) { order = new OrderReference(this.Builder, this.Order); return this; } } }