Basic order parsing and a unit test

This commit is contained in:
Tim Van Baak 2024-08-27 02:43:12 +00:00
parent 24e80af7ef
commit 4f276df6c1
7 changed files with 152 additions and 30 deletions

View File

@ -59,6 +59,12 @@ public class Location
return (split[0], split[1]); return (split[0], split[1]);
} }
/// <summary>
/// Whether a name is the name or abbreviation of this location.
/// </summary>
public bool Is(string name)
=> name.EqualsAnyCase(Name) || name.EqualsAnyCase(Abbreviation);
public override string ToString() public override string ToString()
{ {
return this.Name == "land" || this.Name == "water" return this.Name == "land" || this.Name == "water"

View File

@ -57,12 +57,8 @@ public class Map
/// Get a province by name. Throws if the province is not found. /// Get a province by name. Throws if the province is not found.
/// </summary> /// </summary>
private static Province GetProvince(string provinceName, IEnumerable<Province> provinces) private static Province GetProvince(string provinceName, IEnumerable<Province> provinces)
=> provinces.SingleOrDefault( => provinces.SingleOrDefault(province => province!.Is(provinceName), null)
p => p!.Name.Equals(provinceName, StringComparison.InvariantCultureIgnoreCase) ?? throw new KeyNotFoundException($"Province {provinceName} not found");
|| p.Abbreviations.Any(
a => a.Equals(provinceName, StringComparison.InvariantCultureIgnoreCase)),
null)
?? throw new KeyNotFoundException($"Province {provinceName} not found");
/// <summary> /// <summary>
/// Get the location in a province matching a predicate. Throws if there is not exactly one /// Get the location in a province matching a predicate. Throws if there is not exactly one

View File

@ -50,6 +50,12 @@ public class Province
return this.Name; return this.Name;
} }
/// <summary>
/// Whether a name is the name or abbreviation of this province.
/// </summary>
public bool Is(string name)
=> name.EqualsAnyCase(Name) || Abbreviations.Any(name.EqualsAnyCase);
/// <summary> /// <summary>
/// Create a new province with no supply center. /// Create a new province with no supply center.
/// </summary> /// </summary>

View File

@ -174,7 +174,6 @@ public class OrderRegex(World world)
$"^{world.Map.PowerRegex} {Type} {world.Map.ProvinceRegex}(?:{SlashLocation}|{ParenLocation})?$"); $"^{world.Map.PowerRegex} {Type} {world.Map.ProvinceRegex}(?:{SlashLocation}|{ParenLocation})?$");
Match match = reUnit.Match(unitSpec); Match match = reUnit.Match(unitSpec);
if (!match.Success) { if (!match.Success) {
Console.WriteLine($"Could not match unit spec \"{unitSpec}\"");
return false; return false;
} }
@ -196,28 +195,123 @@ public class OrderRegex(World world)
return true; 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; order = null;
OrderRegex re = new(world); 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();
}
} }
}
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;
}
}

View File

@ -55,7 +55,7 @@ public class World
public Dictionary<string, OrderHistory> OrderHistory { get; } public Dictionary<string, OrderHistory> OrderHistory { get; }
/// <summary> /// <summary>
/// The shared timeline number generator. /// The state of the multiverse.
/// </summary> /// </summary>
public Timelines Timelines { get; } public Timelines Timelines { get; }

View File

@ -15,7 +15,7 @@ public class MoveOrder : UnitOrder
public Season Season { get; } public Season Season { get; }
/// <summary> /// <summary>
/// The destination location to which the unit should move. /// The destination province/location to which the unit should move.
/// </summary> /// </summary>
public string Location { get; } public string Location { get; }

View File

@ -1,6 +1,7 @@
using NUnit.Framework; using NUnit.Framework;
using MultiversalDiplomacy.Model; using MultiversalDiplomacy.Model;
using MultiversalDiplomacy.Orders;
namespace MultiversalDiplomacyTests; namespace MultiversalDiplomacyTests;
@ -196,4 +197,23 @@ public class RegexTest
Assert.That(actual, Is.EquivalentTo(expected), "Unexpected parse results"); Assert.That(actual, Is.EquivalentTo(expected), "Unexpected parse results");
Assert.That(actual, Is.EqualTo(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>());
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"));
}
} }