452 lines
18 KiB
C#
452 lines
18 KiB
C#
using System.Diagnostics.CodeAnalysis;
|
|
using System.Text.RegularExpressions;
|
|
|
|
using MultiversalDiplomacy.Orders;
|
|
|
|
namespace MultiversalDiplomacy.Model;
|
|
|
|
/// <summary>
|
|
/// This class defines the regular expressions that are used to build up larger expressions for matching orders
|
|
/// and other script inputs. It also provides helper functions to extract the captured order elements as tuples,
|
|
/// which function as the structured intermediate representation between raw user input and full Order objects.
|
|
/// </summary>
|
|
public class OrderParser(World world)
|
|
{
|
|
public const string Type = "(A|F|Army|Fleet)";
|
|
|
|
public const string Timeline = "([A-Za-z]+)";
|
|
|
|
public const string Turn = "([0-9]+)";
|
|
|
|
public const string SlashLocation = "(?:/([A-Za-z]+))";
|
|
|
|
public const string ParenLocation = "(?:\\(([A-Za-z ]+)\\))";
|
|
|
|
public string FullLocation => $"(?:{Timeline}-)?{world.Map.ProvinceRegex}(?:{SlashLocation}|{ParenLocation})?(?:@{Turn})?";
|
|
|
|
public string UnitSpec => $"(?:{Type} )?{FullLocation}";
|
|
|
|
public const string HoldVerb = "(h|hold|holds)";
|
|
|
|
public const string MoveVerb = "(-|(?:->)|(?:=>)|(?:attack(?:s)?)|(?:move(?:s)?(?: to)?))";
|
|
|
|
public const string SupportVerb = "(s|support|supports)";
|
|
|
|
public const string ViaConvoy = "(convoy|via convoy|by convoy)";
|
|
|
|
public Regex UnitDeclaration = new(
|
|
$"^{world.Map.PowerRegex} {Type} {world.Map.ProvinceRegex}(?:{SlashLocation}|{ParenLocation})?$",
|
|
RegexOptions.IgnoreCase);
|
|
|
|
public static (
|
|
string power,
|
|
string type,
|
|
string province,
|
|
string location)
|
|
ParseUnitDeclaration(Match match) => (
|
|
match.Groups[1].Value,
|
|
match.Groups[2].Value,
|
|
match.Groups[3].Value,
|
|
match.Groups[4].Value.Length > 0
|
|
? match.Groups[4].Value
|
|
: match.Groups[5].Value);
|
|
|
|
public Regex Hold => new(
|
|
$"^{UnitSpec} {HoldVerb}$",
|
|
RegexOptions.IgnoreCase);
|
|
|
|
public static (
|
|
string type,
|
|
string timeline,
|
|
string province,
|
|
string location,
|
|
string turn,
|
|
string holdVerb)
|
|
ParseHold(Match match) => (
|
|
match.Groups[1].Value,
|
|
match.Groups[2].Value,
|
|
match.Groups[3].Value,
|
|
match.Groups[4].Length > 0
|
|
? match.Groups[4].Value
|
|
: match.Groups[5].Value,
|
|
match.Groups[6].Value,
|
|
match.Groups[7].Value);
|
|
|
|
public Regex Move => new(
|
|
$"^{UnitSpec} {MoveVerb} {FullLocation}(?: {ViaConvoy})?$",
|
|
RegexOptions.IgnoreCase);
|
|
|
|
public static (
|
|
string type,
|
|
string timeline,
|
|
string province,
|
|
string location,
|
|
string turn,
|
|
string moveVerb,
|
|
string destTimeline,
|
|
string destProvince,
|
|
string destLocation,
|
|
string destTurn,
|
|
string viaConvoy)
|
|
ParseMove(Match match) => (
|
|
match.Groups[1].Value,
|
|
match.Groups[2].Value,
|
|
match.Groups[3].Value,
|
|
match.Groups[4].Length > 0
|
|
? match.Groups[4].Value
|
|
: match.Groups[5].Value,
|
|
match.Groups[6].Value,
|
|
match.Groups[7].Value,
|
|
match.Groups[8].Value,
|
|
match.Groups[9].Value,
|
|
match.Groups[10].Length > 0
|
|
? match.Groups[10].Value
|
|
: match.Groups[11].Value,
|
|
match.Groups[12].Value,
|
|
match.Groups[13].Value);
|
|
|
|
public Regex SupportHold => new(
|
|
$"^{UnitSpec} {SupportVerb} {UnitSpec}$",
|
|
RegexOptions.IgnoreCase);
|
|
|
|
public static (
|
|
string type,
|
|
string timeline,
|
|
string province,
|
|
string location,
|
|
string turn,
|
|
string supportVerb,
|
|
string targetType,
|
|
string targetTimeline,
|
|
string targetProvince,
|
|
string targetLocation,
|
|
string targetTurn)
|
|
ParseSupportHold(Match match) => (
|
|
match.Groups[1].Value,
|
|
match.Groups[2].Value,
|
|
match.Groups[3].Value,
|
|
match.Groups[4].Length > 0
|
|
? match.Groups[4].Value
|
|
: match.Groups[5].Value,
|
|
match.Groups[6].Value,
|
|
match.Groups[7].Value,
|
|
match.Groups[8].Value,
|
|
match.Groups[9].Value,
|
|
match.Groups[10].Value,
|
|
match.Groups[11].Length > 0
|
|
? match.Groups[11].Value
|
|
: match.Groups[12].Value,
|
|
match.Groups[13].Value);
|
|
|
|
public Regex SupportMove => new(
|
|
$"{UnitSpec} {SupportVerb} {UnitSpec} {MoveVerb} {FullLocation}$",
|
|
RegexOptions.IgnoreCase);
|
|
|
|
public static (
|
|
string type,
|
|
string timeline,
|
|
string province,
|
|
string location,
|
|
string turn,
|
|
string supportVerb,
|
|
string targetType,
|
|
string targetTimeline,
|
|
string targetProvince,
|
|
string targetLocation,
|
|
string targetTurn,
|
|
string moveVerb,
|
|
string destTimeline,
|
|
string destProvince,
|
|
string destLocation,
|
|
string destTurn)
|
|
ParseSupportMove(Match match) => (
|
|
match.Groups[1].Value,
|
|
match.Groups[2].Value,
|
|
match.Groups[3].Value,
|
|
match.Groups[4].Length > 0
|
|
? match.Groups[4].Value
|
|
: match.Groups[5].Value,
|
|
match.Groups[6].Value,
|
|
match.Groups[7].Value,
|
|
match.Groups[8].Value,
|
|
match.Groups[9].Value,
|
|
match.Groups[10].Value,
|
|
match.Groups[11].Length > 0
|
|
? match.Groups[11].Value
|
|
: match.Groups[12].Value,
|
|
match.Groups[13].Value,
|
|
match.Groups[14].Value,
|
|
match.Groups[15].Value,
|
|
match.Groups[16].Value,
|
|
match.Groups[17].Length > 0
|
|
? match.Groups[17].Value
|
|
: match.Groups[18].Value,
|
|
match.Groups[19].Value);
|
|
|
|
public static bool TryParseUnit(World world, string unitSpec, [NotNullWhen(true)] out Unit? newUnit)
|
|
{
|
|
newUnit = null;
|
|
OrderParser re = new(world);
|
|
|
|
Match match = re.UnitDeclaration.Match(unitSpec);
|
|
if (!match.Success) return false;
|
|
var unit = ParseUnitDeclaration(match);
|
|
|
|
string power = world.Map.Powers.First(p => p.EqualsAnyCase(unit.power));
|
|
|
|
string typeName = Enum.GetNames<UnitType>().First(name => name.StartsWithAnyCase(unit.type));
|
|
UnitType type = Enum.Parse<UnitType>(typeName);
|
|
|
|
Province province = world.Map.Provinces.First(prov => prov.Is(unit.province));
|
|
Location? location;
|
|
if (unit.location.Length > 0) {
|
|
location = province.Locations.FirstOrDefault(loc => loc!.Is(unit.location), null);
|
|
} else {
|
|
location = type switch {
|
|
UnitType.Army => province.Locations.FirstOrDefault(loc => loc.Type == LocationType.Land),
|
|
UnitType.Fleet => province.Locations.FirstOrDefault(loc => loc.Type == LocationType.Water),
|
|
_ => null,
|
|
};
|
|
}
|
|
if (location is null) return false;
|
|
|
|
newUnit = Unit.Build(location.Key, Season.First, power, type);
|
|
return true;
|
|
}
|
|
|
|
public static bool TryParseOrder(World world, string power, string command, [NotNullWhen(true)] out Order? order)
|
|
{
|
|
order = null;
|
|
OrderParser re = new(world);
|
|
|
|
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) {
|
|
return TryParseSupportHoldOrder(world, power, sholdMatch, out order);
|
|
} else if (re.SupportMove.Match(command) is Match smoveMatch && smoveMatch.Success) {
|
|
return TryParseSupportMoveOrder(world, power, smoveMatch, out order);
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public static bool TryParseOrderSubject(
|
|
World world,
|
|
string parsedTimeline,
|
|
string parsedTurn,
|
|
string parsedProvince,
|
|
[NotNullWhen(true)] out Unit? subject)
|
|
{
|
|
subject = null;
|
|
|
|
string timeline = parsedTimeline.Length > 0
|
|
? parsedTimeline
|
|
// 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 = parsedTurn.Length > 0
|
|
? int.Parse(parsedTurn)
|
|
// 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(parsedProvince));
|
|
|
|
// 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.
|
|
subject = world.Units.FirstOrDefault(unit
|
|
=> world.Map.GetLocation(unit!.Location).ProvinceName == province.Name
|
|
&& unit!.Season.Timeline == timeline
|
|
&& unit!.Season.Turn == turn,
|
|
null);
|
|
return subject is not null;
|
|
}
|
|
|
|
public static bool TryParseHoldOrder(
|
|
World world,
|
|
string power,
|
|
Match match,
|
|
[NotNullWhen(true)] out Order? order)
|
|
{
|
|
order = null;
|
|
var hold = ParseHold(match);
|
|
|
|
if (!TryParseOrderSubject(world, hold.timeline, hold.turn, hold.province, out Unit? subject)) {
|
|
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);
|
|
|
|
if (!TryParseOrderSubject(world, move.timeline, move.turn, move.province, out Unit? subject)) {
|
|
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));
|
|
string? destLocationKey = null;
|
|
|
|
// DATC 4.B specifies how to interpret orders with missing or incorrect locations. These issues arise because
|
|
// of provinces with multiple locations of the same type, i.e. two-coast provinces in Classical. In general,
|
|
// DATC's only concern is to disambiguate the order, failing the order only when it is ineluctably ambiguous
|
|
// (4.B.1) or explicitly incorrect (4.B.3). Irrelevant or nonexistent locations can be ignored.
|
|
|
|
// If there is only one possible location for the moving unit, that location is used. The idea of land and
|
|
// water locations is an implementation detail of 5dplomacy and not part of the Diplomacy rules, so they will
|
|
// usually be omitted, and so moving an army to any land province or a fleet to a non-multi-coast province is
|
|
// naturally unambiguous even without the location.
|
|
var unitLocations = destProvince.Locations.Where(loc => loc.Type switch {
|
|
LocationType.Land => subject.Type == UnitType.Army,
|
|
LocationType.Water => subject.Type == UnitType.Fleet,
|
|
_ => false,
|
|
});
|
|
if (!unitLocations.Any()) return false; // If *no* locations match, the move is illegal
|
|
if (unitLocations.Count() == 1) destLocationKey ??= unitLocations.Single().Key;
|
|
|
|
// If more than one location is possible for the unit, the order must be disambiguated by the dest location
|
|
// or the physical realities of which coast is accessible. DATC 4.B.3 makes an order illegal if the location
|
|
// is specified but it isn't an accessible coast, so successfully specifying a location takes precedence over
|
|
// there being one accessible coast.
|
|
if (destLocationKey is null) {
|
|
var matchingLocations = unitLocations.Where(loc => loc.Is(move.destLocation));
|
|
if (matchingLocations.Any()) destLocationKey ??= matchingLocations.Single().Key;
|
|
}
|
|
|
|
// If the order location didn't disambiguate the coasts, either because it's missing or it's nonsense, the
|
|
// order can be disambiguated by there being one accessible coast from the order source.
|
|
if (destLocationKey is null) {
|
|
Location source = world.Map.GetLocation(subject.Location);
|
|
var accessibleLocations = destProvince.Locations.Where(loc => loc.Adjacents.Contains(source));
|
|
if (accessibleLocations.Count() == 1) destLocationKey ??= accessibleLocations.Single().Key;
|
|
}
|
|
|
|
// If the order is still ambiguous, fail per DATC 4.B.1.
|
|
if (destLocationKey is null) return false;
|
|
|
|
order = new MoveOrder(power, subject, new(destTimeline, destTurn), destLocationKey);
|
|
return true;
|
|
}
|
|
|
|
public static bool TryParseSupportHoldOrder(
|
|
World world,
|
|
string power,
|
|
Match match,
|
|
[NotNullWhen(true)] out Order? order)
|
|
{
|
|
order = null;
|
|
var support = ParseSupportHold(match);
|
|
|
|
if (!TryParseOrderSubject(world, support.timeline, support.turn, support.province, out Unit? subject)) {
|
|
return false;
|
|
}
|
|
|
|
if (!TryParseOrderSubject(
|
|
world, support.targetTimeline, support.targetTurn, support.targetProvince, out Unit? target))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
order = new SupportHoldOrder(power, subject, target);
|
|
return true;
|
|
}
|
|
|
|
public static bool TryParseSupportMoveOrder(
|
|
World world,
|
|
string power,
|
|
Match match,
|
|
[NotNullWhen(true)] out Order? order)
|
|
{
|
|
order = null;
|
|
var support = ParseSupportMove(match);
|
|
|
|
if (!TryParseOrderSubject(world, support.timeline, support.turn, support.province, out Unit? subject)) {
|
|
return false;
|
|
}
|
|
|
|
if (!TryParseOrderSubject(
|
|
world, support.targetTimeline, support.targetTurn, support.targetProvince, out Unit? target))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
string destTimeline = support.destTimeline.Length > 0
|
|
? support.destTimeline
|
|
// If the destination is unspecified, use the target's
|
|
: target.Season.Timeline;
|
|
|
|
int destTurn = support.destTurn.Length > 0
|
|
? int.Parse(support.destTurn)
|
|
// If the destination is unspecified, use the unit's
|
|
: target.Season.Turn;
|
|
|
|
var destProvince = world.Map.Provinces.Single(province => province.Is(support.destProvince));
|
|
string? destLocationKey = null;
|
|
|
|
// DATC 4.B specifies how to interpret orders with missing or incorrect locations. These issues arise because
|
|
// of provinces with multiple locations of the same type, i.e. two-coast provinces in Classical. In general,
|
|
// DATC's only concern is to disambiguate the order, failing the order only when it is ineluctably ambiguous
|
|
// (4.B.1) or explicitly incorrect (4.B.3). Irrelevant or nonexistent locations can be ignored.
|
|
|
|
// If there is only one possible location for the moving unit, that location is used. The idea of land and
|
|
// water locations is an implementation detail of 5dplomacy and not part of the Diplomacy rules, so they will
|
|
// usually be omitted, and so moving an army to any land province or a fleet to a non-multi-coast province is
|
|
// naturally unambiguous even without the location.
|
|
var unitLocations = destProvince.Locations.Where(loc => loc.Type switch {
|
|
LocationType.Land => target.Type == UnitType.Army,
|
|
LocationType.Water => target.Type == UnitType.Fleet,
|
|
_ => false,
|
|
});
|
|
if (!unitLocations.Any()) return false; // If *no* locations match, the move is illegal
|
|
if (unitLocations.Count() == 1) destLocationKey ??= unitLocations.Single().Key;
|
|
|
|
// If more than one location is possible for the unit, the order must be disambiguated by the dest location
|
|
// or the physical realities of which coast is accessible. DATC 4.B.3 makes an order illegal if the location
|
|
// is specified but it isn't an accessible coast, so successfully specifying a location takes precedence over
|
|
// there being one accessible coast.
|
|
if (destLocationKey is null) {
|
|
var matchingLocations = unitLocations.Where(loc => loc.Is(support.destLocation));
|
|
if (matchingLocations.Any()) destLocationKey ??= matchingLocations.Single().Key;
|
|
}
|
|
|
|
// If the order location didn't disambiguate the coasts, either because it's missing or it's nonsense, the
|
|
// order can be disambiguated by there being one accessible coast from the order source.
|
|
if (destLocationKey is null) {
|
|
Location source = world.Map.GetLocation(target.Location);
|
|
var accessibleLocations = destProvince.Locations.Where(loc => loc.Adjacents.Contains(source));
|
|
if (accessibleLocations.Count() == 1) destLocationKey ??= accessibleLocations.Single().Key;
|
|
}
|
|
|
|
// If the order is still ambiguous, fail per DATC 4.B.1. This also satisfies 4.B.4, which prefers for
|
|
// programmatic adjudicators with order validation to require the coasts instead of interpreting the ambiguous
|
|
// support by referring to the move order it supports.
|
|
if (destLocationKey is null) return false;
|
|
|
|
var destLocation = world.Map.GetLocation(destLocationKey);
|
|
order = new SupportMoveOrder(power, subject, target, new(destTimeline, destTurn), destLocation);
|
|
return true;
|
|
}
|
|
}
|