From c50dbf6b468e26963985e006b74d2382043e2378 Mon Sep 17 00:00:00 2001 From: Jaculabilis Date: Tue, 22 Mar 2022 21:27:06 -0700 Subject: [PATCH] Add movement order validation tests --- .../Adjudicate/AdjudicatorHelpers.cs | 8 +- .../Adjudicate/MovementPhaseAdjudicator.cs | 4 +- MultiversalDiplomacy/Model/World.cs | 192 ++++++++++++++++-- .../MovementAdjudicatorTest.cs | 110 ++++++++++ 4 files changed, 295 insertions(+), 19 deletions(-) create mode 100644 MultiversalDiplomacyTests/MovementAdjudicatorTest.cs diff --git a/MultiversalDiplomacy/Adjudicate/AdjudicatorHelpers.cs b/MultiversalDiplomacy/Adjudicate/AdjudicatorHelpers.cs index e613539..9e73438 100644 --- a/MultiversalDiplomacy/Adjudicate/AdjudicatorHelpers.cs +++ b/MultiversalDiplomacy/Adjudicate/AdjudicatorHelpers.cs @@ -51,11 +51,12 @@ internal static class AdjudicatorHelpers .ToList(); if (nonOrderTypes.Any()) { - throw new ArgumentException($"Unknown order type: {nonOrderTypes.Select(t => t.FullName).First()}"); + throw new ArgumentException( + $"Unknown order type: {nonOrderTypes.Select(t => t.FullName).First()}"); } InvalidateIfNotMatching( - order => !validOrderTypes.Contains(order.GetType()), + order => validOrderTypes.Contains(order.GetType()), ValidationReason.InvalidOrderTypeForPhase, ref orders, ref invalidOrders); @@ -83,7 +84,8 @@ internal static class AdjudicatorHelpers RetreatOrder retreat => retreat.Power == retreat.Unit.Power, SupportHoldOrder support => support.Power == support.Unit.Power, SupportMoveOrder support => support.Power == support.Unit.Power, - // Any order not given to a unit by definition cannot be given to a unit of the wrong power + // Any order not given to a unit, by definition, cannot be given to a unit of the + // wrong power _ => true, }, ValidationReason.InvalidUnitForPower, diff --git a/MultiversalDiplomacy/Adjudicate/MovementPhaseAdjudicator.cs b/MultiversalDiplomacy/Adjudicate/MovementPhaseAdjudicator.cs index 9011f70..27305fe 100644 --- a/MultiversalDiplomacy/Adjudicate/MovementPhaseAdjudicator.cs +++ b/MultiversalDiplomacy/Adjudicate/MovementPhaseAdjudicator.cs @@ -6,7 +6,7 @@ namespace MultiversalDiplomacy.Adjudicate; /// /// Adjudicator for the movement phase. /// -internal class MovementPhaseAdjudicator : IPhaseAdjudicator +public class MovementPhaseAdjudicator : IPhaseAdjudicator { public List ValidateOrders(World world, List orders) { @@ -35,7 +35,7 @@ internal class MovementPhaseAdjudicator : IPhaseAdjudicator AdjudicatorHelpers.InvalidateWrongPower(orders, ref orders, ref validationResults); // Since all the order types in this phase are UnitOrders, downcast to get the Unit. - List unitOrders = orders.OfType().ToList(); + List unitOrders = orders.Cast().ToList(); // Invalidate any order given to a unit in the past. AdjudicatorHelpers.InvalidateIfNotMatching( diff --git a/MultiversalDiplomacy/Model/World.cs b/MultiversalDiplomacy/Model/World.cs index 8360bb7..7b08cce 100644 --- a/MultiversalDiplomacy/Model/World.cs +++ b/MultiversalDiplomacy/Model/World.cs @@ -96,7 +96,7 @@ public class World { "A" => UnitType.Army, "F" => UnitType.Fleet, - _ => throw new ArgumentOutOfRangeException($"Unknown unit type {splits[1]}") + _ => throw new ApplicationException($"Unknown unit type {splits[1]}") }; Location location = type == UnitType.Army ? this.GetLand(splits[2]) @@ -152,14 +152,20 @@ public class World /// Get a province by name. Throws if the province is not found. /// private Province GetProvince(string provinceName) + => GetProvince(provinceName, this.Provinces); + + /// + /// Get a province by name. Throws if the province is not found. + /// + private static Province GetProvince(string provinceName, IEnumerable provinces) { string provinceNameUpper = provinceName.ToUpperInvariant(); - Province? foundProvince = this.Provinces.SingleOrDefault( + Province? foundProvince = provinces.SingleOrDefault( p => p != null && (p.Name.ToUpperInvariant() == provinceNameUpper || p.Abbreviations.Any(a => a.ToUpperInvariant() == provinceNameUpper)), null); - if (foundProvince == null) throw new ArgumentOutOfRangeException( + if (foundProvince == null) throw new KeyNotFoundException( $"Province {provinceName} not found"); return foundProvince; } @@ -172,7 +178,7 @@ public class World { Location? foundLocation = GetProvince(provinceName).Locations.SingleOrDefault( l => l != null && predicate(l), null); - if (foundLocation == null) throw new ArgumentException( + if (foundLocation == null) throw new KeyNotFoundException( $"No such location in {provinceName}"); return foundLocation; } @@ -201,7 +207,7 @@ public class World p != null && (p.Name == powerName || p.Name.StartsWith(powerName)), null); - if (foundPower == null) throw new ArgumentOutOfRangeException( + if (foundPower == null) throw new KeyNotFoundException( $"Power {powerName} not found"); return foundPower; } @@ -427,15 +433,13 @@ public class World }; // Declare some helpers for border definitions - Location Land(string provinceName) => standardProvinces - .Single(p => p.Name == provinceName || p.Abbreviations.Contains(provinceName)) + Location Land(string provinceName) => GetProvince(provinceName, standardProvinces) .Locations.Single(l => l.Type == LocationType.Land); - Location Water(string provinceName) => standardProvinces - .Single(p => p.Name == provinceName || p.Abbreviations.Contains(provinceName)) + Location Water(string provinceName) => GetProvince(provinceName, standardProvinces) .Locations.Single(l => l.Type == LocationType.Water); - Location Coast(string provinceName, string coastName) => standardProvinces - .Single(p => p.Name == provinceName || p.Abbreviations.Contains(provinceName)) - .Locations.Single(l => l.Name == coastName || l.Abbreviation == coastName); + Location Coast(string provinceName, string coastName) + => GetProvince(provinceName, standardProvinces) + .Locations.Single(l => l.Name == coastName || l.Abbreviation == coastName); Land("NAF").AddBorder(Land("TUN")); Water("NAF").AddBorder(Water("MAO")); @@ -517,7 +521,165 @@ public class World Water("GRE").AddBorder(Water("AEG")); Water("GRE").AddBorder(Coast("BUL", "sc")); - // TODO + // RUM + + // SER + + // CLY + + // EDI + + // LVP + + Land("LON").AddBorder(Land("WAL")); + Land("LON").AddBorder(Land("YOR")); + Water("LON").AddBorder(Water("ENC")); + Water("LON").AddBorder(Water("NTH")); + + // WAL + + // YOR + + // BRE + + // BUR + + // GAS + + // MAR + + // PAR + + // PIC + + Land("BER").AddBorder(Land("PRU")); + Land("BER").AddBorder(Land("SIL")); + Land("BER").AddBorder(Land("MUN")); + Land("BER").AddBorder(Land("KIE")); + Water("BER").AddBorder(Water("KIE")); + Water("BER").AddBorder(Water("BAL")); + Water("BER").AddBorder(Water("PRU")); + + Land("KIE").AddBorder(Land("BER")); + Land("KIE").AddBorder(Land("MUN")); + Land("KIE").AddBorder(Land("RUH")); + Land("KIE").AddBorder(Land("HOL")); + Land("KIE").AddBorder(Land("DEN")); + Water("KIE").AddBorder(Water("HOL")); + Water("KIE").AddBorder(Water("HEL")); + Water("KIE").AddBorder(Water("DEN")); + Water("KIE").AddBorder(Water("BAL")); + Water("KIE").AddBorder(Water("BER")); + + Land("MUN").AddBorder(Land("BUR")); + Land("MUN").AddBorder(Land("RUH")); + Land("MUN").AddBorder(Land("KIE")); + Land("MUN").AddBorder(Land("BER")); + Land("MUN").AddBorder(Land("SIL")); + Land("MUN").AddBorder(Land("BOH")); + Land("MUN").AddBorder(Land("TYR")); + + // PRU + + // RUH + + // SIL + + // SPA + + // POR + + // APU + + // NAP + + // PIE + + // ROM + + // TUS + + // VEN + + // BEL + + Land("HOL").AddBorder(Land("BEL")); + Land("HOL").AddBorder(Land("RUH")); + Land("HOL").AddBorder(Land("KIE")); + Water("HOL").AddBorder(Water("NTH")); + Water("HOL").AddBorder(Water("HEL")); + + // FIN + + // LVN + + // MOS + + // SEV + + // STP + + // UKR + + // WAR + + // DEN + + // NWY + + // SWE + + // ANK + + // ARM + + // CON + + // SMY + + // SYR + + // BAR + + // ENC + + // HEL + + // IRS + + // MAO + + // NAO + + Water("NTH").AddBorder(Water("NWG")); + Water("NTH").AddBorder(Water("NWY")); + Water("NTH").AddBorder(Water("SKA")); + Water("NTH").AddBorder(Water("DEN")); + Water("NTH").AddBorder(Water("HEL")); + Water("NTH").AddBorder(Water("HOL")); + Water("NTH").AddBorder(Water("BEL")); + Water("NTH").AddBorder(Water("ENC")); + Water("NTH").AddBorder(Water("LON")); + Water("NTH").AddBorder(Water("YOR")); + Water("NTH").AddBorder(Water("EDI")); + + // NWS + + // SKA + + // BAL + + // GOB + + // ADS + + // AEG + + // BLA + + // EMS + + // GOL Water("IOS").AddBorder(Water("TUN")); Water("IOS").AddBorder(Water("TYS")); @@ -528,7 +690,9 @@ public class World Water("IOS").AddBorder(Water("GRE")); Water("IOS").AddBorder(Water("AEG")); - // TODO + // TYS + + // WMS return new(standardProvinces); } diff --git a/MultiversalDiplomacyTests/MovementAdjudicatorTest.cs b/MultiversalDiplomacyTests/MovementAdjudicatorTest.cs new file mode 100644 index 0000000..8c81001 --- /dev/null +++ b/MultiversalDiplomacyTests/MovementAdjudicatorTest.cs @@ -0,0 +1,110 @@ +using MultiversalDiplomacy.Adjudicate; +using MultiversalDiplomacy.Model; +using MultiversalDiplomacy.Orders; + +using NUnit.Framework; + +namespace MultiversalDiplomacyTests; + +public class MovementAdjudicatorTest +{ + [Test] + public void Validation_ValidHold() + { + TestCaseBuilder setup = new TestCaseBuilder(World.WithStandardMap().WithInitialSeason()); + setup["Germany"] + .Army("Mun").Holds().GetReference(out var order); + + setup.ValidateOrders(new MovementPhaseAdjudicator()); + + Assert.Multiple(() => + { + Assert.That(order.Validation.Valid, Is.True, "Unexpected validation result"); + Assert.That( + order.Validation.Reason, + Is.EqualTo(ValidationReason.Valid), + "Unexpected validation reason"); + Assert.That(order.Replacement, Is.Null, "Unexpected order replacement"); + }); + } + + [Test] + public void Validation_ValidMove() + { + TestCaseBuilder setup = new TestCaseBuilder(World.WithStandardMap().WithInitialSeason()); + setup["Germany"] + .Army("Mun").MovesTo("Tyr").GetReference(out var order); + + setup.ValidateOrders(new MovementPhaseAdjudicator()); + + Assert.Multiple(() => + { + Assert.That(order.Validation.Valid, Is.True, "Unexpected validation result"); + Assert.That( + order.Validation.Reason, + Is.EqualTo(ValidationReason.Valid), + "Unexpected validation reason"); + Assert.That(order.Replacement, Is.Null, "Unexpected order replacement"); + }); + } + + [Test] + public void Validation_ValidConvoy() + { + TestCaseBuilder setup = new TestCaseBuilder(World.WithStandardMap().WithInitialSeason()); + setup["Germany"] + .Fleet("Nth").Convoys.Army("Hol").To("Lon").GetReference(out var order); + + setup.ValidateOrders(new MovementPhaseAdjudicator()); + + Assert.Multiple(() => + { + Assert.That(order.Validation.Valid, Is.True, "Unexpected validation result"); + Assert.That( + order.Validation.Reason, + Is.EqualTo(ValidationReason.Valid), + "Unexpected validation reason"); + Assert.That(order.Replacement, Is.Null, "Unexpected order replacement"); + }); + } + + [Test] + public void Validation_ValidSupportHold() + { + TestCaseBuilder setup = new TestCaseBuilder(World.WithStandardMap().WithInitialSeason()); + setup["Germany"] + .Army("Mun").Supports.Army("Kie").Hold().GetReference(out var order); + + setup.ValidateOrders(new MovementPhaseAdjudicator()); + + Assert.Multiple(() => + { + Assert.That(order.Validation.Valid, Is.True, "Unexpected validation result"); + Assert.That( + order.Validation.Reason, + Is.EqualTo(ValidationReason.Valid), + "Unexpected validation reason"); + Assert.That(order.Replacement, Is.Null, "Unexpected order replacement"); + }); + } + + [Test] + public void Validation_ValidSupportMove() + { + TestCaseBuilder setup = new TestCaseBuilder(World.WithStandardMap().WithInitialSeason()); + setup["Germany"] + .Army("Mun").Supports.Army("Kie").MoveTo("Ber").GetReference(out var order); + + setup.ValidateOrders(new MovementPhaseAdjudicator()); + + Assert.Multiple(() => + { + Assert.That(order.Validation.Valid, Is.True, "Unexpected validation result"); + Assert.That( + order.Validation.Reason, + Is.EqualTo(ValidationReason.Valid), + "Unexpected validation reason"); + Assert.That(order.Replacement, Is.Null, "Unexpected order replacement"); + }); + } +} \ No newline at end of file