Compare commits

..

No commits in common. "3242186208335832d9650fa636ceeb0b949ed70d" and "dab37c239b196ae80bfde5f216c2c52a5b2f1ae9" have entirely different histories.

15 changed files with 337 additions and 445 deletions

View File

@ -236,13 +236,17 @@
<a name="2.B"><h3>2.B. ORDER NOTATION</h3></a> <a name="2.B"><h3>2.B. ORDER NOTATION</h3></a>
<p>The order notation in this document is as in DATC, with the following additions for multiversal time travel.</p> <p>The order notation in this document is as in DATC, with the following additions for multiversal time travel.</p>
<ul> <ul>
<li>Timelines are designated by letters, e.g. "a", "b". Turns are designated by numbers, e.g. 0, 1, 2. A board in a timeline X at turn N is designated XN. A location LOC on that board is designated X-LOC@N.</li> <li>A season within a particular timeline is designated in the format X:Y, where X is the turn (starting from 0 and advancing with each movement phase) and Y is the timeline number (starting from 0 and advancing with each timeline fork).</li>
<li>In examples that cover multiple turns, orders are given in sets. Each order set is adjudicated before moving on to the next set.</li> <li>Adjudication is implied to be done between successive seasons. For example, if orders are listed for 0:0 and then for 1:0, it is implied that the orders for 0:0 were adjudicated.</li>
<li>Units are fully designated by unit type and multiversal location, e.g. "A b-Munich@3". Destinations for move orders or support-to-move orders are fully designated by multiversal location, e.g. <code>a-Munich@1</code>. Where orders are not fully designated, the full designations are implied according to these rules:</li> <li>Units are designated by unit type, province, and season, e.g. "A Munich 1:0". A destination for a move order or support-to-move order is designated by province and season, e.g. "Munich 1:0".
<ul> <ul>
<li>If only the timeline of a location is specified, the turn is the latest turn in that timeline. E.g. if timeline "a" is at turn 2, <code>a-Munich</code> is interpreted as <code>a-Munich@2</code>.</li> <li>If season of the ordered unit is not specified, the season is the season to which the orders are being given.</li>
<li>If the timeline or turn are unspecified for the target of a move or support-hold order, the timeline and turn are those of the ordered unit. E.g. if timeline "b" is at turn 1, <code>A b-Tyrolia - Munich</code> is interpreted as <code>b-Tyrolia@1 - b-Munich@1</code>.</li> <li>If the season of a unit supported to hold is not specified, the season is the same season as the supporting unit.</li>
<li>If only the province is specified for the target of a support-move order, the timeline and turn are those of the supported unit. E.g. if timeline "a" is at turn 2 and "b" at turn 1, <code>A a-Munich supports A b-Tyrolia - Munich</code> is interpreted as <code>A a-Munich@2 supports A b-Tyrolia@1 - b-Munich@1</code>.</li> <li>If the season of the destination of a move order or the season of the destination of a supported move order is not specified, the season is the season of the moving unit.</li>
<li>For example:
<pre>Germany 2:0
A Munich supports A Munich 1:1 - Tyrolia</pre>
The order here is for Army Munich in 2:0. The move being supported is for Army Munich in 1:1 to move to Tyrolia in 1:1.</li>
</ul> </ul>
</ul> </ul>
@ -254,15 +258,13 @@
<summary><h4><a href="#3.A.1">3.A.1</a>. TEST CASE, MOVE INTO OWN PAST FORKS TIMELINE</h4></summary> <summary><h4><a href="#3.A.1">3.A.1</a>. TEST CASE, MOVE INTO OWN PAST FORKS TIMELINE</h4></summary>
<p>A unit that moves into its own immediate past causes the timeline to fork.</p> <p>A unit that moves into its own immediate past causes the timeline to fork.</p>
<pre> <pre>
Germany: Germany 0:0
A a-Munich hold A Munich hold
--- Germany 1:0
A Munich - Tyrolia 0:0
Germany:
A a-Munich - a-Tyrolia@0
</pre> </pre>
<p>A a-Munich@1 moves to a-Tyrolia@0. The main timeline advances to a2 with an empty board. A forked timeline advances to b1 with armies in Munich and Tyrolia.</p> <p>A Munich 1:0 moves to Tyrolia 0:0. The main timeline advances to 2:0 with an empty board. A forked timeline advances to 1:1 with armies in Munich and Tyrolia.</p>
<div class="figures"> <div class="figures">
<canvas id="canvas-3-A-1-before" width="0" height="0"></canvas> <canvas id="canvas-3-A-1-before" width="0" height="0"></canvas>
<script> <script>
@ -309,21 +311,19 @@
<summary><h4><a href="#3.A.2">3.A.2</a>. TEST CASE, SUPPORT TO REPELLED PAST MOVE FORKS TIMELINE</h4></summary> <summary><h4><a href="#3.A.2">3.A.2</a>. TEST CASE, SUPPORT TO REPELLED PAST MOVE FORKS TIMELINE</h4></summary>
<p>A unit that supports a move that previously failed in the past, such that it now succeeds, causes the timeline to fork.</p> <p>A unit that supports a move that previously failed in the past, such that it now succeeds, causes the timeline to fork.</p>
<pre> <pre>
Austria: Austria 0:0
A Tyrolia hold A Tyrolia hold
Germany: Germany 0:0
A Munich - Tyrolia A Munich - Tyrolia
--- Austria 1:0
Austria:
A Tyrolia hold A Tyrolia hold
Germany: Germany 1:0
A Munich supports A a-Munich@0 - Tyrolia A Munich supports A Munich 0:0 - Tyrolia 0:0
</pre> </pre>
<p>With the support from A a-Munich@1, A a-Munich@0 dislodges A a-Tyrolia@0. A forked timeline advances to b1 where A Tyrolia has been dislodged. The main timeline advances to a2 where A Munich and A Tyrolia are in their initial positions.</p> <p>With the support from A Munich 1:0, A Munich 0:0 dislodges A Tyrolia 0:0. A forked timeline advances to 1:1 where A Tyrolia 0:0 has been dislodged. The main timeline advances to 2:0 where A Munich and A Tyrolia are in their initial positions.</p>
<div class="figures"> <div class="figures">
<canvas id="canvas-3-A-2-before" width="0" height="0"></canvas> <canvas id="canvas-3-A-2-before" width="0" height="0"></canvas>
<script> <script>
@ -379,21 +379,19 @@
<summary><h4><a href="#3.A.3">3.A.3</a>. TEST CASE, FAILED MOVE DOES NOT FORK TIMELINE</h4></summary> <summary><h4><a href="#3.A.3">3.A.3</a>. TEST CASE, FAILED MOVE DOES NOT FORK TIMELINE</h4></summary>
<p>A unit that attempts to move into the past, but is repelled, does not cause the timeline to fork.</p> <p>A unit that attempts to move into the past, but is repelled, does not cause the timeline to fork.</p>
<pre> <pre>
Austria: Austria 0:0
A Tyrolia hold A Tyrolia hold
Germany: Germany 0:0
A Munich hold A Munich hold
--- Austria 1:0
Austria:
A Tyrolia hold A Tyrolia hold
Germany: Germany 1:0
A Munich - a-Tyrolia@0 A Munich - Tyrolia 0:0
</pre> </pre>
<p>The move by A a-Munich@1 fails. The main timeline advances to a2 with both armies in their initial positions. No alternate timeline is created.</p> <p>The move by A Munich 1:0 fails. The main timeline advances to 2:0 with both armies in their initial positions. No alternate timeline is created.</p>
<div class="figures"> <div class="figures">
<canvas id="canvas-3-A-3-before" width="0" height="0"></canvas> <canvas id="canvas-3-A-3-before" width="0" height="0"></canvas>
<script> <script>
@ -443,17 +441,15 @@
<summary><h4><a href="#3.A.4">3.A.4</a>. TEST CASE, SUPERFLUOUS SUPPORT DOES NOT FORK TIMELINE</h4></summary> <summary><h4><a href="#3.A.4">3.A.4</a>. TEST CASE, SUPERFLUOUS SUPPORT DOES NOT FORK TIMELINE</h4></summary>
<p>A unit that supports a move that succeeded in the past and still succeeds after the additional future support does not cause the timeline to fork.</p> <p>A unit that supports a move that succeeded in the past and still succeeds after the additional future support does not cause the timeline to fork.</p>
<pre> <pre>
Germany: Germany 0:0
A Munich - Tyrolia A Munich - Tyrolia
A Bohemia hold A Bohemia hold
--- Germany 1:0
Germany:
A Tyrolia hold A Tyrolia hold
A Bohemia supports A a-Munich@0 - Tyrolia A Bohemia supports A Munich 0:0 - Tyrolia
</pre> </pre>
<p>Both units in a1 continue to a2. No alternate timeline is created.</p> <p>Both units in 1:0 continue to 2:0. No alternate timeline is created.</p>
<div class="figures"> <div class="figures">
<canvas id="canvas-3-A-4-before" width="0" height="0"></canvas> <canvas id="canvas-3-A-4-before" width="0" height="0"></canvas>
<script> <script>
@ -505,15 +501,15 @@
<summary><h4><a href="#3.A.5">3.A.5</a>. TEST CASE, CROSS-TIMELINE SUPPORT DOES NOT FORK HEAD</h4></summary> <summary><h4><a href="#3.A.5">3.A.5</a>. TEST CASE, CROSS-TIMELINE SUPPORT DOES NOT FORK HEAD</h4></summary>
<p>In this test case, a unit elsewhere on the map moves into the past to cause a timeline fork. Once there are two parallel timelines, a support from one to the head of the other should not cause any forking, since timeline forks only occur when the past changes, not the present.</p> <p>In this test case, a unit elsewhere on the map moves into the past to cause a timeline fork. Once there are two parallel timelines, a support from one to the head of the other should not cause any forking, since timeline forks only occur when the past changes, not the present.</p>
<pre> <pre>
Austria: Austria
A a-Tyrolia hold A Tyrolia 2:0 hold
A b-Tyrolia hold A Tyrolia 1:1 hold
Germany Germany
A a-Munich - Tyrolia A Munich 2:0 - Tyrolia
A b-Munich supports A a-Munich - Tyrolia A Munich 1:1 supports A Munich 2:0 - Tyrolia
</pre> </pre>
<p>A a-Munich dislodges A a-Tyrolia. No alternate timeline is created.</p> <p>A Munich 2:0 dislodges A Tyrolia 2:0. No alternate timeline is created.</p>
<div class="figures"> <div class="figures">
<canvas id="canvas-3-A-5-before" width="0" height="0"></canvas> <canvas id="canvas-3-A-5-before" width="0" height="0"></canvas>
<script> <script>
@ -571,11 +567,19 @@
<p>Following <a href="#3.A.5">3.A.5</a>, a cross-timeline support that previously succeeded is cut.</p> <p>Following <a href="#3.A.5">3.A.5</a>, a cross-timeline support that previously succeeded is cut.</p>
<pre> <pre>
Germany Germany
A a-Tyrolia holds A Munich 2:0 - Tyrolia
A b-Munich holds A Munich 1:1 supports A Munich 2:0 - Tyrolia
Austria Austria
A b-Tyrolia@2 - b-Munich@1 A Tyrolia 2:0 holds
A Tyrolia 1:1 holds
Germany
A Tyrolia 3:0 holds
A Munich 2:1 holds
Austria
A Tyrolia 2:1 - Munich 1:1
</pre> </pre>
<p>Cutting the support does not change the past or cause a timeline fork.</p> <p>Cutting the support does not change the past or cause a timeline fork.</p>
<div class="figures"> <div class="figures">

