Basic order parsing and a unit test
This commit is contained in:
parent
24e80af7ef
commit
4f276df6c1
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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; }
|
||||||
|
|
||||||
|
|
|
@ -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; }
|
||||||
|
|
||||||
|
|
|
@ -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"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue