Compare commits

..

6 Commits

14 changed files with 124 additions and 89 deletions

View File

@ -1,6 +1,8 @@
using MultiversalDiplomacy.Model;
using MultiversalDiplomacy.Orders;
using static MultiversalDiplomacy.Model.Location;
namespace MultiversalDiplomacy.Adjudicate.Decision;
public class MovementDecisions
@ -8,7 +10,7 @@ public class MovementDecisions
public Dictionary<Unit, IsDislodged> IsDislodged { get; }
public Dictionary<MoveOrder, HasPath> HasPath { get; }
public Dictionary<SupportOrder, GivesSupport> GivesSupport { get; }
public Dictionary<(Province, string), HoldStrength> HoldStrength { get; }
public Dictionary<(string, string), HoldStrength> HoldStrength { get; }
public Dictionary<MoveOrder, AttackStrength> AttackStrength { get; }
public Dictionary<MoveOrder, DefendStrength> DefendStrength { get; }
public Dictionary<MoveOrder, PreventStrength> PreventStrength { get; }
@ -91,10 +93,10 @@ public class MovementDecisions
.Distinct()
.ToList();
(Province province, string season) UnitPoint(Unit unit)
=> (world.Map.GetLocation(unit.Location).Province, unit.Season.Key);
(Province province, string season) MovePoint(MoveOrder move)
=> (move.Province, move.Season.Key);
(string province, string season) UnitPoint(Unit unit)
=> (world.Map.GetLocation(unit.Location).Province.Name, unit.Season.Key);
(string province, string season) MovePoint(MoveOrder move)
=> (SplitKey(move.Location).province, move.Season.Key);
// Create a hold strength decision with an associated order for every province with a unit.
foreach (UnitOrder order in relevantOrders)
@ -108,13 +110,23 @@ public class MovementDecisions
bool IsIncoming(UnitOrder me, MoveOrder other)
=> me != other
&& other.Season == me.Unit.Season
&& other.Province == world.Map.GetLocation(me.Unit).Province;
&& SplitKey(other.Location).province == world.Map.GetLocation(me.Unit).Province.Name;
bool IsSupportFor(SupportMoveOrder me, MoveOrder move)
=> me.Target == move.Unit
&& me.Season == move.Season
&& me.Location.Key == move.Location;
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;
&& SplitKey(one.Location).province == world.Map.GetLocation(two.Unit).Province.Name
&& SplitKey(two.Location).province == world.Map.GetLocation(one.Unit).Province.Name;
bool AreCompeting(MoveOrder one, MoveOrder two)
=> one != two
&& one.Season == two.Season
&& SplitKey(one.Location).province == SplitKey(two.Location).province;
// Create all other relevant decisions for each order in the affected timelines.
foreach (UnitOrder order in relevantOrders)
@ -131,7 +143,7 @@ public class MovementDecisions
// Find supports corresponding to this move.
List<SupportMoveOrder> supports = relevantOrders
.OfType<SupportMoveOrder>()
.Where(support => support.IsSupportFor(move))
.Where(support => IsSupportFor(support, move))
.ToList();
// Determine if this move is a head-to-head battle.
@ -142,7 +154,7 @@ public class MovementDecisions
// Find competing moves.
List<MoveOrder> competing = relevantOrders
.OfType<MoveOrder>()
.Where(move.IsCompeting)
.Where(other => AreCompeting(move, other))
.ToList();
// Create the move-related decisions.
@ -153,7 +165,7 @@ public class MovementDecisions
DoesMove[move] = new(move, opposingMove, competing);
// Ensure a hold strength decision exists for the destination.
HoldStrength.Ensure(MovePoint(move), () => new(move.Province, move.Season));
HoldStrength.Ensure(MovePoint(move), () => new(world.Map.GetLocation(move.Location).Province, move.Season));
}
else if (order is SupportOrder support)
{
@ -173,7 +185,7 @@ public class MovementDecisions
{
// Ensure a hold strength decision exists for the target's destination.
HoldStrength.Ensure(
(supportMove.Province, supportMove.Season.Key),
(supportMove.Province.Name, supportMove.Season.Key),
() => new(supportMove.Province, supportMove.Season));
}
}

View File