View File

@ -1,10 +0,0 @@
.PHONY: *
help: ## display this help
@awk 'BEGIN{FS = ":.*##"; printf "\033[1m\nUsage\n \033[1;92m make\033[0;36m <target>\033[0m\n"} /^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } ' $(MAKEFILE_LIST)
tests: ## run all tests
dotnet test MultiversalDiplomacyTests
test: ## name=[test name]: run a single test with logging
dotnet test MultiversalDiplomacyTests -l "console;verbosity=normal" --filter $(name)

View File

@ -94,7 +94,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
// 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
&& world.InAdjacentTimeline(order.Unit.Season, order.Season)); && order.Unit.Season.InAdjacentTimeline(order.Season));
List<MoveOrder> adjacentMoveOrders = moveOrdersByAdjacency[true].ToList(); List<MoveOrder> adjacentMoveOrders = moveOrdersByAdjacency[true].ToList();
List<MoveOrder> nonAdjacentMoveOrders = moveOrdersByAdjacency[false].ToList(); List<MoveOrder> nonAdjacentMoveOrders = moveOrdersByAdjacency[false].ToList();
@ -180,7 +180,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
// 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
&& world.InAdjacentTimeline(order.Unit.Season, order.Target.Season), && order.Unit.Season.InAdjacentTimeline(order.Target.Season),
ValidationReason.UnreachableSupport, ValidationReason.UnreachableSupport,
ref supportHoldOrders, ref supportHoldOrders,
ref validationResults); ref validationResults);
@ -212,7 +212,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
// 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
&& world.InAdjacentTimeline(order.Unit.Season, order.Season), && order.Unit.Season.InAdjacentTimeline(order.Season),
ValidationReason.UnreachableSupport, ValidationReason.UnreachableSupport,
ref supportMoveOrders, ref supportMoveOrders,
ref validationResults); ref validationResults);
@ -639,7 +639,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
// 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
&& world.InAdjacentTimeline(decision.Order.Unit.Season, decision.Order.Season)) && decision.Order.Unit.Season.InAdjacentTimeline(decision.Order.Season))
{ {
bool update = LoggedUpdate(decision, true, depth, "Adjacent move"); bool update = LoggedUpdate(decision, true, depth, "Adjacent move");
return progress | update; return progress | update;

View File

@ -60,7 +60,7 @@ public static class PathFinder
(Location currentLocation, Season currentSeason) = toVisit.Dequeue(); (Location currentLocation, Season currentSeason) = toVisit.Dequeue();
visited.Add((currentLocation, currentSeason)); visited.Add((currentLocation, currentSeason));
var adjacents = GetAdjacentPoints(world, currentLocation, currentSeason); var adjacents = GetAdjacentPoints(currentLocation, currentSeason);
foreach ((Location adjLocation, Season adjSeason) in adjacents) foreach ((Location adjLocation, Season adjSeason) in adjacents)
{ {
// If the destination is adjacent, then a path exists. // If the destination is adjacent, then a path exists.
@ -81,11 +81,11 @@ public static class PathFinder
return false; return false;
} }
private static List<(Location, Season)> GetAdjacentPoints(World world, Location location, Season season) private static List<(Location, Season)> GetAdjacentPoints(Location location, Season season)
{ {
List<(Location, Season)> adjacentPoints = []; List<(Location, Season)> adjacentPoints = new();
List<Location> adjacentLocations = location.Adjacents.ToList(); List<Location> adjacentLocations = location.Adjacents.ToList();
List<Season> adjacentSeasons = GetAdjacentSeasons(world, season).ToList(); List<Season> adjacentSeasons = season.GetAdjacentSeasons().ToList();
foreach (Location adjacentLocation in adjacentLocations) foreach (Location adjacentLocation in adjacentLocations)
{ {
@ -105,55 +105,4 @@ public static class PathFinder
return adjacentPoints; return adjacentPoints;
} }
/// <summary>
/// Returns all seasons that are adjacent to a season.
/// </summary>
public static IEnumerable<Season> GetAdjacentSeasons(World world, Season season)
{
List<Season> adjacents = [];
// The immediate past and all immediate futures are adjacent.
if (season.Past != null) adjacents.Add(world.GetSeason(season.Past));
adjacents.AddRange(season.Futures);
// Find all adjacent timelines by finding all timelines that branched off of this season's
// timeline, i.e. all futures of this season's past that have different timelines. Also
// include any timelines that branched off of the timeline this timeline branched off from.
List<Season> adjacentTimelineRoots = [];
Season? current;
for (current = season;
current?.Past != null && world.GetSeason(current.Past).Timeline == current.Timeline;
current = world.GetSeason(current.Past))
{
adjacentTimelineRoots.AddRange(
current.Futures.Where(s => s.Timeline != current.Timeline));
}
// At the end of the for loop, if this season is part of the first timeline, then current
// is the root season (current.past == null); if this season is in a branched timeline,
// then current is the branch timeline's root season (current.past.timeline !=
// current.timeline). There are co-branches if this season is in a branched timeline, since
// the first timeline by definition cannot have co-branches.
if (current?.Past != null && world.GetSeason(current.Past) is Season past)
{
IEnumerable<Season> cobranchRoots = past.Futures
.Where(s => s.Timeline != current.Timeline && s.Timeline != past.Timeline);
adjacentTimelineRoots.AddRange(cobranchRoots);
}
// Walk up all alternate timelines to find seasons within one turn of this season.
foreach (Season timelineRoot in adjacentTimelineRoots)
{
for (Season? branchSeason = timelineRoot;
branchSeason != null && branchSeason.Turn <= season.Turn + 1;
branchSeason = branchSeason.Futures
.FirstOrDefault(s => s!.Timeline == branchSeason.Timeline, null))
{
if (branchSeason.Turn >= season.Turn - 1) adjacents.Add(branchSeason);
}
}
return adjacents;
}
} }

View File

@ -5,17 +5,27 @@ namespace MultiversalDiplomacy.Model;
/// </summary> /// </summary>
public class Season public class Season
{ {
/// <summary>
/// A shared counter for handing out new timeline numbers.
/// </summary>
private class TimelineFactory
{
private int nextTimeline = 0;
public int NextTimeline() => nextTimeline++;
}
/// <summary> /// <summary>
/// The first turn number. /// The first turn number.
/// </summary> /// </summary>
public const int FIRST_TURN = 0; public const int FIRST_TURN = 0;
/// <summary> /// <summary>
/// The designation of the season immediately preceding this season. /// The season immediately preceding this season.
/// If this season is an alternate timeline root, the past is from the origin timeline. /// If this season is an alternate timeline root, the past is from the origin timeline.
/// The initial season does not have a past. /// The initial season does not have a past.
/// </summary> /// </summary>
public string? Past { get; } public Season? Past { get; }
/// <summary> /// <summary>
/// The current turn, beginning at 0. Each season (spring and fall) is one turn. /// The current turn, beginning at 0. Each season (spring and fall) is one turn.
@ -27,12 +37,12 @@ public class Season
/// <summary> /// <summary>
/// The timeline to which this season belongs. /// The timeline to which this season belongs.
/// </summary> /// </summary>
public string Timeline { get; } public int Timeline { get; }
/// <summary> /// <summary>
/// The season's spatial location as a timeline-turn tuple. /// The season's spatial location as a turn-timeline tuple.
/// </summary> /// </summary>
public (string Timeline, int Turn) Coord => (this.Timeline, this.Turn); public (int Turn, int Timeline) Coord => (this.Turn, this.Timeline);
/// <summary> /// <summary>
/// The shared timeline number generator. /// The shared timeline number generator.
@ -45,20 +55,23 @@ public class Season
public IEnumerable<Season> Futures => this.FutureList; public IEnumerable<Season> Futures => this.FutureList;
private List<Season> FutureList { get; } private List<Season> FutureList { get; }
private Season(Season? past, int turn, string timeline, TimelineFactory factory) private Season(Season? past, int turn, int timeline, TimelineFactory factory)
{ {
this.Past = past?.ToString(); this.Past = past;
this.Turn = turn; this.Turn = turn;
this.Timeline = timeline; this.Timeline = timeline;
this.Timelines = factory; this.Timelines = factory;
this.FutureList = []; this.FutureList = new();
past?.FutureList.Add(this); if (past != null)
{
past.FutureList.Add(this);
}
} }
public override string ToString() public override string ToString()
{ {
return $"{this.Timeline}{this.Turn}"; return $"{this.Timeline}@{this.Turn}";
} }
/// <summary> /// <summary>
@ -78,11 +91,95 @@ public class Season
/// Create a season immediately after this one in the same timeline. /// Create a season immediately after this one in the same timeline.
/// </summary> /// </summary>
public Season MakeNext() public Season MakeNext()
=> new(this, Turn + 1, Timeline, Timelines); => new Season(this, this.Turn + 1, this.Timeline, this.Timelines);
/// <summary> /// <summary>
/// Create a season immediately after this one in a new timeline. /// Create a season immediately after this one in a new timeline.
/// </summary> /// </summary>
public Season MakeFork() public Season MakeFork()
=> new(this, Turn + 1, Timelines.NextTimeline(), Timelines); => new Season(this, this.Turn + 1, this.Timelines.NextTimeline(), this.Timelines);
/// <summary>
/// Returns the first season in this season's timeline. The first season is the
/// root of the first timeline. The earliest season in each alternate timeline is
/// the root of that timeline.
/// </summary>
public Season TimelineRoot()
=> this.Past != null && this.Timeline == this.Past.Timeline
? this.Past.TimelineRoot()
: this;
/// <summary>
/// Returns whether this season is in an adjacent timeline to another season.
/// Seasons are considered to be in adjacent timelines if they are in the same timeline,
/// one is in a timeline that branched from the other's timeline, or both are in timelines
/// that branched from the same point.
/// </summary>
public bool InAdjacentTimeline(Season other)
{
// Timelines are adjacent to themselves. Early out in that case.
if (this.Timeline == other.Timeline) return true;
// If the timelines aren't identical, one of them isn't the initial trunk.
// They can still be adjacent if one of them branched off of the other, or
// if they both branched off of the same point.
Season thisRoot = this.TimelineRoot();
Season otherRoot = other.TimelineRoot();
return // One branched off the other
thisRoot.Past?.Timeline == other.Timeline
|| otherRoot.Past?.Timeline == this.Timeline
// Both branched off of the same point
|| thisRoot.Past == otherRoot.Past;
}
/// <summary>
/// Returns all seasons that are adjacent to this season.
/// </summary>
public IEnumerable<Season> GetAdjacentSeasons()
{
List<Season> adjacents = new();
// The immediate past and all immediate futures are adjacent.
if (this.Past != null) adjacents.Add(this.Past);
adjacents.AddRange(this.FutureList);
// Find all adjacent timelines by finding all timelines that branched off of this season's
// timeline, i.e. all futures of this season's past that have different timelines. Also
// include any timelines that branched off of the timeline this timeline branched off from.
List<Season> adjacentTimelineRoots = new();
Season? current;
for (current = this;
current?.Past?.Timeline != null && current.Past.Timeline == current.Timeline;
current = current.Past)
{
adjacentTimelineRoots.AddRange(
current.FutureList.Where(s => s.Timeline != current.Timeline));
}
// At the end of the for loop, if this season is part of the first timeline, then current
// is the root season (current.past == null); if this season is in a branched timeline,
// then current is the branch timeline's root season (current.past.timeline !=
// current.timeline). There are co-branches if this season is in a branched timeline, since
// the first timeline by definition cannot have co-branches.
if (current?.Past != null)
{
IEnumerable<Season> cobranchRoots = current.Past.FutureList
.Where(s => s.Timeline != current.Timeline && s.Timeline != current.Past.Timeline);
adjacentTimelineRoots.AddRange(cobranchRoots);
}
// Walk up all alternate timelines to find seasons within one turn of this season.
foreach (Season timelineRoot in adjacentTimelineRoots)
{
for (Season? branchSeason = timelineRoot;
branchSeason != null && branchSeason.Turn <= this.Turn + 1;
branchSeason = branchSeason.FutureList
.FirstOrDefault(s => s!.Timeline == branchSeason.Timeline, null))
{
if (branchSeason.Turn >= this.Turn - 1) adjacents.Add(branchSeason);
}
}
return adjacents;
}
} }

View File

@ -1,50 +0,0 @@
namespace MultiversalDiplomacy.Model;
/// <summary>
/// A shared counter for handing out new timeline designations.
/// </summary>
internal class TimelineFactory
{
private static readonly char[] Letters = [
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
'u', 'v', 'w', 'x', 'y', 'z',
];
/// <summary>
/// Convert a string timeline identifier to its serial number.
/// </summary>
/// <param name="timeline">Timeline identifier.</param>
/// <returns>Integer.</returns>
public static int StringToInt(string timeline)
{
int result = Array.IndexOf(Letters, timeline[0]);
for (int i = 1; i < timeline.Length; i++) {
// The result is incremented by one because timeline designations are not a true base26 system.
// The "ones digit" maps a-z 0-25, but the "tens digit" maps a to 1, so "10" (26) is "aa" and not "a0"
result = (result + 1) * 26;
result += Array.IndexOf(Letters, timeline[i]);
}
return result;
}
/// <summary>
/// Convert a timeline serial number to its string identifier.
/// </summary>
/// <param name="designation">Integer.</param>
/// <returns>Timeline identifier.</returns>
public static string IntToString(int designation) {
static int downshift(int i ) => (i - (i % 26)) / 26;
IEnumerable<char> result = [Letters[designation % 26]];
for (int remainder = downshift(designation); remainder > 0; remainder = downshift(remainder) - 1) {
// We subtract 1 after downshifting for the same reason we add 1 above after upshifting.
//
result = result.Prepend(Letters[(remainder % 26 + 25) % 26]);
}
return new string(result.ToArray());
}
private int nextTimeline = 0;
public string NextTimeline() => IntToString(nextTimeline++);
}

View File

@ -29,15 +29,10 @@ public class World
/// </summary> /// </summary>
public ReadOnlyCollection<Season> Seasons { get; } public ReadOnlyCollection<Season> Seasons { get; }
/// <summary>
/// Lookup for seasons by designation.
/// </summary>
public ReadOnlyDictionary<string, Season> SeasonLookup { get; }
/// <summary> /// <summary>
/// The first season of the game. /// The first season of the game.
/// </summary> /// </summary>
public Season RootSeason => GetSeason("a0"); public Season RootSeason { get; }
/// <summary> /// <summary>
/// All units in the multiverse. /// All units in the multiverse.
@ -65,6 +60,7 @@ public class World
private World( private World(
Map map, Map map,
ReadOnlyCollection<Season> seasons, ReadOnlyCollection<Season> seasons,
Season rootSeason,
ReadOnlyCollection<Unit> units, ReadOnlyCollection<Unit> units,
ReadOnlyCollection<RetreatingUnit> retreatingUnits, ReadOnlyCollection<RetreatingUnit> retreatingUnits,
ReadOnlyDictionary<Season, OrderHistory> orderHistory, ReadOnlyDictionary<Season, OrderHistory> orderHistory,
@ -72,12 +68,11 @@ public class World
{ {
this.Map = map; this.Map = map;
this.Seasons = seasons; this.Seasons = seasons;
this.RootSeason = rootSeason;
this.Units = units; this.Units = units;
this.RetreatingUnits = retreatingUnits; this.RetreatingUnits = retreatingUnits;
this.OrderHistory = orderHistory; this.OrderHistory = orderHistory;
this.Options = options; this.Options = options;
this.SeasonLookup = new(Seasons.ToDictionary(season => $"{season.Timeline}{season.Turn}"));
} }
/// <summary> /// <summary>
@ -93,6 +88,7 @@ public class World
: this( : this(
previous.Map, previous.Map,
seasons ?? previous.Seasons, seasons ?? previous.Seasons,
previous.RootSeason, // Can't change the root season
units ?? previous.Units, units ?? previous.Units,
retreatingUnits ?? previous.RetreatingUnits, retreatingUnits ?? previous.RetreatingUnits,
orderHistory ?? previous.OrderHistory, orderHistory ?? previous.OrderHistory,
@ -105,11 +101,13 @@ public class World
/// </summary> /// </summary>
public static World WithMap(Map map) public static World WithMap(Map map)
{ {
Season root = Season.MakeRoot();
return new World( return new World(
map, map,
new([Season.MakeRoot()]), new(new List<Season> { root }),
new([]), root,
new([]), new(new List<Unit>()),
new(new List<RetreatingUnit>()),
new(new Dictionary<Season, OrderHistory>()), new(new Dictionary<Season, OrderHistory>()),
new Options()); new Options());
} }
@ -125,7 +123,7 @@ public class World
IEnumerable<Unit>? units = null, IEnumerable<Unit>? units = null,
IEnumerable<RetreatingUnit>? retreats = null, IEnumerable<RetreatingUnit>? retreats = null,
IEnumerable<KeyValuePair<Season, OrderHistory>>? orders = null) IEnumerable<KeyValuePair<Season, OrderHistory>>? orders = null)
=> new( => new World(
previous: this, previous: this,
seasons: seasons == null seasons: seasons == null
? this.Seasons ? this.Seasons
@ -199,23 +197,6 @@ public class World
); );
} }
/// <summary>
/// Create a season immediately after this one in the same timeline.
/// </summary>
public World ContinueSeason(string season)
=> Update(seasons: Seasons.Append(SeasonLookup[season].MakeNext()));
/// <summary>
/// Create a season immediately after this one in the same timeline.
/// </summary>
public World ContinueSeason(Season season) => ContinueSeason(season.ToString());
/// <summary>
/// Create a season immediately after this one in a new timeline.
/// </summary>
public World ForkSeason(string season)
=> Update(seasons: Seasons.Append(SeasonLookup[season].MakeFork()));
/// <summary> /// <summary>
/// A standard Diplomacy game setup. /// A standard Diplomacy game setup.
/// </summary> /// </summary>
@ -226,62 +207,29 @@ public class World
/// <summary> /// <summary>
/// Get a season by coordinate. Throws if the season is not found. /// Get a season by coordinate. Throws if the season is not found.
/// </summary> /// </summary>
public Season GetSeason(string timeline, int turn) public Season GetSeason(int turn, int timeline)
=> GetSeason($"{timeline}{turn}");
public Season GetSeason(string designation)
=> SeasonLookup[designation];
/// <summary>
/// Returns the first season in this season's timeline. The first season is the
/// root of the first timeline. The earliest season in each alternate timeline is
/// the root of that timeline.
/// </summary>
public Season GetTimelineRoot(Season season)
{ {
if (season.Past is null) { Season? foundSeason = this.Seasons.SingleOrDefault(
return season; s => s!.Turn == turn && s.Timeline == timeline,
} null);
Season past = SeasonLookup[season.Past]; if (foundSeason == null) throw new KeyNotFoundException(
return season.Timeline == past.Timeline $"Season {turn}:{timeline} not found");
? GetTimelineRoot(past) return foundSeason;
: season;
}
/// <summary>
/// Returns whether this season is in an adjacent timeline to another season.
/// Seasons are considered to be in adjacent timelines if they are in the same timeline,
/// one is in a timeline that branched from the other's timeline, or both are in timelines
/// that branched from the same point.
/// </summary>
public bool InAdjacentTimeline(Season one, Season two)
{
// Timelines are adjacent to themselves. Early out in that case.
if (one == two) return true;
// If the timelines aren't identical, one of them isn't the initial trunk.
// They can still be adjacent if one of them branched off of the other, or
// if they both branched off of the same point.
Season rootOne = GetTimelineRoot(one);
Season rootTwo = GetTimelineRoot(two);
bool oneForked = rootOne.Past != null && GetSeason(rootOne.Past).Timeline == two.Timeline;
bool twoForked = rootTwo.Past != null && GetSeason(rootTwo.Past).Timeline == one.Timeline;
bool bothForked = rootOne.Past == rootTwo.Past;
return oneForked || twoForked || bothForked;
} }
/// <summary> /// <summary>
/// Returns a unit in a province. Throws if there are duplicate units. /// Returns a unit in a province. Throws if there are duplicate units.
/// </summary> /// </summary>
public Unit GetUnitAt(string provinceName, (string timeline, int turn)? seasonCoord = null) public Unit GetUnitAt(string provinceName, (int turn, int timeline)? seasonCoord = null)
{ {
Province province = Map.GetProvince(provinceName); Province province = Map.GetProvince(provinceName);
seasonCoord ??= (this.RootSeason.Timeline, this.RootSeason.Turn); seasonCoord ??= (this.RootSeason.Turn, this.RootSeason.Timeline);
Season season = GetSeason(seasonCoord.Value.timeline, seasonCoord.Value.turn); Season season = GetSeason(seasonCoord.Value.turn, seasonCoord.Value.timeline);
Unit? foundUnit = this.Units.SingleOrDefault( Unit? foundUnit = this.Units.SingleOrDefault(
u => 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"); if (foundUnit == null) throw new KeyNotFoundException(
$"Unit at {province} at {season} not found");
return foundUnit; return foundUnit;
} }
} }

View File

@ -11,10 +11,4 @@
<PackageReference Include="CommandLineParser" Version="2.9.1" /> <PackageReference Include="CommandLineParser" Version="2.9.1" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>MultiversalDiplomacyTests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
</Project> </Project>

View File

@ -14,12 +14,12 @@ public class TimeTravelTest
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance); TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance);
// Hold to move into the future, then move back into the past. // Hold to move into the future, then move back into the past.
setup[("a", 0)] setup[(0, 0)]
.GetReference(out Season s0) .GetReference(out Season s0)
["Germany"] ["Germany"]
.Army("Mun").Holds().GetReference(out var mun0) .Army("Mun").Holds().GetReference(out var mun0)
.Execute() .Execute()
[("a", 1)] [(1, 0)]
.GetReference(out Season s1) .GetReference(out Season s1)
["Germany"] ["Germany"]
.Army("Mun").MovesTo("Tyr", season: s0).GetReference(out var mun1); .Army("Mun").MovesTo("Tyr", season: s0).GetReference(out var mun1);
@ -41,15 +41,15 @@ public class TimeTravelTest
Is.EqualTo(1), Is.EqualTo(1),
"Failed to fork timeline when unit moved in"); "Failed to fork timeline when unit moved in");
// Confirm that there is a unit in Tyr b1 originating from Mun a1 // Confirm that there is a unit in Tyr 1:1 originating from Mun 1:0
Season fork = world.GetSeason("b1"); Season fork = world.GetSeason(1, 1);
Unit originalUnit = world.GetUnitAt("Mun", s0.Coord); Unit originalUnit = world.GetUnitAt("Mun", s0.Coord);
Unit aMun0 = world.GetUnitAt("Mun", s1.Coord); Unit aMun0 = world.GetUnitAt("Mun", s1.Coord);
Unit aTyr = world.GetUnitAt("Tyr", fork.Coord); Unit aTyr = world.GetUnitAt("Tyr", fork.Coord);
Assert.That(aTyr.Past, Is.EqualTo(mun1.Order.Unit)); Assert.That(aTyr.Past, Is.EqualTo(mun1.Order.Unit));
Assert.That(aTyr.Past?.Past, Is.EqualTo(mun0.Order.Unit)); 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 1:1 originating from Mun 0:0
Unit aMun1 = world.GetUnitAt("Mun", fork.Coord); Unit aMun1 = world.GetUnitAt("Mun", fork.Coord);
Assert.That(aMun1.Past, Is.EqualTo(originalUnit)); Assert.That(aMun1.Past, Is.EqualTo(originalUnit));
} }
@ -60,7 +60,7 @@ public class TimeTravelTest
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance); TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance);
// Fail to dislodge on the first turn, then support the move so it succeeds. // Fail to dislodge on the first turn, then support the move so it succeeds.
setup[("a", 0)] setup[(0, 0)]
.GetReference(out Season s0) .GetReference(out Season s0)
["Germany"] ["Germany"]
.Army("Mun").MovesTo("Tyr").GetReference(out var mun0) .Army("Mun").MovesTo("Tyr").GetReference(out var mun0)
@ -75,7 +75,7 @@ public class TimeTravelTest
Assert.That(tyr0, Is.NotDislodged); Assert.That(tyr0, Is.NotDislodged);
setup.UpdateWorld(); setup.UpdateWorld();
setup[("a", 1)] setup[(1, 0)]
["Germany"] ["Germany"]
.Army("Mun").Supports.Army("Mun", season: s0).MoveTo("Tyr").GetReference(out var mun1) .Army("Mun").Supports.Army("Mun", season: s0).MoveTo("Tyr").GetReference(out var mun1)
["Austria"] ["Austria"]
@ -91,16 +91,16 @@ public class TimeTravelTest
// Confirm that an alternate future is created. // Confirm that an alternate future is created.
World world = setup.UpdateWorld(); World world = setup.UpdateWorld();
Season fork = world.GetSeason("b1"); Season fork = world.GetSeason(1, 1);
Unit tyr1 = world.GetUnitAt("Tyr", fork.Coord); Unit tyr1 = world.GetUnitAt("Tyr", fork.Coord);
Assert.That( Assert.That(
tyr1.Past, tyr1.Past,
Is.EqualTo(mun0.Order.Unit), Is.EqualTo(mun0.Order.Unit),
"Expected A Mun a0 to advance to Tyr b1"); "Expected A Mun 0:0 to advance to Tyr 1:1");
Assert.That( Assert.That(
world.RetreatingUnits.Count, world.RetreatingUnits.Count,
Is.EqualTo(1), Is.EqualTo(1),
"Expected A Tyr a0 to be in retreat"); "Expected A Tyr 0:0 to be in retreat");
Assert.That(world.RetreatingUnits.First().Unit, Is.EqualTo(tyr0.Order.Unit)); Assert.That(world.RetreatingUnits.First().Unit, Is.EqualTo(tyr0.Order.Unit));
} }
@ -110,14 +110,14 @@ public class TimeTravelTest
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance); TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance);
// Hold to create a future, then attempt to attack in the past. // Hold to create a future, then attempt to attack in the past.
setup[("a", 0)] setup[(0, 0)]
.GetReference(out Season s0) .GetReference(out Season s0)
["Germany"] ["Germany"]
.Army("Mun").Holds() .Army("Mun").Holds()
["Austria"] ["Austria"]
.Army("Tyr").Holds().GetReference(out var tyr0) .Army("Tyr").Holds().GetReference(out var tyr0)
.Execute() .Execute()
[("a", 1)] [(1, 0)]
.GetReference(out Season s1) .GetReference(out Season s1)
["Germany"] ["Germany"]
.Army("Mun").MovesTo("Tyr", season: s0).GetReference(out var mun1) .Army("Mun").MovesTo("Tyr", season: s0).GetReference(out var mun1)
@ -128,7 +128,7 @@ public class TimeTravelTest
Assert.That(mun1, Is.Valid); Assert.That(mun1, Is.Valid);
setup.AdjudicateOrders(); setup.AdjudicateOrders();
Assert.That(mun1, Is.Repelled); Assert.That(mun1, Is.Repelled);
// The order to Tyr a0 should have been pulled into the adjudication set, so the reference // The order to Tyr 0:0 should have been pulled into the adjudication set, so the reference
// should not throw when accessing it. // should not throw when accessing it.
Assert.That(tyr0, Is.NotDislodged); Assert.That(tyr0, Is.NotDislodged);
@ -140,7 +140,7 @@ public class TimeTravelTest
Is.EqualTo(1), Is.EqualTo(1),
"A failed move incorrectly forked the timeline"); "A failed move incorrectly forked the timeline");
Assert.That(s1.Futures.Count(), Is.EqualTo(1)); Assert.That(s1.Futures.Count(), Is.EqualTo(1));
Season s2 = world.GetSeason(s1.Timeline, s1.Turn + 1); Season s2 = world.GetSeason(s1.Turn + 1, s1.Timeline);
Assert.That(s2.Futures.Count(), Is.EqualTo(0)); Assert.That(s2.Futures.Count(), Is.EqualTo(0));
} }
@ -150,7 +150,7 @@ public class TimeTravelTest
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance); TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance);
// Move, then support the past move even though it succeeded already. // Move, then support the past move even though it succeeded already.
setup[("a", 0)] setup[(0, 0)]
.GetReference(out Season s0) .GetReference(out Season s0)
["Germany"] ["Germany"]
.Army("Mun").MovesTo("Tyr").GetReference(out var mun0) .Army("Mun").MovesTo("Tyr").GetReference(out var mun0)
@ -162,7 +162,7 @@ public class TimeTravelTest
Assert.That(mun0, Is.Victorious); Assert.That(mun0, Is.Victorious);
setup.UpdateWorld(); setup.UpdateWorld();
setup[("a", 1)] setup[(1, 0)]
.GetReference(out Season s1) .GetReference(out Season s1)
["Germany"] ["Germany"]
.Army("Tyr").Holds() .Army("Tyr").Holds()
@ -182,7 +182,7 @@ public class TimeTravelTest
Is.EqualTo(1), Is.EqualTo(1),
"A superfluous support incorrectly forked the timeline"); "A superfluous support incorrectly forked the timeline");
Assert.That(s1.Futures.Count(), Is.EqualTo(1)); Assert.That(s1.Futures.Count(), Is.EqualTo(1));
Season s2 = world.GetSeason(s1.Timeline, s1.Turn + 1); Season s2 = world.GetSeason(s1.Turn + 1, s1.Timeline);
Assert.That(s2.Futures.Count(), Is.EqualTo(0)); Assert.That(s2.Futures.Count(), Is.EqualTo(0));
} }
@ -192,51 +192,51 @@ public class TimeTravelTest
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance); TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance);
// London creates two timelines by moving into the past. // London creates two timelines by moving into the past.
setup[("a", 0)] setup[(0, 0)]
.GetReference(out var a0) .GetReference(out var s0_0)
["England"].Army("Lon").Holds() ["England"].Army("Lon").Holds()
["Austria"].Army("Tyr").Holds() ["Austria"].Army("Tyr").Holds()
["Germany"].Army("Mun").Holds() ["Germany"].Army("Mun").Holds()
.Execute() .Execute()
[("a", 1)] [(1, 0)]
["England"].Army("Lon").MovesTo("Yor", a0) ["England"].Army("Lon").MovesTo("Yor", s0_0)
.Execute() .Execute()
// Head seasons: a2 b1 // Head seasons: 2:0 1:1
// Both contain copies of the armies in Mun and Tyr. // Both contain copies of the armies in Mun and Tyr.
// Now Germany dislodges Austria in Tyr by supporting the move across timelines. // Now Germany dislodges Austria in Tyr by supporting the move across timelines.
[("a", 2)] [(2, 0)]
.GetReference(out var a2) .GetReference(out var s2_0)
["Germany"].Army("Mun").MovesTo("Tyr").GetReference(out var mun_a2) ["Germany"].Army("Mun").MovesTo("Tyr").GetReference(out var mun2_0)
["Austria"].Army("Tyr").Holds().GetReference(out var tyr_a2) ["Austria"].Army("Tyr").Holds().GetReference(out var tyr2_0)
[("b", 1)] [(1, 1)]
.GetReference(out var b1) .GetReference(out var s1_1)
["Germany"].Army("Mun").Supports.Army("Mun", a2).MoveTo("Tyr").GetReference(out var mun_b1) ["Germany"].Army("Mun").Supports.Army("Mun", s2_0).MoveTo("Tyr").GetReference(out var mun1_1)
["Austria"].Army("Tyr").Holds(); ["Austria"].Army("Tyr").Holds();
// The attack against Tyr a2 succeeds. // The attack against Tyr 2:0 succeeds.
setup.ValidateOrders(); setup.ValidateOrders();
Assert.That(mun_a2, Is.Valid); Assert.That(mun2_0, Is.Valid);
Assert.That(tyr_a2, Is.Valid); Assert.That(tyr2_0, Is.Valid);
Assert.That(mun_b1, Is.Valid); Assert.That(mun1_1, Is.Valid);
setup.AdjudicateOrders(); setup.AdjudicateOrders();
Assert.That(mun_a2, Is.Victorious); Assert.That(mun2_0, Is.Victorious);
Assert.That(tyr_a2, Is.Dislodged); Assert.That(tyr2_0, Is.Dislodged);
Assert.That(mun_b1, Is.NotCut); Assert.That(mun1_1, Is.NotCut);
// Since both seasons were at the head of their timelines, there should be no forking. // Since both seasons were at the head of their timelines, there should be no forking.
World world = setup.UpdateWorld(); World world = setup.UpdateWorld();
Assert.That( Assert.That(
a2.Futures.Count(), s2_0.Futures.Count(),
Is.EqualTo(1), Is.EqualTo(1),
"A cross-timeline support incorrectly forked the head of the timeline"); "A cross-timeline support incorrectly forked the head of the timeline");
Assert.That( Assert.That(
b1.Futures.Count(), s1_1.Futures.Count(),
Is.EqualTo(1), Is.EqualTo(1),
"A cross-timeline support incorrectly forked the head of the timeline"); "A cross-timeline support incorrectly forked the head of the timeline");
Season a3 = world.GetSeason(a2.Timeline, a2.Turn + 1); Season s3_0 = world.GetSeason(s2_0.Turn + 1, s2_0.Timeline);
Assert.That(a3.Futures.Count(), Is.EqualTo(0)); Assert.That(s3_0.Futures.Count(), Is.EqualTo(0));
Season b2 = world.GetSeason(b1.Timeline, b1.Turn + 1); Season s2_1 = world.GetSeason(s1_1.Turn + 1, s1_1.Timeline);
Assert.That(b2.Futures.Count(), Is.EqualTo(0)); Assert.That(s2_1.Futures.Count(), Is.EqualTo(0));
} }
[Test] [Test]
@ -245,63 +245,63 @@ public class TimeTravelTest
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance); TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance);
// As above, only now London creates three timelines. // As above, only now London creates three timelines.
setup[("a", 0)] setup[(0, 0)]
.GetReference(out var a0) .GetReference(out var s0_0)
["England"].Army("Lon").Holds() ["England"].Army("Lon").Holds()
["Austria"].Army("Boh").Holds() ["Austria"].Army("Boh").Holds()
["Germany"].Army("Mun").Holds() ["Germany"].Army("Mun").Holds()
.Execute() .Execute()
[("a", 1)] [(1, 0)]
["England"].Army("Lon").MovesTo("Yor", a0) ["England"].Army("Lon").MovesTo("Yor", s0_0)
.Execute() .Execute()
// Head seasons: a2, b1 // Head seasons: 2:0 1:1
[("a", 2)] [(2, 0)]
[("b", 1)] [(1, 1)]
["England"].Army("Yor").MovesTo("Edi", a0) ["England"].Army("Yor").MovesTo("Edi", s0_0)
.Execute() .Execute()
// Head seasons: a3, b2, c1 // Head seasons: 3:0 2:1 1:2
// All three of these contain copies of the armies in Mun and Tyr. // All three of these contain copies of the armies in Mun and Tyr.
// As above, Germany dislodges Austria in Tyr a3 by supporting the move from b2. // As above, Germany dislodges Austria in Tyr 3:0 by supporting the move from 2:1.
[("a", 3)] [(3, 0)]
.GetReference(out var a3) .GetReference(out var s3_0)
["Germany"].Army("Mun").MovesTo("Tyr") ["Germany"].Army("Mun").MovesTo("Tyr")
["Austria"].Army("Tyr").Holds() ["Austria"].Army("Tyr").Holds()
[("b", 2)] [(2, 1)]
.GetReference(out Season b2) .GetReference(out Season s2_1)
["Germany"].Army("Mun").Supports.Army("Mun", a3).MoveTo("Tyr").GetReference(out var mun_b2) ["Germany"].Army("Mun").Supports.Army("Mun", s3_0).MoveTo("Tyr").GetReference(out var mun2_1)
["Austria"].Army("Tyr").Holds() ["Austria"].Army("Tyr").Holds()
[("c", 1)] [(1, 2)]
["Germany"].Army("Mun").Holds() ["Germany"].Army("Mun").Holds()
["Austria"].Army("Tyr").Holds() ["Austria"].Army("Tyr").Holds()
.Execute() .Execute()
// Head seasons: a4, b3, c2 // Head seasons: 4:0 3:1 2:2
// Now Austria cuts the support in b2 by attacking from c2. // Now Austria cuts the support in 2:1 by attacking from 2:2.
[("a", 4)] [(4, 0)]
["Germany"].Army("Tyr").Holds() ["Germany"].Army("Tyr").Holds()
[("b", 3)] [(3, 1)]
["Germany"].Army("Mun").Holds() ["Germany"].Army("Mun").Holds()
["Austria"].Army("Tyr").Holds() ["Austria"].Army("Tyr").Holds()
[("c", 2)] [(2, 2)]
["Germany"].Army("Mun").Holds() ["Germany"].Army("Mun").Holds()
["Austria"].Army("Tyr").MovesTo("Mun", b2).GetReference(out var tyr_c2); ["Austria"].Army("Tyr").MovesTo("Mun", s2_1).GetReference(out var tyr2_2);
// The attack on Mun b2 is repelled, but the support is cut. // The attack on Mun 2:1 is repelled, but the support is cut.
setup.ValidateOrders(); setup.ValidateOrders();
Assert.That(tyr_c2, Is.Valid); Assert.That(tyr2_2, Is.Valid);
setup.AdjudicateOrders(); setup.AdjudicateOrders();
Assert.That(tyr_c2, Is.Repelled); Assert.That(tyr2_2, Is.Repelled);
Assert.That(mun_b2, Is.NotDislodged); Assert.That(mun2_1, Is.NotDislodged);
Assert.That(mun_b2, Is.Cut); Assert.That(mun2_1, Is.Cut);
// Though the support was cut, the timeline doesn't fork because the outcome of a battle // Though the support was cut, the timeline doesn't fork because the outcome of a battle
// wasn't changed in this timeline. // wasn't changed in this timeline.
World world = setup.UpdateWorld(); World world = setup.UpdateWorld();
Assert.That( Assert.That(
a3.Futures.Count(), s3_0.Futures.Count(),
Is.EqualTo(1), Is.EqualTo(1),
"A cross-timeline support cut incorrectly forked the timeline"); "A cross-timeline support cut incorrectly forked the timeline");
Assert.That( Assert.That(
b2.Futures.Count(), s2_1.Futures.Count(),
Is.EqualTo(1), Is.EqualTo(1),
"A cross-timeline support cut incorrectly forked the timeline"); "A cross-timeline support cut incorrectly forked the timeline");
} }

