Compare commits

...

10 Commits

Author SHA1 Message Date
Tim Van Baak 3242186208 Use a simpler override where available 2024-08-12 14:57:16 -07:00
Tim Van Baak 11dfa403e4 Rename PastId back to Past 2024-08-12 14:52:50 -07:00
Tim Van Baak ae77c3c708 Remove Season.Past so all lookups go through World 2024-08-12 14:51:07 -07:00
Tim Van Baak f5afb4105b Move more timeline logic from Season to World 2024-08-12 14:50:15 -07:00
Tim Van Baak 7d4f7760be Move GetAdjacentSeasons to PathFinder 2024-08-12 14:12:49 -07:00
Tim Van Baak 5dfe9a5bb5 Replace most uses of Season creators to World 2024-08-12 13:56:06 -07:00
Tim Van Baak 5472dda931 Eliminate RootSeason field 2024-08-12 10:59:48 -07:00
Tim Van Baak 9696919773 Update timeline designator usage
Timelines are now identified by strings and come first in timeline-turn tuples.
2024-08-12 09:28:56 -07:00
Tim Van Baak 3d48a7d6f6 Refactor timeline factory to generate string ids
The strings are immediately shimmed back to ints for now
2024-08-12 07:51:18 -07:00
Tim Van Baak d7c07d1ff1 Add Makefile 2024-08-11 21:45:25 -07:00
15 changed files with 445 additions and 337 deletions

View File

@ -236,17 +236,13 @@
<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>
<ul>
<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>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 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".
<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>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>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>
<ul>
<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 season of a unit supported to hold is not specified, the season is the same season as the supporting unit.</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>
<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 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 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>
</ul>
</ul>
@ -258,13 +254,15 @@ The order here is for Army Munich in 2:0. The move being supported is for Army M
<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>
<pre>
Germany 0:0
A Munich hold
Germany:
A a-Munich hold
Germany 1:0
A Munich - Tyrolia 0:0
---
Germany:
A a-Munich - a-Tyrolia@0
</pre>
<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>
<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>
<div class="figures">
<canvas id="canvas-3-A-1-before" width="0" height="0"></canvas>
<script>
@ -311,19 +309,21 @@ The order here is for Army Munich in 2:0. The move being supported is for Army M
<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>
<pre>
Austria 0:0
Austria:
A Tyrolia hold
Germany 0:0
Germany:
A Munich - Tyrolia
Austria 1:0
---
Austria:
A Tyrolia hold
Germany 1:0
A Munich supports A Munich 0:0 - Tyrolia 0:0
Germany:
A Munich supports A a-Munich@0 - Tyrolia
</pre>
<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>
<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>
<div class="figures">
<canvas id="canvas-3-A-2-before" width="0" height="0"></canvas>
<script>
@ -379,19 +379,21 @@ The order here is for Army Munich in 2:0. The move being supported is for Army M
<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>
<pre>
Austria 0:0
Austria:
A Tyrolia hold
Germany 0:0
Germany:
A Munich hold
Austria 1:0
---
Austria:
A Tyrolia hold
Germany 1:0
A Munich - Tyrolia 0:0
Germany:
A Munich - a-Tyrolia@0
</pre>
<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>
<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>
<div class="figures">
<canvas id="canvas-3-A-3-before" width="0" height="0"></canvas>
<script>
@ -441,15 +443,17 @@ The order here is for Army Munich in 2:0. The move being supported is for Army M
<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>
<pre>
Germany 0:0
Germany:
A Munich - Tyrolia
A Bohemia hold
Germany 1:0
---
Germany:
A Tyrolia hold
A Bohemia supports A Munich 0:0 - Tyrolia
A Bohemia supports A a-Munich@0 - Tyrolia
</pre>
<p>Both units in 1:0 continue to 2:0. No alternate timeline is created.</p>
<p>Both units in a1 continue to a2. No alternate timeline is created.</p>
<div class="figures">
<canvas id="canvas-3-A-4-before" width="0" height="0"></canvas>
<script>
@ -501,15 +505,15 @@ The order here is for Army Munich in 2:0. The move being supported is for Army M
<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>
<pre>
Austria
A Tyrolia 2:0 hold
A Tyrolia 1:1 hold
Austria:
A a-Tyrolia hold
A b-Tyrolia hold
Germany
A Munich 2:0 - Tyrolia
A Munich 1:1 supports A Munich 2:0 - Tyrolia
A a-Munich - Tyrolia
A b-Munich supports A a-Munich - Tyrolia
</pre>
<p>A Munich 2:0 dislodges A Tyrolia 2:0. No alternate timeline is created.</p>
<p>A a-Munich dislodges A a-Tyrolia. No alternate timeline is created.</p>
<div class="figures">
<canvas id="canvas-3-A-5-before" width="0" height="0"></canvas>
<script>
@ -567,19 +571,11 @@ The order here is for Army Munich in 2:0. The move being supported is for Army M
<p>Following <a href="#3.A.5">3.A.5</a>, a cross-timeline support that previously succeeded is cut.</p>
<pre>
Germany
A Munich 2:0 - Tyrolia
A Munich 1:1 supports A Munich 2:0 - Tyrolia
A a-Tyrolia holds
A b-Munich holds
Austria
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
A b-Tyrolia@2 - b-Munich@1
</pre>
<p>Cutting the support does not change the past or cause a timeline fork.</p>
<div class="figures">