@ -3,6 +3,8 @@ using MultiversalDiplomacy.Adjudicate.Logging;
using MultiversalDiplomacy.Model;
using MultiversalDiplomacy.Orders;
using static MultiversalDiplomacy.Model.Location;
namespace MultiversalDiplomacy.Adjudicate;
/// <summary>
@ -69,15 +71,15 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
// Trivial check: armies cannot move to water and fleets cannot move to land.
AdjudicatorHelpers.InvalidateIfNotMatching(
order => (order.Unit.Type == UnitType.Army && order.Location.Type == LocationType.Land)
|| (order.Unit.Type == UnitType.Fleet && order.Location.Type == LocationType.Water),
order => (order.Unit.Type == UnitType.Army && world.Map.GetLocation(order.Location).Type == LocationType.Land)
|| (order.Unit.Type == UnitType.Fleet && world.Map.GetLocation(order.Location).Type == LocationType.Water),
ValidationReason.IllegalDestinationType,
ref moveOrders,
ref validationResults);
// Trivial check: a unit cannot move to where it already is.
AdjudicatorHelpers.InvalidateIfNotMatching(
order => !(order.Location.Key == order.Unit.Location && order.Season == order.Unit.Season),
order => !(order.Location == order.Unit.Location && order.Season == order.Unit.Season),
ValidationReason.DestinationMatchesOrigin,
ref moveOrders,
ref validationResults);
@ -90,7 +92,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
ILookup<bool, MoveOrder> moveOrdersByAdjacency = moveOrders
.ToLookup(order =>
// Map adjacency
world.Map.GetLocation(order.Unit).Adjacents.Contains(order.Location)
world.Map.GetLocation(order.Unit).Adjacents.Select(loc => loc.Key).Contains(order.Location)
// Turn adjacency
&& Math.Abs(order.Unit.Season.Turn - order.Season.Turn) <= 1
// Timeline adjacency
@ -339,7 +341,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
Season moveSeason = doesMove.Order.Season;
if (doesMove.Outcome == true && createdFutures.TryGetValue(moveSeason, out Season future))
{
Unit next = doesMove.Order.Unit.Next(doesMove.Order.Location.Key, future);
Unit next = doesMove.Order.Unit.Next(doesMove.Order.Location, future);
logger.Log(3, "Advancing unit to {0}", next);
createdUnits.Add(next);
}
@ -618,7 +620,10 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
if (!potentialDislodger)
{
progress |= LoggedUpdate(decision, false, depth, "No invader can move");
string reason = decision.Incoming.Count == 0
? "No unit is attacking"
: "All attacks failed";
progress |= LoggedUpdate(decision, false, depth, reason);
}
return progress;
@ -635,7 +640,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
// If the origin and destination are adjacent, then there is a path.
if (// Map adjacency
world.Map.GetLocation(decision.Order.Unit).Adjacents.Contains(decision.Order.Location)
world.Map.GetLocation(decision.Order.Unit).Adjacents.Select(loc => loc.Key).Contains(decision.Order.Location)
// Turn adjacency
&& Math.Abs(decision.Order.Unit.Season.Turn - decision.Order.Season.Turn) <= 1
// Timeline adjacency
@ -750,7 +755,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
if (givesSupport.Outcome == true) min += 1;
if (givesSupport.Outcome != false) max += 1;
}
progress |= LoggedUpdate(decision, min, max, depth, "Updated based on unit's supports");
progress |= LoggedUpdate(decision, min, max, depth, $"Updated based on {decision.Supports.Count} hold supports");
return progress;
}
}
@ -776,7 +781,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
// If there is a head to head battle, a unit at the destination that isn't moving away, or
// a unit at the destination that will fail to move away, then the attacking unit will have
// to dislodge it.
UnitOrder? destOrder = decisions.HoldStrength[(decision.Order.Province, decision.Order.Season.Key)].Order;
UnitOrder? destOrder = decisions.HoldStrength[(SplitKey(decision.Order.Location).province, decision.Order.Season.Key)].Order;
DoesMove? destMoveAway = destOrder is MoveOrder moveAway
? decisions.DoesMove[moveAway]
: null;
@ -786,7 +791,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
}
if (// In any case here, there will have to be a unit at the destination with an order,
// which means that destOrder will have to be populated. Including this in the if
//condition lets the compiler know it won't be null in the if block.
// condition lets the compiler know it won't be null in the if block.
destOrder != null
&& (// Is head to head
decision.OpposingMove != null
@ -815,7 +820,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
if (givesSupport.Outcome == true) min += 1;
if (givesSupport.Outcome != false) max += 1;
}
progress |= LoggedUpdate(decision, min, max, depth, "Updated with supports from other powers");
progress |= LoggedUpdate(decision, min, max, depth, $"Updated with {decision.Supports.Count} (?) move supports from third parties");
return progress;
}
}
@ -837,7 +842,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
}
// Force min to zero in case of an attempt to disloge a unit of the same power.
if (decision.Order.Unit.Power == destPower) min = 0;
progress |= LoggedUpdate(decision, min, max, depth, "Updated with supports");
progress |= LoggedUpdate(decision, min, max, depth, $"Updated with {decision.Supports.Count} (?) move supports");
return progress;
}
else
@ -853,7 +858,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
if (givesSupport.Outcome == true) min += 1;
if (givesSupport.Outcome != false) max += 1;
}
progress |= LoggedUpdate(decision, min, max, depth, "Updated with supports from all powers");
progress |= LoggedUpdate(decision, min, max, depth, $"Updated with {decision.Supports.Count} move supports from all powers");
return progress;
}
}
@ -878,7 +883,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
if (givesSupport.Outcome == true) min += 1;
if (givesSupport.Outcome != false) max += 1;
}
progress |= LoggedUpdate(decision, min, max, depth, "Updated based on unit's supports");
progress |= LoggedUpdate(decision, min, max, depth, $"Updated based on {decision.Supports.Count} supports");
return progress;
}
@ -932,7 +937,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
min = 0;
}
progress |= LoggedUpdate(decision, min, max, depth, "Updated based on unit's supports");
progress |= LoggedUpdate(decision, min, max, depth, $"Updated based on {decision.Supports.Count} supports");
return progress;
}
@ -955,7 +960,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
// strength.
NumericAdjudicationDecision defense = decision.OpposingMove != null
? decisions.DefendStrength[decision.OpposingMove]
: decisions.HoldStrength[(decision.Order.Province, decision.Order.Season.Key)];
: decisions.HoldStrength[(SplitKey(decision.Order.Location).province, decision.Order.Season.Key)];
progress |= ResolveDecision(defense, world, decisions, depth + 1);
// If the attack doesn't beat the defense, resolve the move to false.
@ -990,7 +995,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
decision,
attack.MinValue > defense.MaxValue && beatsAllCompetingMoves,
depth,
"Updated based on competing moves");
$"Updated based on {decision.Competing.Count} competing moves");
return progress;
}
}