View File

@ -1,38 +0,0 @@
using MultiversalDiplomacy.Model;
using NUnit.Framework;
namespace MultiversalDiplomacyTests.Model;
public class TimelineFactoryTest
{
[TestCase(0, "a")]
[TestCase(1, "b")]
[TestCase(25, "z")]
[TestCase(26, "aa")]
[TestCase(27, "ab")]
[TestCase(51, "az")]
[TestCase(52, "ba")]
[TestCase(53, "bb")]
[TestCase(77, "bz")]
[TestCase(78, "ca")]
public void RoundTripTimelineDesignations(int number, string designation)
{
Assert.That(TimelineFactory.IntToString(number), Is.EqualTo(designation), "Incorrect string");
Assert.That(TimelineFactory.StringToInt(designation), Is.EqualTo(number), "Incorrect number");
}
[Test]
public void NoSharedFactoryState()
{
TimelineFactory one = new();
TimelineFactory two = new();
Assert.That(one.NextTimeline(), Is.EqualTo("a"));
Assert.That(one.NextTimeline(), Is.EqualTo("b"));
Assert.That(one.NextTimeline(), Is.EqualTo("c"));
Assert.That(two.NextTimeline(), Is.EqualTo("a"));
Assert.That(two.NextTimeline(), Is.EqualTo("b"));
}
}