10
Makefile Normal file
View File

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

View File

@ -60,7 +60,7 @@ public static class PathFinder
(Location currentLocation, Season currentSeason) = toVisit.Dequeue();
visited.Add((currentLocation, currentSeason));
var adjacents = GetAdjacentPoints(currentLocation, currentSeason);
var adjacents = GetAdjacentPoints(world, currentLocation, currentSeason);
foreach ((Location adjLocation, Season adjSeason) in adjacents)
{
// If the destination is adjacent, then a path exists.
@ -81,11 +81,11 @@ public static class PathFinder
return false;
}
private static List<(Location, Season)> GetAdjacentPoints(Location location, Season season)
private static List<(Location, Season)> GetAdjacentPoints(World world, Location location, Season season)
{
List<(Location, Season)> adjacentPoints = new();
List<(Location, Season)> adjacentPoints = [];
List<Location> adjacentLocations = location.Adjacents.ToList();
List<Season> adjacentSeasons = season.GetAdjacentSeasons().ToList();
List<Season> adjacentSeasons = GetAdjacentSeasons(world, season).ToList();
foreach (Location adjacentLocation in adjacentLocations)
{
@ -105,4 +105,55 @@ public static class PathFinder
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,27 +5,17 @@ namespace MultiversalDiplomacy.Model;
/// </summary>
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>
/// The first turn number.
/// </summary>
public const int FIRST_TURN = 0;
/// <summary>
/// The season immediately preceding this season.
/// The designation of the season immediately preceding this season.
/// If this season is an alternate timeline root, the past is from the origin timeline.
/// The initial season does not have a past.
/// </summary>
public Season? Past { get; }
public string? Past { get; }
/// <summary>
/// The current turn, beginning at 0. Each season (spring and fall) is one turn.
@ -37,12 +27,12 @@ public class Season
/// <summary>
/// The timeline to which this season belongs.
/// </summary>
public int Timeline { get; }
public string Timeline { get; }
/// <summary>
/// The season's spatial location as a turn-timeline tuple.
/// The season's spatial location as a timeline-turn tuple.
/// </summary>
public (int Turn, int Timeline) Coord => (this.Turn, this.Timeline);
public (string Timeline, int Turn) Coord => (this.Timeline, this.Turn);
/// <summary>
/// The shared timeline number generator.
@ -55,23 +45,20 @@ public class Season
public IEnumerable<Season> Futures => this.FutureList;
private List<Season> FutureList { get; }
private Season(Season? past, int turn, int timeline, TimelineFactory factory)
private Season(Season? past, int turn, string timeline, TimelineFactory factory)
{
this.Past = past;
this.Past = past?.ToString();
this.Turn = turn;
this.Timeline = timeline;
this.Timelines = factory;
this.FutureList = new();
this.FutureList = [];
if (past != null)
{
past.FutureList.Add(this);
}
past?.FutureList.Add(this);
}
public override string ToString()
{
return $"{this.Timeline}@{this.Turn}";
return $"{this.Timeline}{this.Turn}";
}
/// <summary>
@ -91,95 +78,11 @@ public class Season
/// Create a season immediately after this one in the same timeline.
/// </summary>
public Season MakeNext()
=> new Season(this, this.Turn + 1, this.Timeline, this.Timelines);
=> new(this, Turn + 1, Timeline, Timelines);
/// <summary>
/// Create a season immediately after this one in a new timeline.
/// </summary>
public Season MakeFork()
=> 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;
}
=> new(this, Turn + 1, Timelines.NextTimeline(), Timelines);
}

