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"));
+ }
}