View File

@ -18,7 +18,7 @@ public static class PathFinder
/// Determines if a convoy path exists for a move order.
/// </summary>
public static bool ConvoyPathExists(World world, MoveOrder order)
=> ConvoyPathExists(world, order.Unit, order.Location, order.Season);
=> ConvoyPathExists(world, order.Unit, world.Map.GetLocation(order.Location), order.Season);
private static bool ConvoyPathExists(
World world,

View File

@ -53,6 +53,12 @@ public class Location
this.AdjacentList = new List<Location>();
}
public static (string province, string location) SplitKey(string locationKey)
{
var split = locationKey.Split(['/'], 2);
return (split[0], split[1]);
}
public override string ToString()
{
return this.Name == "land" || this.Name == "water"

View File

@ -10,9 +10,10 @@ namespace MultiversalDiplomacy.Model;
public struct Season(string timeline, int turn)
{
/// <summary>
/// The first turn number. This is defined to reduce confusion about whether the first turn is 0 or 1.
/// The root season of every game. This is defined to avoid any confusion about what the first turn or timeline
/// should be or what season to use to key into a fresh <see cref="Timelines"/>.
/// </summary>
public const int FIRST_TURN = 0;
public static readonly Season First = new(Timelines.IntToString(0), 0);
/// <summary>
/// The timeline to which this season belongs.

View File

@ -75,10 +75,7 @@ public class Timelines(int next, Dictionary<string, Season?> pasts)
/// Create a new multiverse with an initial season.
/// </summary>
public static Timelines Create()
{
Season first = new(IntToString(0), Season.FIRST_TURN);
return new Timelines(1, new() { {first.Key, null} });
}
=> new(StringToInt(Season.First.Timeline) + 1, new() { {Season.First.Key, null} });
/// <summary>
/// Create a continuation of a season if it has no futures, otherwise create a fork.
@ -119,7 +116,6 @@ public class Timelines(int next, Dictionary<string, Season?> pasts)
/// </summary>
public Season GetTimelineRoot(Season season)
{
Console.WriteLine($"GetTimelineRoot({season.Key})");
return Pasts[season.Key] is Season past && season.Timeline == past.Timeline
? GetTimelineRoot(past)
: season;

View File

@ -181,7 +181,7 @@ public class World
: splits.Length == 3
? Map.GetWater(splits[2])
: Map.GetWater(splits[2], splits[3]);
Unit unit = Unit.Build(location.Key, new("a0"), power, type);
Unit unit = Unit.Build(location.Key, Season.First, power, type);
return unit;
});
return this.Update(units: units);
@ -229,7 +229,7 @@ public class World
public Unit GetUnitAt(string provinceName, Season? season = null)
{
Province province = Map.GetProvince(provinceName);
season ??= new("a0");
season ??= Season.First;
Unit? foundUnit = this.Units.SingleOrDefault(
u => Map.GetLocation(u!).Province == province && u!.Season == season,
null)

View File

@ -1,5 +1,7 @@
using MultiversalDiplomacy.Model;
using static MultiversalDiplomacy.Model.Location;
namespace MultiversalDiplomacy.Orders;
/// <summary>
@ -15,14 +17,9 @@ public class MoveOrder : UnitOrder
/// <summary>
/// The destination location to which the unit should move.
/// </summary>
public Location Location { get; }
public string Location { get; }
/// <summary>
/// The destination province to which the unit should move.
/// </summary>
public Province Province => this.Location.Province;
public MoveOrder(string power, Unit unit, Season season, Location location)
public MoveOrder(string power, Unit unit, Season season, string location)
: base (power, unit)
{
this.Season = season;
@ -31,14 +28,6 @@ public class MoveOrder : UnitOrder
public override string ToString()
{
return $"{this.Unit} -> {Season.Timeline}-{Province}@{Season.Turn}";
return $"{this.Unit} -> {Season.Timeline}-{SplitKey(Location).province}@{Season.Turn}";
}
/// <summary>
/// Returns whether another move order has the same destination as this order.
/// </summary>
public bool IsCompeting(MoveOrder other)
=> this != other
&& this.Season == other.Season
&& this.Province == other.Province;
}

View File

@ -38,9 +38,4 @@ public class SupportMoveOrder : SupportOrder
{
return $"{this.Unit} S {this.Target} -> {(this.Province, this.Season).ToShort()}";
}
public bool IsSupportFor(MoveOrder move)
=> this.Target == move.Unit
&& this.Season == move.Season
&& this.Location == move.Location;
}

View File

@ -14,6 +14,31 @@ public class SerializationTest
WriteIndented = true,
};
[Test]
public void SerializeRoundTrip_Timelines()
{
Timelines one = Timelines.Create();
string serial1 = JsonSerializer.Serialize(one, Options);
Timelines two = JsonSerializer.Deserialize<Timelines>(serial1, Options)
?? throw new AssertionException("Failed to deserialize");
Assert.That(two.Next, Is.EqualTo(one.Next), "Failed to reserialize next timeline");
Assert.That(two.Pasts, Is.EquivalentTo(one.Pasts), "Failed to reserialize pasts");
Timelines three = two
.WithNewSeason(Season.First, out var a1)
.WithNewSeason(a1, out var a2)
.WithNewSeason(a1, out var b2);
string serial2 = JsonSerializer.Serialize(three, Options);
Timelines four = JsonSerializer.Deserialize<Timelines>(serial2, Options)
?? throw new AssertionException("Failed to deserialize");
Assert.That(four.Next, Is.EqualTo(three.Next), "Failed to reserialize next timeline");
Assert.That(four.Pasts, Is.EquivalentTo(three.Pasts), "Failed to reserialize pasts");
}
[Test]
public void SerializeRoundTrip_NewGame()
{
@ -65,36 +90,42 @@ public class SerializationTest
Assert.That(tyr0, Is.NotDislodged);
setup.UpdateWorld();
Assert.That(setup.World.OrderHistory["a0"].Orders.Count, Is.GreaterThan(0), "Missing orders");
Assert.That(setup.World.OrderHistory["a0"].DoesMoveOutcomes.Count, Is.GreaterThan(0), "Missing moves");
Assert.That(setup.World.OrderHistory["a0"].IsDislodgedOutcomes.Count, Is.GreaterThan(0), "Missing dislodges");
Assert.That(setup.World.OrderHistory[s0.Key].Orders.Count, Is.GreaterThan(0), "Missing orders");
Assert.That(setup.World.OrderHistory[s0.Key].DoesMoveOutcomes.Count, Is.GreaterThan(0), "Missing moves");
Assert.That(setup.World.OrderHistory[s0.Key].IsDislodgedOutcomes.Count, Is.GreaterThan(0), "Missing dislodges");
// Assert.Ignore("Serialization doesn't fully work yet");
// Serialize and deserialize the world
string serialized = JsonSerializer.Serialize(setup.World, Options);
Console.WriteLine(serialized);
World reserialized = JsonSerializer.Deserialize<World>(serialized, Options)
?? throw new AssertionException("Failed to reserialize world");
Assert.Multiple(() => {
Assert.That(reserialized.OrderHistory["a0"].Orders.Count, Is.GreaterThan(0), "Missing orders");
Assert.That(reserialized.OrderHistory["a0"].DoesMoveOutcomes.Count, Is.GreaterThan(0), "Missing moves");
Assert.That(reserialized.OrderHistory["a0"].IsDislodgedOutcomes.Count, Is.GreaterThan(0), "Missing dislodges");
Assert.That(reserialized.OrderHistory[s0.Key].Orders.Count, Is.GreaterThan(0), "Missing orders");
Assert.That(reserialized.OrderHistory[s0.Key].DoesMoveOutcomes.Count, Is.GreaterThan(0), "Missing moves");
Assert.That(reserialized.OrderHistory[s0.Key].IsDislodgedOutcomes.Count, Is.GreaterThan(0), "Missing dislodges");
Assert.That(reserialized.Timelines.Pasts, Is.Not.Empty, "Missing timeline history");
});
Assert.Ignore("Serialization doesn't fully work yet");
// Resume the test case
setup = new(reserialized, MovementPhaseAdjudicator.Instance);
setup[("a", 1)]
["Germany"]
.Army("Mun").Supports.Army("Mun", season: new("a0")).MoveTo("Tyr").GetReference(out var mun1)
.Army("Mun").Supports.Army("Mun", season: s0).MoveTo("Tyr").GetReference(out var mun1)
["Austria"]
.Army("Tyr").Holds();
setup.ValidateOrders();
Assert.That(mun1, Is.Valid);
var adjudications = setup.AdjudicateOrders();
foreach (var adj in adjudications)
{
Console.WriteLine($"{adj}");
}
Assert.That(mun1, Is.NotCut);
Console.WriteLine(string.Join(", ", adjudications.Select(a => a.ToString())));
DoesMove mun0move = adjudications.OfType<DoesMove>().Single(move => move.Order.Unit.Key == mun0.Order.Unit.Key);
Assert.That(mun0move.Outcome, Is.True);
IsDislodged tyr0dislodge = adjudications.OfType<IsDislodged>().Single(dis => dis.Order.Unit.Key == tyr0.Order.Unit.Key);

View File

@ -423,7 +423,7 @@ public class TestCaseBuilder
this.PowerContext.Power,
this.Unit,
destSeason,
destination);
destination.Key);
this.Builder.OrderList.Add(moveOrder);
return new OrderDefinedContext<MoveOrder>(this, moveOrder);
}

