318 lines
12 KiB
C#
318 lines
12 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 OrderRegex(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 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;
|
|
|
|
Regex reUnit = new(
|
|
$"^{world.Map.PowerRegex} {Type} {world.Map.ProvinceRegex}(?:{SlashLocation}|{ParenLocation})?$");
|
|
Match match = reUnit.Match(unitSpec);
|
|
if (!match.Success) {
|
|
return false;
|
|
}
|
|
|
|
string power = world.Map.Powers.First(p => p.EqualsAnyCase(match.Groups[1].Value));
|
|
|
|
string typeName = Enum.GetNames<UnitType>().First(name => name.StartsWithAnyCase(match.Groups[2].Value));
|
|
UnitType type = Enum.Parse<UnitType>(typeName);
|
|
|
|
Province province = world.Map.Provinces.First(prov
|
|
=> prov.Name.EqualsAnyCase(match.Groups[3].Value)
|
|
|| prov.Abbreviations.Any(abv => abv.EqualsAnyCase(match.Groups[3].Value)));
|
|
|
|
string locationName = match.Groups[4].Length > 0 ? match.Groups[4].Value : match.Groups[5].Value;
|
|
Location location = province.Locations.First(loc
|
|
=> loc.Name.StartsWithAnyCase(locationName)
|
|
|| loc.Abbreviation.StartsWithAnyCase(locationName));
|
|
|
|
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;
|
|
OrderRegex 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) {
|
|
// 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;
|
|
}
|
|
}
|