Compare commits

..

6 Commits

22 changed files with 465 additions and 14 deletions

View File

@ -194,6 +194,21 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
// unit can move to the destination.
List<SupportMoveOrder> supportMoveOrders = unitOrders.OfType<SupportMoveOrder>().ToList();
// Trivial check: armies cannot move to water and fleets cannot move to land.
AdjudicatorHelpers.InvalidateIfNotMatching(
order => (order.Target.Type == UnitType.Army && order.Location.Type == LocationType.Land)
|| (order.Target.Type == UnitType.Fleet && order.Location.Type == LocationType.Water),
ValidationReason.IllegalDestinationType,
ref supportMoveOrders,
ref validationResults);
// Trivial check: a unit cannot move to where it already is.
AdjudicatorHelpers.InvalidateIfNotMatching(
order => !(order.Location.Key == order.Target.Location && order.Season == order.Unit.Season),
ValidationReason.DestinationMatchesOrigin,
ref supportMoveOrders,
ref validationResults);
// Support-move orders are invalid if the unit supports a move to any location in its own
// province.
AdjudicatorHelpers.InvalidateIfNotMatching(

View File

@ -314,13 +314,13 @@ public class Map
.AddOceanLocation(),
Province.Empty("Eastern Mediterranean Sea", "EMS", "EAS")
.AddOceanLocation(),
Province.Empty("Gulf of Lyons", "GOL", "LYO")
Province.Empty("Gulf of Lyons", "Gulf of Lyon", "GOL", "LYO")
.AddOceanLocation(),
Province.Empty("Ionian Sea", "IOS", "ION", "INS")
.AddOceanLocation(),
Province.Empty("Tyrrhenian Sea", "TYS", "TYN")
.AddOceanLocation(),
Province.Empty("Western Mediterranean Sea", "WMS", "WES")
Province.Empty("Western Mediterranean Sea", "Western Mediterranean", "WMS", "WES")
.AddOceanLocation(),
#endregion
];

View File

@ -28,12 +28,14 @@ public class OrderParser(World world)
public const string HoldVerb = "(h|hold|holds)";
public const string MoveVerb = "(-|(?:->)|(?:=>)|(?:attack(?:s)?)|(?:move(?:s)?(?: to)?))";
public const string MoveVerb = "(-|(?:->)|(?:=>)|(?:to)|(?:attack(?:s)?)|(?:move(?:s)?(?: to)?))";
public const string SupportVerb = "(s|support|supports)";
public const string ViaConvoy = "(convoy|via convoy|by convoy)";
public const string ConvoyVerb = "(c|convoy|convoys)";
public Regex UnitDeclaration = new(
$"^{world.Map.PowerRegex} {Type} {world.Map.ProvinceRegex}(?:{SlashLocation}|{ParenLocation})?$",
RegexOptions.IgnoreCase);
@ -183,6 +185,51 @@ public class OrderParser(World world)
: match.Groups[18].Value,
match.Groups[19].Value);
public Regex Convoy => new(
$"{UnitSpec} {ConvoyVerb} {UnitSpec} {MoveVerb} {FullLocation}$",
RegexOptions.IgnoreCase);
public static (
string type,
string timeline,
string province,
string location,
string turn,
string convoyVerb,
string targetType,
string targetTimeline,
string targetProvince,
string targetLocation,
string targetTurn,
string moveVerb,
string destTimeline,
string destProvince,
string destLocation,
string destTurn)
ParseConvoy(Match match) => (
match.Groups[1].Value,
match.Groups[2].Value,
match.Groups[3].Value,
match.Groups[4].Length > 0
? match.Groups[4].Value
: match.Groups[5].Value,
match.Groups[6].Value,
match.Groups[7].Value,
match.Groups[8].Value,
match.Groups[9].Value,
match.Groups[10].Value,
match.Groups[11].Length > 0
? match.Groups[11].Value
: match.Groups[12].Value,
match.Groups[13].Value,
match.Groups[14].Value,
match.Groups[15].Value,
match.Groups[16].Value,
match.Groups[17].Length > 0
? match.Groups[17].Value
: match.Groups[18].Value,
match.Groups[19].Value);
public static bool TryParseUnit(World world, string unitSpec, [NotNullWhen(true)] out Unit? newUnit)
{
newUnit = null;
@ -219,15 +266,28 @@ public class OrderParser(World world)
order = null;
OrderParser re = new(world);
if (re.Hold.Match(command) is Match holdMatch && holdMatch.Success) {
if (re.Hold.Match(command) is Match holdMatch && holdMatch.Success)
{
return TryParseHoldOrder(world, power, holdMatch, out order);
} else if (re.Move.Match(command) is Match moveMatch && moveMatch.Success) {
}
else if (re.Move.Match(command) is Match moveMatch && moveMatch.Success)
{
return TryParseMoveOrder(world, power, moveMatch, out order);
} else if (re.SupportHold.Match(command) is Match sholdMatch && sholdMatch.Success) {
}
else if (re.SupportHold.Match(command) is Match sholdMatch && sholdMatch.Success)
{
return TryParseSupportHoldOrder(world, power, sholdMatch, out order);
} else if (re.SupportMove.Match(command) is Match smoveMatch && smoveMatch.Success) {
}
else if (re.SupportMove.Match(command) is Match smoveMatch && smoveMatch.Success)
{
return TryParseSupportMoveOrder(world, power, smoveMatch, out order);
} else {
}
else if (re.Convoy.Match(command) is Match convoyMatch && convoyMatch.Success)
{
return TryParseConvoyOrder(world, power, convoyMatch, out order);
}
else
{
return false;
}
}
@ -448,4 +508,45 @@ public class OrderParser(World world)
order = new SupportMoveOrder(power, subject, target, new(destTimeline, destTurn), destLocation);
return true;
}
public static bool TryParseConvoyOrder(
World world,
string power,
Match match,
[NotNullWhen(true)] out Order? order)
{
order = null;
var convoy = ParseConvoy(match);
if (!TryParseOrderSubject(world, convoy.timeline, convoy.turn, convoy.province, out Unit? subject)) {
return false;
}
if (!TryParseOrderSubject(
world, convoy.targetTimeline, convoy.targetTurn, convoy.targetProvince, out Unit? target))
{
return false;
}
string destTimeline = convoy.destTimeline.Length > 0
? convoy.destTimeline
// If the destination is unspecified, use the target's
: target.Season.Timeline;
int destTurn = convoy.destTurn.Length > 0
? int.Parse(convoy.destTurn)
// If the destination is unspecified, use the unit's
: target.Season.Turn;
var destProvince = world.Map.Provinces.Single(province => province.Is(convoy.destProvince));
// Only armies can be convoyed, which means the destination location can only be land.
var landLocations = destProvince.Locations.Where(loc => loc.Type == LocationType.Land);
if (!landLocations.Any()) return false; // Can't convoy to water
string destLocationKey = landLocations.First().Key;
var destLocation = world.Map.GetLocation(destLocationKey);
order = new ConvoyOrder(power, subject, target, new(destTimeline, destTurn), destLocation);
return true;
}
}

View File

@ -198,6 +198,47 @@ public class OrderParserTest
Assert.That(actual, Is.EqualTo(expected), "Unexpected parse results");
}
static IEnumerable<TestCaseData> ConvoyRegexMatchesTestCases()
{
// Full specification
yield return Test(
"Fleet a-Nth/w@0 c A a-London/l@0 - a-Belgium/l@0",
"Fleet", "a", "Nth", "w", "0", "c", "A", "a", "London", "l", "0", "-", "a", "Belgium", "l", "0");
// Case insensitivity
yield return Test(
"fleet B-nth/W@0 CONVOYS a B-lon/L@0 MOVE TO B-bel/L@0",
"fleet", "B", "nth", "W", "0", "CONVOYS", "a", "B", "lon", "L", "0", "MOVE TO", "B", "bel", "L", "0");
// All optionals missing
yield return Test(
"TYN c ROM - TUN",
"", "", "TYN", "", "", "c", "", "", "ROM", "", "", "-", "", "TUN", "", "");
// No confusion of unit type and timeline
yield return Test(
"F A-BOT C FIN - A-LVN",
"F", "A", "BOT", "", "", "C", "", "", "FIN", "", "", "-", "A", "LVN", "", "");
// Elements with spaces
yield return Test(
"Western Mediterranean Sea convoys Spain move to North Africa",
"", "", "Western Mediterranean Sea", "", "", "convoys", "", "", "Spain", "", "", "move to", "", "North Africa", "", "");
}
[TestCaseSource(nameof(ConvoyRegexMatchesTestCases))]
public void ConvoyRegexMatches(string order, string[] expected)
{
OrderParser re = new(World.WithStandardMap());
var match = re.Convoy.Match(order);
Assert.True(match.Success, "Match failed");
var (type, timeline, province, location, turn, convoyVerb,
targetType, targetTimeline, targetProvince, targetLocation, targetTurn, moveVerb,
destTimeline, destProvince, destLocation, destTurn) = OrderParser.ParseConvoy(match);
string[] actual = [type, timeline, province, location, turn, convoyVerb,
targetType, targetTimeline, targetProvince, targetLocation, targetTurn, moveVerb,
destTimeline, destProvince, destLocation, destTurn];
// Use EquivalentTo for more detailed error message
Assert.That(actual, Is.EquivalentTo(expected), "Unexpected parse results");
Assert.That(actual, Is.EqualTo(expected), "Unexpected parse results");
}
[Test]
public void OrderParsingTest()
{
@ -332,4 +373,17 @@ public class OrderParserTest
Is.False,
"Should not parse ambiguous coastal support");
}
[Test]
public void DisambiguateConvoyDestination()
{
World world = World.WithStandardMap().AddUnits("France F MID", "France A Brest");
Assert.That(
OrderParser.TryParseOrder(world, "France", "MID C Brest - Spain", out Order? order),
"Failed to parse convoy order");
Assert.That(order, Is.TypeOf<ConvoyOrder>(), "Unexpected order type");
Location dest = ((ConvoyOrder)order!).Location;
Assert.That(dest.Name, Is.EqualTo("land"), "Unexpected destination location");
}
}

View File

@ -1,9 +1,6 @@
# 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...").
# TODO convoy order parsing
#test:skip
unit England F North Sea
unit England A Yorkshire
unit England A Liverpool

View File

@ -1,9 +1,6 @@
# 6.A.7. TEST CASE, ONLY ARMIES CAN BE CONVOYED
# A fleet cannot be convoyed.
# TODO convoy order parsing
#test:skip
unit England F London
unit England F North Sea

View File

@ -0,0 +1,15 @@
# 6.B.1. TEST CASE, MOVING WITH UNSPECIFIED COAST WHEN COAST IS NECESSARY
# Coast is significant in this case:
unit France F Portugal
---
France:
#test:fails
F Portugal - Spain
---
# Move should fail.
assert hold-order Portugal

View File

@ -0,0 +1,16 @@
# 6.B.10. TEST CASE, UNIT ORDERED WITH WRONG COAST
# A player might specify the wrong coast for the ordered unit.
# France has a fleet on the south coast of Spain and orders:
unit France F Spain/sc
---
France:
F Spain(nc) - Gulf of Lyon
---
# If only perfect orders are accepted, then the move will fail, but since the coast for the ordered unit has no purpose, it might also be ignored (see issue 4.B.5).
# I prefer that a move will be attempted.
assert moves Spain

View File

@ -0,0 +1,15 @@
# 6.B.11. TEST CASE, COAST CANNOT BE ORDERED TO CHANGE
# The coast cannot change by just ordering the other coast.
# France has a fleet on the north coast of Spain and orders:
unit France F Spain/nc
---
France:
F Spain(sc) - Gulf of Lyon
---
# The move fails.
assert hold-order Spain

View File

@ -0,0 +1,16 @@
# 6.B.12. TEST CASE, ARMY MOVEMENT WITH COASTAL SPECIFICATION
# For armies the coasts are irrelevant:
unit France A Gascony
---
France:
A Gascony - Spain(nc)
---
# If only perfect orders are accepted, then the move will fail. But it is also possible that coasts are ignored in this case and a move will be attempted (see issue 4.B.6).
# I prefer that a move will be attempted.
assert moves Gascony

View File

@ -0,0 +1,19 @@
# 6.B.13. TEST CASE, COASTAL CRAWL NOT ALLOWED
# If a fleet is leaving a sector from a certain coast while in the opposite direction another fleet is moving to another coast of the sector, it is still a head-to-head battle. This has been decided in the great revision of the 1961 rules that resulted in the 1971 rules.
unit Turkey F Bulgaria/sc
unit Turkey F Constantinople
# Currently this test crashes with a stack overflow
#test:skip
---
Turkey:
F Bulgaria(sc) - Constantinople
F Constantinople - Bulgaria(ec)
---
# Both moves fail.
assert no-move Bulgaria
assert no-move Constantinople

View File

@ -0,0 +1,9 @@
# 6.B.14. TEST CASE, BUILDING WITH UNSPECIFIED COAST
# Coast must be specified in certain build cases:
#test:skip
Russia:
Build F St Petersburg
# See issue 4.B.7. Build fails.

View File

@ -0,0 +1,26 @@
# 6.B.15. TEST CASE, SUPPORTING FOREIGN UNIT WITH UNSPECIFIED COAST
# Opinions differ on this.
unit France F Portugal
unit England F MAO
unit Italy F Gulf of Lyon
unit Italy F Western Mediterranean
---
France:
#test:fails
F Portugal Supports F Mid-Atlantic Ocean - Spain
England:
F Mid-Atlantic Ocean - Spain(nc)
Italy:
F Gulf of Lyon Supports F Western Mediterranean - Spain(sc)
F Western Mediterranean - Spain(sc)
---
# See issue 4.B.4.
# Although the move to the north coast of Spain might be a surprise for France, it is hard to believe that England somehow tricked France. Therefore, I prefer that the support succeeds and the Italian fleet in the Western Mediterranean bounces. However, if orders are checked on submission (such as in webbased play), support without coast should not be given as an option.
assert moves Western Mediterranean

View File

@ -0,0 +1,15 @@
# 6.B.2. TEST CASE, MOVING WITH UNSPECIFIED COAST WHEN COAST IS NOT NECESSARY
# There is only one coast possible in this case:
unit France F Gascony
---
France:
F Gascony - Spain
---
# Since the North Coast is the only coast that can be reached, it seems logical that a move is attempted to the north coast of Spain. See issue 4.B.2.
# I prefer that an attempt is made to the only possible coast, the north coast of Spain.
assert moves Gascony

View File

@ -0,0 +1,14 @@
# 6.B.3. TEST CASE, MOVING WITH WRONG COAST WHEN COAST IS NOT NECESSARY
# If only one coast is possible, but the wrong coast can be specified.
unit France F Gascony
---
France:
F Gascony - Spain(sc)
---
# If the rules are given a lenient interpretation, a move will be attempted to the north coast of Spain. However, this order is very precisely wrong. The order should be declared illegal and fleet should hold. See issue 4.B.3.
assert hold-order Gascony

View File

@ -0,0 +1,22 @@
# 6.B.4. TEST CASE, SUPPORT TO UNREACHABLE COAST ALLOWED
# A fleet can give support to a coast where it cannot go.
unit France F Gascony
unit France F Marseilles
unit Italy F Western Mediterranean
---
France:
F Gascony - Spain(nc)
F Marseilles Supports F Gascony - Spain(nc)
Italy:
F Western Mediterranean - Spain(sc)
---
# Although the fleet in Marseilles cannot go to the north coast it can still support targeting the north coast. So, the support is successful, the move of the fleet in Gascony succeeds and the move of the Italian fleet fails.
assert moves Gascony
assert support-given Marseilles
assert no-move Western Mediterranean

View File

@ -0,0 +1,20 @@
# 6.B.5. TEST CASE, SUPPORT FROM UNREACHABLE COAST NOT ALLOWED
# A fleet cannot give support to an area that cannot be reached from the current coast of the fleet.
unit France F Marseilles
unit France F Spain/nc
unit Italy F Gulf of Lyon
---
France:
F Marseilles - Gulf of Lyon
F Spain(nc) Supports F Marseilles - Gulf of Lyon
Italy:
F Gulf of Lyon Hold
---
# The Gulf of Lyon cannot be reached from the North Coast of Spain. Therefore, the support of Spain is illegal and the fleet in the Gulf of Lyon is not dislodged.
assert not-dislodged Gulf of Lyon

