using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
using MultiversalDiplomacy.Orders;
namespace MultiversalDiplomacy.Model;
///
/// 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.
///
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().First(name => name.StartsWithAnyCase(unit.type));
UnitType type = Enum.Parse(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));
// 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;
}
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));
// 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.
// Note that target is used instead of subject, since it is possible to support a move to an inaccessible
// coast as long as the subject can reach the province and the target can reach the location.
var unitLocations = destProvince.Locations.Where(loc => loc.Type switch {
LocationType.Land => target.Type == UnitType.Army,
LocationType.Water => target.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(support.destLocation));
// If one location matched, use that location. If the coast is inaccessible to the target, 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;
}
var destLocation = world.Map.GetLocation(destLocationKey);
order = new SupportMoveOrder(power, subject, target, new(destTimeline, destTurn), destLocation);
return true;
}
}