Compare commits

...

20 Commits

Author SHA1 Message Date
Tim Van Baak 7c0cdb0a21 Add the rest of DATC 6.A to the script tests 2024-09-06 16:17:55 +00:00
Tim Van Baak 5e2d495fa5 Update script tests 2024-09-06 16:04:25 +00:00
Tim Van Baak 7773c571e3 Add hold-order assertion for unparseable orders 2024-09-06 16:03:43 +00:00
Tim Van Baak 4096e4d517 Implement coastal accessibility checks for support-move orders 2024-09-06 15:19:58 +00:00
Tim Van Baak aaf2e78730 Implement coastal accessibility checks for move orders 2024-09-06 15:09:30 +00:00
Tim Van Baak 864a933ba0 Implement dislodge/hold assertions 2024-09-05 23:16:22 +00:00
Tim Van Baak c6f10868ae Implement support assertions 2024-09-05 23:11:29 +00:00
Tim Van Baak 5b32786904 Implement support-move parsing 2024-09-05 05:27:48 +00:00
Tim Van Baak ae5eb22010 Implement support-hold parsing 2024-09-05 05:22:07 +00:00
Tim Van Baak 26f7cee070 Add unit tests for move location disambiguation
Some of the coastal tests fail because the coast accessibility check isn't implemented yet
2024-09-05 05:11:12 +00:00
Tim Van Baak 80f340c0b2 Implement assert moves/no-move 2024-09-03 04:25:37 +00:00
Tim Van Baak e9c9999268 Implement assert has-past 2024-09-03 03:49:38 +00:00
Tim Van Baak 4fee854c4c Refactor script handlers to return a result type
This moves the point of strictness from the handler to the driver, which makes more sense and keeps it in one place. Drivers choose to be strict when a script result is a failure but still gives a continuation handler. The CLI driver prints an error and continues, while the test driver fails if it wasn't expecting the failure.
2024-09-03 03:20:59 +00:00
Tim Van Baak 569c9021e6 Disable some broken tests for now 2024-09-02 19:52:27 +00:00
Tim Van Baak 3984b814ca Implement assert order-valid 2024-09-01 04:54:28 +00:00
Tim Van Baak f18147f666 Enable suppressing adjudicator output in tests 2024-08-28 21:27:35 +00:00
Tim Van Baak 9f52c78b40 Enable repl output to /dev/null 2024-08-28 21:10:41 +00:00
Tim Van Baak 7b890046b6 Add assertion stubs and unit tests 2024-08-28 19:14:19 +00:00
Tim Van Baak 720ccc4329 Add standard repl helper 2024-08-28 15:09:57 +00:00
Tim Van Baak d2a46aa02d Implement dummy assertions 2024-08-28 15:01:27 +00:00
30 changed files with 1029 additions and 225 deletions

View File

@ -54,7 +54,7 @@ public class ReplOptions
// The last null is returned because an EOF means we should quit the repl. // The last null is returned because an EOF means we should quit the repl.
} }
IScriptHandler? handler = new ReplScriptHandler(); IScriptHandler? handler = new ReplScriptHandler(Console.WriteLine);
Console.Write(handler.Prompt); Console.Write(handler.Prompt);
foreach (string? nextInput in GetInputs()) foreach (string? nextInput in GetInputs())
@ -69,13 +69,21 @@ public class ReplOptions
outputWriter?.Flush(); outputWriter?.Flush();
// Delegate all other command parsing to the handler. // Delegate all other command parsing to the handler.
handler = handler.HandleInput(input); var result = handler.HandleInput(input);
// Quit if the handler ends processing, otherwise prompt for the next command. // Report errors if they occured.
if (handler is null) if (!result.Success)
{
Console.WriteLine($"Error: {result.Message}");
}
// Quit if the handler didn't continue processing.
if (result.NextHandler is null)
{ {
break; break;
} }
// Otherwise prompt for the next command.
Console.Write(handler.Prompt); Console.Write(handler.Prompt);
} }

View File

@ -214,7 +214,8 @@ public class OrderParser(World world)
return true; return true;
} }
public static bool TryParseOrder(World world, string power, string command, [NotNullWhen(true)] out Order? order) { public static bool TryParseOrder(World world, string power, string command, [NotNullWhen(true)] out Order? order)
{
order = null; order = null;
OrderParser re = new(world); OrderParser re = new(world);
@ -227,7 +228,7 @@ public class OrderParser(World world)
} else if (re.SupportMove.Match(command) is Match smoveMatch && smoveMatch.Success) { } else if (re.SupportMove.Match(command) is Match smoveMatch && smoveMatch.Success) {
return TryParseSupportMoveOrder(world, power, smoveMatch, out order); return TryParseSupportMoveOrder(world, power, smoveMatch, out order);
} else { } else {
throw new NotImplementedException(); return false;
} }
} }
@ -306,33 +307,44 @@ public class OrderParser(World world)
: subject.Season.Turn; : subject.Season.Turn;
var destProvince = world.Map.Provinces.Single(province => province.Is(move.destProvince)); var destProvince = world.Map.Provinces.Single(province => province.Is(move.destProvince));
string? destLocationKey = null;
// DATC 4.B.6 requires that "irrelevant" locations like army to Spain nc be ignored. // DATC 4.B specifies how to interpret orders with missing or incorrect locations. These issues arise because
// To satisfy this, any location of the wrong type is categorically ignored, so for an army the // of provinces with multiple locations of the same type, i.e. two-coast provinces in Classical. In general,
// "north coast" location effectively doesn't exist here. // 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 { var unitLocations = destProvince.Locations.Where(loc => loc.Type switch {
LocationType.Land => subject.Type == UnitType.Army, LocationType.Land => subject.Type == UnitType.Army,
LocationType.Water => subject.Type == UnitType.Fleet, LocationType.Water => subject.Type == UnitType.Fleet,
_ => false, _ => false,
}); });
// DATC 4.6.B also requires that unknown coasts be ignored. To satisfy this, an additional filter by name. if (!unitLocations.Any()) return false; // If *no* locations match, the move is illegal
// Doing both of these filters means "A - Spain/nc" is as meaningful as "F - Spain/wc". if (unitLocations.Count() == 1) destLocationKey ??= unitLocations.Single().Key;
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 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) { if (destLocationKey is null) {
// If no location matched, location was omitted, nonexistent, or the wrong type. var matchingLocations = unitLocations.Where(loc => loc.Is(move.destLocation));
// If one location is accessible, DATC 4.B.2 requires that it be used. if (matchingLocations.Any()) destLocationKey ??= matchingLocations.Single().Key;
// 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;
} }
// 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); order = new MoveOrder(power, subject, new(destTimeline, destTurn), destLocationKey);
return true; return true;
@ -346,7 +358,19 @@ public class OrderParser(World world)
{ {
order = null; order = null;
var support = ParseSupportHold(match); var support = ParseSupportHold(match);
throw new NotImplementedException();
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( public static bool TryParseSupportMoveOrder(
@ -357,9 +381,71 @@ public class OrderParser(World world)
{ {
order = null; order = null;
var support = ParseSupportMove(match); var support = ParseSupportMove(match);
throw new NotImplementedException();
// It is possible to support a move to an inaccessible coast if another coast is accessible to the subject. if (!TryParseOrderSubject(world, support.timeline, support.turn, support.province, out Unit? subject)) {
// DATC 4.B.4 prefers that automatic adjudicators strictly require matching coasts in supports. 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;
} }
} }

View File

@ -1,60 +1,276 @@
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using MultiversalDiplomacy.Adjudicate;
using MultiversalDiplomacy.Adjudicate.Decision;
using MultiversalDiplomacy.Model; using MultiversalDiplomacy.Model;
using MultiversalDiplomacy.Orders;
namespace MultiversalDiplomacy.Script; namespace MultiversalDiplomacy.Script;
public class AdjudicationQueryScriptHandler(World world, bool strict = false) : IScriptHandler public class AdjudicationQueryScriptHandler(
Action<string> WriteLine,
List<OrderValidation> validations,
List<AdjudicationDecision> adjudications,
World world,
IPhaseAdjudicator adjudicator)
: IScriptHandler
{ {
public string Prompt => "valid> "; public string Prompt => "valid> ";
public List<OrderValidation> Validations { get; } = validations;
public List<AdjudicationDecision> Adjudications { get; } = adjudications;
public World World { get; private set; } = world; public World World { get; private set; } = world;
/// <summary> public ScriptResult HandleInput(string input)
/// Whether unsuccessful commands should terminate the script.
/// </summary>
public bool Strict { get; } = strict;
public IScriptHandler? HandleInput(string input)
{ {
var args = input.Split(' ', StringSplitOptions.RemoveEmptyEntries); var args = input.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
if (args.Length == 0 || input.StartsWith('#')) if (args.Length == 0 || input.StartsWith('#'))
{ {
return this; return ScriptResult.Succeed(this);
} }
var command = args[0]; var command = args[0];
switch (command) switch (command)
{ {
case "---": case "---":
Console.WriteLine("Ready for orders"); WriteLine("Ready for orders");
return new GameScriptHandler(World, Strict); return ScriptResult.Succeed(new GameScriptHandler(WriteLine, World, adjudicator));
case "assert" when args.Length == 1: case "assert" when args.Length == 1:
Console.WriteLine("Usage:"); WriteLine("Usage:");
break; break;
case "assert": case "assert":
string assertion = input["assert ".Length..]; return EvaluateAssertion(args[1]);
Regex prov = new($"{World.Map.ProvinceRegex} (.*)");
Match match = prov.Match(assertion);
if (!match.Success) {
Console.WriteLine($"Could not parse province from \"{assertion}\"");
return Strict ? null : this;
}
// TODO look up order once orders are validated and adjudicated
Console.WriteLine("Order lookup not implemented yet");
return null;
case "status": case "status":
throw new NotImplementedException(); throw new NotImplementedException();
default: default:
Console.WriteLine($"Unrecognized command: \"{command}\""); return ScriptResult.Fail($"Unrecognized command: \"{command}\"", this);
if (Strict) return null;
break;
} }
return 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<Season> 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<IsDislodged>().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<DoesMove>().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<GivesSupport>().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);
}
} }
} }

