Compare commits

..

No commits in common. "7749d8df4e0e5189aa36ad808f9ed395830cc67d" and "b2ff8896b2ff16071afe724f19d0b32a5ddd7964" have entirely different histories.

17 changed files with 163 additions and 149 deletions

View File

@ -91,33 +91,19 @@ public class MovementDecisions
.Distinct() .Distinct()
.ToList(); .ToList();
(Province province, Season season) Point(Unit unit)
=> (world.Map.GetLocation(unit.Location).Province, unit.Season);
// Create a hold strength decision with an associated order for every province with a unit. // Create a hold strength decision with an associated order for every province with a unit.
foreach (UnitOrder order in relevantOrders) foreach (UnitOrder order in relevantOrders)
{ {
HoldStrength[Point(order.Unit)] = new(Point(order.Unit), order); HoldStrength[order.Unit.Point] = new(order.Unit.Point, order);
} }
bool IsIncoming(UnitOrder me, MoveOrder other)
=> me != other
&& other.Season == me.Unit.Season
&& other.Province == world.Map.GetLocation(me.Unit).Province;
bool AreOpposing(MoveOrder one, MoveOrder two)
=> one.Season == two.Unit.Season
&& two.Season == one.Unit.Season
&& one.Province == world.Map.GetLocation(two.Unit).Province
&& two.Province == world.Map.GetLocation(one.Unit).Province;
// Create all other relevant decisions for each order in the affected timelines. // Create all other relevant decisions for each order in the affected timelines.
foreach (UnitOrder order in relevantOrders) foreach (UnitOrder order in relevantOrders)
{ {
// Create a dislodge decision for this unit. // Create a dislodge decision for this unit.
List<MoveOrder> incoming = relevantOrders List<MoveOrder> incoming = relevantOrders
.OfType<MoveOrder>() .OfType<MoveOrder>()
.Where(other => IsIncoming(order, other)) .Where(order.IsIncoming)
.ToList(); .ToList();
IsDislodged[order.Unit] = new(order, incoming); IsDislodged[order.Unit] = new(order, incoming);
@ -132,7 +118,7 @@ public class MovementDecisions
// Determine if this move is a head-to-head battle. // Determine if this move is a head-to-head battle.
MoveOrder? opposingMove = relevantOrders MoveOrder? opposingMove = relevantOrders
.OfType<MoveOrder>() .OfType<MoveOrder>()
.FirstOrDefault(other => AreOpposing(move, other!), null); .FirstOrDefault(other => other!.IsOpposing(move), null);
// Find competing moves. // Find competing moves.
List<MoveOrder> competing = relevantOrders List<MoveOrder> competing = relevantOrders
@ -156,11 +142,11 @@ public class MovementDecisions
GivesSupport[support] = new(support, incoming); GivesSupport[support] = new(support, incoming);
// Ensure a hold strength decision exists for the target's province. // Ensure a hold strength decision exists for the target's province.
HoldStrength.Ensure(Point(support.Target), () => new(Point(support.Target))); HoldStrength.Ensure(support.Target.Point, () => new(support.Target.Point));
if (support is SupportHoldOrder supportHold) if (support is SupportHoldOrder supportHold)
{ {
HoldStrength[Point(support.Target)].Supports.Add(supportHold); HoldStrength[support.Target.Point].Supports.Add(supportHold);
} }
else if (support is SupportMoveOrder supportMove) else if (support is SupportMoveOrder supportMove)
{ {

View File

@ -77,7 +77,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
// Trivial check: a unit cannot move to where it already is. // Trivial check: a unit cannot move to where it already is.
AdjudicatorHelpers.InvalidateIfNotMatching( AdjudicatorHelpers.InvalidateIfNotMatching(
order => !(order.Location.Designation == order.Unit.Location && order.Season == order.Unit.Season), order => !(order.Location == order.Unit.Location && order.Season == order.Unit.Season),
ValidationReason.DestinationMatchesOrigin, ValidationReason.DestinationMatchesOrigin,
ref moveOrders, ref moveOrders,
ref validationResults); ref validationResults);
@ -90,7 +90,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
ILookup<bool, MoveOrder> moveOrdersByAdjacency = moveOrders ILookup<bool, MoveOrder> moveOrdersByAdjacency = moveOrders
.ToLookup(order => .ToLookup(order =>
// Map adjacency // Map adjacency
world.Map.GetLocation(order.Unit).Adjacents.Contains(order.Location) order.Unit.Location.Adjacents.Contains(order.Location)
// Turn adjacency // Turn adjacency
&& Math.Abs(order.Unit.Season.Turn - order.Season.Turn) <= 1 && Math.Abs(order.Unit.Season.Turn - order.Season.Turn) <= 1
// Timeline adjacency // Timeline adjacency
@ -138,7 +138,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
// Trivial check: cannot convoy a unit to its own location // Trivial check: cannot convoy a unit to its own location
AdjudicatorHelpers.InvalidateIfNotMatching( AdjudicatorHelpers.InvalidateIfNotMatching(
order => !( order => !(
order.Location.Designation == order.Target.Location order.Location == order.Target.Location
&& order.Season == order.Target.Season), && order.Season == order.Target.Season),
ValidationReason.DestinationMatchesOrigin, ValidationReason.DestinationMatchesOrigin,
ref convoyOrders, ref convoyOrders,
@ -175,8 +175,8 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
AdjudicatorHelpers.InvalidateIfNotMatching( AdjudicatorHelpers.InvalidateIfNotMatching(
order => order =>
// Map adjacency with respect to province // Map adjacency with respect to province
world.Map.GetLocation(order.Unit).Adjacents.Any( order.Unit.Location.Adjacents.Any(
adjLocation => adjLocation.Province == world.Map.GetLocation(order.Target).Province) adjLocation => adjLocation.Province == order.Target.Province)
// Turn adjacency // Turn adjacency
&& Math.Abs(order.Unit.Season.Turn - order.Target.Season.Turn) <= 1 && Math.Abs(order.Unit.Season.Turn - order.Target.Season.Turn) <= 1
// Timeline adjacency // 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 // Support-move orders are invalid if the unit supports a move to any location in its own
// province. // province.
AdjudicatorHelpers.InvalidateIfNotMatching( AdjudicatorHelpers.InvalidateIfNotMatching(
order => world.Map.GetLocation(order.Unit).Province != order.Province, order => order.Unit.Province != order.Province,
ValidationReason.NoSupportMoveAgainstSelf, ValidationReason.NoSupportMoveAgainstSelf,
ref supportMoveOrders, ref supportMoveOrders,
ref validationResults); ref validationResults);
@ -207,7 +207,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
AdjudicatorHelpers.InvalidateIfNotMatching( AdjudicatorHelpers.InvalidateIfNotMatching(
order => order =>
// Map adjacency with respect to province // Map adjacency with respect to province
world.Map.GetLocation(order.Unit).Adjacents.Any( order.Unit.Location.Adjacents.Any(
adjLocation => adjLocation.Province == order.Province) adjLocation => adjLocation.Province == order.Province)
// Turn adjacency // Turn adjacency
&& Math.Abs(order.Unit.Season.Turn - order.Season.Turn) <= 1 && Math.Abs(order.Unit.Season.Turn - order.Season.Turn) <= 1
@ -337,7 +337,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
Season moveSeason = doesMove.Order.Season; Season moveSeason = doesMove.Order.Season;
if (doesMove.Outcome == true && createdFutures.ContainsKey(moveSeason)) if (doesMove.Outcome == true && createdFutures.ContainsKey(moveSeason))
{ {
Unit next = doesMove.Order.Unit.Next(doesMove.Order.Location.Designation, createdFutures[moveSeason]); Unit next = doesMove.Order.Unit.Next(doesMove.Order.Location, createdFutures[moveSeason]);
logger.Log(3, "Advancing unit to {0}", next); logger.Log(3, "Advancing unit to {0}", next);
createdUnits.Add(next); createdUnits.Add(next);
} }
@ -366,7 +366,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
if (isDislodged.Outcome == false) if (isDislodged.Outcome == false)
{ {
// Non-dislodged units continue into the future. // Non-dislodged units continue into the future.
Unit next = order.Unit.Next(world.Map.GetLocation(order.Unit).Designation, future); Unit next = order.Unit.Next(order.Unit.Location, future);
logger.Log(3, "Advancing unit to {0}", next); logger.Log(3, "Advancing unit to {0}", next);
createdUnits.Add(next); createdUnits.Add(next);
} }
@ -375,7 +375,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
// Create a retreat for each dislodged unit. // Create a retreat for each dislodged unit.
// TODO check valid retreats and disbands // TODO check valid retreats and disbands
logger.Log(3, "Creating retreat for {0}", order.Unit); logger.Log(3, "Creating retreat for {0}", order.Unit);
var validRetreats = world.Map.GetLocation(order.Unit).Adjacents var validRetreats = order.Unit.Location.Adjacents
.Select(loc => (future, loc)) .Select(loc => (future, loc))
.ToList(); .ToList();
RetreatingUnit retreat = new(order.Unit, validRetreats); 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 the origin and destination are adjacent, then there is a path.
if (// Map adjacency if (// Map adjacency
world.Map.GetLocation(decision.Order.Unit).Adjacents.Contains(decision.Order.Location) decision.Order.Unit.Location.Adjacents.Contains(decision.Order.Location)
// Turn adjacency // Turn adjacency
&& Math.Abs(decision.Order.Unit.Season.Turn - decision.Order.Season.Turn) <= 1 && Math.Abs(decision.Order.Unit.Season.Turn - decision.Order.Season.Turn) <= 1
// Timeline adjacency // Timeline adjacency

View File

@ -30,13 +30,13 @@ public static class PathFinder
// also have coasts, and between those coasts there is a path of adjacent sea provinces // 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 // (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. // belong to another power or were not given convoy orders; it will simply fail.
IDictionary<(string location, Season season), Unit> fleets = world.Units IDictionary<(Location location, Season season), Unit> fleets = world.Units
.Where(unit => unit.Type == UnitType.Fleet) .Where(unit => unit.Type == UnitType.Fleet)
.ToDictionary(unit => (unit.Location, unit.Season)); .ToDictionary(unit => (unit.Location, unit.Season));
// Verify that the origin is a coastal province. // Verify that the origin is a coastal province.
if (world.Map.GetLocation(movingUnit).Type != LocationType.Land) return false; if (movingUnit.Location.Type != LocationType.Land) return false;
IEnumerable<Location> originCoasts = world.Map.GetLocation(movingUnit).Province.Locations IEnumerable<Location> originCoasts = movingUnit.Province.Locations
.Where(location => location.Type == LocationType.Water); .Where(location => location.Type == LocationType.Water);
if (!originCoasts.Any()) return false; 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, // 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. // and hasn't already been visited.
if (!adjLocation.Province.Locations.Any(l => l.Type == LocationType.Land) if (!adjLocation.Province.Locations.Any(l => l.Type == LocationType.Land)
&& fleets.ContainsKey((adjLocation.Designation, adjSeason)) && fleets.ContainsKey((adjLocation, adjSeason))
&& !visited.Contains((adjLocation, adjSeason))) && !visited.Contains((adjLocation, adjSeason)))
{ {
toVisit.Enqueue((adjLocation, adjSeason)); toVisit.Enqueue((adjLocation, adjSeason));

View File

@ -20,12 +20,12 @@ public class Location
/// <summary> /// <summary>
/// The location's full human-readable name. /// The location's full human-readable name.
/// </summary> /// </summary>
public string Name { get; } public string? Name { get; }
/// <summary> /// <summary>
/// The location's shorthand abbreviation. /// The location's shorthand abbreviation.
/// </summary> /// </summary>
public string Abbreviation { get; } public string? Abbreviation { get; }
/// <summary> /// <summary>
/// The location's type. /// The location's type.
@ -39,12 +39,7 @@ public class Location
public IEnumerable<Location> Adjacents => this.AdjacentList; public IEnumerable<Location> Adjacents => this.AdjacentList;
private List<Location> AdjacentList { get; set; } private List<Location> AdjacentList { get; set; }
/// <summary> public Location(Province province, string? name, string? abbreviation, LocationType type)
/// The unique name of this location in the map.
/// </summary>
public string Designation => $"{this.ProvinceName}/{this.Abbreviation}";
public Location(Province province, string name, string abbreviation, LocationType type)
{ {
this.Province = province; this.Province = province;
this.Name = name; this.Name = name;
@ -55,7 +50,7 @@ public class Location
public override string ToString() public override string ToString()
{ {
return this.Name == "land" || this.Name == "water" return this.Name == null
? $"{this.Province.Name} ({this.Type})" ? $"{this.Province.Name} ({this.Type})"
: $"{this.Province.Name} ({this.Type}:{this.Name}]"; : $"{this.Province.Name} ({this.Type}:{this.Name}]";
} }

View File

@ -17,8 +17,6 @@ public class Map
private List<Province> _Provinces { get; } private List<Province> _Provinces { get; }
private Dictionary<string, Location> LocationLookup { get; }
/// <summary> /// <summary>
/// The game powers. /// The game powers.
/// </summary> /// </summary>
@ -31,10 +29,6 @@ public class Map
Type = type; Type = type;
_Provinces = provinces.ToList(); _Provinces = provinces.ToList();
_Powers = powers.ToList(); _Powers = powers.ToList();
LocationLookup = Provinces
.SelectMany(province => province.Locations)
.ToDictionary(location => location.Designation);
} }
/// <summary> /// <summary>
@ -47,27 +41,29 @@ public class Map
/// Get a province by name. Throws if the province is not found. /// Get a province by name. Throws if the province is not found.
/// </summary> /// </summary>
private static Province GetProvince(string provinceName, IEnumerable<Province> provinces) private static Province GetProvince(string provinceName, IEnumerable<Province> provinces)
=> provinces.SingleOrDefault( {
p => p!.Name.Equals(provinceName, StringComparison.InvariantCultureIgnoreCase) string provinceNameUpper = provinceName.ToUpperInvariant();
|| p.Abbreviations.Any( Province? foundProvince = provinces.SingleOrDefault(
a => a.Equals(provinceName, StringComparison.InvariantCultureIgnoreCase)), p => p!.Name.ToUpperInvariant() == provinceNameUpper
null) || p.Abbreviations.Any(a => a.ToUpperInvariant() == provinceNameUpper),
?? throw new KeyNotFoundException($"Province {provinceName} not found"); null);
if (foundProvince == null) throw new KeyNotFoundException(
$"Province {provinceName} not found");
return foundProvince;
}
/// <summary> /// <summary>
/// Get the location in a province matching a predicate. Throws if there is not exactly one /// Get the location in a province matching a predicate. Throws if there is not exactly one
/// such location. /// such location.
/// </summary> /// </summary>
private Location GetLocation(string provinceName, Func<Location, bool> predicate) private Location GetLocation(string provinceName, Func<Location, bool> predicate)
=> GetProvince(provinceName).Locations.SingleOrDefault( {
l => l != null && predicate(l), null) Location? foundLocation = GetProvince(provinceName).Locations.SingleOrDefault(
?? throw new KeyNotFoundException($"No such location in {provinceName}"); l => l != null && predicate(l), null);
if (foundLocation == null) throw new KeyNotFoundException(
public Location GetLocation(string designation) $"No such location in {provinceName}");
=> LocationLookup[designation]; return foundLocation;
}
public Location GetLocation(Unit unit)
=> GetLocation(unit.Location);
/// <summary> /// <summary>
/// Get the sole land location of a province. /// Get the sole land location of a province.
@ -125,10 +121,10 @@ public class Map
#region Provinces #region Provinces
Province.Empty("North Africa", "NAF") Province.Empty("North Africa", "NAF")
.AddLandLocation() .AddLandLocation()
.AddOceanLocation(), .AddCoastLocation(),
Province.Supply("Tunis", "TUN") Province.Supply("Tunis", "TUN")
.AddLandLocation() .AddLandLocation()
.AddOceanLocation(), .AddCoastLocation(),
Province.Empty("Bohemia", "BOH") Province.Empty("Bohemia", "BOH")
.AddLandLocation(), .AddLandLocation(),
Province.Supply("Budapest", "BUD") Province.Supply("Budapest", "BUD")
@ -137,71 +133,71 @@ public class Map
.AddLandLocation(), .AddLandLocation(),
Province.Supply("Trieste", "TRI") Province.Supply("Trieste", "TRI")
.AddLandLocation() .AddLandLocation()
.AddOceanLocation(), .AddCoastLocation(),
Province.Empty("Tyrolia", "TYR") Province.Empty("Tyrolia", "TYR")
.AddLandLocation(), .AddLandLocation(),
Province.Time("Vienna", "VIE") Province.Time("Vienna", "VIE")
.AddLandLocation(), .AddLandLocation(),
Province.Empty("Albania", "ALB") Province.Empty("Albania", "ALB")
.AddLandLocation() .AddLandLocation()
.AddOceanLocation(), .AddCoastLocation(),
Province.Supply("Bulgaria", "BUL") Province.Supply("Bulgaria", "BUL")
.AddLandLocation() .AddLandLocation()
.AddCoastLocation("east coast", "ec") .AddCoastLocation("east coast", "ec")
.AddCoastLocation("south coast", "sc"), .AddCoastLocation("south coast", "sc"),
Province.Supply("Greece", "GRE") Province.Supply("Greece", "GRE")
.AddLandLocation() .AddLandLocation()
.AddOceanLocation(), .AddCoastLocation(),
Province.Supply("Rumania", "RUM", "RMA") Province.Supply("Rumania", "RUM", "RMA")
.AddLandLocation() .AddLandLocation()
.AddOceanLocation(), .AddCoastLocation(),
Province.Supply("Serbia", "SER") Province.Supply("Serbia", "SER")
.AddLandLocation(), .AddLandLocation(),
Province.Empty("Clyde", "CLY") Province.Empty("Clyde", "CLY")
.AddLandLocation() .AddLandLocation()
.AddOceanLocation(), .AddCoastLocation(),
Province.Supply("Edinburgh", "EDI") Province.Supply("Edinburgh", "EDI")
.AddLandLocation() .AddLandLocation()
.AddOceanLocation(), .AddCoastLocation(),
Province.Supply("Liverpool", "LVP", "LPL") Province.Supply("Liverpool", "LVP", "LPL")
.AddLandLocation() .AddLandLocation()
.AddOceanLocation(), .AddCoastLocation(),
Province.Time("London", "LON") Province.Time("London", "LON")
.AddLandLocation() .AddLandLocation()
.AddOceanLocation(), .AddCoastLocation(),
Province.Empty("Wales", "WAL") Province.Empty("Wales", "WAL")
.AddLandLocation() .AddLandLocation()
.AddOceanLocation(), .AddCoastLocation(),
Province.Empty("Yorkshire", "YOR") Province.Empty("Yorkshire", "YOR")
.AddLandLocation() .AddLandLocation()
.AddOceanLocation(), .AddCoastLocation(),
Province.Supply("Brest", "BRE") Province.Supply("Brest", "BRE")
.AddLandLocation() .AddLandLocation()
.AddOceanLocation(), .AddCoastLocation(),
Province.Empty("Burgundy", "BUR") Province.Empty("Burgundy", "BUR")
.AddLandLocation(), .AddLandLocation(),
Province.Empty("Gascony", "GAS") Province.Empty("Gascony", "GAS")
.AddLandLocation() .AddLandLocation()
.AddOceanLocation(), .AddCoastLocation(),
Province.Supply("Marseilles", "MAR") Province.Supply("Marseilles", "MAR")
.AddLandLocation() .AddLandLocation()
.AddOceanLocation(), .AddCoastLocation(),
Province.Time("Paris", "PAR") Province.Time("Paris", "PAR")
.AddLandLocation(), .AddLandLocation(),
Province.Empty("Picardy", "PIC") Province.Empty("Picardy", "PIC")
.AddLandLocation() .AddLandLocation()
.AddOceanLocation(), .AddCoastLocation(),
Province.Time("Berlin", "BER") Province.Time("Berlin", "BER")
.AddLandLocation() .AddLandLocation()
.AddOceanLocation(), .AddCoastLocation(),
Province.Supply("Kiel", "KIE") Province.Supply("Kiel", "KIE")
.AddLandLocation() .AddLandLocation()
.AddOceanLocation(), .AddCoastLocation(),
Province.Supply("Munich", "MUN") Province.Supply("Munich", "MUN")
.AddLandLocation(), .AddLandLocation(),
Province.Empty("Prussia", "PRU") Province.Empty("Prussia", "PRU")
.AddLandLocation() .AddLandLocation()
.AddOceanLocation(), .AddCoastLocation(),
Province.Empty("Ruhr", "RUH", "RHR") Province.Empty("Ruhr", "RUH", "RHR")
.AddLandLocation(), .AddLandLocation(),
Province.Empty("Silesia", "SIL") Province.Empty("Silesia", "SIL")
@ -212,43 +208,43 @@ public class Map
.AddCoastLocation("south coast", "sc"), .AddCoastLocation("south coast", "sc"),
Province.Supply("Portugal", "POR") Province.Supply("Portugal", "POR")
.AddLandLocation() .AddLandLocation()
.AddOceanLocation(), .AddCoastLocation(),
Province.Empty("Apulia", "APU") Province.Empty("Apulia", "APU")
.AddLandLocation() .AddLandLocation()
.AddOceanLocation(), .AddCoastLocation(),
Province.Supply("Naples", "NAP") Province.Supply("Naples", "NAP")
.AddLandLocation() .AddLandLocation()
.AddOceanLocation(), .AddCoastLocation(),
Province.Empty("Piedmont", "PIE") Province.Empty("Piedmont", "PIE")
.AddLandLocation() .AddLandLocation()
.AddOceanLocation(), .AddCoastLocation(),
Province.Time("Rome", "ROM", "RME") Province.Time("Rome", "ROM", "RME")
.AddLandLocation() .AddLandLocation()
.AddOceanLocation(), .AddCoastLocation(),
Province.Empty("Tuscany", "TUS") Province.Empty("Tuscany", "TUS")
.AddLandLocation() .AddLandLocation()
.AddOceanLocation(), .AddCoastLocation(),
Province.Supply("Venice", "VEN") Province.Supply("Venice", "VEN")
.AddLandLocation() .AddLandLocation()
.AddOceanLocation(), .AddCoastLocation(),
Province.Supply("Belgium", "BEL") Province.Supply("Belgium", "BEL")
.AddLandLocation() .AddLandLocation()
.AddOceanLocation(), .AddCoastLocation(),
Province.Supply("Holland", "HOL") Province.Supply("Holland", "HOL")
.AddLandLocation() .AddLandLocation()
.AddOceanLocation(), .AddCoastLocation(),
Province.Empty("Finland", "FIN") Province.Empty("Finland", "FIN")
.AddLandLocation() .AddLandLocation()
.AddOceanLocation(), .AddCoastLocation(),
Province.Empty("Livonia", "LVN", "LVA") Province.Empty("Livonia", "LVN", "LVA")
.AddLandLocation() .AddLandLocation()
.AddOceanLocation(), .AddCoastLocation(),
Province.Time("Moscow", "MOS") Province.Time("Moscow", "MOS")
.AddLandLocation() .AddLandLocation()
.AddOceanLocation(), .AddCoastLocation(),
Province.Supply("Sevastopol", "SEV") Province.Supply("Sevastopol", "SEV")
.AddLandLocation() .AddLandLocation()
.AddOceanLocation(), .AddCoastLocation(),
Province.Supply("Saint Petersburg", "STP") Province.Supply("Saint Petersburg", "STP")
.AddLandLocation() .AddLandLocation()
.AddCoastLocation("north coast", "nc") .AddCoastLocation("north coast", "nc")
@ -259,28 +255,28 @@ public class Map
.AddLandLocation(), .AddLandLocation(),
Province.Supply("Denmark", "DEN") Province.Supply("Denmark", "DEN")
.AddLandLocation() .AddLandLocation()
.AddOceanLocation(), .AddCoastLocation(),
Province.Supply("Norway", "NWY") Province.Supply("Norway", "NWY")
.AddLandLocation() .AddLandLocation()
.AddOceanLocation(), .AddCoastLocation(),
Province.Supply("Sweden", "SWE") Province.Supply("Sweden", "SWE")
.AddLandLocation() .AddLandLocation()
.AddOceanLocation(), .AddCoastLocation(),
Province.Supply("Ankara", "ANK") Province.Supply("Ankara", "ANK")
.AddLandLocation() .AddLandLocation()
.AddOceanLocation(), .AddCoastLocation(),
Province.Empty("Armenia", "ARM") Province.Empty("Armenia", "ARM")
.AddLandLocation() .AddLandLocation()
.AddOceanLocation(), .AddCoastLocation(),
Province.Time("Constantinople", "CON") Province.Time("Constantinople", "CON")
.AddLandLocation() .AddLandLocation()
.AddOceanLocation(), .AddCoastLocation(),
Province.Supply("Smyrna", "SMY") Province.Supply("Smyrna", "SMY")
.AddLandLocation() .AddLandLocation()
.AddOceanLocation(), .AddCoastLocation(),
Province.Empty("Syria", "SYR") Province.Empty("Syria", "SYR")
.AddLandLocation() .AddLandLocation()
.AddOceanLocation(), .AddCoastLocation(),
Province.Empty("Barents Sea", "BAR") Province.Empty("Barents Sea", "BAR")
.AddOceanLocation(), .AddOceanLocation(),
Province.Empty("English Channel", "ENC", "ECH") Province.Empty("English Channel", "ENC", "ECH")

View File

@ -37,7 +37,7 @@ public class Province
this.Abbreviations = abbreviations; this.Abbreviations = abbreviations;
this.IsSupplyCenter = isSupply; this.IsSupplyCenter = isSupply;
this.IsTimeCenter = isTime; this.IsTimeCenter = isTime;
this.LocationList = []; this.LocationList = new List<Location>();
} }
public override string ToString() public override string ToString()
@ -49,26 +49,26 @@ public class Province
/// Create a new province with no supply center. /// Create a new province with no supply center.
/// </summary> /// </summary>
public static Province Empty(string name, params string[] abbreviations) public static Province Empty(string name, params string[] abbreviations)
=> new(name, abbreviations, isSupply: false, isTime: false); => new Province(name, abbreviations, isSupply: false, isTime: false);
/// <summary> /// <summary>
/// Create a new province with a supply center. /// Create a new province with a supply center.
/// </summary> /// </summary>
public static Province Supply(string name, params string[] abbreviations) public static Province Supply(string name, params string[] abbreviations)
=> new(name, abbreviations, isSupply: true, isTime: false); => new Province(name, abbreviations, isSupply: true, isTime: false);
/// <summary> /// <summary>
/// Create a new province with a time center. /// Create a new province with a time center.
/// </summary> /// </summary>
public static Province Time(string name, params string[] abbreviations) public static Province Time(string name, params string[] abbreviations)
=> new(name, abbreviations, isSupply: true, isTime: true); => new Province(name, abbreviations, isSupply: true, isTime: true);
/// <summary> /// <summary>
/// Create a new land location in this province. /// Create a new land location in this province.
/// </summary> /// </summary>
public Province AddLandLocation() public Province AddLandLocation()
{ {
Location location = new(this, "land", "l", LocationType.Land); Location location = new Location(this, name: null, abbreviation: null, LocationType.Land);
this.LocationList.Add(location); this.LocationList.Add(location);
return this; return this;
} }
@ -78,7 +78,19 @@ public class Province
/// </summary> /// </summary>
public Province AddOceanLocation() public Province AddOceanLocation()
{ {
Location location = new(this, "water", "w", LocationType.Water); Location location = new Location(this, name: null, abbreviation: null, LocationType.Water);
this.LocationList.Add(location);
return this;
}
/// <summary>
/// Create a new coastal location. Coastal locations must have names to disambiguate them
/// from the single land location in coastal provinces.
/// </summary>
public Province AddCoastLocation()
{
// Use a default name for provinces with only one coastal location
Location location = new Location(this, "coast", "c", LocationType.Water);
this.LocationList.Add(location); this.LocationList.Add(location);
return this; return this;
} }
@ -89,7 +101,7 @@ public class Province
/// </summary> /// </summary>
public Province AddCoastLocation(string name, string abbreviation) public Province AddCoastLocation(string name, string abbreviation)
{ {
Location location = new(this, name, abbreviation, LocationType.Water); Location location = new Location(this, name, abbreviation, LocationType.Water);
this.LocationList.Add(location); this.LocationList.Add(location);
return this; return this;
} }

View File

@ -1,3 +1,5 @@
using System.Text.Json.Serialization;
namespace MultiversalDiplomacy.Model; namespace MultiversalDiplomacy.Model;
/// <summary> /// <summary>
@ -8,12 +10,18 @@ public class Unit
/// <summary> /// <summary>
/// The previous iteration of a unit. This is null if the unit was just built. /// The previous iteration of a unit. This is null if the unit was just built.
/// </summary> /// </summary>
public string? Past { get; } public Unit? Past { get; }
/// <summary> /// <summary>
/// The location on the map where the unit is. /// The location on the map where the unit is.
/// </summary> /// </summary>
public string Location { get; } public Location Location { get; }
/// <summary>
/// The province where the unit is.
/// </summary>
[JsonIgnore]
public Province Province => this.Location.Province;
/// <summary> /// <summary>
/// The season in time when the unit is. /// The season in time when the unit is.
@ -31,11 +39,11 @@ public class Unit
public UnitType Type { get; } public UnitType Type { get; }
/// <summary> /// <summary>
/// A unique designation for this unit. /// The unit's spatiotemporal location as a province-season tuple.
/// </summary> /// </summary>
public string Designation => $"{Type.ToShort()} {Season.Timeline}-{Location}@{Season.Turn}"; public (Province province, Season season) Point => (this.Province, this.Season);
private Unit(string? past, string location, Season season, Power power, UnitType type) private Unit(Unit? past, Location location, Season season, Power power, UnitType type)
{ {
this.Past = past; this.Past = past;
this.Location = location; this.Location = location;
@ -45,18 +53,20 @@ public class Unit
} }
public override string ToString() public override string ToString()
=> $"{Power.Name[0]} {Type.ToShort()} {Season.Timeline}-{Location}@{Season.Turn}"; {
return $"{this.Power.Name[0]} {this.Type.ToShort()} {(this.Province, this.Season).ToShort()}";
}
/// <summary> /// <summary>
/// Create a new unit. No validation is performed; the adjudicator should only call this /// Create a new unit. No validation is performed; the adjudicator should only call this
/// method after accepting a build order. /// method after accepting a build order.
/// </summary> /// </summary>
public static Unit Build(string location, Season season, Power power, UnitType type) public static Unit Build(Location location, Season season, Power power, UnitType type)
=> new(past: null, location, season, power, type); => new(past: null, location, season, power, type);
/// <summary> /// <summary>
/// Advance this unit's timeline to a new location and season. /// Advance this unit's timeline to a new location and season.
/// </summary> /// </summary>
public Unit Next(string location, Season season) public Unit Next(Location location, Season season)
=> new(past: this.Designation, location, season, this.Power, this.Type); => new(past: this, location, season, this.Power, this.Type);
} }

View File

@ -1,3 +1,4 @@
using System.Collections.ObjectModel;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace MultiversalDiplomacy.Model; namespace MultiversalDiplomacy.Model;
@ -204,7 +205,7 @@ public class World
: splits.Length == 3 : splits.Length == 3
? Map.GetWater(splits[2]) ? Map.GetWater(splits[2])
: Map.GetWater(splits[2], splits[3]); : Map.GetWater(splits[2], splits[3]);
Unit unit = Unit.Build(location.Designation, this.RootSeason, power, type); Unit unit = Unit.Build(location, this.RootSeason, power, type);
return unit; return unit;
}); });
return this.Update(units: units); return this.Update(units: units);
@ -327,13 +328,9 @@ public class World
Province province = Map.GetProvince(provinceName); Province province = Map.GetProvince(provinceName);
season ??= RootSeason; season ??= RootSeason;
Unit? foundUnit = this.Units.SingleOrDefault( Unit? foundUnit = this.Units.SingleOrDefault(
u => Map.GetLocation(u!).Province == province && u!.Season == season, u => u!.Province == province && u.Season == season,
null) null)
?? throw new KeyNotFoundException($"Unit at {province} at {season} not found"); ?? throw new KeyNotFoundException($"Unit at {province} at {season} not found");
return foundUnit; return foundUnit;
} }
public Unit GetUnitByDesignation(string designation)
=> Units.SingleOrDefault(u => u!.Designation == designation, null)
?? throw new KeyNotFoundException($"Unit {designation} not found");
} }

View File

@ -39,6 +39,15 @@ public class MoveOrder : UnitOrder
return $"{this.Unit} -> {(this.Province, this.Season).ToShort()}"; return $"{this.Unit} -> {(this.Province, this.Season).ToShort()}";
} }
/// <summary>
/// Returns whether another move order is in a head-to-head battle with this order.
/// </summary>
public bool IsOpposing(MoveOrder other)
=> this.Season == other.Unit.Season
&& other.Season == this.Unit.Season
&& this.Province == other.Unit.Province
&& other.Province == this.Unit.Province;
/// <summary> /// <summary>
/// Returns whether another move order has the same destination as this order. /// Returns whether another move order has the same destination as this order.
/// </summary> /// </summary>

View File

@ -16,4 +16,12 @@ public abstract class UnitOrder : Order
{ {
this.Unit = unit; this.Unit = unit;
} }
/// <summary>
/// Returns whether a move order is moving into this order's unit's province.
/// </summary>
public bool IsIncoming(MoveOrder other)
=> this != other
&& other.Season == this.Unit.Season
&& other.Province == this.Unit.Province;
} }

