diff --git a/MultiversalDiplomacy/Script/AdjudicationQueryScriptHandler.cs b/MultiversalDiplomacy/Script/AdjudicationQueryScriptHandler.cs index 236bff7..5128a64 100644 --- a/MultiversalDiplomacy/Script/AdjudicationQueryScriptHandler.cs +++ b/MultiversalDiplomacy/Script/AdjudicationQueryScriptHandler.cs @@ -58,6 +58,14 @@ public class AdjudicationQueryScriptHandler( 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]) { @@ -67,28 +75,56 @@ public class AdjudicationQueryScriptHandler( case "false": return ScriptResult.Fail("assert false", this); - case "order-valid": - case "order-invalid": - OrderParser re = new(World); - Regex prov = new($"^{re.FullLocation}$", RegexOptions.IgnoreCase); - Match match = prov.Match(args[1]); - if (!match.Success) return ScriptResult.Fail($"Could not parse province from \"{args[1]}\"", 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]); - string timeline = match.Groups[1].Length > 0 + timeline = match.Groups[1].Length > 0 ? match.Groups[1].Value : Season.First.Timeline; - var seasonsInTimeline = World.Timelines.Seasons.Where(season => season.Timeline == timeline); + seasonsInTimeline = World.Timelines.Seasons.Where(season => season.Timeline == timeline); if (!seasonsInTimeline.Any()) return ScriptResult.Fail($"No seasons in timeline {timeline}", this); - int turn = match.Groups[4].Length > 0 + 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 season = new(timeline, turn); + season = new(timeline, turn); - Province province = World.Map.Provinces.Single(province => province.Is(match.Groups[2].Value)); + 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 @@ -106,20 +142,20 @@ public class AdjudicationQueryScriptHandler( case "has-past": Regex hasPast = new($"^([a-z]+[0-9]+)>([a-z]+[0-9]+)$"); - Match hpMatch = hasPast.Match(args[1]); - if (!hpMatch.Success) return ScriptResult.Fail("Expected format s1>s2", this); + match = hasPast.Match(args[1]); + if (!match.Success) return ScriptResult.Fail("Expected format s1>s2", this); - Season future = new(hpMatch.Groups[1].Value); + 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(hpMatch.Groups[2].Value); + 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 "holds": + case "not-dislodged": case "dislodged": re = new(World); prov = new($"^{re.FullLocation}$", RegexOptions.IgnoreCase); @@ -149,7 +185,7 @@ public class AdjudicationQueryScriptHandler( if (!matchingDislodges.Any()) return ScriptResult.Fail("No matching dislodge decisions"); var isDislodged = matchingDislodges.Cast().First(); - if (args[0] == "holds" && isDislodged.Outcome != false) { + if (args[0] == "not-dislodged" && isDislodged.Outcome != false) { return ScriptResult.Fail($"Adjudication {isDislodged} is true"); } if (args[0] == "dislodged" && isDislodged.Outcome != true) { diff --git a/MultiversalDiplomacyTests/ReplTest.cs b/MultiversalDiplomacyTests/ReplTest.cs index c90dd26..36ea9ec 100644 --- a/MultiversalDiplomacyTests/ReplTest.cs +++ b/MultiversalDiplomacyTests/ReplTest.cs @@ -133,6 +133,24 @@ public class ReplTest repl.AssertFails("assert has-past a2>a1"); } + [Test] + public void AssertHoldOrder() + { + var repl = StandardRepl(); + + repl.ExecuteAll(""" + unit Germany A Mun + --- + """); + repl.AssertFails("Germany A Mun - The Sun"); + repl.Execute("---"); + + // Order is invalid + repl.Execute("assert hold-order Mun"); + // order-invalid requires the order be parsable, which this isn't + repl.AssertFails("assert order-invalid Mun"); + } + [Test] public void AssertMovement() { @@ -241,7 +259,7 @@ public class ReplTest """); // Move repelled - repl.Execute("assert holds Tyr"); + repl.Execute("assert not-dislodged Tyr"); repl.AssertFails("assert dislodged Tyr"); repl.ExecuteAll(""" @@ -253,6 +271,6 @@ public class ReplTest // Move succeeds repl.Execute("assert dislodged Tyr"); - repl.AssertFails("assert holds Tyr"); + repl.AssertFails("assert not-dislodged Tyr"); } }