View File

@ -6,33 +6,31 @@ using MultiversalDiplomacy.Orders;
namespace MultiversalDiplomacy.Script; namespace MultiversalDiplomacy.Script;
public class GameScriptHandler(World world, bool strict = false) : IScriptHandler public class GameScriptHandler(
Action<string> WriteLine,
World world,
IPhaseAdjudicator adjudicator)
: IScriptHandler
{ {
public string Prompt => "orders> "; public string Prompt => "orders> ";
public World World { get; private set; } = world; public World World { get; private set; } = world;
/// <summary>
/// Whether unsuccessful commands should terminate the script.
/// </summary>
public bool Strict { get; } = strict;
private string? CurrentPower { get; set; } = null; private string? CurrentPower { get; set; } = null;
public List<Order> Orders { get; } = []; public List<Order> Orders { get; } = [];
public IScriptHandler? HandleInput(string input) public ScriptResult HandleInput(string input)
{ {
if (input == "") { if (input == "") {
CurrentPower = null; CurrentPower = null;
return this; return ScriptResult.Succeed(this);
} }
if (input.StartsWith('#')) return this; if (input.StartsWith('#')) return ScriptResult.Succeed(this);
// "---" submits the orders and allows queries about the outcome // "---" submits the orders and allows queries about the outcome
if (input == "---") { if (input == "---") {
Console.WriteLine("Submitting orders for adjudication"); WriteLine("Submitting orders for adjudication");
var adjudicator = MovementPhaseAdjudicator.Instance;
var validation = adjudicator.ValidateOrders(World, Orders); var validation = adjudicator.ValidateOrders(World, Orders);
var validOrders = validation var validOrders = validation
.Where(v => v.Valid) .Where(v => v.Valid)
@ -40,14 +38,14 @@ public class GameScriptHandler(World world, bool strict = false) : IScriptHandle
.ToList(); .ToList();
var adjudication = adjudicator.AdjudicateOrders(World, validOrders); var adjudication = adjudicator.AdjudicateOrders(World, validOrders);
var newWorld = adjudicator.UpdateWorld(World, adjudication); var newWorld = adjudicator.UpdateWorld(World, adjudication);
return new AdjudicationQueryScriptHandler(newWorld, Strict); return ScriptResult.Succeed(new AdjudicationQueryScriptHandler(
WriteLine, validation, adjudication, newWorld, adjudicator));
} }
// "===" submits the orders and moves immediately to taking the next set of orders // "===" submits the orders and moves immediately to taking the next set of orders
// i.e. it's "---" twice // i.e. it's "---" twice
if (input == "===") { if (input == "===") {
Console.WriteLine("Submitting orders for adjudication"); WriteLine("Submitting orders for adjudication");
var adjudicator = MovementPhaseAdjudicator.Instance;
var validation = adjudicator.ValidateOrders(World, Orders); var validation = adjudicator.ValidateOrders(World, Orders);
var validOrders = validation var validOrders = validation
.Where(v => v.Valid) .Where(v => v.Valid)
@ -55,14 +53,14 @@ public class GameScriptHandler(World world, bool strict = false) : IScriptHandle
.ToList(); .ToList();
var adjudication = adjudicator.AdjudicateOrders(World, validOrders); var adjudication = adjudicator.AdjudicateOrders(World, validOrders);
World = adjudicator.UpdateWorld(World, adjudication); World = adjudicator.UpdateWorld(World, adjudication);
Console.WriteLine("Ready for orders"); WriteLine("Ready for orders");
return this; return ScriptResult.Succeed(this);
} }
// A block of orders for a single power beginning with "{name}:" // A block of orders for a single power beginning with "{name}:"
if (World.Powers.FirstOrDefault(p => input.EqualsAnyCase($"{p}:"), null) is string power) { if (World.Powers.FirstOrDefault(p => input.EqualsAnyCase($"{p}:"), null) is string power) {
CurrentPower = power; CurrentPower = power;
return this; return ScriptResult.Succeed(this);
} }
// If it's not a comment, submit, or order block, assume it's an order. // If it's not a comment, submit, or order block, assume it's an order.
@ -76,21 +74,17 @@ public class GameScriptHandler(World world, bool strict = false) : IScriptHandle
// Outside a power block, the power is prefixed to each order. // Outside a power block, the power is prefixed to each order.
Regex re = new($"^{World.Map.PowerRegex}(?:[:])? (.*)$", RegexOptions.IgnoreCase); Regex re = new($"^{World.Map.PowerRegex}(?:[:])? (.*)$", RegexOptions.IgnoreCase);
var match = re.Match(input); var match = re.Match(input);
if (!match.Success) { if (!match.Success) return ScriptResult.Fail($"Could not determine ordering power in \"{input}\"", this);
Console.WriteLine($"Could not determine ordering power in \"{input}\"");
return Strict ? null : this;
}
orderPower = match.Groups[1].Value; orderPower = match.Groups[1].Value;
orderText = match.Groups[2].Value; orderText = match.Groups[2].Value;
} }
if (OrderParser.TryParseOrder(World, orderPower, orderText, out Order? order)) { if (OrderParser.TryParseOrder(World, orderPower, orderText, out Order? order)) {
Console.WriteLine($"Parsed {orderPower} order: {order}"); WriteLine($"Parsed {orderPower} order: {order}");
Orders.Add(order); Orders.Add(order);
return this; return ScriptResult.Succeed(this);
} }
Console.WriteLine($"Failed to parse \"{orderText}\""); return ScriptResult.Fail($"Failed to parse \"{orderText}\"", this);
return Strict ? null : this;
} }
} }

View File

@ -14,8 +14,5 @@ public interface IScriptHandler
/// <summary> /// <summary>
/// Process a line of input. /// Process a line of input.
/// </summary> /// </summary>
/// <returns> public ScriptResult HandleInput(string input);
/// The handler that should handle the next line of input, or null if script handling should end.
/// </returns>
public IScriptHandler? HandleInput(string input);
} }

View File