View File

@ -171,7 +171,7 @@ public class MovementAdjudicatorTest
// Confirm the future was created // Confirm the future was created
Assert.That(updated.Seasons.Count, Is.EqualTo(2)); Assert.That(updated.Seasons.Count, Is.EqualTo(2));
Season future = updated.Seasons.Single(s => s != updated.RootSeason); Season future = updated.Seasons.Single(s => s != updated.RootSeason);
Assert.That(future.Past, Is.EqualTo(updated.RootSeason.ToString())); Assert.That(future.Past, Is.EqualTo(updated.RootSeason));
Assert.That(future.Futures, Is.Empty); Assert.That(future.Futures, Is.Empty);
Assert.That(future.Timeline, Is.EqualTo(updated.RootSeason.Timeline)); Assert.That(future.Timeline, Is.EqualTo(updated.RootSeason.Timeline));
Assert.That(future.Turn, Is.EqualTo(Season.FIRST_TURN + 1)); Assert.That(future.Turn, Is.EqualTo(Season.FIRST_TURN + 1));
@ -186,7 +186,7 @@ public class MovementAdjudicatorTest
public void Update_DoubleHold() public void Update_DoubleHold()
{ {
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance); TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance);
setup[("a", 0)] setup[(0, 0)]
.GetReference(out Season s1) .GetReference(out Season s1)
["Germany"] ["Germany"]
.Army("Mun").Holds().GetReference(out var mun1); .Army("Mun").Holds().GetReference(out var mun1);
@ -199,8 +199,8 @@ public class MovementAdjudicatorTest
World updated = setup.UpdateWorld(); World updated = setup.UpdateWorld();
// Confirm the future was created // Confirm the future was created
Season s2 = updated.GetSeason("a1"); Season s2 = updated.GetSeason(1, 0);
Assert.That(s2.Past, Is.EqualTo(s1.ToString())); Assert.That(s2.Past, Is.EqualTo(s1));
Assert.That(s2.Futures, Is.Empty); Assert.That(s2.Futures, Is.Empty);
Assert.That(s2.Timeline, Is.EqualTo(s1.Timeline)); Assert.That(s2.Timeline, Is.EqualTo(s1.Timeline));
Assert.That(s2.Turn, Is.EqualTo(s1.Turn + 1)); Assert.That(s2.Turn, Is.EqualTo(s1.Turn + 1));
@ -212,7 +212,7 @@ public class MovementAdjudicatorTest
Assert.That(u2.Past, Is.EqualTo(mun1.Order.Unit)); 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[(1, 0)]
["Germany"] ["Germany"]
.Army("Mun").Holds().GetReference(out var mun2); .Army("Mun").Holds().GetReference(out var mun2);
@ -227,7 +227,7 @@ public class MovementAdjudicatorTest
// Update the world again // Update the world again
updated = setup.UpdateWorld(); updated = setup.UpdateWorld();
Season s3 = updated.GetSeason(s2.Timeline, s2.Turn + 1); Season s3 = updated.GetSeason(s2.Turn + 1, s2.Timeline);
Unit u3 = updated.GetUnitAt("Mun", s3.Coord); Unit u3 = updated.GetUnitAt("Mun", s3.Coord);
Assert.That(u3.Past, Is.EqualTo(mun2.Order.Unit)); Assert.That(u3.Past, Is.EqualTo(mun2.Order.Unit));
} }
@ -236,7 +236,7 @@ public class MovementAdjudicatorTest
public void Update_DoubleMove() public void Update_DoubleMove()
{ {
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance); TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance);
setup[("a", 0)] setup[(0, 0)]
.GetReference(out Season s1) .GetReference(out Season s1)
["Germany"] ["Germany"]
.Army("Mun").MovesTo("Tyr").GetReference(out var mun1); .Army("Mun").MovesTo("Tyr").GetReference(out var mun1);
@ -249,8 +249,8 @@ public class MovementAdjudicatorTest
World updated = setup.UpdateWorld(); World updated = setup.UpdateWorld();
// Confirm the future was created // Confirm the future was created
Season s2 = updated.GetSeason(s1.Timeline, s1.Turn + 1); Season s2 = updated.GetSeason(s1.Turn + 1, s1.Timeline);
Assert.That(s2.Past, Is.EqualTo(s1.ToString())); Assert.That(s2.Past, Is.EqualTo(s1));
Assert.That(s2.Futures, Is.Empty); Assert.That(s2.Futures, Is.Empty);
Assert.That(s2.Timeline, Is.EqualTo(s1.Timeline)); Assert.That(s2.Timeline, Is.EqualTo(s1.Timeline));
Assert.That(s2.Turn, Is.EqualTo(s1.Turn + 1)); Assert.That(s2.Turn, Is.EqualTo(s1.Turn + 1));
@ -262,7 +262,7 @@ public class MovementAdjudicatorTest
Assert.That(u2.Past, Is.EqualTo(mun1.Order.Unit)); 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[(1, 0)]
["Germany"] ["Germany"]
.Army("Tyr").MovesTo("Mun").GetReference(out var tyr2); .Army("Tyr").MovesTo("Mun").GetReference(out var tyr2);
@ -277,7 +277,7 @@ public class MovementAdjudicatorTest
// Update the world again // Update the world again
updated = setup.UpdateWorld(); updated = setup.UpdateWorld();
Season s3 = updated.GetSeason(s2.Timeline, s2.Turn + 1); Season s3 = updated.GetSeason(s2.Turn + 1, s2.Timeline);
Unit u3 = updated.GetUnitAt("Mun", s3.Coord); Unit u3 = updated.GetUnitAt("Mun", s3.Coord);
Assert.That(u3.Past, Is.EqualTo(u2)); Assert.That(u3.Past, Is.EqualTo(u2));
} }

