Compare commits
No commits in common. "3242186208335832d9650fa636ceeb0b949ed70d" and "dab37c239b196ae80bfde5f216c2c52a5b2f1ae9" have entirely different histories.
3242186208
...
dab37c239b
92
MDATC.html
92
MDATC.html
|
@ -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">
|
||||||
|
|
10
Makefile
10
Makefile
|
@ -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)
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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++);
|
|
||||||
}
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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]
|
||||||
|
|
|
@ -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");
|
||||||
|
|
||||||
|
|
|
@ -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");
|
||||||
|
|
Loading…
Reference in New Issue