@ -1,3 +1,4 @@
using MultiversalDiplomacy.Adjudicate;
using MultiversalDiplomacy.Model; using MultiversalDiplomacy.Model;
namespace MultiversalDiplomacy.Script; namespace MultiversalDiplomacy.Script;
@ -5,16 +6,16 @@ namespace MultiversalDiplomacy.Script;
/// <summary> /// <summary>
/// A script handler for the interactive repl. /// A script handler for the interactive repl.
/// </summary> /// </summary>
public class ReplScriptHandler : IScriptHandler public class ReplScriptHandler(Action<string> WriteLine) : IScriptHandler
{ {
public string Prompt => "5dp> "; public string Prompt => "5dp> ";
public IScriptHandler? HandleInput(string input) public ScriptResult HandleInput(string input)
{ {
var args = input.Split(' ', StringSplitOptions.RemoveEmptyEntries); var args = input.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (args.Length == 0 || input.StartsWith('#')) if (args.Length == 0 || input.StartsWith('#'))
{ {
return this; return ScriptResult.Succeed(this);
} }
var command = args[0]; var command = args[0];
@ -22,40 +23,42 @@ public class ReplScriptHandler : IScriptHandler
{ {
case "help": case "help":
case "?": case "?":
Console.WriteLine("Commands:"); WriteLine("Commands:");
Console.WriteLine(" help, ?: print this message"); WriteLine(" help, ?: print this message");
Console.WriteLine(" map <variant>: start a new game of the given variant"); WriteLine(" map <variant>: start a new game of the given variant");
Console.WriteLine(" stab: stab"); WriteLine(" stab: stab");
break; break;
case "stab": case "stab":
Console.WriteLine("stab"); WriteLine("stab");
break; break;
case "map" when args.Length == 1: case "map" when args.Length == 1:
Console.WriteLine("Usage:"); WriteLine("Usage:");
Console.WriteLine(" map <variant>"); WriteLine(" map <variant>");
Console.WriteLine("Available variants:"); WriteLine("Available variants:");
Console.WriteLine($" {string.Join(", ", Enum.GetNames<MapType>().Select(s => s.ToLowerInvariant()))}"); WriteLine($" {string.Join(", ", Enum.GetNames<MapType>().Select(s => s.ToLowerInvariant()))}");
break; break;
case "map" when args.Length > 1: case "map" when args.Length > 1:
string mapType = args[1].Trim(); string mapType = args[1].Trim();
if (!Enum.TryParse(mapType, ignoreCase: true, out MapType map)) { if (!Enum.TryParse(mapType, ignoreCase: true, out MapType map)) {
Console.WriteLine($"Unknown variant \"{mapType}\""); WriteLine($"Unknown variant \"{mapType}\"");
Console.WriteLine("Available variants:"); WriteLine("Available variants:");
Console.WriteLine($" {string.Join(", ", Enum.GetNames<MapType>().Select(s => s.ToLowerInvariant()))}"); WriteLine($" {string.Join(", ", Enum.GetNames<MapType>().Select(s => s.ToLowerInvariant()))}");
break; break;
} }
World world = World.WithMap(Map.FromType(map)); World world = World.WithMap(Map.FromType(map));
Console.WriteLine($"Created a new {map} game"); WriteLine($"Created a new {map} game");
return new SetupScriptHandler(world); return ScriptResult.Succeed(new SetupScriptHandler(
WriteLine,
world,
MovementPhaseAdjudicator.Instance));
default: default:
Console.WriteLine($"Unrecognized command: \"{command}\""); return ScriptResult.Fail($"Unrecognized command: \"{command}\"", this);
break;
} }
return this; return ScriptResult.Succeed(this);
} }
} }

View File

@ -0,0 +1,36 @@
namespace MultiversalDiplomacy.Script;
/// <summary>
/// The result of an <see cref="IScriptHandler"/> processing a line of input.
/// </summary>
/// <param name="success">Whether processing was successful.</param>
/// <param name="next">The handler to continue script processing with.</param>
/// <param name="message">If processing failed, the error message.</param>
public class ScriptResult(bool success, IScriptHandler? next, string message)
{
/// <summary>
/// Whether processing was successful.
/// </summary>
public bool Success { get; } = success;
/// <summary>
/// The handler to continue script processing with.
/// </summary>
public IScriptHandler? NextHandler { get; } = next;
/// <summary>
/// If processing failed, the error message.
/// </summary>
public string Message { get; } = message;
/// <summary>
/// Mark the processing as successful and continue processing with the next handler.
/// </summary>
public static ScriptResult Succeed(IScriptHandler next) => new(true, next, "");
/// <summary>
/// Mark the processing as a failure and optionally continue with the next handler.
/// </summary>
/// <param name="message">The reason for the processing failure.</param>
public static ScriptResult Fail(string message, IScriptHandler? next = null) => new(false, next, message);
}

View File

@ -1,3 +1,4 @@
using MultiversalDiplomacy.Adjudicate;
using MultiversalDiplomacy.Model; using MultiversalDiplomacy.Model;
namespace MultiversalDiplomacy.Script; namespace MultiversalDiplomacy.Script;
@ -5,23 +6,22 @@ namespace MultiversalDiplomacy.Script;
/// <summary> /// <summary>
/// A script handler for modifying a game before it begins. /// A script handler for modifying a game before it begins.
/// </summary> /// </summary>
public class SetupScriptHandler(World world, bool strict = false) : IScriptHandler public class SetupScriptHandler(
Action<string> WriteLine,
World world,
IPhaseAdjudicator adjudicator)
: IScriptHandler
{ {
public string Prompt => "setup> "; public string Prompt => "setup> ";
public World World { get; private set; } = world; public World World { get; private set; } = world;
/// <summary> public ScriptResult HandleInput(string input)
/// Whether unsuccessful commands should terminate the script.
/// </summary>
public bool Strict { get; } = strict;
public IScriptHandler? HandleInput(string input)
{ {
var args = input.Split(' ', StringSplitOptions.RemoveEmptyEntries); var args = input.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (args.Length == 0 || input.StartsWith('#')) if (args.Length == 0 || input.StartsWith('#'))
{ {
return this; return ScriptResult.Succeed(this);
} }
var command = args[0]; var command = args[0];
@ -29,39 +29,39 @@ public class SetupScriptHandler(World world, bool strict = false) : IScriptHandl
{ {
case "help": case "help":
case "?": case "?":
Console.WriteLine("commands:"); WriteLine("commands:");
Console.WriteLine(" begin: complete setup and start the game (alias: ---)"); WriteLine(" begin: complete setup and start the game (alias: ---)");
Console.WriteLine(" list <type>: list things in a game category"); WriteLine(" list <type>: list things in a game category");
Console.WriteLine(" option <name> <value>: set a game option"); WriteLine(" option <name> <value>: set a game option");
Console.WriteLine(" unit <power> <type> <province> [location]: add a unit to the game"); WriteLine(" unit <power> <type> <province> [location]: add a unit to the game");
Console.WriteLine(" <province> may be \"province/location\""); WriteLine(" <province> may be \"province/location\"");
break; break;
case "begin": case "begin":
case "---": case "---":
Console.WriteLine("Starting game"); WriteLine("Starting game");
Console.WriteLine("Ready for orders"); WriteLine("Ready for orders");
return new GameScriptHandler(World, Strict); return ScriptResult.Succeed(new GameScriptHandler(WriteLine, World, adjudicator));
case "list" when args.Length == 1: case "list" when args.Length == 1:
Console.WriteLine("usage:"); WriteLine("usage:");
Console.WriteLine(" list powers: the powers in the game"); WriteLine(" list powers: the powers in the game");
Console.WriteLine(" list units: units created so far"); WriteLine(" list units: units created so far");
break; break;
case "list" when args[1] == "powers": case "list" when args[1] == "powers":
Console.WriteLine("Powers:"); WriteLine("Powers:");
foreach (string powerName in World.Powers) foreach (string powerName in World.Powers)
{ {
Console.WriteLine($" {powerName}"); WriteLine($" {powerName}");
} }
break; break;
case "list" when args[1] == "units": case "list" when args[1] == "units":
Console.WriteLine("Units:"); WriteLine("Units:");
foreach (Unit unit in World.Units) foreach (Unit unit in World.Units)
{ {
Console.WriteLine($" {unit}"); WriteLine($" {unit}");
} }
break; break;
@ -69,26 +69,23 @@ public class SetupScriptHandler(World world, bool strict = false) : IScriptHandl
throw new NotImplementedException("There are no supported options yet"); throw new NotImplementedException("There are no supported options yet");
case "unit" when args.Length < 2: case "unit" when args.Length < 2:
Console.WriteLine("usage: unit [power] [type] [province]</location>"); WriteLine("usage: unit [power] [type] [province]</location>");
break; break;
case "unit": case "unit":
string unitSpec = input["unit ".Length..]; string unitSpec = input["unit ".Length..];
if (OrderParser.TryParseUnit(World, unitSpec, out Unit? newUnit)) { if (OrderParser.TryParseUnit(World, unitSpec, out Unit? newUnit)) {
World = World.Update(units: World.Units.Append(newUnit)); World = World.Update(units: World.Units.Append(newUnit));
Console.WriteLine($"Created {newUnit}"); WriteLine($"Created {newUnit}");
return this; return ScriptResult.Succeed(this);
} }
Console.WriteLine($"Could not match unit spec \"{unitSpec}\""); return ScriptResult.Fail($"Could not match unit spec \"{unitSpec}\"", this);
if (Strict) return null;
break;
default: default:
Console.WriteLine($"Unrecognized command: \"{command}\""); ScriptResult.Fail($"Unrecognized command: \"{command}\"", this);
if (Strict) return null;
break; break;
} }
return this; return ScriptResult.Succeed(this);
} }
} }

View File