View File

@ -74,7 +74,7 @@ class TestCaseBuilderTest
Assert.That(orderBer, Is.InstanceOf<MoveOrder>(), "Unexpected order type");
Assert.That(
(orderBer as MoveOrder)?.Location,
Is.EqualTo(setup.World.Map.GetLand("Kiel")),
Is.EqualTo(setup.World.Map.GetLand("Kiel").Key),
"Unexpected move order destination");
UnitOrder orderPru = orders.Single(OrderForProvince("Prussia"));

View File

@ -2,9 +2,9 @@ using MultiversalDiplomacy.Model;
using NUnit.Framework;
namespace MultiversalDiplomacyTests.Model;
namespace MultiversalDiplomacyTests;
public class TimelineFactoryTest
public class TimelinesTest
{
[TestCase(0, "a")]
[TestCase(1, "b")]
@ -36,12 +36,12 @@ public class TimelineFactoryTest
public void NoSharedFactoryState()
{
Timelines one = Timelines.Create()
.WithNewSeason(new Season("a0"), out var s1)
.WithNewSeason(new Season("a0"), out var s2)
.WithNewSeason(new Season("a0"), out var s3);
.WithNewSeason(Season.First, out var s1)
.WithNewSeason(Season.First, out var s2)
.WithNewSeason(Season.First, out var s3);
Timelines two = Timelines.Create()
.WithNewSeason(new Season("a0"), out var s4)
.WithNewSeason(new Season("a0"), out var s5);
.WithNewSeason(Season.First, out var s4)
.WithNewSeason(Season.First, out var s5);
Assert.That(s1.Timeline, Is.EqualTo("a"));
Assert.That(s2.Timeline, Is.EqualTo("b"));
@ -54,14 +54,14 @@ public class TimelineFactoryTest
public void TimelineForking()
{
Timelines timelines = Timelines.Create()
.WithNewSeason("a0", out var a1)
.WithNewSeason(Season.First, out var a1)
.WithNewSeason(a1, out var a2)
.WithNewSeason(a2, out var a3)
.WithNewSeason(a1, out var b2)
.WithNewSeason(b2, out var b3)
.WithNewSeason(a1, out var c2)
.WithNewSeason(a2, out var d3);
Season a0 = new("a0");
Season a0 = Season.First;
Assert.That(
timelines.Pasts.Keys,
@ -76,13 +76,13 @@ public class TimelineFactoryTest
Assert.That(c2.Timeline, Is.EqualTo("c"), "Unexpected second alt");
Assert.That(d3.Timeline, Is.EqualTo("d"), "Unexpected third alt");
Assert.That(a1.Turn, Is.EqualTo(Season.FIRST_TURN + 1), "Unexpected a1 turn number");
Assert.That(a2.Turn, Is.EqualTo(Season.FIRST_TURN + 2), "Unexpected a2 turn number");
Assert.That(a3.Turn, Is.EqualTo(Season.FIRST_TURN + 3), "Unexpected a3 turn number");
Assert.That(b2.Turn, Is.EqualTo(Season.FIRST_TURN + 2), "Unexpected b2 turn number");
Assert.That(b3.Turn, Is.EqualTo(Season.FIRST_TURN + 3), "Unexpected b3 turn number");
Assert.That(c2.Turn, Is.EqualTo(Season.FIRST_TURN + 2), "Unexpected c2 turn number");
Assert.That(d3.Turn, Is.EqualTo(Season.FIRST_TURN + 3), "Unexpected d3 turn number");
Assert.That(a1.Turn, Is.EqualTo(Season.First.Turn + 1), "Unexpected a1 turn number");
Assert.That(a2.Turn, Is.EqualTo(Season.First.Turn + 2), "Unexpected a2 turn number");
Assert.That(a3.Turn, Is.EqualTo(Season.First.Turn + 3), "Unexpected a3 turn number");
Assert.That(b2.Turn, Is.EqualTo(Season.First.Turn + 2), "Unexpected b2 turn number");
Assert.That(b3.Turn, Is.EqualTo(Season.First.Turn + 3), "Unexpected b3 turn number");
Assert.That(c2.Turn, Is.EqualTo(Season.First.Turn + 2), "Unexpected c2 turn number");
Assert.That(d3.Turn, Is.EqualTo(Season.First.Turn + 3), "Unexpected d3 turn number");
Assert.That(timelines.GetTimelineRoot(a0), Is.EqualTo(a0), "Expected timeline root to be reflexive");
Assert.That(timelines.GetTimelineRoot(a3), Is.EqualTo(a0), "Expected trunk timeline to have root");

View File

@ -13,7 +13,7 @@ public class UnitTests
Location Mun = world.Map.GetLand("Mun"),
Boh = world.Map.GetLand("Boh"),
Tyr = world.Map.GetLand("Tyr");
Season a0 = new("a0");
Season a0 = Season.First;
Unit u1 = Unit.Build(Mun.Key, a0, "Austria", UnitType.Army);
world = world.WithNewSeason(a0, out Season a1);