View File

@ -46,12 +46,12 @@ public class TimeTravelTest
Unit originalUnit = world.GetUnitAt("Mun", s0); Unit originalUnit = world.GetUnitAt("Mun", s0);
Unit aMun0 = world.GetUnitAt("Mun", s1); Unit aMun0 = world.GetUnitAt("Mun", s1);
Unit aTyr = world.GetUnitAt("Tyr", fork); Unit aTyr = world.GetUnitAt("Tyr", fork);
Assert.That(aTyr.Past, Is.EqualTo(mun1.Order.Unit.Designation)); Assert.That(aTyr.Past, Is.EqualTo(mun1.Order.Unit));
Assert.That(world.GetUnitByDesignation(aTyr.Past!).Past, Is.EqualTo(mun0.Order.Unit.Designation)); Assert.That(aTyr.Past?.Past, Is.EqualTo(mun0.Order.Unit));
// Confirm that there is a unit in Mun b1 originating from Mun a0 // Confirm that there is a unit in Mun b1 originating from Mun a0
Unit aMun1 = world.GetUnitAt("Mun", fork); Unit aMun1 = world.GetUnitAt("Mun", fork);
Assert.That(aMun1.Past, Is.EqualTo(originalUnit.Designation)); Assert.That(aMun1.Past, Is.EqualTo(originalUnit));
} }
[Test] [Test]
@ -95,7 +95,7 @@ public class TimeTravelTest
Unit tyr1 = world.GetUnitAt("Tyr", fork); Unit tyr1 = world.GetUnitAt("Tyr", fork);
Assert.That( Assert.That(
tyr1.Past, tyr1.Past,
Is.EqualTo(mun0.Order.Unit.Designation), Is.EqualTo(mun0.Order.Unit),
"Expected A Mun a0 to advance to Tyr b1"); "Expected A Mun a0 to advance to Tyr b1");
Assert.That( Assert.That(
world.RetreatingUnits.Count, world.RetreatingUnits.Count,

View File

@ -209,7 +209,7 @@ public class MovementAdjudicatorTest
Unit u2 = updated.GetUnitAt("Mun", s2); Unit u2 = updated.GetUnitAt("Mun", s2);
Assert.That(updated.Units.Count, Is.EqualTo(2)); Assert.That(updated.Units.Count, Is.EqualTo(2));
Assert.That(u2, Is.Not.EqualTo(mun1.Order.Unit)); Assert.That(u2, Is.Not.EqualTo(mun1.Order.Unit));
Assert.That(u2.Past, Is.EqualTo(mun1.Order.Unit.Designation)); Assert.That(u2.Past, Is.EqualTo(mun1.Order.Unit));
Assert.That(u2.Season, Is.EqualTo(s2)); Assert.That(u2.Season, Is.EqualTo(s2));
setup[("a", 1)] setup[("a", 1)]
@ -229,7 +229,7 @@ public class MovementAdjudicatorTest
updated = setup.UpdateWorld(); updated = setup.UpdateWorld();
Season s3 = updated.GetSeason(s2.Timeline, s2.Turn + 1); Season s3 = updated.GetSeason(s2.Timeline, s2.Turn + 1);
Unit u3 = updated.GetUnitAt("Mun", s3); Unit u3 = updated.GetUnitAt("Mun", s3);
Assert.That(u3.Past, Is.EqualTo(mun2.Order.Unit.Designation)); Assert.That(u3.Past, Is.EqualTo(mun2.Order.Unit));
} }
[Test] [Test]
@ -259,7 +259,7 @@ public class MovementAdjudicatorTest
Unit u2 = updated.GetUnitAt("Tyr", s2); Unit u2 = updated.GetUnitAt("Tyr", s2);
Assert.That(updated.Units.Count, Is.EqualTo(2)); Assert.That(updated.Units.Count, Is.EqualTo(2));
Assert.That(u2, Is.Not.EqualTo(mun1.Order.Unit)); Assert.That(u2, Is.Not.EqualTo(mun1.Order.Unit));
Assert.That(u2.Past, Is.EqualTo(mun1.Order.Unit.Designation)); Assert.That(u2.Past, Is.EqualTo(mun1.Order.Unit));
Assert.That(u2.Season, Is.EqualTo(s2)); Assert.That(u2.Season, Is.EqualTo(s2));
setup[("a", 1)] setup[("a", 1)]
@ -279,6 +279,6 @@ public class MovementAdjudicatorTest
updated = setup.UpdateWorld(); updated = setup.UpdateWorld();
Season s3 = updated.GetSeason(s2.Timeline, s2.Turn + 1); Season s3 = updated.GetSeason(s2.Timeline, s2.Turn + 1);
Unit u3 = updated.GetUnitAt("Mun", s3); Unit u3 = updated.GetUnitAt("Mun", s3);
Assert.That(u3.Past, Is.EqualTo(u2.Designation)); Assert.That(u3.Past, Is.EqualTo(u2));
} }
} }