@ -0,0 +1,8 @@
using MultiversalDiplomacy.Adjudicate;
namespace MultiversalDiplomacyTests;
public static class Adjudicator
{
public static MovementPhaseAdjudicator MovementPhase { get; } = new MovementPhaseAdjudicator(NullLogger.Instance);
}

View File

@ -3,6 +3,8 @@ using MultiversalDiplomacy.Model;
using MultiversalDiplomacy.Orders; using MultiversalDiplomacy.Orders;
using NUnit.Framework; using NUnit.Framework;
using static MultiversalDiplomacyTests.Adjudicator;
namespace MultiversalDiplomacyTests; namespace MultiversalDiplomacyTests;
public class DATC_A public class DATC_A
@ -17,7 +19,7 @@ public class DATC_A
.Fleet("North Sea").MovesTo("Picardy").GetReference(out var order); .Fleet("North Sea").MovesTo("Picardy").GetReference(out var order);
// Order should fail. // Order should fail.
setup.ValidateOrders(MovementPhaseAdjudicator.Instance); setup.ValidateOrders(MovementPhase);
Assert.That(order, Is.Invalid(ValidationReason.UnreachableDestination)); Assert.That(order, Is.Invalid(ValidationReason.UnreachableDestination));
} }
@ -59,7 +61,7 @@ public class DATC_A
.Fleet("Kiel").MovesTo("Kiel").GetReference(out var order); .Fleet("Kiel").MovesTo("Kiel").GetReference(out var order);
// Program should not crash. // Program should not crash.
setup.ValidateOrders(MovementPhaseAdjudicator.Instance); setup.ValidateOrders(MovementPhase);
Assert.That(order, Is.Invalid(ValidationReason.DestinationMatchesOrigin)); Assert.That(order, Is.Invalid(ValidationReason.DestinationMatchesOrigin));
} }
@ -77,14 +79,14 @@ public class DATC_A
.Army("Wales").Supports.Fleet("London").MoveTo("Yorkshire"); .Army("Wales").Supports.Fleet("London").MoveTo("Yorkshire");
// The move of the army in Yorkshire is illegal. This makes the support of Liverpool also illegal. // The move of the army in Yorkshire is illegal. This makes the support of Liverpool also illegal.
setup.ValidateOrders(MovementPhaseAdjudicator.Instance); setup.ValidateOrders(MovementPhase);
Assert.That(orderLon, Is.Valid); Assert.That(orderLon, Is.Valid);
Assert.That(orderNth, Is.Invalid(ValidationReason.DestinationMatchesOrigin)); Assert.That(orderNth, Is.Invalid(ValidationReason.DestinationMatchesOrigin));
Assert.That(orderYor, Is.Invalid(ValidationReason.DestinationMatchesOrigin)); Assert.That(orderYor, Is.Invalid(ValidationReason.DestinationMatchesOrigin));
var orderYorRepl = orderYor.GetReplacementReference<HoldOrder>(); var orderYorRepl = orderYor.GetReplacementReference<HoldOrder>();
// Without the support, the Germans have a stronger force. The army in London dislodges the army in Yorkshire. // Without the support, the Germans have a stronger force. The army in London dislodges the army in Yorkshire.
setup.AdjudicateOrders(MovementPhaseAdjudicator.Instance); setup.AdjudicateOrders(MovementPhase);
Assert.That(orderLon, Is.Victorious); Assert.That(orderLon, Is.Victorious);
Assert.That(orderYorRepl, Is.Dislodged); Assert.That(orderYorRepl, Is.Dislodged);
} }
@ -98,7 +100,7 @@ public class DATC_A
.Fleet("London", powerName: "England").MovesTo("North Sea").GetReference(out var order); .Fleet("London", powerName: "England").MovesTo("North Sea").GetReference(out var order);
// Order should fail. // Order should fail.
setup.ValidateOrders(MovementPhaseAdjudicator.Instance); setup.ValidateOrders(MovementPhase);
Assert.That(order, Is.Invalid(ValidationReason.InvalidUnitForPower)); Assert.That(order, Is.Invalid(ValidationReason.InvalidUnitForPower));
} }
@ -112,7 +114,7 @@ public class DATC_A
.Fleet("North Sea").Convoys.Army("London").To("Belgium").GetReference(out var order); .Fleet("North Sea").Convoys.Army("London").To("Belgium").GetReference(out var order);
// Move from London to Belgium should fail. // Move from London to Belgium should fail.
setup.ValidateOrders(MovementPhaseAdjudicator.Instance); setup.ValidateOrders(MovementPhase);
Assert.That(order, Is.Invalid(ValidationReason.InvalidOrderTypeForUnit)); Assert.That(order, Is.Invalid(ValidationReason.InvalidOrderTypeForUnit));
} }
@ -127,12 +129,12 @@ public class DATC_A
["Austria"] ["Austria"]
.Fleet("Trieste").Supports.Fleet("Trieste").Hold().GetReference(out var orderTri); .Fleet("Trieste").Supports.Fleet("Trieste").Hold().GetReference(out var orderTri);
setup.ValidateOrders(MovementPhaseAdjudicator.Instance); setup.ValidateOrders(MovementPhase);
Assert.That(orderTri, Is.Invalid(ValidationReason.NoSelfSupport)); Assert.That(orderTri, Is.Invalid(ValidationReason.NoSelfSupport));
var orderTriRepl = orderTri.GetReplacementReference<HoldOrder>(); var orderTriRepl = orderTri.GetReplacementReference<HoldOrder>();
// The army in Trieste should be dislodged. // The army in Trieste should be dislodged.
setup.AdjudicateOrders(MovementPhaseAdjudicator.Instance); setup.AdjudicateOrders(MovementPhase);
Assert.That(orderTriRepl, Is.Dislodged); Assert.That(orderTriRepl, Is.Dislodged);
} }
@ -145,7 +147,7 @@ public class DATC_A
.Fleet("Rome").MovesTo("Venice").GetReference(out var order); .Fleet("Rome").MovesTo("Venice").GetReference(out var order);
// Move fails. An army can go from Rome to Venice, but a fleet can not. // Move fails. An army can go from Rome to Venice, but a fleet can not.
setup.ValidateOrders(MovementPhaseAdjudicator.Instance); setup.ValidateOrders(MovementPhase);
Assert.That(order, Is.Invalid(ValidationReason.UnreachableDestination)); Assert.That(order, Is.Invalid(ValidationReason.UnreachableDestination));
} }
@ -160,13 +162,13 @@ public class DATC_A
.Army("Apulia").MovesTo("Venice") .Army("Apulia").MovesTo("Venice")
.Fleet("Rome").Supports.Army("Apulia").MoveTo("Venice").GetReference(out var orderRom); .Fleet("Rome").Supports.Army("Apulia").MoveTo("Venice").GetReference(out var orderRom);
setup.ValidateOrders(MovementPhaseAdjudicator.Instance); setup.ValidateOrders(MovementPhase);
// The support of Rome is illegal, because Venice can not be reached from Rome by a fleet. // The support of Rome is illegal, because Venice can not be reached from Rome by a fleet.
Assert.That(orderRom, Is.Invalid(ValidationReason.UnreachableSupport)); Assert.That(orderRom, Is.Invalid(ValidationReason.UnreachableSupport));
// Venice is not dislodged. // Venice is not dislodged.
setup.AdjudicateOrders(MovementPhaseAdjudicator.Instance); setup.AdjudicateOrders(MovementPhase);
Assert.That(orderVen, Is.NotDislodged); Assert.That(orderVen, Is.NotDislodged);
} }
@ -180,12 +182,12 @@ public class DATC_A
["Italy"] ["Italy"]
.Army("Venice").MovesTo("Tyrolia").GetReference(out var orderVen); .Army("Venice").MovesTo("Tyrolia").GetReference(out var orderVen);
setup.ValidateOrders(MovementPhaseAdjudicator.Instance); setup.ValidateOrders(MovementPhase);
Assert.That(orderVie, Is.Valid); Assert.That(orderVie, Is.Valid);
Assert.That(orderVen, Is.Valid); Assert.That(orderVen, Is.Valid);
// The two units bounce. // The two units bounce.
var adjudications = setup.AdjudicateOrders(MovementPhaseAdjudicator.Instance); var adjudications = setup.AdjudicateOrders(MovementPhase);
Assert.That(orderVie, Is.Repelled); Assert.That(orderVie, Is.Repelled);
Assert.That(orderVie, Is.NotDislodged); Assert.That(orderVie, Is.NotDislodged);
Assert.That(orderVen, Is.Repelled); Assert.That(orderVen, Is.Repelled);
@ -204,12 +206,12 @@ public class DATC_A
["Italy"] ["Italy"]
.Army("Venice").MovesTo("Tyrolia").GetReference(out var orderVen); .Army("Venice").MovesTo("Tyrolia").GetReference(out var orderVen);
var validations = setup.ValidateOrders(MovementPhaseAdjudicator.Instance); var validations = setup.ValidateOrders(MovementPhase);
Assert.That(orderVie, Is.Valid); Assert.That(orderVie, Is.Valid);
Assert.That(orderMun, Is.Valid); Assert.That(orderMun, Is.Valid);
Assert.That(orderVen, Is.Valid); Assert.That(orderVen, Is.Valid);
var adjudications = setup.AdjudicateOrders(MovementPhaseAdjudicator.Instance); var adjudications = setup.AdjudicateOrders(MovementPhase);
// The three units bounce. // The three units bounce.
Assert.That(orderVie, Is.Repelled); Assert.That(orderVie, Is.Repelled);
Assert.That(orderVie, Is.NotDislodged); Assert.That(orderVie, Is.NotDislodged);