View File

@ -0,0 +1,50 @@
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,10 +29,15 @@ public class World
/// </summary>
public ReadOnlyCollection<Season> Seasons { get; }
/// <summary>
/// Lookup for seasons by designation.
/// </summary>
public ReadOnlyDictionary<string, Season> SeasonLookup { get; }
/// <summary>
/// The first season of the game.
/// </summary>
public Season RootSeason { get; }
public Season RootSeason => GetSeason("a0");
/// <summary>
/// All units in the multiverse.
@ -60,7 +65,6 @@ public class World
private World(
Map map,
ReadOnlyCollection<Season> seasons,
Season rootSeason,
ReadOnlyCollection<Unit> units,
ReadOnlyCollection<RetreatingUnit> retreatingUnits,
ReadOnlyDictionary<Season, OrderHistory> orderHistory,
@ -68,11 +72,12 @@ public class World
{
this.Map = map;
this.Seasons = seasons;
this.RootSeason = rootSeason;
this.Units = units;
this.RetreatingUnits = retreatingUnits;
this.OrderHistory = orderHistory;
this.Options = options;
this.SeasonLookup = new(Seasons.ToDictionary(season => $"{season.Timeline}{season.Turn}"));
}
/// <summary>
@ -88,7 +93,6 @@ public class World
: this(
previous.Map,
seasons ?? previous.Seasons,
previous.RootSeason, // Can't change the root season
units ?? previous.Units,
retreatingUnits ?? previous.RetreatingUnits,
orderHistory ?? previous.OrderHistory,
@ -101,13 +105,11 @@ public class World
/// </summary>
public static World WithMap(Map map)
{
Season root = Season.MakeRoot();
return new World(
map,
new(new List<Season> { root }),
root,
new(new List<Unit>()),
new(new List<RetreatingUnit>()),
new([Season.MakeRoot()]),
new([]),
new([]),
new(new Dictionary<Season, OrderHistory>()),
new Options());
}
@ -123,7 +125,7 @@ public class World
IEnumerable<Unit>? units = null,
IEnumerable<RetreatingUnit>? retreats = null,
IEnumerable<KeyValuePair<Season, OrderHistory>>? orders = null)
=> new World(
=> new(
previous: this,
seasons: seasons == null
? this.Seasons
@ -197,6 +199,23 @@ 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>
/// A standard Diplomacy game setup.
/// </summary>
@ -207,29 +226,62 @@ public class World
/// <summary>
/// Get a season by coordinate. Throws if the season is not found.
/// </summary>
public Season GetSeason(int turn, int timeline)
public Season GetSeason(string timeline, int turn)
=> 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)
{
Season? foundSeason = this.Seasons.SingleOrDefault(
s => s!.Turn == turn && s.Timeline == timeline,
null);
if (foundSeason == null) throw new KeyNotFoundException(
$"Season {turn}:{timeline} not found");
return foundSeason;
if (season.Past is null) {
return season;
}
Season past = SeasonLookup[season.Past];
return season.Timeline == past.Timeline
? GetTimelineRoot(past)
: 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>
/// Returns a unit in a province. Throws if there are duplicate units.
/// </summary>
public Unit GetUnitAt(string provinceName, (int turn, int timeline)? seasonCoord = null)
public Unit GetUnitAt(string provinceName, (string timeline, int turn)? seasonCoord = null)
{
Province province = Map.GetProvince(provinceName);
seasonCoord ??= (this.RootSeason.Turn, this.RootSeason.Timeline);
Season season = GetSeason(seasonCoord.Value.turn, seasonCoord.Value.timeline);
seasonCoord ??= (this.RootSeason.Timeline, this.RootSeason.Turn);
Season season = GetSeason(seasonCoord.Value.timeline, seasonCoord.Value.turn);
Unit? foundUnit = this.Units.SingleOrDefault(
u => u!.Province == province && u.Season == season,
null);
if (foundUnit == null) throw new KeyNotFoundException(
$"Unit at {province} at {season} not found");
null)
?? throw new KeyNotFoundException($"Unit at {province} at {season} not found");
return foundUnit;
}
}
}

