diff --git a/MultiversalDiplomacy/Adjudicate/MovementPhaseAdjudicator.cs b/MultiversalDiplomacy/Adjudicate/MovementPhaseAdjudicator.cs index c097d0d..29cf196 100644 --- a/MultiversalDiplomacy/Adjudicate/MovementPhaseAdjudicator.cs +++ b/MultiversalDiplomacy/Adjudicate/MovementPhaseAdjudicator.cs @@ -77,7 +77,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator // Trivial check: a unit cannot move to where it already is. AdjudicatorHelpers.InvalidateIfNotMatching( - order => !(order.Location == order.Unit.Location && order.Season == order.Unit.Season), + order => !(order.Location.Designation == order.Unit.LocationId && order.Season == order.Unit.Season), ValidationReason.DestinationMatchesOrigin, ref moveOrders, ref validationResults); @@ -90,7 +90,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator ILookup moveOrdersByAdjacency = moveOrders .ToLookup(order => // Map adjacency - order.Unit.Location.Adjacents.Contains(order.Location) + world.Map.GetLocation(order.Unit).Adjacents.Contains(order.Location) // Turn adjacency && Math.Abs(order.Unit.Season.Turn - order.Season.Turn) <= 1 // Timeline adjacency @@ -138,7 +138,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator // Trivial check: cannot convoy a unit to its own location AdjudicatorHelpers.InvalidateIfNotMatching( order => !( - order.Location == order.Target.Location + order.Location.Designation == order.Target.LocationId && order.Season == order.Target.Season), ValidationReason.DestinationMatchesOrigin, ref convoyOrders, @@ -175,8 +175,8 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator AdjudicatorHelpers.InvalidateIfNotMatching( order => // Map adjacency with respect to province - order.Unit.Location.Adjacents.Any( - adjLocation => adjLocation.Province == order.Target.Province) + world.Map.GetLocation(order.Unit).Adjacents.Any( + adjLocation => adjLocation.Province == world.Map.GetLocation(order.Target).Province) // Turn adjacency && Math.Abs(order.Unit.Season.Turn - order.Target.Season.Turn) <= 1 // Timeline adjacency @@ -195,7 +195,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator // Support-move orders are invalid if the unit supports a move to any location in its own // province. AdjudicatorHelpers.InvalidateIfNotMatching( - order => order.Unit.Province != order.Province, + order => world.Map.GetLocation(order.Unit).Province != order.Province, ValidationReason.NoSupportMoveAgainstSelf, ref supportMoveOrders, ref validationResults); @@ -207,7 +207,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator AdjudicatorHelpers.InvalidateIfNotMatching( order => // Map adjacency with respect to province - order.Unit.Location.Adjacents.Any( + world.Map.GetLocation(order.Unit).Adjacents.Any( adjLocation => adjLocation.Province == order.Province) // Turn adjacency && Math.Abs(order.Unit.Season.Turn - order.Season.Turn) <= 1 @@ -366,7 +366,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator if (isDislodged.Outcome == false) { // Non-dislodged units continue into the future. - Unit next = order.Unit.Next(order.Unit.Location, future); + Unit next = order.Unit.Next(world.Map.GetLocation(order.Unit), future); logger.Log(3, "Advancing unit to {0}", next); createdUnits.Add(next); } @@ -375,7 +375,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator // Create a retreat for each dislodged unit. // TODO check valid retreats and disbands logger.Log(3, "Creating retreat for {0}", order.Unit); - var validRetreats = order.Unit.Location.Adjacents + var validRetreats = world.Map.GetLocation(order.Unit).Adjacents .Select(loc => (future, loc)) .ToList(); RetreatingUnit retreat = new(order.Unit, validRetreats); @@ -633,7 +633,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator // If the origin and destination are adjacent, then there is a path. if (// Map adjacency - decision.Order.Unit.Location.Adjacents.Contains(decision.Order.Location) + world.Map.GetLocation(decision.Order.Unit).Adjacents.Contains(decision.Order.Location) // Turn adjacency && Math.Abs(decision.Order.Unit.Season.Turn - decision.Order.Season.Turn) <= 1 // Timeline adjacency diff --git a/MultiversalDiplomacy/Adjudicate/PathFinder.cs b/MultiversalDiplomacy/Adjudicate/PathFinder.cs index cc5d217..25aeeba 100644 --- a/MultiversalDiplomacy/Adjudicate/PathFinder.cs +++ b/MultiversalDiplomacy/Adjudicate/PathFinder.cs @@ -30,13 +30,13 @@ public static class PathFinder // also have coasts, and between those coasts there is a path of adjacent sea provinces // (not coastal) that are occupied by fleets. The move order is valid even if the fleets // belong to another power or were not given convoy orders; it will simply fail. - IDictionary<(Location location, Season season), Unit> fleets = world.Units + IDictionary<(string location, Season season), Unit> fleets = world.Units .Where(unit => unit.Type == UnitType.Fleet) - .ToDictionary(unit => (unit.Location, unit.Season)); + .ToDictionary(unit => (unit.LocationId, unit.Season)); // Verify that the origin is a coastal province. - if (movingUnit.Location.Type != LocationType.Land) return false; - IEnumerable originCoasts = movingUnit.Province.Locations + if (world.Map.GetLocation(movingUnit).Type != LocationType.Land) return false; + IEnumerable originCoasts = world.Map.GetLocation(movingUnit).Province.Locations .Where(location => location.Type == LocationType.Water); if (!originCoasts.Any()) return false; @@ -69,7 +69,7 @@ public static class PathFinder // If not, add this location to the to-visit set if it isn't a coast, has a fleet, // and hasn't already been visited. if (!adjLocation.Province.Locations.Any(l => l.Type == LocationType.Land) - && fleets.ContainsKey((adjLocation, adjSeason)) + && fleets.ContainsKey((adjLocation.Designation, adjSeason)) && !visited.Contains((adjLocation, adjSeason))) { toVisit.Enqueue((adjLocation, adjSeason)); diff --git a/MultiversalDiplomacy/Model/Location.cs b/MultiversalDiplomacy/Model/Location.cs index 10d0eee..65de76d 100644 --- a/MultiversalDiplomacy/Model/Location.cs +++ b/MultiversalDiplomacy/Model/Location.cs @@ -39,6 +39,11 @@ public class Location public IEnumerable Adjacents => this.AdjacentList; private List AdjacentList { get; set; } + /// + /// The unique name of this location in the map. + /// + public string Designation => $"{this.ProvinceName}/{this.Abbreviation}"; + public Location(Province province, string name, string abbreviation, LocationType type) { this.Province = province; diff --git a/MultiversalDiplomacy/Model/Map.cs b/MultiversalDiplomacy/Model/Map.cs index ab98092..9da2a38 100644 --- a/MultiversalDiplomacy/Model/Map.cs +++ b/MultiversalDiplomacy/Model/Map.cs @@ -17,6 +17,8 @@ public class Map private List _Provinces { get; } + private Dictionary LocationLookup { get; } + /// /// The game powers. /// @@ -29,6 +31,10 @@ public class Map Type = type; _Provinces = provinces.ToList(); _Powers = powers.ToList(); + + LocationLookup = Provinces + .SelectMany(province => province.Locations) + .ToDictionary(location => location.Designation); } /// @@ -41,29 +47,27 @@ public class Map /// Get a province by name. Throws if the province is not found. /// private static Province GetProvince(string provinceName, IEnumerable provinces) - { - string provinceNameUpper = provinceName.ToUpperInvariant(); - Province? foundProvince = provinces.SingleOrDefault( - p => p!.Name.ToUpperInvariant() == provinceNameUpper - || p.Abbreviations.Any(a => a.ToUpperInvariant() == provinceNameUpper), - null); - if (foundProvince == null) throw new KeyNotFoundException( - $"Province {provinceName} not found"); - return foundProvince; - } + => provinces.SingleOrDefault( + p => p!.Name.Equals(provinceName, StringComparison.InvariantCultureIgnoreCase) + || p.Abbreviations.Any( + a => a.Equals(provinceName, StringComparison.InvariantCultureIgnoreCase)), + null) + ?? throw new KeyNotFoundException($"Province {provinceName} not found"); /// /// Get the location in a province matching a predicate. Throws if there is not exactly one /// such location. /// private Location GetLocation(string provinceName, Func predicate) - { - Location? foundLocation = GetProvince(provinceName).Locations.SingleOrDefault( - l => l != null && predicate(l), null); - if (foundLocation == null) throw new KeyNotFoundException( - $"No such location in {provinceName}"); - return foundLocation; - } + => GetProvince(provinceName).Locations.SingleOrDefault( + l => l != null && predicate(l), null) + ?? throw new KeyNotFoundException($"No such location in {provinceName}"); + + public Location GetLocation(string designation) + => LocationLookup[designation]; + + public Location GetLocation(Unit unit) + => GetLocation(unit.LocationId); /// /// Get the sole land location of a province. diff --git a/MultiversalDiplomacy/Model/Unit.cs b/MultiversalDiplomacy/Model/Unit.cs index 736ec33..ec512e2 100644 --- a/MultiversalDiplomacy/Model/Unit.cs +++ b/MultiversalDiplomacy/Model/Unit.cs @@ -17,6 +17,8 @@ public class Unit /// public Location Location { get; } + public string LocationId => Location.Designation; + /// /// The province where the unit is. /// diff --git a/MultiversalDiplomacy/Model/World.cs b/MultiversalDiplomacy/Model/World.cs index abc5169..b90309e 100644 --- a/MultiversalDiplomacy/Model/World.cs +++ b/MultiversalDiplomacy/Model/World.cs @@ -328,7 +328,7 @@ public class World Province province = Map.GetProvince(provinceName); season ??= RootSeason; Unit? foundUnit = this.Units.SingleOrDefault( - u => u!.Province == province && u.Season == season, + u => Map.GetLocation(u!).Province == province && u!.Season == season, null) ?? throw new KeyNotFoundException($"Unit at {province} at {season} not found"); return foundUnit; diff --git a/MultiversalDiplomacyTests/MovementAdjudicatorTest.cs b/MultiversalDiplomacyTests/MovementAdjudicatorTest.cs index 100e624..73f77a0 100644 --- a/MultiversalDiplomacyTests/MovementAdjudicatorTest.cs +++ b/MultiversalDiplomacyTests/MovementAdjudicatorTest.cs @@ -179,7 +179,7 @@ public class MovementAdjudicatorTest // Confirm the unit was created Assert.That(updated.Units.Count, Is.EqualTo(2)); Unit second = updated.Units.Single(u => u.Past != null); - Assert.That(second.Location, Is.EqualTo(mun.Order.Unit.Location)); + Assert.That(second.LocationId, Is.EqualTo(mun.Order.Unit.LocationId)); } [Test] diff --git a/MultiversalDiplomacyTests/TestCaseBuilderTest.cs b/MultiversalDiplomacyTests/TestCaseBuilderTest.cs index 5ee8032..de2e931 100644 --- a/MultiversalDiplomacyTests/TestCaseBuilderTest.cs +++ b/MultiversalDiplomacyTests/TestCaseBuilderTest.cs @@ -39,8 +39,8 @@ class TestCaseBuilderTest Assert.That(fleetSTP.Power.Name, Is.EqualTo("Russia"), "Unit created with wrong power"); Assert.That(fleetSTP.Type, Is.EqualTo(UnitType.Fleet), "Unit created with wrong type"); Assert.That( - fleetSTP.Location, - Is.EqualTo(setup.World.Map.GetWater("STP", "wc")), + fleetSTP.LocationId, + Is.EqualTo(setup.World.Map.GetWater("STP", "wc").Designation), "Unit created on wrong coast"); } @@ -127,8 +127,8 @@ class TestCaseBuilderTest Is.EqualTo(setup.World.Map.GetPower("Germany")), "Wrong power"); Assert.That( - orderMun.Order.Unit.Location, - Is.EqualTo(setup.World.Map.GetLand("Mun")), + orderMun.Order.Unit.LocationId, + Is.EqualTo(setup.World.Map.GetLand("Mun").Designation), "Wrong unit"); Assert.That( diff --git a/MultiversalDiplomacyTests/UnitTests.cs b/MultiversalDiplomacyTests/UnitTests.cs index 46b869c..689aede 100644 --- a/MultiversalDiplomacyTests/UnitTests.cs +++ b/MultiversalDiplomacyTests/UnitTests.cs @@ -31,8 +31,8 @@ public class UnitTests Assert.That(u2.Season, Is.EqualTo(a1), "Unexpected unit season"); Assert.That(u3.Season, Is.EqualTo(a2), "Unexpected unit season"); - Assert.That(u1.Location, Is.EqualTo(Mun), "Unexpected unit location"); - Assert.That(u2.Location, Is.EqualTo(Boh), "Unexpected unit location"); - Assert.That(u3.Location, Is.EqualTo(Tyr), "Unexpected unit location"); + Assert.That(u1.LocationId, Is.EqualTo(Mun.Designation), "Unexpected unit location"); + Assert.That(u2.LocationId, Is.EqualTo(Boh.Designation), "Unexpected unit location"); + Assert.That(u3.LocationId, Is.EqualTo(Tyr.Designation), "Unexpected unit location"); } } \ No newline at end of file