View File

@ -1,7 +1,8 @@
using MultiversalDiplomacy.Adjudicate; using MultiversalDiplomacy.Adjudicate;
using MultiversalDiplomacy.Adjudicate.Decision;
using MultiversalDiplomacy.Model; using MultiversalDiplomacy.Model;
using static MultiversalDiplomacyTests.Adjudicator;
using NUnit.Framework; using NUnit.Framework;
namespace MultiversalDiplomacyTests; namespace MultiversalDiplomacyTests;
@ -11,7 +12,7 @@ public class TimeTravelTest
[Test] [Test]
public void MDATC_3_A_1_MoveIntoOwnPastForksTimeline() public void MDATC_3_A_1_MoveIntoOwnPastForksTimeline()
{ {
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance); TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhase);
// Hold to move into the future, then move back into the past. // Hold to move into the future, then move back into the past.
setup[("a", 0)] setup[("a", 0)]
@ -57,7 +58,7 @@ public class TimeTravelTest
[Test] [Test]
public void MDATC_3_A_2_SupportToRepelledPastMoveForksTimeline() public void MDATC_3_A_2_SupportToRepelledPastMoveForksTimeline()
{ {
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance); TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhase);
// Fail to dislodge on the first turn, then support the move so it succeeds. // Fail to dislodge on the first turn, then support the move so it succeeds.
setup[("a", 0)] setup[("a", 0)]
@ -107,7 +108,7 @@ public class TimeTravelTest
[Test] [Test]
public void MDATC_3_A_3_FailedMoveDoesNotForkTimeline() public void MDATC_3_A_3_FailedMoveDoesNotForkTimeline()
{ {
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance); TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhase);
// Hold to create a future, then attempt to attack in the past. // Hold to create a future, then attempt to attack in the past.
setup[("a", 0)] setup[("a", 0)]
@ -147,7 +148,7 @@ public class TimeTravelTest
[Test] [Test]
public void MDATC_3_A_4_SuperfluousSupportDoesNotForkTimeline() public void MDATC_3_A_4_SuperfluousSupportDoesNotForkTimeline()
{ {
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance); TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhase);
// Move, then support the past move even though it succeeded already. // Move, then support the past move even though it succeeded already.
setup[("a", 0)] setup[("a", 0)]
@ -189,7 +190,7 @@ public class TimeTravelTest
[Test] [Test]
public void MDATC_3_A_5_CrossTimelineSupportDoesNotForkHead() public void MDATC_3_A_5_CrossTimelineSupportDoesNotForkHead()
{ {
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance); TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhase);
// London creates two timelines by moving into the past. // London creates two timelines by moving into the past.
setup[("a", 0)] setup[("a", 0)]
@ -242,7 +243,7 @@ public class TimeTravelTest
[Test] [Test]
public void MDATC_3_A_6_CuttingCrossTimelineSupportDoesNotFork() public void MDATC_3_A_6_CuttingCrossTimelineSupportDoesNotFork()
{ {
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance); TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhase);
// As above, only now London creates three timelines. // As above, only now London creates three timelines.
setup[("a", 0)] setup[("a", 0)]

View File

@ -1,9 +1,10 @@
using MultiversalDiplomacy.Adjudicate;
using MultiversalDiplomacy.Adjudicate.Decision; using MultiversalDiplomacy.Adjudicate.Decision;
using MultiversalDiplomacy.Model; using MultiversalDiplomacy.Model;
using NUnit.Framework; using NUnit.Framework;
using static MultiversalDiplomacyTests.Adjudicator;
namespace MultiversalDiplomacyTests; namespace MultiversalDiplomacyTests;
public class MovementAdjudicatorTest public class MovementAdjudicatorTest
@ -11,7 +12,7 @@ public class MovementAdjudicatorTest
[Test] [Test]
public void Validation_ValidHold() public void Validation_ValidHold()
{ {
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance); TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhase);
setup["Germany"] setup["Germany"]
.Army("Mun").Holds().GetReference(out var order); .Army("Mun").Holds().GetReference(out var order);
@ -24,7 +25,7 @@ public class MovementAdjudicatorTest
[Test] [Test]
public void Validation_ValidMove() public void Validation_ValidMove()
{ {
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance); TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhase);
setup["Germany"] setup["Germany"]
.Army("Mun").MovesTo("Tyr").GetReference(out var order); .Army("Mun").MovesTo("Tyr").GetReference(out var order);
@ -37,7 +38,7 @@ public class MovementAdjudicatorTest
[Test] [Test]
public void Validation_ValidConvoy() public void Validation_ValidConvoy()
{ {
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance); TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhase);
setup["Germany"] setup["Germany"]
.Fleet("Nth").Convoys.Army("Hol").To("Lon").GetReference(out var order); .Fleet("Nth").Convoys.Army("Hol").To("Lon").GetReference(out var order);
@ -50,7 +51,7 @@ public class MovementAdjudicatorTest
[Test] [Test]
public void Validation_ValidSupportHold() public void Validation_ValidSupportHold()
{ {
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance); TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhase);
setup["Germany"] setup["Germany"]
.Army("Mun").Supports.Army("Kie").Hold().GetReference(out var order); .Army("Mun").Supports.Army("Kie").Hold().GetReference(out var order);
@ -63,7 +64,7 @@ public class MovementAdjudicatorTest
[Test] [Test]
public void Validation_ValidSupportMove() public void Validation_ValidSupportMove()
{ {
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance); TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhase);
setup["Germany"] setup["Germany"]
.Army("Mun").Supports.Army("Kie").MoveTo("Ber").GetReference(out var order); .Army("Mun").Supports.Army("Kie").MoveTo("Ber").GetReference(out var order);
@ -76,12 +77,12 @@ public class MovementAdjudicatorTest
[Test] [Test]
public void Adjudication_Hold() public void Adjudication_Hold()
{ {
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance); TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhase);
setup["Germany"] setup["Germany"]
.Army("Mun").Holds().GetReference(out var order); .Army("Mun").Holds().GetReference(out var order);
setup.ValidateOrders(); setup.ValidateOrders();
setup.AdjudicateOrders(MovementPhaseAdjudicator.Instance); setup.AdjudicateOrders(MovementPhase);
var adjMun = order.Adjudications; var adjMun = order.Adjudications;
Assert.That(adjMun.All(adj => adj.Resolved), Is.True); Assert.That(adjMun.All(adj => adj.Resolved), Is.True);
@ -96,7 +97,7 @@ public class MovementAdjudicatorTest
[Test] [Test]
public void Adjudication_Move() public void Adjudication_Move()
{ {
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance); TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhase);
setup["Germany"] setup["Germany"]
.Army("Mun").MovesTo("Tyr").GetReference(out var order); .Army("Mun").MovesTo("Tyr").GetReference(out var order);
@ -122,7 +123,7 @@ public class MovementAdjudicatorTest
[Test] [Test]
public void Adjudication_Support() public void Adjudication_Support()
{ {
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance); TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhase);
setup["Germany"] setup["Germany"]
.Army("Mun").MovesTo("Tyr").GetReference(out var move) .Army("Mun").MovesTo("Tyr").GetReference(out var move)
.Army("Boh").Supports.Army("Mun").MoveTo("Tyr").GetReference(out var support); .Army("Boh").Supports.Army("Mun").MoveTo("Tyr").GetReference(out var support);
@ -156,7 +157,7 @@ public class MovementAdjudicatorTest
[Test] [Test]
public void Update_SingleHold() public void Update_SingleHold()
{ {
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance); TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhase);
setup["Germany"] setup["Germany"]
.Army("Mun").Holds().GetReference(out var mun); .Army("Mun").Holds().GetReference(out var mun);
@ -183,7 +184,7 @@ public class MovementAdjudicatorTest
[Test] [Test]
public void Update_DoubleHold() public void Update_DoubleHold()
{ {
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance); TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhase);
setup[("a", 0)] setup[("a", 0)]
.GetReference(out Season s1) .GetReference(out Season s1)
["Germany"] ["Germany"]
@ -233,7 +234,7 @@ public class MovementAdjudicatorTest
[Test] [Test]
public void Update_DoubleMove() public void Update_DoubleMove()
{ {
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance); TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhase);
setup[("a", 0)] setup[("a", 0)]
.GetReference(out Season s1) .GetReference(out Season s1)
["Germany"] ["Germany"]