View File

@ -108,7 +108,8 @@ public abstract class OrderReference
DefendStrength defend => defend.Order == this.Order, DefendStrength defend => defend.Order == this.Order,
PreventStrength prevent => prevent.Order == this.Order, PreventStrength prevent => prevent.Order == this.Order,
HoldStrength hold => this.Order is UnitOrder unitOrder HoldStrength hold => this.Order is UnitOrder unitOrder
&& hold.Province == Builder.World.Map.GetLocation(unitOrder.Unit).Province, ? hold.Province == unitOrder.Unit.Province
: false,
_ => false, _ => false,
}).ToList(); }).ToList();
return adjudications; return adjudications;

View File

@ -94,7 +94,7 @@ public class SerializationTest
Unit tyr1 = world.GetUnitAt("Tyr", fork); Unit tyr1 = world.GetUnitAt("Tyr", fork);
Assert.That( Assert.That(
tyr1.Past, tyr1.Past,
Is.EqualTo(mun0.Order.Unit.Designation), Is.EqualTo(mun0.Order.Unit),
"Expected A Mun a0 to advance to Tyr b1"); "Expected A Mun a0 to advance to Tyr b1");
Assert.That( Assert.That(
world.RetreatingUnits.Count, world.RetreatingUnits.Count,

View File

@ -262,7 +262,7 @@ public class TestCaseBuilder
foreach (Unit unit in this.World.Units) foreach (Unit unit in this.World.Units)
{ {
if (unit.Power == power if (unit.Power == power
&& World.Map.GetLocation(unit).Province == location.Province && unit.Province == location.Province
&& unit.Season == season) && unit.Season == season)
{ {
return unit; return unit;
@ -270,7 +270,7 @@ public class TestCaseBuilder
} }
// Not found // Not found
Unit newUnit = Unit.Build(location.Designation, season, power, type); Unit newUnit = Unit.Build(location, season, power, type);
this.World = this.World.Update(units: this.World.Units.Append(newUnit)); this.World = this.World.Update(units: this.World.Units.Append(newUnit));
return newUnit; return newUnit;
} }

View File

@ -40,7 +40,7 @@ class TestCaseBuilderTest
Assert.That(fleetSTP.Type, Is.EqualTo(UnitType.Fleet), "Unit created with wrong type"); Assert.That(fleetSTP.Type, Is.EqualTo(UnitType.Fleet), "Unit created with wrong type");
Assert.That( Assert.That(
fleetSTP.Location, fleetSTP.Location,
Is.EqualTo(setup.World.Map.GetWater("STP", "wc").Designation), Is.EqualTo(setup.World.Map.GetWater("STP", "wc")),
"Unit created on wrong coast"); "Unit created on wrong coast");
} }
@ -68,7 +68,7 @@ class TestCaseBuilderTest
List<UnitOrder> orders = setup.Orders.OfType<UnitOrder>().ToList(); List<UnitOrder> orders = setup.Orders.OfType<UnitOrder>().ToList();
Func<UnitOrder, bool> OrderForProvince(string name) Func<UnitOrder, bool> OrderForProvince(string name)
=> order => setup.World.Map.GetLocation(order.Unit).Province.Name == name; => order => order.Unit.Province.Name == name;
UnitOrder orderBer = orders.Single(OrderForProvince("Berlin")); UnitOrder orderBer = orders.Single(OrderForProvince("Berlin"));
Assert.That(orderBer, Is.InstanceOf<MoveOrder>(), "Unexpected order type"); Assert.That(orderBer, Is.InstanceOf<MoveOrder>(), "Unexpected order type");
@ -128,7 +128,7 @@ class TestCaseBuilderTest
"Wrong power"); "Wrong power");
Assert.That( Assert.That(
orderMun.Order.Unit.Location, orderMun.Order.Unit.Location,
Is.EqualTo(setup.World.Map.GetLand("Mun").Designation), Is.EqualTo(setup.World.Map.GetLand("Mun")),
"Wrong unit"); "Wrong unit");
Assert.That( Assert.That(

View File

@ -15,24 +15,24 @@ public class UnitTests
Tyr = world.Map.GetLand("Tyr"); Tyr = world.Map.GetLand("Tyr");
Power pw1 = world.Map.GetPower("Austria"); Power pw1 = world.Map.GetPower("Austria");
Season a0 = world.RootSeason; Season a0 = world.RootSeason;
Unit u1 = Unit.Build(Mun.Designation, a0, pw1, UnitType.Army); Unit u1 = Unit.Build(Mun, a0, pw1, UnitType.Army);
world = world.ContinueOrFork(a0, out Season a1); world = world.ContinueOrFork(a0, out Season a1);
Unit u2 = u1.Next(Boh.Designation, a1); Unit u2 = u1.Next(Boh, a1);
_ = world.ContinueOrFork(a1, out Season a2); _ = world.ContinueOrFork(a1, out Season a2);
Unit u3 = u2.Next(Tyr.Designation, a2); Unit u3 = u2.Next(Tyr, a2);
Assert.That(u3.Past, Is.EqualTo(u2.Designation), "Missing unit past"); Assert.That(u3.Past, Is.EqualTo(u2), "Missing unit past");
Assert.That(u2.Past, Is.EqualTo(u1.Designation), "Missing unit past"); Assert.That(u2.Past, Is.EqualTo(u1), "Missing unit past");
Assert.That(u1.Past, Is.Null, "Unexpected unit past"); Assert.That(u1.Past, Is.Null, "Unexpected unit past");
Assert.That(u1.Season, Is.EqualTo(a0), "Unexpected unit season"); Assert.That(u1.Season, Is.EqualTo(a0), "Unexpected unit season");
Assert.That(u2.Season, Is.EqualTo(a1), "Unexpected unit season"); Assert.That(u2.Season, Is.EqualTo(a1), "Unexpected unit season");
Assert.That(u3.Season, Is.EqualTo(a2), "Unexpected unit season"); Assert.That(u3.Season, Is.EqualTo(a2), "Unexpected unit season");
Assert.That(u1.Location, Is.EqualTo(Mun.Designation), "Unexpected unit location"); Assert.That(u1.Location, Is.EqualTo(Mun), "Unexpected unit location");
Assert.That(u2.Location, Is.EqualTo(Boh.Designation), "Unexpected unit location"); Assert.That(u2.Location, Is.EqualTo(Boh), "Unexpected unit location");
Assert.That(u3.Location, Is.EqualTo(Tyr.Designation), "Unexpected unit location"); Assert.That(u3.Location, Is.EqualTo(Tyr), "Unexpected unit location");
} }
} }