View File

@ -9,57 +9,57 @@ public class SeasonTests
[Test] [Test]
public void TimelineForking() public void TimelineForking()
{ {
World world = World Season a0 = Season.MakeRoot();
.WithMap(Map.Test) Season a1 = a0.MakeNext();
.ContinueSeason("a0") Season a2 = a1.MakeNext();
.ContinueSeason("a1") Season a3 = a2.MakeNext();
.ContinueSeason("a2") Season b1 = a1.MakeFork();
.ForkSeason("a1") Season b2 = b1.MakeNext();
.ContinueSeason("b2") Season c1 = a1.MakeFork();
.ForkSeason("a1") Season d1 = a2.MakeFork();
.ForkSeason("a2");
Assert.That( Assert.That(a0.Timeline, Is.EqualTo(0), "Unexpected trunk timeline number");
world.Seasons.Select(season => season.ToString()), Assert.That(a1.Timeline, Is.EqualTo(0), "Unexpected trunk timeline number");
Is.EquivalentTo(new List<string> { "a0", "a1", "a2", "a3", "b2", "b3", "c2", "d3" }), Assert.That(a2.Timeline, Is.EqualTo(0), "Unexpected trunk timeline number");
"Unexpected seasons"); Assert.That(a3.Timeline, Is.EqualTo(0), "Unexpected trunk timeline number");
Assert.That(b1.Timeline, Is.EqualTo(1), "Unexpected first alt number");
Season a0 = world.GetSeason("a0"); Assert.That(b2.Timeline, Is.EqualTo(1), "Unexpected first alt number");
Season a1 = world.GetSeason("a1"); Assert.That(c1.Timeline, Is.EqualTo(2), "Unexpected second alt number");
Season a2 = world.GetSeason("a2"); Assert.That(d1.Timeline, Is.EqualTo(3), "Unexpected third alt number");
Season a3 = world.GetSeason("a3");
Season b2 = world.GetSeason("b2");
Season b3 = world.GetSeason("b3");
Season c2 = world.GetSeason("c2");
Season d3 = world.GetSeason("d3");
Assert.That(a0.Timeline, Is.EqualTo("a"), "Unexpected trunk timeline");
Assert.That(a1.Timeline, Is.EqualTo("a"), "Unexpected trunk timeline");
Assert.That(a2.Timeline, Is.EqualTo("a"), "Unexpected trunk timeline");
Assert.That(a3.Timeline, Is.EqualTo("a"), "Unexpected trunk timeline");
Assert.That(b2.Timeline, Is.EqualTo("b"), "Unexpected first alt");
Assert.That(b3.Timeline, Is.EqualTo("b"), "Unexpected first alt");
Assert.That(c2.Timeline, Is.EqualTo("c"), "Unexpected second alt");
Assert.That(d3.Timeline, Is.EqualTo("d"), "Unexpected third alt");
Assert.That(a0.Turn, Is.EqualTo(Season.FIRST_TURN + 0), "Unexpected first turn number"); Assert.That(a0.Turn, Is.EqualTo(Season.FIRST_TURN + 0), "Unexpected first turn number");
Assert.That(a1.Turn, Is.EqualTo(Season.FIRST_TURN + 1), "Unexpected next turn number"); Assert.That(a1.Turn, Is.EqualTo(Season.FIRST_TURN + 1), "Unexpected next turn number");
Assert.That(a2.Turn, Is.EqualTo(Season.FIRST_TURN + 2), "Unexpected next turn number"); Assert.That(a2.Turn, Is.EqualTo(Season.FIRST_TURN + 2), "Unexpected next turn number");
Assert.That(a3.Turn, Is.EqualTo(Season.FIRST_TURN + 3), "Unexpected next turn number"); Assert.That(a3.Turn, Is.EqualTo(Season.FIRST_TURN + 3), "Unexpected next turn number");
Assert.That(b2.Turn, Is.EqualTo(Season.FIRST_TURN + 2), "Unexpected fork turn number"); Assert.That(b1.Turn, Is.EqualTo(Season.FIRST_TURN + 2), "Unexpected fork turn number");
Assert.That(b3.Turn, Is.EqualTo(Season.FIRST_TURN + 3), "Unexpected fork turn number"); Assert.That(b2.Turn, Is.EqualTo(Season.FIRST_TURN + 3), "Unexpected fork turn number");
Assert.That(c2.Turn, Is.EqualTo(Season.FIRST_TURN + 2), "Unexpected fork turn number"); Assert.That(c1.Turn, Is.EqualTo(Season.FIRST_TURN + 2), "Unexpected fork turn number");
Assert.That(d3.Turn, Is.EqualTo(Season.FIRST_TURN + 3), "Unexpected fork turn number"); Assert.That(d1.Turn, Is.EqualTo(Season.FIRST_TURN + 3), "Unexpected fork turn number");
Assert.That(world.GetTimelineRoot(a0), Is.EqualTo(a0), "Expected timeline root to be reflexive"); Assert.That(a0.TimelineRoot(), Is.EqualTo(a0), "Expected timeline root to be reflexive");
Assert.That(world.GetTimelineRoot(a3), Is.EqualTo(a0), "Expected trunk timeline to have root"); Assert.That(a3.TimelineRoot(), Is.EqualTo(a0), "Expected trunk timeline to have root");
Assert.That(world.GetTimelineRoot(b2), Is.EqualTo(b2), "Expected alt timeline root to be reflexive"); Assert.That(b1.TimelineRoot(), Is.EqualTo(b1), "Expected alt timeline root to be reflexive");
Assert.That(world.GetTimelineRoot(b3), Is.EqualTo(b2), "Expected alt timeline to root at first fork"); Assert.That(b2.TimelineRoot(), Is.EqualTo(b1), "Expected alt timeline to root at first fork");
Assert.That(world.GetTimelineRoot(c2), Is.EqualTo(c2), "Expected alt timeline root to be reflexive"); Assert.That(c1.TimelineRoot(), Is.EqualTo(c1), "Expected alt timeline root to be reflexive");
Assert.That(world.GetTimelineRoot(d3), Is.EqualTo(d3), "Expected alt timeline root to be reflexive"); Assert.That(d1.TimelineRoot(), Is.EqualTo(d1), "Expected alt timeline root to be reflexive");
Assert.That(world.InAdjacentTimeline(b3, a3), Is.True, "Expected alts to be adjacent to origin"); Assert.That(b2.InAdjacentTimeline(a3), Is.True, "Expected alts to be adjacent to origin");
Assert.That(world.InAdjacentTimeline(b3, c2), Is.True, "Expected alts with common origin to be adjacent"); Assert.That(b2.InAdjacentTimeline(c1), Is.True, "Expected alts with common origin to be adjacent");
Assert.That(world.InAdjacentTimeline(b3, d3), Is.False, "Expected alts from different origins not to be adjacent"); Assert.That(b2.InAdjacentTimeline(d1), Is.False, "Expected alts from different origins not to be adjacent");
} }
}
[Test]
public void LookupTest()
{
World world = World.WithStandardMap();
Season s2 = world.RootSeason.MakeNext();
Season s3 = s2.MakeNext();
Season s4 = s2.MakeFork();
World updated = world.Update(seasons: world.Seasons.Append(s2).Append(s3).Append(s4));
Assert.That(updated.GetSeason(Season.FIRST_TURN, 0), Is.EqualTo(updated.RootSeason));
Assert.That(updated.GetSeason(Season.FIRST_TURN + 1, 0), Is.EqualTo(s2));
Assert.That(updated.GetSeason(Season.FIRST_TURN + 2, 0), Is.EqualTo(s3));
Assert.That(updated.GetSeason(Season.FIRST_TURN + 2, 1), Is.EqualTo(s4));
}
}