View File

@ -0,0 +1,10 @@
using MultiversalDiplomacy.Adjudicate.Logging;
namespace MultiversalDiplomacyTests;
public class NullLogger : IAdjudicatorLogger
{
public static NullLogger Instance { get; } = new();
public void Log(int contextLevel, string message, params object[] args) {}
}

View File

@ -5,7 +5,7 @@ using MultiversalDiplomacy.Orders;
namespace MultiversalDiplomacyTests; namespace MultiversalDiplomacyTests;
public class RegexTest public class OrderParserTest
{ {
private static TestCaseData Test(string order, params string[] expected) private static TestCaseData Test(string order, params string[] expected)
=> new TestCaseData(order, expected).SetName($"{{m}}(\"{order}\")"); => new TestCaseData(order, expected).SetName($"{{m}}(\"{order}\")");
@ -216,4 +216,120 @@ public class RegexTest
Assert.That(move.Location, Is.EqualTo("Tyrolia/l")); Assert.That(move.Location, Is.EqualTo("Tyrolia/l"));
Assert.That(move.Season.Key, Is.EqualTo("a0")); Assert.That(move.Season.Key, Is.EqualTo("a0"));
} }
[Test]
public void OrderDisambiguation()
{
World world = World.WithStandardMap().AddUnits("Germany A Mun");
OrderParser.TryParseOrder(world, "Germany", "Mun h", out Order? parsed);
Assert.That(parsed?.ToString(), Is.EqualTo("G A a-Munich/l@0 holds"));
}
[Test]
public void UnitTypeDisambiguatesCoastalLocation()
{
World world = World.WithStandardMap().AddUnits("England F Nth", "Germany A Ruhr");
Assert.That(
OrderParser.TryParseOrder(world, "England", "North Sea - Holland", out Order? fleetOrder),
"Failed to parse fleet order");
Assert.That(
OrderParser.TryParseOrder(world, "Germany", "Ruhr - Holland", out Order? armyOrder),
"Failed to parse army order");
Assert.That(fleetOrder, Is.TypeOf<MoveOrder>(), "Unexpected fleet order");
Assert.That(armyOrder, Is.TypeOf<MoveOrder>(), "Unexpected army order");
Location fleetDest = world.Map.GetLocation(((MoveOrder)fleetOrder!).Location);
Location armyDest = world.Map.GetLocation(((MoveOrder)armyOrder!).Location);
Assert.That(fleetDest.ProvinceName, Is.EqualTo(armyDest.ProvinceName));
Assert.That(fleetDest.Type, Is.EqualTo(LocationType.Water), "Unexpected fleet movement location");
Assert.That(armyDest.Type, Is.EqualTo(LocationType.Land), "Unexpected army movement location");
}
[Test]
public void UnitTypeOverrulesNonsenseLocation()
{
World world = World.WithStandardMap().AddUnits("England F Nth", "Germany A Ruhr");
Assert.That(
OrderParser.TryParseOrder(world, "England", "F North Sea - Holland/l", out Order? fleetOrder),
"Failed to parse fleet order");
Assert.That(
OrderParser.TryParseOrder(world, "Germany", "A Ruhr - Holland/w", out Order? armyOrder),
"Failed to parse army order");
Assert.That(fleetOrder, Is.TypeOf<MoveOrder>(), "Unexpected fleet order");
Assert.That(armyOrder, Is.TypeOf<MoveOrder>(), "Unexpected army order");
Location fleetDest = world.Map.GetLocation(((MoveOrder)fleetOrder!).Location);
Location armyDest = world.Map.GetLocation(((MoveOrder)armyOrder!).Location);
Assert.That(fleetDest.ProvinceName, Is.EqualTo(armyDest.ProvinceName));
Assert.That(fleetDest.Type, Is.EqualTo(LocationType.Water), "Unexpected fleet movement location");
Assert.That(armyDest.Type, Is.EqualTo(LocationType.Land), "Unexpected army movement location");
}
[Test]
public void DisambiguateSingleAccessibleCoast()
{
World world = World.WithStandardMap().AddUnits("France F Gascony", "France F Marseilles");
Assert.That(
OrderParser.TryParseOrder(world, "France", "Gascony - Spain", out Order? northOrder),
"Failed to parse north coast order");
Assert.That(
OrderParser.TryParseOrder(world, "France", "Marseilles - Spain", out Order? southOrder),
"Failed to parse south coast order");
Assert.That(northOrder, Is.TypeOf<MoveOrder>(), "Unexpected north coast order");
Assert.That(southOrder, Is.TypeOf<MoveOrder>(), "Unexpected south coast order");
Location north = world.Map.GetLocation(((MoveOrder)northOrder!).Location);
Location south = world.Map.GetLocation(((MoveOrder)southOrder!).Location);
Assert.That(north.Name, Is.EqualTo("north coast"), "Unexpected disambiguation");
Assert.That(south.Name, Is.EqualTo("south coast"), "Unexpected disambiguation");
}
[Test]
public void DisambiguateMultipleAccessibleCoasts()
{
World world = World.WithStandardMap().AddUnits("France F Portugal");
Assert.That(
OrderParser.TryParseOrder(world, "France", "Portugal - Spain", out Order? _),
Is.False,
"Should not parse ambiguous coastal move");
}
[Test]
public void DisambiguateSupportToSingleAccessibleCoast()
{
World world = World.WithStandardMap().AddUnits("France F Gascony", "France F Marseilles");
Assert.That(
OrderParser.TryParseOrder(world, "France", "Gascony S Marseilles - Spain", out Order? northOrder),
"Failed to parse north coast order");
Assert.That(
OrderParser.TryParseOrder(world, "France", "Marseilles S Gascony - Spain", out Order? southOrder),
"Failed to parse south coast order");
Assert.That(northOrder, Is.TypeOf<SupportMoveOrder>(), "Unexpected north coast order");
Assert.That(southOrder, Is.TypeOf<SupportMoveOrder>(), "Unexpected south coast order");
Location northTarget = ((SupportMoveOrder)northOrder!).Location;
Location southTarget = ((SupportMoveOrder)southOrder!).Location;
Assert.That(northTarget.Name, Is.EqualTo("south coast"), "Unexpected disambiguation");
Assert.That(southTarget.Name, Is.EqualTo("north coast"), "Unexpected disambiguation");
}
[Test]
public void DisambiguateSupportToMultipleAccessibleCoasts()
{
World world = World.WithStandardMap().AddUnits("France F Portugal", "France F Marseilles");
Assert.That(
OrderParser.TryParseOrder(world, "France", "Marseilles S Portugal - Spain", out Order? _),
Is.False,
"Should not parse ambiguous coastal support");
}
} }

View File

@ -15,15 +15,9 @@ public class ReplDriver(IScriptHandler initialHandler, bool echo = false)
/// </summary> /// </summary>
bool Echo { get; } = echo; bool Echo { get; } = echo;
/// <summary>
/// Input a multiline string into the repl. Call <see cref="Ready"/> or <see cref="Closed"/> at the end so the
/// statement is valid.
/// </summary>
public ReplDriver this[string input] => ExecuteAll(input);
public ReplDriver ExecuteAll(string multiline) public ReplDriver ExecuteAll(string multiline)
{ {
var lines = multiline.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); var lines = multiline.Split('\n', StringSplitOptions.TrimEntries);
return lines.Aggregate(this, (repl, line) => repl.Execute(line)); return lines.Aggregate(this, (repl, line) => repl.Execute(line));
} }
@ -33,19 +27,22 @@ public class ReplDriver(IScriptHandler initialHandler, bool echo = false)
$"Cannot execute \"{inputLine}\", handler quit. Last input was \"{LastInput}\""); $"Cannot execute \"{inputLine}\", handler quit. Last input was \"{LastInput}\"");
if (Echo) Console.WriteLine($"{Handler.Prompt}{inputLine}"); if (Echo) Console.WriteLine($"{Handler.Prompt}{inputLine}");
Handler = Handler.HandleInput(inputLine); var result = Handler.HandleInput(inputLine);
if (!result.Success) Assert.Fail($"Script failed at \"{inputLine}\": {result.Message}");
Handler = result.NextHandler;
LastInput = inputLine; LastInput = inputLine;
return this; return this;
} }
public void Ready() public void AssertFails(string inputLine)
{ {
Assert.That(Handler, Is.Not.Null, "Handler is closed"); if (Handler is null) throw new AssertionException(
} $"Cannot execute \"{inputLine}\", handler quit. Last input was \"{LastInput}\"");
if (Echo) Console.WriteLine($"{Handler.Prompt}{inputLine}");
public void Closed() var result = Handler.HandleInput(inputLine);
{ if (result.Success) Assert.Fail($"Expected \"{inputLine}\" to fail, but it succeeded.");
Assert.That(Handler, Is.Null, "Handler is not closed");
} }
} }

