using System.Text.RegularExpressions; using MultiversalDiplomacy.Adjudicate; using MultiversalDiplomacy.Adjudicate.Decision; using MultiversalDiplomacy.Model; using MultiversalDiplomacy.Orders; namespace MultiversalDiplomacy.Script; public class AdjudicationQueryScriptHandler( Action WriteLine, List validations, List adjudications, World world, IPhaseAdjudicator adjudicator) : IScriptHandler { public string Prompt => "valid> "; public List Validations { get; } = validations; public List Adjudications { get; } = adjudications; public World World { get; private set; } = world; public ScriptResult HandleInput(string input) { var args = input.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries); if (args.Length == 0 || input.StartsWith('#')) { return ScriptResult.Succeed(this); } var command = args[0]; switch (command) { case "---": WriteLine("Ready for orders"); return ScriptResult.Succeed(new GameScriptHandler(WriteLine, World, adjudicator)); case "assert" when args.Length == 1: WriteLine("Usage:"); break; case "assert": return EvaluateAssertion(args[1]); case "status": throw new NotImplementedException(); default: return ScriptResult.Fail($"Unrecognized command: \"{command}\"", this); } return ScriptResult.Succeed(this); } private ScriptResult EvaluateAssertion(string assertion) { var args = assertion.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries); OrderParser re = new(World); Regex prov = new($"^{re.FullLocation}$", RegexOptions.IgnoreCase); Match match; string timeline; IEnumerable seasonsInTimeline; int turn; Season season; Province province; switch (args[0]) { case "true": return ScriptResult.Succeed(this); case "false": return ScriptResult.Fail("assert false", this); case "hold-order": // The hold-order assertion primarily serves to verify that a unit's order was illegal in cases where // a written non-hold order was rejected before order validation and replaced with a hold order. match = prov.Match(args[1]); timeline = match.Groups[1].Length > 0 ? match.Groups[1].Value : Season.First.Timeline; seasonsInTimeline = World.Timelines.Seasons.Where(season => season.Timeline == timeline); if (!seasonsInTimeline.Any()) return ScriptResult.Fail($"No seasons in timeline {timeline}", this); turn = match.Groups[4].Length > 0 ? int.Parse(match.Groups[4].Value) // If turn is unspecified, use the second-latest turn in the timeline, // since we want to assert against the subjects of the orders just adjudicated, // and adjudication created a new set of seasons. : seasonsInTimeline.Max(season => season.Turn) - 1; season = new(timeline, turn); province = World.Map.Provinces.Single(province => province.Is(match.Groups[2].Value)); var matchingHolds = Validations.Where(val => val.Valid && val.Order is HoldOrder hold && hold.Unit.Season == season && World.Map.GetLocation(hold.Unit.Location).ProvinceName == province.Name); if (!matchingHolds.Any()) return ScriptResult.Fail("No matching holds"); return ScriptResult.Succeed(this); case "order-valid": case "order-invalid": match = prov.Match(args[1]); if (!match.Success) return ScriptResult.Fail($"Could not parse province from \"{args[1]}\"", this); timeline = match.Groups[1].Length > 0 ? match.Groups[1].Value : Season.First.Timeline; seasonsInTimeline = World.Timelines.Seasons.Where(season => season.Timeline == timeline); if (!seasonsInTimeline.Any()) return ScriptResult.Fail($"No seasons in timeline {timeline}", this); turn = match.Groups[4].Length > 0 ? int.Parse(match.Groups[4].Value) // If turn is unspecified, use the second-latest turn in the timeline, // since we want to assert against the subjects of the orders just adjudicated, // and adjudication created a new set of seasons. : seasonsInTimeline.Max(season => season.Turn) - 1; season = new(timeline, turn); province = World.Map.Provinces.Single(province => province.Is(match.Groups[2].Value)); var matching = Validations.Where(val => val.Order is UnitOrder order && order.Unit.Season == season && World.Map.GetLocation(order.Unit.Location).ProvinceName == province.Name); if (!matching.Any()) return ScriptResult.Fail("No matching validations"); if (args[0] == "order-valid" && !matching.First().Valid) { return ScriptResult.Fail($"Order \"{matching.First().Order} is invalid"); } if (args[0] == "order-invalid" && matching.First().Valid) { return ScriptResult.Fail($"Order \"{matching.First().Order} is valid"); } return ScriptResult.Succeed(this); case "has-past": Regex hasPast = new($"^([a-z]+[0-9]+)>([a-z]+[0-9]+)$"); match = hasPast.Match(args[1]); if (!match.Success) return ScriptResult.Fail("Expected format s1>s2", this); Season future = new(match.Groups[1].Value); if (!World.Timelines.Pasts.TryGetValue(future.Key, out Season? actual)) { return ScriptResult.Fail($"No such season \"{future}\""); } Season expected = new(match.Groups[2].Value); if (actual != expected) return ScriptResult.Fail( $"Expected past of {future} to be {expected}, but it was {actual}"); return ScriptResult.Succeed(this); case "not-dislodged": case "dislodged": re = new(World); prov = new($"^{re.FullLocation}$", RegexOptions.IgnoreCase); match = prov.Match(args[1]); if (!match.Success) return ScriptResult.Fail($"Could not parse province from \"{args[1]}\"", this); timeline = match.Groups[1].Length > 0 ? match.Groups[1].Value : Season.First.Timeline; seasonsInTimeline = World.Timelines.Seasons.Where(season => season.Timeline == timeline); if (!seasonsInTimeline.Any()) return ScriptResult.Fail($"No seasons in timeline {timeline}", this); turn = match.Groups[4].Length > 0 ? int.Parse(match.Groups[4].Value) // If turn is unspecified, use the second-latest turn in the timeline, // since we want to assert against the subjects of the orders just adjudicated, // and adjudication created a new set of seasons. : seasonsInTimeline.Max(season => season.Turn) - 1; season = new(timeline, turn); province = World.Map.Provinces.Single(province => province.Is(match.Groups[2].Value)); var matchingDislodges = Adjudications.Where(adj => adj is IsDislodged dislodge && dislodge.Order.Unit.Season == season && World.Map.GetLocation(dislodge.Order.Unit.Location).ProvinceName == province.Name); if (!matchingDislodges.Any()) return ScriptResult.Fail("No matching dislodge decisions"); var isDislodged = matchingDislodges.Cast().First(); if (args[0] == "not-dislodged" && isDislodged.Outcome != false) { return ScriptResult.Fail($"Adjudication {isDislodged} is true"); } if (args[0] == "dislodged" && isDislodged.Outcome != true) { return ScriptResult.Fail($"Adjudication {isDislodged} is false"); } return ScriptResult.Succeed(this); case "moves": case "no-move": re = new(World); prov = new($"^{re.FullLocation}$", RegexOptions.IgnoreCase); match = prov.Match(args[1]); if (!match.Success) return ScriptResult.Fail($"Could not parse province from \"{args[1]}\"", this); timeline = match.Groups[1].Length > 0 ? match.Groups[1].Value : Season.First.Timeline; seasonsInTimeline = World.Timelines.Seasons.Where(season => season.Timeline == timeline); if (!seasonsInTimeline.Any()) return ScriptResult.Fail($"No seasons in timeline {timeline}", this); turn = match.Groups[4].Length > 0 ? int.Parse(match.Groups[4].Value) // If turn is unspecified, use the second-latest turn in the timeline, // since we want to assert against the subjects of the orders just adjudicated, // and adjudication created a new set of seasons. : seasonsInTimeline.Max(season => season.Turn) - 1; season = new(timeline, turn); province = World.Map.Provinces.Single(province => province.Is(match.Groups[2].Value)); var matchingMoves = Adjudications.Where(adj => adj is DoesMove moves && moves.Order.Unit.Season == season && World.Map.GetLocation(moves.Order.Unit.Location).ProvinceName == province.Name); if (!matchingMoves.Any()) return ScriptResult.Fail("No matching movement decisions"); var doesMove = matchingMoves.Cast().First(); if (args[0] == "moves" && doesMove.Outcome != true) { return ScriptResult.Fail($"Adjudication {doesMove} is false"); } if (args[0] == "no-move" && doesMove.Outcome != false) { return ScriptResult.Fail($"Adjudication {doesMove} is true"); } return ScriptResult.Succeed(this); case "support-given": case "support-cut": re = new(World); prov = new($"^{re.FullLocation}$", RegexOptions.IgnoreCase); match = prov.Match(args[1]); if (!match.Success) return ScriptResult.Fail($"Could not parse province from \"{args[1]}\"", this); timeline = match.Groups[1].Length > 0 ? match.Groups[1].Value : Season.First.Timeline; seasonsInTimeline = World.Timelines.Seasons.Where(season => season.Timeline == timeline); if (!seasonsInTimeline.Any()) return ScriptResult.Fail($"No seasons in timeline {timeline}", this); turn = match.Groups[4].Length > 0 ? int.Parse(match.Groups[4].Value) // If turn is unspecified, use the second-latest turn in the timeline, // since we want to assert against the subjects of the orders just adjudicated, // and adjudication created a new set of seasons. : seasonsInTimeline.Max(season => season.Turn) - 1; season = new(timeline, turn); province = World.Map.Provinces.Single(province => province.Is(match.Groups[2].Value)); var matchingSupports = Adjudications.Where(adj => adj is GivesSupport sup && sup.Order.Unit.Season == season && World.Map.GetLocation(sup.Order.Unit.Location).ProvinceName == province.Name); if (!matchingSupports.Any()) return ScriptResult.Fail("No matching support decisions"); var supports = matchingSupports.Cast().First(); if (args[0] == "support-given" && supports.Outcome != true) { return ScriptResult.Fail($"Adjudication {supports} is false"); } if (args[0] == "support-cut" && supports.Outcome != false) { return ScriptResult.Fail($"Adjudication {supports} is true"); } return ScriptResult.Succeed(this); default: return ScriptResult.Fail($"Unknown assertion \"{args[0]}\"", this); } } }