From 4f276df6c1bec660f0d83e17c61baa8d957527b1 Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Tue, 27 Aug 2024 02:43:12 +0000 Subject: [PATCH] Basic order parsing and a unit test --- MultiversalDiplomacy/Model/Location.cs | 6 + MultiversalDiplomacy/Model/Map.cs | 8 +- MultiversalDiplomacy/Model/Province.cs | 6 + MultiversalDiplomacy/Model/Regex.cs | 138 +++++++++++++++++++---- MultiversalDiplomacy/Model/World.cs | 2 +- MultiversalDiplomacy/Orders/MoveOrder.cs | 2 +- MultiversalDiplomacyTests/RegexTest.cs | 20 ++++ 7 files changed, 152 insertions(+), 30 deletions(-) diff --git a/MultiversalDiplomacy/Model/Location.cs b/MultiversalDiplomacy/Model/Location.cs index c25079f..f83f849 100644 --- a/MultiversalDiplomacy/Model/Location.cs +++ b/MultiversalDiplomacy/Model/Location.cs @@ -59,6 +59,12 @@ public class Location return (split[0], split[1]); } + /// + /// Whether a name is the name or abbreviation of this location. + /// + public bool Is(string name) + => name.EqualsAnyCase(Name) || name.EqualsAnyCase(Abbreviation); + public override string ToString() { return this.Name == "land" || this.Name == "water" diff --git a/MultiversalDiplomacy/Model/Map.cs b/MultiversalDiplomacy/Model/Map.cs index db258d6..d1f76a0 100644 --- a/MultiversalDiplomacy/Model/Map.cs +++ b/MultiversalDiplomacy/Model/Map.cs @@ -57,12 +57,8 @@ public class Map /// Get a province by name. Throws if the province is not found. /// private static Province GetProvince(string provinceName, IEnumerable provinces) - => provinces.SingleOrDefault( - p => p!.Name.Equals(provinceName, StringComparison.InvariantCultureIgnoreCase) - || p.Abbreviations.Any( - a => a.Equals(provinceName, StringComparison.InvariantCultureIgnoreCase)), - null) - ?? throw new KeyNotFoundException($"Province {provinceName} not found"); + => provinces.SingleOrDefault(province => province!.Is(provinceName), null) + ?? throw new KeyNotFoundException($"Province {provinceName} not found"); /// /// Get the location in a province matching a predicate. Throws if there is not exactly one diff --git a/MultiversalDiplomacy/Model/Province.cs b/MultiversalDiplomacy/Model/Province.cs index ede7b38..19bbd6d 100644 --- a/MultiversalDiplomacy/Model/Province.cs +++ b/MultiversalDiplomacy/Model/Province.cs @@ -50,6 +50,12 @@ public class Province return this.Name; } + /// + /// Whether a name is the name or abbreviation of this province. + /// + public bool Is(string name) + => name.EqualsAnyCase(Name) || Abbreviations.Any(name.EqualsAnyCase); + /// /// Create a new province with no supply center. /// diff --git a/MultiversalDiplomacy/Model/Regex.cs b/MultiversalDiplomacy/Model/Regex.cs index 7667481..938bfce 100644 --- a/MultiversalDiplomacy/Model/Regex.cs +++ b/MultiversalDiplomacy/Model/Regex.cs @@ -174,7 +174,6 @@ public class OrderRegex(World world) $"^{world.Map.PowerRegex} {Type} {world.Map.ProvinceRegex}(?:{SlashLocation}|{ParenLocation})?$"); Match match = reUnit.Match(unitSpec); if (!match.Success) { - Console.WriteLine($"Could not match unit spec \"{unitSpec}\""); return false; } @@ -196,28 +195,123 @@ public class OrderRegex(World world) return true; } - public static bool TryParseOrder(World world, string power, string command, out Order? order) { + public static bool TryParseOrder(World world, string power, string command, [NotNullWhen(true)] out Order? order) { order = null; OrderRegex re = new(world); - Match match; - if ((match = re.Hold.Match(command)).Success) { - var hold = ParseHold(match); - Unit unit = world.Units.First(unit - => world.Map.GetLocation(unit.Location).ProvinceName == hold.province - && unit.Season.Timeline == (hold.timeline.Length > 0 ? hold.timeline : "a") - && unit.Season.Turn.ToString() == (hold.turn.Length > 0 ? hold.turn : "0")); - order = new HoldOrder(power, unit); - return true; - } else if ((match = re.Move.Match(command)).Success) { - var move = ParseMove(match); - Unit unit = world.Units.First(unit - => world.Map.GetLocation(unit.Location).ProvinceName == move.province - && unit.Season.Timeline == (move.timeline.Length > 0 ? move.timeline : "a") - && unit.Season.Turn.ToString() == (move.turn.Length > 0 ? move.turn : "0")); - order = new MoveOrder(power, unit, Season.First, "l"); - return true; - } - return false; + if (re.Hold.Match(command) is Match holdMatch && holdMatch.Success) { + return TryParseHoldOrder(world, power, holdMatch, out order); + } else if (re.Move.Match(command) is Match moveMatch && moveMatch.Success) { + return TryParseMoveOrder(world, power, moveMatch, out order); + } else if (re.SupportHold.Match(command) is Match sholdMatch && sholdMatch.Success) { + // DATC 4.B.4: coast specification in support orders + throw new NotImplementedException(); + } else if (re.SupportMove.Match(command) is Match smoveMatch && smoveMatch.Success) { + throw new NotImplementedException(); + } else { + throw new NotImplementedException(); + } } -} \ No newline at end of file + + public static bool TryParseHoldOrder(World world, string power, Match match, [NotNullWhen(true)] out Order? order) + { + order = null; + var hold = ParseHold(match); + + string timeline = hold.timeline.Length > 0 + ? hold.timeline + // If timeline is unspecified, use the root timeline + : Season.First.Timeline; + var seasonsInTimeline = world.Timelines.Seasons.Where(season => season.Timeline == timeline); + if (!seasonsInTimeline.Any()) return false; + + int turn = hold.turn.Length > 0 + ? int.Parse(hold.turn) + // If turn is unspecified, use the latest turn in the timeline + : seasonsInTimeline.Max(season => season.Turn); + + Province province = world.Map.Provinces.Single(province => province.Is(hold.province)); + + Unit? subject = world.Units.FirstOrDefault(unit + => world.Map.GetLocation(unit!.Location).ProvinceName == province.Name + && unit!.Season.Timeline == timeline + && unit!.Season.Turn == turn, + null); + if (subject is null) return false; + + order = new HoldOrder(power, subject); + return true; + } + + public static bool TryParseMoveOrder(World world, string power, Match match, [NotNullWhen(true)] out Order? order) + { + order = null; + var move = ParseMove(match); + + string timeline = move.timeline.Length > 0 + ? move.timeline + // If timeline is unspecified, use the root timeline + : Season.First.Timeline; + var seasonsInTimeline = world.Timelines.Seasons.Where(season => season.Timeline == timeline); + if (!seasonsInTimeline.Any()) return false; + + int turn = move.turn.Length > 0 + ? int.Parse(move.turn) + // If turn is unspecified, use the latest turn in the timeline + : seasonsInTimeline.Max(season => season.Turn); + + Province province = world.Map.Provinces.Single(province => province.Is(move.province)); + + // Because only one unit can be in a province at a time, the province is sufficient to identify the subject + // and the location is ignored. This also satisfies DATC 4.B.5, which requires that a wrong coast for the + // subject be ignored. + Unit? subject = world.Units.FirstOrDefault(unit + => world.Map.GetLocation(unit!.Location).ProvinceName == province.Name + && unit!.Season.Timeline == timeline + && unit!.Season.Turn == turn, + null); + if (subject is null) return false; + + string destTimeline = move.destTimeline.Length > 0 + ? move.destTimeline + // If the destination is unspecified, use the unit's + : subject.Season.Timeline; + + int destTurn = move.destTurn.Length > 0 + ? int.Parse(move.destTurn) + // If the destination is unspecified, use the unit's + : subject.Season.Turn; + + var destProvince = world.Map.Provinces.Single(province => province.Is(move.destProvince)); + + // DATC 4.B.6 requires that "irrelevant" locations like army to Spain nc be ignored. + // To satisfy this, any location of the wrong type is categorically ignored, so for an army the + // "north coast" location effectively doesn't exist here. + var unitLocations = destProvince.Locations.Where(loc => loc.Type switch { + LocationType.Land => subject.Type == UnitType.Army, + LocationType.Water => subject.Type == UnitType.Fleet, + _ => false, + }); + // DATC 4.6.B also requires that unknown coasts be ignored. To satisfy this, an additional filter by name. + // Doing both of these filters means "A - Spain/nc" is as meaningful as "F - Spain/wc". + var matchingLocations = unitLocations.Where(loc => loc.Is(move.destLocation)); + + // If one location matched, use that location. If the coast is inaccessible to the subject, the order will + // be invalidated by a path check later to satisfy DATC 4.B.3. + string? destLocationKey = matchingLocations.FirstOrDefault(defaultValue: null)?.Key; + + if (destLocationKey is null) { + // If no location matched, location was omitted, nonexistent, or the wrong type. + // If one location is accessible, DATC 4.B.2 requires that it be used. + // If more than one location is accessible, DATC 4.B.1 requires the order fail. + + // TODO check which locations are accessible per the above + destLocationKey = unitLocations.First().Key; + + // return false; + } + + order = new MoveOrder(power, subject, new(destTimeline, destTurn), destLocationKey); + return true; + } +} diff --git a/MultiversalDiplomacy/Model/World.cs b/MultiversalDiplomacy/Model/World.cs index 702f2d5..4aa6b9f 100644 --- a/MultiversalDiplomacy/Model/World.cs +++ b/MultiversalDiplomacy/Model/World.cs @@ -55,7 +55,7 @@ public class World public Dictionary OrderHistory { get; } /// - /// The shared timeline number generator. + /// The state of the multiverse. /// public Timelines Timelines { get; } diff --git a/MultiversalDiplomacy/Orders/MoveOrder.cs b/MultiversalDiplomacy/Orders/MoveOrder.cs index a15d366..36e3c46 100644 --- a/MultiversalDiplomacy/Orders/MoveOrder.cs +++ b/MultiversalDiplomacy/Orders/MoveOrder.cs @@ -15,7 +15,7 @@ public class MoveOrder : UnitOrder public Season Season { get; } /// - /// The destination location to which the unit should move. + /// The destination province/location to which the unit should move. /// public string Location { get; } diff --git a/MultiversalDiplomacyTests/RegexTest.cs b/MultiversalDiplomacyTests/RegexTest.cs index 2f8b7ab..d6aeb71 100644 --- a/MultiversalDiplomacyTests/RegexTest.cs +++ b/MultiversalDiplomacyTests/RegexTest.cs @@ -1,6 +1,7 @@ using NUnit.Framework; using MultiversalDiplomacy.Model; +using MultiversalDiplomacy.Orders; namespace MultiversalDiplomacyTests; @@ -196,4 +197,23 @@ public class RegexTest Assert.That(actual, Is.EquivalentTo(expected), "Unexpected parse results"); Assert.That(actual, Is.EqualTo(expected), "Unexpected parse results"); } + + [Test] + public void OrderParsingTest() + { + World world = World.WithStandardMap().AddUnits("Germany A Mun"); + OrderRegex re = new(world); + + var match = re.Move.Match("A Mun - Tyr"); + var success = OrderRegex.TryParseMoveOrder(world, "Germany", match, out Order? order); + + Assert.That(success, Is.True); + Assert.That(order, Is.TypeOf()); + MoveOrder move = (MoveOrder)order!; + + Assert.That(move.Power, Is.EqualTo("Germany")); + Assert.That(move.Unit.Key, Is.EqualTo("A a-Munich/l@0")); + Assert.That(move.Location, Is.EqualTo("Tyrolia/l")); + Assert.That(move.Season.Key, Is.EqualTo("a0")); + } }