View File

@ -19,7 +19,7 @@ public class TestCaseBuilder
/// <summary> /// <summary>
/// Choose a new season to define orders for. /// Choose a new season to define orders for.
/// </summary> /// </summary>
public ISeasonContext this[(string timeline, int turn) seasonCoord] { get; } public ISeasonContext this[(int turn, int timeline) seasonCoord] { get; }
/// <summary> /// <summary>
/// Get the context for defining the orders for a power. /// Get the context for defining the orders for a power.
@ -40,7 +40,7 @@ public class TestCaseBuilder
/// <summary> /// <summary>
/// Choose a new season to define orders for. /// Choose a new season to define orders for.
/// </summary> /// </summary>
public ISeasonContext this[(string timeline, int turn) seasonCoord] { get; } public ISeasonContext this[(int turn, int timeline) seasonCoord] { get; }
/// <summary> /// <summary>
/// Get the context for defining the orders for another power. /// Get the context for defining the orders for another power.
@ -188,7 +188,7 @@ public class TestCaseBuilder
/// <summary> /// <summary>
/// Choose a new season to define orders for. /// Choose a new season to define orders for.
/// </summary> /// </summary>
public ISeasonContext this[(string timeline, int turn) seasonCoord] { get; } public ISeasonContext this[(int turn, int timeline) seasonCoord] { get; }
/// <summary> /// <summary>
/// Get the context for defining the orders for another power. /// Get the context for defining the orders for another power.
@ -234,13 +234,13 @@ public class TestCaseBuilder
/// <summary> /// <summary>
/// Get the context for defining the orders for a power. Defaults to the root season. /// Get the context for defining the orders for a power. Defaults to the root season.
/// </summary> /// </summary>
public IPowerContext this[string powerName] => this[("a", 0)][powerName]; public IPowerContext this[string powerName] => this[(0, 0)][powerName];
/// <summary> /// <summary>
/// Get the context for defining the orders for a season. /// Get the context for defining the orders for a season.
/// </summary> /// </summary>
public ISeasonContext this[(string timeline, int turn) seasonCoord] public ISeasonContext this[(int turn, int timeline) seasonCoord]
=> new SeasonContext(this, this.World.GetSeason(seasonCoord.timeline, seasonCoord.turn)); => new SeasonContext(this, this.World.GetSeason(seasonCoord.turn, seasonCoord.timeline));
/// <summary> /// <summary>
/// Get a unit matching a description. If no such unit exists, one is created and added to the /// Get a unit matching a description. If no such unit exists, one is created and added to the
@ -327,8 +327,8 @@ public class TestCaseBuilder
this.Season = season; this.Season = season;
} }
public ISeasonContext this[(string timeline, int turn) seasonCoord] public ISeasonContext this[(int turn, int timeline) seasonCoord]
=> this.Builder[(seasonCoord.timeline, seasonCoord.turn)]; => this.Builder[(seasonCoord.turn, seasonCoord.timeline)];
public IPowerContext this[string powerName] public IPowerContext this[string powerName]
=> new PowerContext(this, this.Builder.World.Map.GetPower(powerName)); => new PowerContext(this, this.Builder.World.Map.GetPower(powerName));
@ -353,7 +353,7 @@ public class TestCaseBuilder
this.Power = Power; this.Power = Power;
} }
public ISeasonContext this[(string timeline, int turn) seasonCoord] public ISeasonContext this[(int turn, int timeline) seasonCoord]
=> this.SeasonContext[seasonCoord]; => this.SeasonContext[seasonCoord];
public IPowerContext this[string powerName] public IPowerContext this[string powerName]
@ -623,7 +623,7 @@ public class TestCaseBuilder
return this.Builder; return this.Builder;
} }
public ISeasonContext this[(string timeline, int turn) seasonCoord] public ISeasonContext this[(int turn, int timeline) seasonCoord]
=> this.SeasonContext[seasonCoord]; => this.SeasonContext[seasonCoord];
public IPowerContext this[string powerName] public IPowerContext this[string powerName]

