Compare commits
20 Commits
f02e71d4f9
...
7c0cdb0a21
Author | SHA1 | Date |
---|---|---|
Tim Van Baak | 7c0cdb0a21 | |
Tim Van Baak | 5e2d495fa5 | |
Tim Van Baak | 7773c571e3 | |
Tim Van Baak | 4096e4d517 | |
Tim Van Baak | aaf2e78730 | |
Tim Van Baak | 864a933ba0 | |
Tim Van Baak | c6f10868ae | |
Tim Van Baak | 5b32786904 | |
Tim Van Baak | ae5eb22010 | |
Tim Van Baak | 26f7cee070 | |
Tim Van Baak | 80f340c0b2 | |
Tim Van Baak | e9c9999268 | |
Tim Van Baak | 4fee854c4c | |
Tim Van Baak | 569c9021e6 | |
Tim Van Baak | 3984b814ca | |
Tim Van Baak | f18147f666 | |
Tim Van Baak | 9f52c78b40 | |
Tim Van Baak | 7b890046b6 | |
Tim Van Baak | 720ccc4329 | |
Tim Van Baak | d2a46aa02d |
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,34 +307,45 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
using MultiversalDiplomacy.Adjudicate;
|
||||||
|
|
||||||
|
namespace MultiversalDiplomacyTests;
|
||||||
|
|
||||||
|
public static class Adjudicator
|
||||||
|
{
|
||||||
|
public static MovementPhaseAdjudicator MovementPhase { get; } = new MovementPhaseAdjudicator(NullLogger.Instance);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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) {}
|
||||||
|
}
|
|
@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,4 +11,4 @@ F North Sea - Picardy
|
||||||
---
|
---
|
||||||
|
|
||||||
# Order should fail.
|
# Order should fail.
|
||||||
assert North Sea holds
|
assert hold-order North Sea
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -13,4 +13,4 @@ F London - North Sea
|
||||||
---
|
---
|
||||||
|
|
||||||
# Order should fail.
|
# Order should fail.
|
||||||
assert London holds
|
assert hold-order London
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue