From 4096e4d517c772c0b04d148e9fa8fb66385a7169 Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Fri, 6 Sep 2024 15:19:58 +0000 Subject: [PATCH] Implement coastal accessibility checks for support-move orders --- MultiversalDiplomacy/Model/OrderParser.cs | 51 ++++++++++++-------- MultiversalDiplomacyTests/OrderParserTest.cs | 34 ++++++++++++- 2 files changed, 64 insertions(+), 21 deletions(-) diff --git a/MultiversalDiplomacy/Model/OrderParser.cs b/MultiversalDiplomacy/Model/OrderParser.cs index fb99bc8..b8c223d 100644 --- a/MultiversalDiplomacy/Model/OrderParser.cs +++ b/MultiversalDiplomacy/Model/OrderParser.cs @@ -403,36 +403,47 @@ public class OrderParser(World world) : target.Season.Turn; var destProvince = world.Map.Provinces.Single(province => province.Is(support.destProvince)); + string? destLocationKey = null; - // DATC 4.B.6 requires that "irrelevant" locations like army to Spain nc be ignored. - // To satisfy this, any location of the wrong type is categorically ignored, so for an army the - // "north coast" location effectively doesn't exist here. - // Note that target is used instead of subject, since it is possible to support a move to an inaccessible - // coast as long as the subject can reach the province and the target can reach the location. + // 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, }); - // DATC 4.6.B also requires that unknown coasts be ignored. To satisfy this, an additional filter by name. - // Doing both of these filters means "A - Spain/nc" is as meaningful as "F - Spain/wc". - var matchingLocations = unitLocations.Where(loc => loc.Is(support.destLocation)); - - // If one location matched, use that location. If the coast is inaccessible to the target, the order will - // be invalidated by a path check later to satisfy DATC 4.B.3. - string? destLocationKey = matchingLocations.FirstOrDefault(defaultValue: null)?.Key; + 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) { - // If no location matched, location was omitted, nonexistent, or the wrong type. - // If one location is accessible, DATC 4.B.2 requires that it be used. - // 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; + 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; diff --git a/MultiversalDiplomacyTests/OrderParserTest.cs b/MultiversalDiplomacyTests/OrderParserTest.cs index 10882ee..360893c 100644 --- a/MultiversalDiplomacyTests/OrderParserTest.cs +++ b/MultiversalDiplomacyTests/OrderParserTest.cs @@ -296,8 +296,40 @@ public class OrderParserTest World world = World.WithStandardMap().AddUnits("France F Portugal"); Assert.That( - OrderParser.TryParseOrder(world, "France", "Portugal - Spain", out Order? northOrder), + 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(), "Unexpected north coast order"); + Assert.That(southOrder, Is.TypeOf(), "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"); + } }