View File

@ -14,7 +14,7 @@ class TestCaseBuilderTest
{ {
TestCaseBuilder setup = new(World.WithStandardMap()); TestCaseBuilder setup = new(World.WithStandardMap());
Assert.That(setup.World.Powers.Count, Is.EqualTo(7), "Unexpected power count"); Assert.That(setup.World.Powers.Count(), Is.EqualTo(7), "Unexpected power count");
Assert.That(setup.World.Units, Is.Empty, "Expected no units to be created yet"); Assert.That(setup.World.Units, Is.Empty, "Expected no units to be created yet");
setup setup
@ -49,7 +49,7 @@ class TestCaseBuilderTest
{ {
TestCaseBuilder setup = new(World.WithStandardMap()); TestCaseBuilder setup = new(World.WithStandardMap());
Assert.That(setup.World.Powers.Count, Is.EqualTo(7), "Unexpected power count"); Assert.That(setup.World.Powers.Count(), Is.EqualTo(7), "Unexpected power count");
Assert.That(setup.World.Units, Is.Empty, "Expected no units to be created yet"); Assert.That(setup.World.Units, Is.Empty, "Expected no units to be created yet");
Assert.That(setup.Orders, Is.Empty, "Expected no orders to be created yet"); Assert.That(setup.Orders, Is.Empty, "Expected no orders to be created yet");

View File

@ -14,24 +14,22 @@ public class UnitTests
Boh = world.Map.GetLand("Boh"), Boh = world.Map.GetLand("Boh"),
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 s1 = world.RootSeason;
Unit u1 = Unit.Build(Mun, a0, pw1, UnitType.Army); Unit u1 = Unit.Build(Mun, s1, pw1, UnitType.Army);
world = world.ContinueSeason(a0); Season s2 = s1.MakeNext();
Season a1 = world.GetSeason("a1"); Unit u2 = u1.Next(Boh, s2);
Unit u2 = u1.Next(Boh, a1);
world = world.ContinueSeason(a1); Season s3 = s2.MakeNext();
Season a2 = world.GetSeason("a2"); Unit u3 = u2.Next(Tyr, s3);
Unit u3 = u2.Next(Tyr, a2);
Assert.That(u3.Past, Is.EqualTo(u2), "Missing unit past"); Assert.That(u3.Past, Is.EqualTo(u2), "Missing unit past");
Assert.That(u2.Past, Is.EqualTo(u1), "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(s1), "Unexpected unit season");
Assert.That(u2.Season, Is.EqualTo(a1), "Unexpected unit season"); Assert.That(u2.Season, Is.EqualTo(s2), "Unexpected unit season");
Assert.That(u3.Season, Is.EqualTo(a2), "Unexpected unit season"); Assert.That(u3.Season, Is.EqualTo(s3), "Unexpected unit season");
Assert.That(u1.Location, Is.EqualTo(Mun), "Unexpected unit location"); Assert.That(u1.Location, Is.EqualTo(Mun), "Unexpected unit location");
Assert.That(u2.Location, Is.EqualTo(Boh), "Unexpected unit location"); Assert.That(u2.Location, Is.EqualTo(Boh), "Unexpected unit location");