View File

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

View File

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

View File

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

View File

@ -9,57 +9,57 @@ public class SeasonTests
[Test]
public void TimelineForking()
{
Season a0 = Season.MakeRoot();
Season a1 = a0.MakeNext();
Season a2 = a1.MakeNext();
Season a3 = a2.MakeNext();
Season b1 = a1.MakeFork();
Season b2 = b1.MakeNext();
Season c1 = a1.MakeFork();
Season d1 = a2.MakeFork();
World world = World
.WithMap(Map.Test)
.ContinueSeason("a0")
.ContinueSeason("a1")
.ContinueSeason("a2")
.ForkSeason("a1")
.ContinueSeason("b2")
.ForkSeason("a1")
.ForkSeason("a2");
Assert.That(a0.Timeline, Is.EqualTo(0), "Unexpected trunk timeline number");
Assert.That(a1.Timeline, Is.EqualTo(0), "Unexpected trunk timeline number");
Assert.That(a2.Timeline, Is.EqualTo(0), "Unexpected trunk timeline number");
Assert.That(a3.Timeline, Is.EqualTo(0), "Unexpected trunk timeline number");
Assert.That(b1.Timeline, Is.EqualTo(1), "Unexpected first alt number");
Assert.That(b2.Timeline, Is.EqualTo(1), "Unexpected first alt number");
Assert.That(c1.Timeline, Is.EqualTo(2), "Unexpected second alt number");
Assert.That(d1.Timeline, Is.EqualTo(3), "Unexpected third alt number");
Assert.That(
world.Seasons.Select(season => season.ToString()),
Is.EquivalentTo(new List<string> { "a0", "a1", "a2", "a3", "b2", "b3", "c2", "d3" }),
"Unexpected seasons");
Season a0 = world.GetSeason("a0");
Season a1 = world.GetSeason("a1");
Season a2 = world.GetSeason("a2");
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(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(a3.Turn, Is.EqualTo(Season.FIRST_TURN + 3), "Unexpected next turn number");
Assert.That(b1.Turn, Is.EqualTo(Season.FIRST_TURN + 2), "Unexpected fork turn number");
Assert.That(b2.Turn, Is.EqualTo(Season.FIRST_TURN + 3), "Unexpected fork turn number");
Assert.That(c1.Turn, Is.EqualTo(Season.FIRST_TURN + 2), "Unexpected fork turn number");
Assert.That(d1.Turn, Is.EqualTo(Season.FIRST_TURN + 3), "Unexpected fork turn number");
Assert.That(b2.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(c2.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(a0.TimelineRoot(), Is.EqualTo(a0), "Expected timeline root to be reflexive");
Assert.That(a3.TimelineRoot(), Is.EqualTo(a0), "Expected trunk timeline to have root");
Assert.That(b1.TimelineRoot(), Is.EqualTo(b1), "Expected alt timeline root to be reflexive");
Assert.That(b2.TimelineRoot(), Is.EqualTo(b1), "Expected alt timeline to root at first fork");
Assert.That(c1.TimelineRoot(), Is.EqualTo(c1), "Expected alt timeline root to be reflexive");
Assert.That(d1.TimelineRoot(), Is.EqualTo(d1), "Expected alt timeline root to be reflexive");
Assert.That(world.GetTimelineRoot(a0), 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(world.GetTimelineRoot(b2), Is.EqualTo(b2), "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(world.GetTimelineRoot(c2), Is.EqualTo(c2), "Expected alt timeline root to be reflexive");
Assert.That(world.GetTimelineRoot(d3), Is.EqualTo(d3), "Expected alt timeline root to be reflexive");
Assert.That(b2.InAdjacentTimeline(a3), Is.True, "Expected alts to be adjacent to origin");
Assert.That(b2.InAdjacentTimeline(c1), Is.True, "Expected alts with common origin to be adjacent");
Assert.That(b2.InAdjacentTimeline(d1), Is.False, "Expected alts from different origins not to be adjacent");
Assert.That(world.InAdjacentTimeline(b3, 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(world.InAdjacentTimeline(b3, d3), 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>
/// Choose a new season to define orders for.
/// </summary>
public ISeasonContext this[(int turn, int timeline) seasonCoord] { get; }
public ISeasonContext this[(string timeline, int turn) seasonCoord] { get; }
/// <summary>
/// Get the context for defining the orders for a power.
@ -40,7 +40,7 @@ public class TestCaseBuilder
/// <summary>
/// Choose a new season to define orders for.
/// </summary>
public ISeasonContext this[(int turn, int timeline) seasonCoord] { get; }
public ISeasonContext this[(string timeline, int turn) seasonCoord] { get; }
/// <summary>
/// Get the context for defining the orders for another power.
@ -188,7 +188,7 @@ public class TestCaseBuilder
/// <summary>
/// Choose a new season to define orders for.
/// </summary>
public ISeasonContext this[(int turn, int timeline) seasonCoord] { get; }
public ISeasonContext this[(string timeline, int turn) seasonCoord] { get; }
/// <summary>
/// Get the context for defining the orders for another power.
@ -234,13 +234,13 @@ public class TestCaseBuilder
/// <summary>
/// Get the context for defining the orders for a power. Defaults to the root season.
/// </summary>
public IPowerContext this[string powerName] => this[(0, 0)][powerName];
public IPowerContext this[string powerName] => this[("a", 0)][powerName];
/// <summary>
/// Get the context for defining the orders for a season.
/// </summary>
public ISeasonContext this[(int turn, int timeline) seasonCoord]
=> new SeasonContext(this, this.World.GetSeason(seasonCoord.turn, seasonCoord.timeline));
public ISeasonContext this[(string timeline, int turn) seasonCoord]
=> new SeasonContext(this, this.World.GetSeason(seasonCoord.timeline, seasonCoord.turn));
/// <summary>
/// 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;
}
public ISeasonContext this[(int turn, int timeline) seasonCoord]
=> this.Builder[(seasonCoord.turn, seasonCoord.timeline)];
public ISeasonContext this[(string timeline, int turn) seasonCoord]
=> this.Builder[(seasonCoord.timeline, seasonCoord.turn)];
public IPowerContext this[string powerName]
=> new PowerContext(this, this.Builder.World.Map.GetPower(powerName));
@ -353,7 +353,7 @@ public class TestCaseBuilder
this.Power = Power;
}
public ISeasonContext this[(int turn, int timeline) seasonCoord]
public ISeasonContext this[(string timeline, int turn) seasonCoord]
=> this.SeasonContext[seasonCoord];
public IPowerContext this[string powerName]
@ -623,7 +623,7 @@ public class TestCaseBuilder
return this.Builder;
}
public ISeasonContext this[(int turn, int timeline) seasonCoord]
public ISeasonContext this[(string timeline, int turn) seasonCoord]
=> this.SeasonContext[seasonCoord];
public IPowerContext this[string powerName]

View File

@ -14,7 +14,7 @@ class TestCaseBuilderTest
{
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");
setup
@ -49,7 +49,7 @@ class TestCaseBuilderTest
{
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.Orders, Is.Empty, "Expected no orders to be created yet");

View File

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