View File

@ -8,17 +8,22 @@ namespace MultiversalDiplomacyTests;
public class ReplTest public class ReplTest
{ {
private static ReplDriver StandardRepl() => new(
new SetupScriptHandler(
(msg) => {/* discard */},
World.WithStandardMap(),
Adjudicator.MovementPhase));
[Test] [Test]
public void SetupHandler() public void SetupHandler()
{ {
SetupScriptHandler setup = new(World.WithStandardMap(), strict: true); var repl = StandardRepl();
ReplDriver repl = new(setup);
repl[""" repl.ExecuteAll("""
unit Germany A Munich unit Germany A Munich
unit Austria Army Tyrolia unit Austria Army Tyrolia
unit England F Lon unit England F Lon
"""].Ready(); """);
Assert.That(repl.Handler, Is.TypeOf<SetupScriptHandler>()); Assert.That(repl.Handler, Is.TypeOf<SetupScriptHandler>());
SetupScriptHandler handler = (SetupScriptHandler)repl.Handler!; SetupScriptHandler handler = (SetupScriptHandler)repl.Handler!;
@ -27,9 +32,7 @@ public class ReplTest
Assert.That(handler.World.GetUnitAt("Tyr"), Is.Not.Null); Assert.That(handler.World.GetUnitAt("Tyr"), Is.Not.Null);
Assert.That(handler.World.GetUnitAt("Lon"), Is.Not.Null); Assert.That(handler.World.GetUnitAt("Lon"), Is.Not.Null);
repl[""" repl.Execute("---");
---
"""].Ready();
Assert.That(repl.Handler, Is.TypeOf<GameScriptHandler>()); Assert.That(repl.Handler, Is.TypeOf<GameScriptHandler>());
} }
@ -37,20 +40,18 @@ public class ReplTest
[Test] [Test]
public void SubmitOrders() public void SubmitOrders()
{ {
SetupScriptHandler setup = new(World.WithStandardMap(), strict: true); var repl = StandardRepl();
ReplDriver repl = new ReplDriver(setup)["""
repl.ExecuteAll("""
unit Germany A Mun unit Germany A Mun
unit Austria A Tyr unit Austria A Tyr
unit England F Lon unit England F Lon
begin ---
"""];
repl["""
Germany A Mun hold Germany A Mun hold
Austria: Army Tyrolia - Vienna Austria: Army Tyrolia - Vienna
England: England:
Lon h Lon h
"""].Ready(); """);
Assert.That(repl.Handler, Is.TypeOf<GameScriptHandler>()); Assert.That(repl.Handler, Is.TypeOf<GameScriptHandler>());
GameScriptHandler handler = (GameScriptHandler)repl.Handler!; GameScriptHandler handler = (GameScriptHandler)repl.Handler!;
@ -62,13 +63,214 @@ public class ReplTest
World before = handler.World; World before = handler.World;
repl[""" repl.Execute("---");
---
"""].Ready();
Assert.That(repl.Handler, Is.TypeOf<AdjudicationQueryScriptHandler>()); Assert.That(repl.Handler, Is.TypeOf<AdjudicationQueryScriptHandler>());
var newHandler = (AdjudicationQueryScriptHandler)repl.Handler!; var newHandler = (AdjudicationQueryScriptHandler)repl.Handler!;
Assert.That(newHandler.World, Is.Not.EqualTo(before)); Assert.That(newHandler.World, Is.Not.EqualTo(before));
Assert.That(newHandler.World.Timelines.Pasts.Count, Is.EqualTo(2)); Assert.That(newHandler.World.Timelines.Pasts.Count, Is.EqualTo(2));
} }
[Test]
public void AssertBasic()
{
var repl = StandardRepl();
repl.ExecuteAll("""
unit Germany A Munich
---
---
assert true
""");
repl.AssertFails("assert false");
}
[Test]
public void AssertOrderValidity()
{
var repl = StandardRepl();
repl.ExecuteAll("""
unit Germany A Mun
---
Germany A Mun - Stp
---
""");
// Order should be invalid
repl.Execute("assert order-invalid Mun");
repl.AssertFails("assert order-valid Mun");
repl.ExecuteAll("""
---
Germany A Mun - Tyr
---
""");
// Order should be valid
repl.Execute("assert order-valid Mun");
repl.AssertFails("assert order-invalid Mun");
}
[Test]
public void AssertSeasonPast()
{
var repl = StandardRepl();
repl.ExecuteAll("""
unit England F London
---
---
""");
// Expected past
repl.Execute("assert has-past a1>a0");
// Incorrect past
repl.AssertFails("assert has-past a0>a1");
repl.AssertFails("assert has-past a1>a1");
// Missing season
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()
{
var repl = StandardRepl();
repl.ExecuteAll("""
unit Germany A Mun
unit Austria A Tyr
---
Germany Mun - Tyr
---
""");
// Movement fails
repl.Execute("assert no-move Mun");
repl.AssertFails("assert moves Mun");
repl.ExecuteAll("""
---
Germany Mun - Boh
---
""");
// Movement succeeds
repl.Execute("assert moves Mun");
repl.AssertFails("assert no-move Mun");
}
[Test]
public void AssertSupportHold()
{
var repl = StandardRepl();
repl.ExecuteAll("""
unit Germany A Mun
unit Germany A Boh
unit Austria A Tyr
---
Germany Mun s Boh
---
""");
// Support is given
repl.Execute("assert support-given Mun");
repl.AssertFails("assert support-cut Mun");
repl.ExecuteAll("""
---
Germany Mun s Boh
Austria Tyr - Mun
---
""");
// Support is cut
repl.Execute("assert support-cut Mun");
repl.AssertFails("assert support-given Mun");
}
[Test]
public void AssertSupportMove()
{
var repl = StandardRepl();
repl.ExecuteAll("""
unit Germany A Berlin
unit Germany A Bohemia
unit Austria A Tyrolia
---
Germany:
Berlin - Silesia
Bohemia s Berlin - Silesia
---
""");
// Support is given
repl.Execute("assert support-given Boh");
repl.AssertFails("assert support-cut Boh");
repl.ExecuteAll("""
---
Germany:
Silesia - Munich
Bohemia s Silesia - Munich
Austria Tyrolia - Bohemia
---
""");
// Support is cut
repl.AssertFails("assert support-given Boh");
repl.Execute("assert support-cut Boh");
}
[Test]
public void AssertDislodged()
{
var repl = StandardRepl();
repl.ExecuteAll("""
unit Germany A Mun
unit Germany A Boh
unit Austria A Tyr
---
Germany Mun - Tyr
---
""");
// Move repelled
repl.Execute("assert not-dislodged Tyr");
repl.AssertFails("assert dislodged Tyr");
repl.ExecuteAll("""
---
Germany Mun - Tyr
Germany Boh s Mun - Tyr
---
""");
// Move succeeds
repl.Execute("assert dislodged Tyr");
repl.AssertFails("assert not-dislodged Tyr");
}
} }

View File

@ -19,14 +19,37 @@ public class ScriptTests
[TestCaseSource(nameof(DatcTestCases))] [TestCaseSource(nameof(DatcTestCases))]
public void Test_DATC(string testScriptPath) public void Test_DATC(string testScriptPath)
{ {
Assert.Ignore("Script tests postponed until parsing tests are done");
string filename = Path.GetFileName(testScriptPath); string filename = Path.GetFileName(testScriptPath);
int line = 0; int line = 0;
IScriptHandler? handler = new SetupScriptHandler(World.WithStandardMap(), strict: true); bool expectFailure = false;
IScriptHandler handler = new SetupScriptHandler(
(msg) => {/* discard */},
World.WithStandardMap(),
Adjudicator.MovementPhase);
foreach (string input in File.ReadAllLines(testScriptPath)) { foreach (string input in File.ReadAllLines(testScriptPath)) {
line++; line++;
handler = handler?.HandleInput(input);
if (handler is null) Assert.Fail($"Script {filename} quit unexpectedly at line {line}: \"{input}\""); // Handle test directives
if (input == "#test:skip") {
Assert.Ignore($"Script {filename} skipped at line {line}");
}
if (input == "#test:fails") {
expectFailure = true;
continue;
}
var result = handler.HandleInput(input);
if (expectFailure && result.Success) throw new AssertionException(
$"Script {filename} expected line {line} to fail, but it succeeded");
if (!expectFailure && !result.Success) throw new AssertionException(
$"Script {filename} error at line {line}: {result.Message}");
if (result.NextHandler is null) throw new AssertionException(
$"Script {filename} quit unexpectedly at line {line}: \"{input}\"");
handler = result.NextHandler;
expectFailure = false;
} }
} }
} }