View File

@ -0,0 +1,27 @@
# 6.B.6. TEST CASE, SUPPORT CAN BE CUT WITH OTHER COAST
# Support can be cut from the other coast.
unit England F Irish Sea
unit England F North Atlantic Ocean
unit France F Spain/nc
unit France F MAO
unit Italy F Gulf of Lyon
---
England:
F Irish Sea Supports F North Atlantic Ocean - Mid-Atlantic Ocean
F North Atlantic Ocean - Mid-Atlantic Ocean
France:
F Spain(nc) Supports F Mid-Atlantic Ocean
F Mid-Atlantic Ocean Hold
Italy:
F Gulf of Lyon - Spain(sc)
---
# The Italian fleet in the Gulf of Lyon will cut the support in Spain. That means that the French fleet in the Mid Atlantic Ocean will be dislodged by the English fleet in the North Atlantic Ocean.
assert support-cut Spain
assert dislodged MAO

View File

@ -0,0 +1,26 @@
# 6.B.7. TEST CASE, SUPPORTING OWN UNIT WITH UNSPECIFIED COAST
# It is a little bit harsh to reject this.
unit France F Portugal
unit France F MAO
unit Italy F Gulf of Lyon
unit Italy F Western Mediterranean
---
France:
#test:fails
F Portugal Supports F Mid-Atlantic Ocean - Spain
F Mid-Atlantic Ocean - Spain(nc)
Italy:
F Gulf of Lyon Supports F Western Mediterranean - Spain(sc)
F Western Mediterranean - Spain(sc)
---
# See issue 4.B.4.
# I prefer that the support succeeds and the Italian fleet in the Western Mediterranean bounces. However, if orders are checked on submission (such as in webbased play), support without coast should not be given as an option.
assert moves Western Mediterranean
# 5dplomacy takes the stricter interpretation

View File

@ -0,0 +1,23 @@
# 6.B.8. TEST CASE, SUPPORTING WITH UNSPECIFIED COAST WHEN ONLY ONE COAST IS POSSIBLE
# If coast is omitted while only coast is possible, it should be considered a poorly written order, that should be followed.
unit France F Portugal
unit France F Gascony
unit Italy F Gulf of Lyon
unit Italy F Western Mediterranean
---
France:
F Portugal Supports F Gascony - Spain
F Gascony - Spain(nc)
Italy:
F Gulf of Lyon Supports F Western Mediterranean - Spain(sc)
F Western Mediterranean - Spain(sc)
---
# Support of Portugal is successful and the Italian fleet in the Western Mediterranean bounces with the French fleet from Gascony.
assert no-move Gascony
assert no-move Western Mediterranean

View File

@ -0,0 +1,23 @@
# 6.B.9. TEST CASE, SUPPORTING WITH WRONG COAST
# It should be possible to specify a coast and that coast should match.
unit France F Portugal
unit France F MAO
unit Italy F Gulf of Lyon
unit Italy F Western Mediterranean
---
France:
F Portugal Supports F Mid-Atlantic Ocean - Spain(nc)
F Mid-Atlantic Ocean - Spain(sc)
Italy:
F Gulf of Lyon Supports F Western Mediterranean - Spain(sc)
F Western Mediterranean - Spain(sc)
---
# See issue 4.B.4. Support of Portugal is invalid and the Italian fleet in the Western Mediterranean moves successfully.
assert moves Western mediterranean
assert no-move MAO

View File

@ -10,6 +10,7 @@ When the adjudicator is in a more complete state, this section will declare the
The MDATC (Multiversal Diplomacy Adjudicator Test Cases) document defines test cases that involve multiversal time travel.
- 4.B.4 (coast specification in support orders): 5dplomacy does not interpret orders by reference to other orders. Therefore, if a coast is not specified in a move or support-move order and the possible coasts are ambiguous, the order is illegal and will be replaced by a hold, even in the case of a support-move order omitting a coast that is specified by the supported move by the same power.
- 4.C.5 (missing nationality in support order), 4.C.6 (wrong nationality in support order): 5dplomacy does not support specifying the nationality of the supported unit.
## Variant rules