View File

@ -11,4 +11,4 @@ F North Sea - Picardy
--- ---
# Order should fail. # Order should fail.
assert North Sea holds assert hold-order North Sea

View File

@ -0,0 +1,21 @@
# 6.A.10. TEST CASE, SUPPORT ON UNREACHABLE DESTINATION NOT POSSIBLE
# The destination of the move that is supported must be reachable by the supporting unit.
unit Austria A Venice
unit Italy F Rome
unit Italy A Apulia
---
Austria:
A Venice Hold
Italy:
F Rome Supports A Apulia - Venice
A Apulia - Venice
---
# The support of Rome is illegal, because Venice cannot be reached from Rome by a fleet. Venice is not dislodged.
assert hold-order Rome
assert not-dislodged Venice

View File

@ -0,0 +1,19 @@
# 6.A.11. TEST CASE, SIMPLE BOUNCE
# Two armies bouncing on each other.
unit Austria A Vienna
unit Italy A Venice
---
Austria:
A Vienna - Tyrolia
Italy:
A Venice - Tyrolia
---
# The two units bounce.
assert no-move Vienna
assert no-move Venice

View File

@ -0,0 +1,24 @@
# 6.A.12. TEST CASE, BOUNCE OF THREE UNITS
# If three units move to the same area, the adjudicator should not bounce the first two units and then let the third unit go to the now open area.
unit Austria A Vienna
unit Germany A Munich
unit Italy A Venice
---
Austria:
A Vienna - Tyrolia
Germany:
A Munich - Tyrolia
Italy:
A Venice - Tyrolia
---
# The three units bounce.
assert no-move Vienna
assert no-move Munich
assert no-move Venice

View File

@ -6,9 +6,10 @@ unit England A Liverpool
--- ---
England: England:
#test:fails
A Liverpool - Irish Sea A Liverpool - Irish Sea
--- ---
# Order should fail. # Order should fail.
assert Liverpool holds assert hold-order Liverpool

View File

@ -1,14 +1,15 @@
# 6.A.3. TEST CASE, MOVE FLEET TO LAND # 6.A.3. TEST CASE, MOVE FLEET TO LAND
# Check whether a fleet cannot move to land. # Check whether a fleet cannot move to land.
unit Germany Army Kiel unit Germany F Kiel
--- ---
Germany: Germany:
#test:fails
F Kiel - Munich F Kiel - Munich
--- ---
# Order should fail. # Order should fail.
assert Kiel holds assert hold-order Kiel

View File

@ -1,7 +1,7 @@
# 6.A.4. TEST CASE, MOVE TO OWN SECTOR # 6.A.4. TEST CASE, MOVE TO OWN SECTOR
# Moving to the same sector is an illegal move (2023 rulebook, page 7, "An Army can be ordered to move into an adjacent inland or coastal province."). # Moving to the same sector is an illegal move (2023 rulebook, page 7, "An Army can be ordered to move into an adjacent inland or coastal province.").
unit Germany Army Kiel unit Germany F Kiel
--- ---
@ -11,4 +11,4 @@ F Kiel - Kiel
--- ---
# Program should not crash. # Program should not crash.
assert Kiel holds assert hold-order Kiel

View File

@ -1,6 +1,9 @@
# 6.A.5. TEST CASE, MOVE TO OWN SECTOR WITH CONVOY # 6.A.5. TEST CASE, MOVE TO OWN SECTOR WITH CONVOY
# Moving to the same sector is still illegal with convoy (2023 rulebook, page 7, "Note: An Army can move across water provinces from one coastal province to another..."). # Moving to the same sector is still illegal with convoy (2023 rulebook, page 7, "Note: An Army can move across water provinces from one coastal province to another...").
# TODO convoy order parsing
#test:skip
unit England F North Sea unit England F North Sea
unit England A Yorkshire unit England A Yorkshire
unit England A Liverpool unit England A Liverpool
@ -21,11 +24,11 @@ A Wales Supports F London - Yorkshire
--- ---
# The move of the army in Yorkshire is illegal. # The move of the army in Yorkshire is illegal.
assert Yorkshire holds assert hold-order Yorkshire
# This makes the support of Liverpool also illegal and without the support, the Germans have a stronger force. # This makes the support of Liverpool also illegal and without the support, the Germans have a stronger force.
assert North Sea holds assert hold-order North Sea
assert Liverpool holds assert hold-order Liverpool
assert London moves assert moves London
# The army in London dislodges the army in Yorkshire. # The army in London dislodges the army in Yorkshire.
assert Wales supports assert support-given Wales
assert Yorkshire dislodged assert dislodged Yorkshire

View File

@ -13,4 +13,4 @@ F London - North Sea
--- ---
# Order should fail. # Order should fail.
assert London holds assert hold-order London

View File

@ -1,8 +1,11 @@
# 6.A.7. TEST CASE, ONLY ARMIES CAN BE CONVOYED # 6.A.7. TEST CASE, ONLY ARMIES CAN BE CONVOYED
# A fleet cannot be convoyed. # A fleet cannot be convoyed.
# TODO convoy order parsing
#test:skip
unit England F London unit England F London
unit England North Sea unit England F North Sea
--- ---
@ -13,4 +16,4 @@ F North Sea Convoys A London - Belgium
--- ---
# Move from London to Belgium should fail. # Move from London to Belgium should fail.
assert London holds assert hold-order London

View File

@ -0,0 +1,20 @@
# 6.A.8. TEST CASE, SUPPORT TO HOLD YOURSELF IS NOT POSSIBLE
# An army cannot get an additional hold power by supporting itself.
unit Italy A Venice
unit Italy A Tyrolia
unit Austria F Trieste
---
Italy:
A Venice - Trieste
A Tyrolia Supports A Venice - Trieste
Austria:
F Trieste Supports F Trieste
---
# The army in Trieste should be dislodged.
assert dislodged Trieste

View File

@ -0,0 +1,14 @@
# 6.A.9. TEST CASE, FLEETS MUST FOLLOW COAST IF NOT ON SEA
# If two provinces are adjacent, that does not mean that a fleet can move between those two provinces. An implementation that only holds one list of adjacent provinces for each province is incorrect.
unit Italy F Rome
---
Italy:
F Rome - Venice
---
# Move fails. An army can go from Rome to Venice, but a fleet cannot.
assert hold-order Rome

View File

@ -1,11 +1,12 @@
using System.Text.Json; using System.Text.Json;
using MultiversalDiplomacy.Adjudicate;
using MultiversalDiplomacy.Adjudicate.Decision; using MultiversalDiplomacy.Adjudicate.Decision;
using MultiversalDiplomacy.Model; using MultiversalDiplomacy.Model;
using NUnit.Framework; using NUnit.Framework;
using static MultiversalDiplomacyTests.Adjudicator;
namespace MultiversalDiplomacyTests; namespace MultiversalDiplomacyTests;
public class SerializationTest public class SerializationTest
@ -74,7 +75,7 @@ public class SerializationTest
public void SerializeRoundTrip_MDATC_3_A_2() public void SerializeRoundTrip_MDATC_3_A_2()
{ {
// Set up MDATC 3.A.2 // Set up MDATC 3.A.2
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance); TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhase);
setup[("a", 0)] setup[("a", 0)]
.GetReference(out Season s0) .GetReference(out Season s0)
["Germany"] ["Germany"]
@ -107,7 +108,7 @@ public class SerializationTest
}); });
// Resume the test case // Resume the test case
setup = new(reserialized, MovementPhaseAdjudicator.Instance); setup = new(reserialized, MovementPhase);
setup[("a", 1)] setup[("a", 1)]
["Germany"] ["Germany"]
.Army("Mun").Supports.Army("Mun", season: s0).MoveTo("Tyr").GetReference(out var mun1) .Army("Mun").Supports.Army("Mun", season: s0).MoveTo("Tyr").GetReference(out var mun1)