Compare commits

...

24 Commits

Author SHA1 Message Date
Tim Van Baak 5ad57465d8 Remove reference from Unit.Past 2024-08-14 09:06:05 -07:00
Tim Van Baak 885628900b Remove Location reference from Unit 2024-08-14 09:06:05 -07:00
Tim Van Baak e1772ce60b Refactor away Unit.Province 2024-08-14 09:06:05 -07:00
Tim Van Baak abaa7f7a92 Shift usage of Unit.Location to Unit.LocationId
This is in preparation for removing province and location references from Unit
2024-08-14 09:06:04 -07:00
Tim Van Baak 442015b942 Always name locations 2024-08-14 09:06:04 -07:00
Tim Van Baak 228ad53cca Enable basic World serialization 2024-08-14 09:06:04 -07:00
Tim Van Baak f1563b8f5f Delete Season.Coord 2024-08-14 09:06:04 -07:00
Tim Van Baak 345d54f960 Refactor timelines and season creation logic into World 2024-08-14 09:06:04 -07:00
Tim Van Baak 58f877425a Add more JsonIgnores 2024-08-14 09:06:04 -07:00
Tim Van Baak 2f4c8b2a38 Store order history by timeline designation instead of reference 2024-08-14 09:06:04 -07:00
Tim Van Baak ef4e130dbb Add a serialization round trip test
This currently fails because a lot of World still works on references instead of lookups
2024-08-14 09:06:04 -07:00
Tim Van Baak 9606307e12 Update Season ctor 2024-08-14 09:06:04 -07:00
Tim Van Baak 87685ec744 Refactor season futures into World 2024-08-14 09:06:04 -07:00
Tim Van Baak 752a898123 Use a simpler override where available 2024-08-14 09:06:04 -07:00
Tim Van Baak 400263ea0b Rename PastId back to Past 2024-08-14 09:06:04 -07:00
Tim Van Baak 5e5483367d Remove Season.Past so all lookups go through World 2024-08-14 09:06:04 -07:00
Tim Van Baak 81c9aa4859 Move more timeline logic from Season to World 2024-08-14 09:06:04 -07:00
Tim Van Baak fca8b77a21 Move GetAdjacentSeasons to PathFinder 2024-08-14 09:06:04 -07:00
Tim Van Baak b756959b0a Replace most uses of Season creators to World 2024-08-14 09:06:04 -07:00
Tim Van Baak b887e01334 Eliminate RootSeason field 2024-08-14 09:06:04 -07:00
Tim Van Baak 421e84b559 Update timeline designator usage
Timelines are now identified by strings and come first in timeline-turn tuples.
2024-08-14 09:06:04 -07:00
Tim Van Baak 780ae8b948 Refactor timeline factory to generate string ids
The strings are immediately shimmed back to ints for now
2024-08-14 09:06:03 -07:00
Tim Van Baak 40254b0fca Add Makefile 2024-08-14 09:04:56 -07:00
Tim Van Baak bd8e0da6b6 Refactor province and power information into Map 2024-08-14 09:04:56 -07:00
29 changed files with 1399 additions and 1109 deletions

View File

@ -236,17 +236,13 @@
<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>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>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>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>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 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>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> <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 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 season of a unit supported to hold is not specified, the season is the same season as the supporting unit.</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 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>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>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>
@ -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> <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 0:0 Germany:
A Munich hold A a-Munich hold
Germany 1:0 ---
A Munich - Tyrolia 0:0
Germany:
A a-Munich - a-Tyrolia@0
</pre> </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"> <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>
@ -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> <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 0:0 Austria:
A Tyrolia hold A Tyrolia hold
Germany 0:0 Germany:
A Munich - Tyrolia A Munich - Tyrolia
Austria 1:0 ---
Austria:
A Tyrolia hold A Tyrolia hold
Germany 1:0 Germany:
A Munich supports A Munich 0:0 - Tyrolia 0:0 A Munich supports A a-Munich@0 - Tyrolia
</pre> </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"> <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,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> <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 0:0 Austria:
A Tyrolia hold A Tyrolia hold
Germany 0:0 Germany:
A Munich hold A Munich hold
Austria 1:0 ---
Austria:
A Tyrolia hold A Tyrolia hold
Germany 1:0 Germany:
A Munich - Tyrolia 0:0 A Munich - a-Tyrolia@0
</pre> </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"> <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>
@ -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> <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 0:0 Germany:
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 Munich 0:0 - Tyrolia A Bohemia supports A a-Munich@0 - Tyrolia
</pre> </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"> <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>
@ -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> <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 Tyrolia 2:0 hold A a-Tyrolia hold
A Tyrolia 1:1 hold A b-Tyrolia hold
Germany Germany
A Munich 2:0 - Tyrolia A a-Munich - Tyrolia
A Munich 1:1 supports A Munich 2:0 - Tyrolia A b-Munich supports A a-Munich - Tyrolia
</pre> </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"> <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>
@ -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> <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 Munich 2:0 - Tyrolia A a-Tyrolia holds
A Munich 1:1 supports A Munich 2:0 - Tyrolia A b-Munich holds
Austria Austria
A Tyrolia 2:0 holds A b-Tyrolia@2 - b-Munich@1
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 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

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

View File

@ -50,7 +50,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
// Invalidate any order given to a unit in the past. // Invalidate any order given to a unit in the past.
AdjudicatorHelpers.InvalidateIfNotMatching( AdjudicatorHelpers.InvalidateIfNotMatching(
order => !order.Unit.Season.Futures.Any(), order => !world.GetFutures(order.Unit.Season).Any(),
ValidationReason.IneligibleForOrder, ValidationReason.IneligibleForOrder,
ref unitOrders, ref unitOrders,
ref validationResults); ref validationResults);
@ -77,7 +77,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
// Trivial check: a unit cannot move to where it already is. // Trivial check: a unit cannot move to where it already is.
AdjudicatorHelpers.InvalidateIfNotMatching( AdjudicatorHelpers.InvalidateIfNotMatching(
order => !(order.Location == order.Unit.Location && order.Season == order.Unit.Season), order => !(order.Location.Designation == order.Unit.Location && order.Season == order.Unit.Season),
ValidationReason.DestinationMatchesOrigin, ValidationReason.DestinationMatchesOrigin,
ref moveOrders, ref moveOrders,
ref validationResults); ref validationResults);
@ -90,11 +90,11 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
ILookup<bool, MoveOrder> moveOrdersByAdjacency = moveOrders ILookup<bool, MoveOrder> moveOrdersByAdjacency = moveOrders
.ToLookup(order => .ToLookup(order =>
// Map adjacency // Map adjacency
order.Unit.Location.Adjacents.Contains(order.Location) world.Map.GetLocation(order.Unit).Adjacents.Contains(order.Location)
// Turn adjacency // Turn adjacency
&& Math.Abs(order.Unit.Season.Turn - order.Season.Turn) <= 1 && Math.Abs(order.Unit.Season.Turn - order.Season.Turn) <= 1
// Timeline adjacency // Timeline adjacency
&& order.Unit.Season.InAdjacentTimeline(order.Season)); && world.InAdjacentTimeline(order.Unit.Season, 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();
@ -138,7 +138,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
// Trivial check: cannot convoy a unit to its own location // Trivial check: cannot convoy a unit to its own location
AdjudicatorHelpers.InvalidateIfNotMatching( AdjudicatorHelpers.InvalidateIfNotMatching(
order => !( order => !(
order.Location == order.Target.Location order.Location.Designation == order.Target.Location
&& order.Season == order.Target.Season), && order.Season == order.Target.Season),
ValidationReason.DestinationMatchesOrigin, ValidationReason.DestinationMatchesOrigin,
ref convoyOrders, ref convoyOrders,
@ -175,12 +175,12 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
AdjudicatorHelpers.InvalidateIfNotMatching( AdjudicatorHelpers.InvalidateIfNotMatching(
order => order =>
// Map adjacency with respect to province // Map adjacency with respect to province
order.Unit.Location.Adjacents.Any( world.Map.GetLocation(order.Unit).Adjacents.Any(
adjLocation => adjLocation.Province == order.Target.Province) adjLocation => adjLocation.Province == world.Map.GetLocation(order.Target).Province)
// Turn adjacency // Turn adjacency
&& Math.Abs(order.Unit.Season.Turn - order.Target.Season.Turn) <= 1 && Math.Abs(order.Unit.Season.Turn - order.Target.Season.Turn) <= 1
// Timeline adjacency // Timeline adjacency
&& order.Unit.Season.InAdjacentTimeline(order.Target.Season), && world.InAdjacentTimeline(order.Unit.Season, order.Target.Season),
ValidationReason.UnreachableSupport, ValidationReason.UnreachableSupport,
ref supportHoldOrders, ref supportHoldOrders,
ref validationResults); ref validationResults);
@ -195,7 +195,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
// Support-move orders are invalid if the unit supports a move to any location in its own // Support-move orders are invalid if the unit supports a move to any location in its own
// province. // province.
AdjudicatorHelpers.InvalidateIfNotMatching( AdjudicatorHelpers.InvalidateIfNotMatching(
order => order.Unit.Province != order.Province, order => world.Map.GetLocation(order.Unit).Province != order.Province,
ValidationReason.NoSupportMoveAgainstSelf, ValidationReason.NoSupportMoveAgainstSelf,
ref supportMoveOrders, ref supportMoveOrders,
ref validationResults); ref validationResults);
@ -207,12 +207,12 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
AdjudicatorHelpers.InvalidateIfNotMatching( AdjudicatorHelpers.InvalidateIfNotMatching(
order => order =>
// Map adjacency with respect to province // Map adjacency with respect to province
order.Unit.Location.Adjacents.Any( world.Map.GetLocation(order.Unit).Adjacents.Any(
adjLocation => adjLocation.Province == order.Province) adjLocation => adjLocation.Province == order.Province)
// Turn adjacency // Turn adjacency
&& Math.Abs(order.Unit.Season.Turn - order.Season.Turn) <= 1 && Math.Abs(order.Unit.Season.Turn - order.Season.Turn) <= 1
// Timeline adjacency // Timeline adjacency
&& order.Unit.Season.InAdjacentTimeline(order.Season), && world.InAdjacentTimeline(order.Unit.Season, order.Season),
ValidationReason.UnreachableSupport, ValidationReason.UnreachableSupport,
ref supportMoveOrders, ref supportMoveOrders,
ref validationResults); ref validationResults);
@ -255,7 +255,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
// Finally, add implicit hold orders for units without legal orders. // Finally, add implicit hold orders for units without legal orders.
List<Unit> allOrderableUnits = world.Units List<Unit> allOrderableUnits = world.Units
.Where(unit => !unit.Season.Futures.Any()) .Where(unit => !world.GetFutures(unit.Season).Any())
.ToList(); .ToList();
HashSet<Unit> orderedUnits = validOrders.Select(order => order.Unit).ToHashSet(); HashSet<Unit> orderedUnits = validOrders.Select(order => order.Unit).ToHashSet();
List<Unit> unorderedUnits = allOrderableUnits List<Unit> unorderedUnits = allOrderableUnits
@ -312,9 +312,9 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
// All moves to a particular season in a single phase result in the same future. Keep a // All moves to a particular season in a single phase result in the same future. Keep a
// record of when a future season has been created. // record of when a future season has been created.
Dictionary<Season, Season> createdFutures = new(); Dictionary<Season, Season> createdFutures = [];
List<Unit> createdUnits = new(); List<Unit> createdUnits = [];
List<RetreatingUnit> retreats = new(); List<RetreatingUnit> retreats = [];
// Populate createdFutures with the timeline fork decisions // Populate createdFutures with the timeline fork decisions
logger.Log(1, "Processing AdvanceTimeline decisions"); logger.Log(1, "Processing AdvanceTimeline decisions");
@ -324,9 +324,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
if (advanceTimeline.Outcome == true) if (advanceTimeline.Outcome == true)
{ {
// A timeline that doesn't have a future yet simply continues. Otherwise, it forks. // A timeline that doesn't have a future yet simply continues. Otherwise, it forks.
createdFutures[advanceTimeline.Season] = !advanceTimeline.Season.Futures.Any() createdFutures[advanceTimeline.Season] = world.ContinueOrFork(advanceTimeline.Season);
? advanceTimeline.Season.MakeNext()
: advanceTimeline.Season.MakeFork();
} }
} }
@ -339,7 +337,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
Season moveSeason = doesMove.Order.Season; Season moveSeason = doesMove.Order.Season;
if (doesMove.Outcome == true && createdFutures.ContainsKey(moveSeason)) if (doesMove.Outcome == true && createdFutures.ContainsKey(moveSeason))
{ {
Unit next = doesMove.Order.Unit.Next(doesMove.Order.Location, createdFutures[moveSeason]); Unit next = doesMove.Order.Unit.Next(doesMove.Order.Location.Designation, createdFutures[moveSeason]);
logger.Log(3, "Advancing unit to {0}", next); logger.Log(3, "Advancing unit to {0}", next);
createdUnits.Add(next); createdUnits.Add(next);
} }
@ -368,7 +366,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
if (isDislodged.Outcome == false) if (isDislodged.Outcome == false)
{ {
// Non-dislodged units continue into the future. // Non-dislodged units continue into the future.
Unit next = order.Unit.Next(order.Unit.Location, future); Unit next = order.Unit.Next(world.Map.GetLocation(order.Unit).Designation, future);
logger.Log(3, "Advancing unit to {0}", next); logger.Log(3, "Advancing unit to {0}", next);
createdUnits.Add(next); createdUnits.Add(next);
} }
@ -377,7 +375,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
// Create a retreat for each dislodged unit. // Create a retreat for each dislodged unit.
// TODO check valid retreats and disbands // TODO check valid retreats and disbands
logger.Log(3, "Creating retreat for {0}", order.Unit); logger.Log(3, "Creating retreat for {0}", order.Unit);
var validRetreats = order.Unit.Location.Adjacents var validRetreats = world.Map.GetLocation(order.Unit).Adjacents
.Select(loc => (future, loc)) .Select(loc => (future, loc))
.ToList(); .ToList();
RetreatingUnit retreat = new(order.Unit, validRetreats); RetreatingUnit retreat = new(order.Unit, validRetreats);
@ -386,11 +384,11 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
} }
// Record the adjudication results to the season's order history // Record the adjudication results to the season's order history
Dictionary<Season, OrderHistory> newHistory = new(); Dictionary<string, OrderHistory> newHistory = [];
foreach (UnitOrder unitOrder in decisions.OfType<IsDislodged>().Select(d => d.Order)) foreach (UnitOrder unitOrder in decisions.OfType<IsDislodged>().Select(d => d.Order))
{ {
newHistory.Ensure(unitOrder.Unit.Season, () => new()); newHistory.Ensure(unitOrder.Unit.Season.Designation, () => new());
OrderHistory history = newHistory[unitOrder.Unit.Season]; OrderHistory history = newHistory[unitOrder.Unit.Season.Designation];
// TODO does this add every order to every season?? // TODO does this add every order to every season??
history.Orders.Add(unitOrder); history.Orders.Add(unitOrder);
history.IsDislodgedOutcomes[unitOrder.Unit] = dislodges[unitOrder.Unit].Outcome == true; history.IsDislodgedOutcomes[unitOrder.Unit] = dislodges[unitOrder.Unit].Outcome == true;
@ -401,7 +399,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
} }
// Log the new order history // Log the new order history
foreach ((Season season, OrderHistory history) in newHistory) foreach ((string season, OrderHistory history) in newHistory)
{ {
string verb = world.OrderHistory.ContainsKey(season) ? "Updating" : "Adding"; string verb = world.OrderHistory.ContainsKey(season) ? "Updating" : "Adding";
logger.Log(1, "{0} history for {1}", verb, season); logger.Log(1, "{0} history for {1}", verb, season);
@ -411,7 +409,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
} }
} }
IEnumerable<KeyValuePair<Season, OrderHistory>> updatedHistory = world.OrderHistory IEnumerable<KeyValuePair<string, OrderHistory>> updatedHistory = world.OrderHistory
.Where(kvp => !newHistory.ContainsKey(kvp.Key)) .Where(kvp => !newHistory.ContainsKey(kvp.Key))
.Concat(newHistory); .Concat(newHistory);
@ -486,7 +484,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
bool progress = false; bool progress = false;
// A season at the head of a timeline always advances. // A season at the head of a timeline always advances.
if (!decision.Season.Futures.Any()) if (!world.GetFutures(decision.Season).Any())
{ {
progress |= LoggedUpdate(decision, true, depth, "A timeline head always advances"); progress |= LoggedUpdate(decision, true, depth, "A timeline head always advances");
return progress; return progress;
@ -496,7 +494,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
IEnumerable<MoveOrder> newIncomingMoves = decision.Orders IEnumerable<MoveOrder> newIncomingMoves = decision.Orders
.OfType<MoveOrder>() .OfType<MoveOrder>()
.Where(order => order.Season == decision.Season .Where(order => order.Season == decision.Season
&& !world.OrderHistory[order.Season].DoesMoveOutcomes.ContainsKey(order)); && !world.OrderHistory[order.Season.Designation].DoesMoveOutcomes.ContainsKey(order));
foreach (MoveOrder moveOrder in newIncomingMoves) foreach (MoveOrder moveOrder in newIncomingMoves)
{ {
DoesMove doesMove = decisions.DoesMove[moveOrder]; DoesMove doesMove = decisions.DoesMove[moveOrder];
@ -513,7 +511,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
// 1. The outcome of a dislodge decision is changed, // 1. The outcome of a dislodge decision is changed,
// 2. The outcome of an intra-timeline move decision is changed, or // 2. The outcome of an intra-timeline move decision is changed, or
// 3. The outcome of an inter-timeline move decision with that season as the destination is changed. // 3. The outcome of an inter-timeline move decision with that season as the destination is changed.
OrderHistory history = world.OrderHistory[decision.Season]; OrderHistory history = world.OrderHistory[decision.Season.Designation];
bool anyUnresolved = false; bool anyUnresolved = false;
foreach (UnitOrder order in decision.Orders) foreach (UnitOrder order in decision.Orders)
{ {
@ -635,11 +633,11 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
// If the origin and destination are adjacent, then there is a path. // If the origin and destination are adjacent, then there is a path.
if (// Map adjacency if (// Map adjacency
decision.Order.Unit.Location.Adjacents.Contains(decision.Order.Location) world.Map.GetLocation(decision.Order.Unit).Adjacents.Contains(decision.Order.Location)
// Turn adjacency // Turn adjacency
&& Math.Abs(decision.Order.Unit.Season.Turn - decision.Order.Season.Turn) <= 1 && Math.Abs(decision.Order.Unit.Season.Turn - decision.Order.Season.Turn) <= 1
// Timeline adjacency // Timeline adjacency
&& 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"); bool update = LoggedUpdate(decision, true, depth, "Adjacent move");
return progress | update; return progress | update;

View File

@ -30,13 +30,13 @@ public static class PathFinder
// also have coasts, and between those coasts there is a path of adjacent sea provinces // also have coasts, and between those coasts there is a path of adjacent sea provinces
// (not coastal) that are occupied by fleets. The move order is valid even if the fleets // (not coastal) that are occupied by fleets. The move order is valid even if the fleets
// belong to another power or were not given convoy orders; it will simply fail. // belong to another power or were not given convoy orders; it will simply fail.
IDictionary<(Location location, Season season), Unit> fleets = world.Units IDictionary<(string location, Season season), Unit> fleets = world.Units
.Where(unit => unit.Type == UnitType.Fleet) .Where(unit => unit.Type == UnitType.Fleet)
.ToDictionary(unit => (unit.Location, unit.Season)); .ToDictionary(unit => (unit.Location, unit.Season));
// Verify that the origin is a coastal province. // Verify that the origin is a coastal province.
if (movingUnit.Location.Type != LocationType.Land) return false; if (world.Map.GetLocation(movingUnit).Type != LocationType.Land) return false;
IEnumerable<Location> originCoasts = movingUnit.Province.Locations IEnumerable<Location> originCoasts = world.Map.GetLocation(movingUnit).Province.Locations
.Where(location => location.Type == LocationType.Water); .Where(location => location.Type == LocationType.Water);
if (!originCoasts.Any()) return false; if (!originCoasts.Any()) return false;
@ -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(currentLocation, currentSeason); var adjacents = GetAdjacentPoints(world, 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.
@ -69,7 +69,7 @@ public static class PathFinder
// If not, add this location to the to-visit set if it isn't a coast, has a fleet, // If not, add this location to the to-visit set if it isn't a coast, has a fleet,
// and hasn't already been visited. // and hasn't already been visited.
if (!adjLocation.Province.Locations.Any(l => l.Type == LocationType.Land) if (!adjLocation.Province.Locations.Any(l => l.Type == LocationType.Land)
&& fleets.ContainsKey((adjLocation, adjSeason)) && fleets.ContainsKey((adjLocation.Designation, adjSeason))
&& !visited.Contains((adjLocation, adjSeason))) && !visited.Contains((adjLocation, adjSeason)))
{ {
toVisit.Enqueue((adjLocation, adjSeason)); toVisit.Enqueue((adjLocation, adjSeason));
@ -81,11 +81,11 @@ public static class PathFinder
return false; 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<Location> adjacentLocations = location.Adjacents.ToList();
List<Season> adjacentSeasons = season.GetAdjacentSeasons().ToList(); List<Season> adjacentSeasons = GetAdjacentSeasons(world, season).ToList();
foreach (Location adjacentLocation in adjacentLocations) foreach (Location adjacentLocation in adjacentLocations)
{ {
@ -105,4 +105,56 @@ 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(world.GetFutures(season));
// 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(
world.GetFutures(current).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 = world
.GetFutures(past)
.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 = world.GetFutures(branchSeason)
.FirstOrDefault(s => s!.Timeline == branchSeason.Timeline, null))
{
if (branchSeason.Turn >= season.Turn - 1) adjacents.Add(branchSeason);
}
}
return adjacents;
}
} }

View File

@ -1,3 +1,5 @@
using System.Text.Json.Serialization;
namespace MultiversalDiplomacy.Model; namespace MultiversalDiplomacy.Model;
/// <summary> /// <summary>
@ -9,17 +11,21 @@ public class Location
{ {
/// <summary> /// <summary>
/// The province to which this location belongs. /// The province to which this location belongs.
/// </summary>
[JsonIgnore]
public Province Province { get; } public Province Province { get; }
public string ProvinceName => Province.Name;
/// <summary> /// <summary>
/// The location's full human-readable name. /// The location's full human-readable name.
/// </summary> /// </summary>
public string? Name { get; } public string Name { get; }
/// <summary> /// <summary>
/// The location's shorthand abbreviation. /// The location's shorthand abbreviation.
/// </summary> /// </summary>
public string? Abbreviation { get; } public string Abbreviation { get; }
/// <summary> /// <summary>
/// The location's type. /// The location's type.
@ -29,10 +35,16 @@ public class Location
/// <summary> /// <summary>
/// The locations that border this location. /// The locations that border this location.
/// </summary> /// </summary>
[JsonIgnore]
public IEnumerable<Location> Adjacents => this.AdjacentList; public IEnumerable<Location> Adjacents => this.AdjacentList;
private List<Location> AdjacentList { get; set; } private List<Location> AdjacentList { get; set; }
public Location(Province province, string? name, string? abbreviation, LocationType type) /// <summary>
/// The unique name of this location in the map.
/// </summary>
public string Designation => $"{this.ProvinceName}/{this.Abbreviation}";
public Location(Province province, string name, string abbreviation, LocationType type)
{ {
this.Province = province; this.Province = province;
this.Name = name; this.Name = name;
@ -43,7 +55,7 @@ public class Location
public override string ToString() public override string ToString()
{ {
return this.Name == null return this.Name == "land" || this.Name == "water"
? $"{this.Province.Name} ({this.Type})" ? $"{this.Province.Name} ({this.Type})"
: $"{this.Province.Name} ({this.Type}:{this.Name}]"; : $"{this.Province.Name} ({this.Type}:{this.Name}]";
} }

View File

@ -0,0 +1,573 @@
namespace MultiversalDiplomacy.Model;
/// <summary>
/// Encapsulation of the world map and playable powers constituting a Diplomacy variant.
/// </summary>
public class Map
{
/// <summary>
/// The map type.
/// </summary>
public MapType Type { get; }
/// <summary>
/// The game map.
/// </summary>
public IReadOnlyCollection<Province> Provinces => _Provinces.AsReadOnly();
private List<Province> _Provinces { get; }
private Dictionary<string, Location> LocationLookup { get; }
/// <summary>
/// The game powers.
/// </summary>
public IReadOnlyCollection<Power> Powers => _Powers.AsReadOnly();
private List<Power> _Powers { get; }
private Map(MapType type, IEnumerable<Province> provinces, IEnumerable<Power> powers)
{
Type = type;
_Provinces = provinces.ToList();
_Powers = powers.ToList();
LocationLookup = Provinces
.SelectMany(province => province.Locations)
.ToDictionary(location => location.Designation);
}
/// <summary>
/// Get a province by name. Throws if the province is not found.
/// </summary>
public Province GetProvince(string provinceName)
=> GetProvince(provinceName, this.Provinces);
/// <summary>
/// Get a province by name. Throws if the province is not found.
/// </summary>
private static Province GetProvince(string provinceName, IEnumerable<Province> provinces)
=> provinces.SingleOrDefault(
p => p!.Name.Equals(provinceName, StringComparison.InvariantCultureIgnoreCase)
|| p.Abbreviations.Any(
a => a.Equals(provinceName, StringComparison.InvariantCultureIgnoreCase)),
null)
?? throw new KeyNotFoundException($"Province {provinceName} not found");
/// <summary>
/// Get the location in a province matching a predicate. Throws if there is not exactly one
/// such location.
/// </summary>
private Location GetLocation(string provinceName, Func<Location, bool> predicate)
=> GetProvince(provinceName).Locations.SingleOrDefault(
l => l != null && predicate(l), null)
?? throw new KeyNotFoundException($"No such location in {provinceName}");
public Location GetLocation(string designation)
=> LocationLookup[designation];
public Location GetLocation(Unit unit)
=> GetLocation(unit.Location);
/// <summary>
/// Get the sole land location of a province.
/// </summary>
public Location GetLand(string provinceName)
=> GetLocation(provinceName, l => l.Type == LocationType.Land);
/// <summary>
/// Get the sole water location of a province, optionally specifying a named coast.
/// </summary>
public Location GetWater(string provinceName, string? coastName = null)
=> coastName == null
? GetLocation(provinceName, l => l.Type == LocationType.Water)
: GetLocation(provinceName, l => l.Name == coastName || l.Abbreviation == coastName);
/// <summary>
/// Get a power by name. Throws if there is not exactly one such power.
/// </summary>
public Power GetPower(string powerName)
=> Powers.SingleOrDefault(p => p!.Name == powerName || p.Name.StartsWith(powerName), null)
?? throw new KeyNotFoundException($"Power {powerName} not found");
public static Map FromType(MapType type)
=> type switch {
MapType.Test => Test,
MapType.Classical => Test,
_ => throw new NotImplementedException($"Unknown variant {type}"),
};
public static Map Test => _Test.Value;
private static readonly Lazy<Map> _Test = new(() => {
Province lef = Province.Time("Left", "Lef")
.AddLandLocation();
Province cen = Province.Empty("Center", "Cen")
.AddLandLocation();
Province rig = Province.Time("Right", "Rig")
.AddLandLocation();
Location center = cen.Locations.First();
center.AddBorder(lef.Locations.First());
center.AddBorder(rig.Locations.First());
Power a = new("Alpha");
Power b = new("Beta");
return new(MapType.Test, [lef, cen, rig], [a, b]);
});
public static Map Classical => _Classical.Value;
private static readonly Lazy<Map> _Classical = new(() => {
// Define the provinces of the standard world map.
List<Province> provinces =
[
#region Provinces
Province.Empty("North Africa", "NAF")
.AddLandLocation()
.AddOceanLocation(),
Province.Supply("Tunis", "TUN")
.AddLandLocation()
.AddOceanLocation(),
Province.Empty("Bohemia", "BOH")
.AddLandLocation(),
Province.Supply("Budapest", "BUD")
.AddLandLocation(),
Province.Empty("Galacia", "GAL")
.AddLandLocation(),
Province.Supply("Trieste", "TRI")
.AddLandLocation()
.AddOceanLocation(),
Province.Empty("Tyrolia", "TYR")
.AddLandLocation(),
Province.Time("Vienna", "VIE")
.AddLandLocation(),
Province.Empty("Albania", "ALB")
.AddLandLocation()
.AddOceanLocation(),
Province.Supply("Bulgaria", "BUL")
.AddLandLocation()
.AddCoastLocation("east coast", "ec")
.AddCoastLocation("south coast", "sc"),
Province.Supply("Greece", "GRE")
.AddLandLocation()
.AddOceanLocation(),
Province.Supply("Rumania", "RUM", "RMA")
.AddLandLocation()
.AddOceanLocation(),
Province.Supply("Serbia", "SER")
.AddLandLocation(),
Province.Empty("Clyde", "CLY")
.AddLandLocation()
.AddOceanLocation(),
Province.Supply("Edinburgh", "EDI")
.AddLandLocation()
.AddOceanLocation(),
Province.Supply("Liverpool", "LVP", "LPL")
.AddLandLocation()
.AddOceanLocation(),
Province.Time("London", "LON")
.AddLandLocation()
.AddOceanLocation(),
Province.Empty("Wales", "WAL")
.AddLandLocation()
.AddOceanLocation(),
Province.Empty("Yorkshire", "YOR")
.AddLandLocation()
.AddOceanLocation(),
Province.Supply("Brest", "BRE")
.AddLandLocation()
.AddOceanLocation(),
Province.Empty("Burgundy", "BUR")
.AddLandLocation(),
Province.Empty("Gascony", "GAS")
.AddLandLocation()
.AddOceanLocation(),
Province.Supply("Marseilles", "MAR")
.AddLandLocation()
.AddOceanLocation(),
Province.Time("Paris", "PAR")
.AddLandLocation(),
Province.Empty("Picardy", "PIC")
.AddLandLocation()
.AddOceanLocation(),
Province.Time("Berlin", "BER")
.AddLandLocation()
.AddOceanLocation(),
Province.Supply("Kiel", "KIE")
.AddLandLocation()
.AddOceanLocation(),
Province.Supply("Munich", "MUN")
.AddLandLocation(),
Province.Empty("Prussia", "PRU")
.AddLandLocation()
.AddOceanLocation(),
Province.Empty("Ruhr", "RUH", "RHR")
.AddLandLocation(),
Province.Empty("Silesia", "SIL")
.AddLandLocation(),
Province.Supply("Spain", "SPA")
.AddLandLocation()
.AddCoastLocation("north coast", "nc")
.AddCoastLocation("south coast", "sc"),
Province.Supply("Portugal", "POR")
.AddLandLocation()
.AddOceanLocation(),
Province.Empty("Apulia", "APU")
.AddLandLocation()
.AddOceanLocation(),
Province.Supply("Naples", "NAP")
.AddLandLocation()
.AddOceanLocation(),
Province.Empty("Piedmont", "PIE")
.AddLandLocation()
.AddOceanLocation(),
Province.Time("Rome", "ROM", "RME")
.AddLandLocation()
.AddOceanLocation(),
Province.Empty("Tuscany", "TUS")
.AddLandLocation()
.AddOceanLocation(),
Province.Supply("Venice", "VEN")
.AddLandLocation()
.AddOceanLocation(),
Province.Supply("Belgium", "BEL")
.AddLandLocation()
.AddOceanLocation(),
Province.Supply("Holland", "HOL")
.AddLandLocation()
.AddOceanLocation(),
Province.Empty("Finland", "FIN")
.AddLandLocation()
.AddOceanLocation(),
Province.Empty("Livonia", "LVN", "LVA")
.AddLandLocation()
.AddOceanLocation(),
Province.Time("Moscow", "MOS")
.AddLandLocation()
.AddOceanLocation(),
Province.Supply("Sevastopol", "SEV")
.AddLandLocation()
.AddOceanLocation(),
Province.Supply("Saint Petersburg", "STP")
.AddLandLocation()
.AddCoastLocation("north coast", "nc")
.AddCoastLocation("west coast", "wc"),
Province.Empty("Ukraine", "UKR")
.AddLandLocation(),
Province.Supply("Warsaw", "WAR")
.AddLandLocation(),
Province.Supply("Denmark", "DEN")
.AddLandLocation()
.AddOceanLocation(),
Province.Supply("Norway", "NWY")
.AddLandLocation()
.AddOceanLocation(),
Province.Supply("Sweden", "SWE")
.AddLandLocation()
.AddOceanLocation(),
Province.Supply("Ankara", "ANK")
.AddLandLocation()
.AddOceanLocation(),
Province.Empty("Armenia", "ARM")
.AddLandLocation()
.AddOceanLocation(),
Province.Time("Constantinople", "CON")
.AddLandLocation()
.AddOceanLocation(),
Province.Supply("Smyrna", "SMY")
.AddLandLocation()
.AddOceanLocation(),
Province.Empty("Syria", "SYR")
.AddLandLocation()
.AddOceanLocation(),
Province.Empty("Barents Sea", "BAR")
.AddOceanLocation(),
Province.Empty("English Channel", "ENC", "ECH")
.AddOceanLocation(),
Province.Empty("Heligoland Bight", "HEL", "HGB")
.AddOceanLocation(),
Province.Empty("Irish Sea", "IRS", "IRI")
.AddOceanLocation(),
Province.Empty("Mid-Atlantic Ocean", "MAO", "MID")
.AddOceanLocation(),
Province.Empty("North Atlantic Ocean", "NAO", "NAT")
.AddOceanLocation(),
Province.Empty("North Sea", "NTH", "NTS")
.AddOceanLocation(),
Province.Empty("Norwegian Sea", "NWS", "NWG")
.AddOceanLocation(),
Province.Empty("Skagerrak", "SKA", "SKG")
.AddOceanLocation(),
Province.Empty("Baltic Sea", "BAL")
.AddOceanLocation(),
Province.Empty("Guld of Bothnia", "GOB", "BOT")
.AddOceanLocation(),
Province.Empty("Adriatic Sea", "ADS", "ADR")
.AddOceanLocation(),
Province.Empty("Aegean Sea", "AEG")
.AddOceanLocation(),
Province.Empty("Black Sea", "BLA")
.AddOceanLocation(),
Province.Empty("Eastern Mediterranean Sea", "EMS", "EAS")
.AddOceanLocation(),
Province.Empty("Gulf of Lyons", "GOL", "LYO")
.AddOceanLocation(),
Province.Empty("Ionian Sea", "IOS", "ION", "INS")
.AddOceanLocation(),
Province.Empty("Tyrrhenian Sea", "TYS", "TYN")
.AddOceanLocation(),
Province.Empty("Western Mediterranean Sea", "WMS", "WES")
.AddOceanLocation(),
#endregion
];
// Declare some helpers for border definitions
Location Land(string provinceName) => GetProvince(provinceName, provinces)
.Locations.Single(l => l.Type == LocationType.Land);
Location Water(string provinceName) => GetProvince(provinceName, provinces)
.Locations.Single(l => l.Type == LocationType.Water);
Location Coast(string provinceName, string coastName)
=> GetProvince(provinceName, provinces)
.Locations.Single(l => l.Name == coastName || l.Abbreviation == coastName);
static void AddBordersTo(Location location, Func<string, Location> LocationType, params string[] borders)
{
foreach (string bordering in borders)
{
location.AddBorder(LocationType(bordering));
}
}
void AddBorders(string provinceName, Func<string, Location> LocationType, params string[] borders)
=> AddBordersTo(LocationType(provinceName), LocationType, borders);
#region Borders
AddBorders("NAF", Land, "TUN");
AddBorders("NAF", Water, "MAO", "WES", "TUN");
AddBorders("TUN", Land, "NAF");
AddBorders("TUN", Water, "NAF", "WES", "TYS", "ION");
AddBorders("BOH", Land, "MUN", "SIL", "GAL", "VIE", "TYR");
AddBorders("BUD", Land, "VIE", "GAL", "RUM", "SER", "TRI");
AddBorders("GAL", Land, "BOH", "SIL", "WAR", "UKR", "RUM", "BUD", "VIE");
AddBorders("TRI", Land, "TYR", "VIE", "BUD", "SER", "ALB");
AddBorders("TRI", Water, "ALB", "ADR", "VEN");
AddBorders("TYR", Land, "MUN", "BOH", "VIE", "TRI", "VEN", "PIE");
AddBorders("VIE", Land, "TYR", "BOH", "GAL", "BUD", "TRI");
AddBorders("ALB", Land, "TRI", "SER", "GRE");
AddBorders("ALB", Water, "TRI", "ADR", "ION", "GRE");
AddBorders("BUL", Land, "GRE", "SER", "RUM", "CON");
AddBordersTo(Coast("BUL", "ec"), Water, "BLA", "CON");
AddBordersTo(Coast("BUL", "sc"), Water, "CON", "AEG", "GRE");
AddBorders("GRE", Land, "ALB", "SER", "BUL");
AddBorders("GRE", Water, "ALB", "ION", "AEG");
Water("GRE").AddBorder(Coast("BUL", "sc"));
AddBorders("RUM", Land, "BUL", "SER", "BUD", "GAL", "UKR", "SEV");
AddBorders("RUM", Water, "SEV", "BLA");
Water("RUM").AddBorder(Coast("BUL", "ec"));
AddBorders("SER", Land, "BUD", "RUM", "BUL", "GRE", "ALB", "TRI");
AddBorders("CLY", Land, "EDI", "LVP");
AddBorders("CLY", Water, "LVP", "NAO", "NWG", "EDI");
AddBorders("EDI", Land, "YOR", "LVP", "CLY");
AddBorders("EDI", Water, "CLY", "NWG", "NTH", "YOR");
AddBorders("LVP", Land, "CLY", "EDI", "YOR", "WAL");
AddBorders("LVP", Water, "WAL", "IRS", "NAO", "CLY");
AddBorders("LON", Land, "WAL", "YOR");
AddBorders("LON", Water, "WAL", "ENC", "NTH", "YOR");
AddBorders("WAL", Land, "LVP", "YOR", "LON");
AddBorders("WAL", Water, "LON", "ENC", "IRS", "LVP");
AddBorders("YOR", Land, "LON", "WAL", "LVP", "EDI");
AddBorders("YOR", Water, "EDI", "NTH", "LON");
AddBorders("BRE", Land, "PIC", "PAR", "GAS");
AddBorders("BRE", Water, "GAS", "MAO", "ENC", "PIC");
AddBorders("BUR", Land, "BEL", "RUH", "MUN", "MAR", "GAS", "PAR", "PIC");
AddBorders("GAS", Land, "BRE", "PAR", "BUR", "MAR", "SPA");
AddBorders("GAS", Water, "MAO", "BRE");
Water("GAS").AddBorder(Coast("SPA", "nc"));
AddBorders("MAR", Land, "SPA", "GAS", "BUR", "PIE");
AddBorders("MAR", Water, "LYO", "PIE");
Water("MAR").AddBorder(Coast("SPA", "sc"));
AddBorders("PAR", Land, "PIC", "BUR", "GAS", "BRE");
AddBorders("PIC", Land, "BEL", "BUR", "PAR", "BRE");
AddBorders("PIC", Water, "BRE", "ENC", "BEL");
AddBorders("BER", Land, "PRU", "SIL", "MUN", "KIE");
AddBorders("BER", Water, "KIE", "BAL", "PRU");
AddBorders("KIE", Land, "BER", "MUN", "RUH", "HOL", "DEN");
AddBorders("KIE", Water, "HOL", "HEL", "DEN", "BAL", "BER");
AddBorders("MUN", Land, "BUR", "RUH", "KIE", "BER", "SIL", "BOH", "TYR");
AddBorders("PRU", Land, "LVN", "WAR", "SIL", "BER");
AddBorders("PRU", Water, "BER", "BAL", "LVN");
AddBorders("RUH", Land, "KIE", "MUN", "BUR", "BEL", "HOL");
AddBorders("SIL", Land, "PRU", "WAR", "GAL", "BOH", "MUN", "BER");
AddBorders("SPA", Land, "POR", "GAS", "MAR");
AddBordersTo(Coast("SPA", "nc"), Water, "POR", "MAO", "GAS");
AddBordersTo(Coast("SPA", "sc"), Water, "POR", "MAO", "WES", "LYO", "MAR");
AddBorders("POR", Land, "SPA");
AddBorders("POR", Water, "MAO");
Water("POR").AddBorder(Coast("SPA", "nc"));
Water("POR").AddBorder(Coast("SPA", "sc"));
AddBorders("APU", Land, "NAP", "ROM", "VEN");
AddBorders("APU", Water, "VEN", "ADR", "IOS", "NAP");
AddBorders("NAP", Land, "ROM", "APU");
AddBorders("NAP", Water, "APU", "IOS", "TYS", "ROM");
AddBorders("PIE", Land, "MAR", "TYR", "VEN", "TUS");
AddBorders("PIE", Water, "TUS", "LYO", "MAR");
AddBorders("ROM", Land, "TUS", "VEN", "APU", "NAP");
AddBorders("ROM", Water, "NAP", "TYS", "TUS");
AddBorders("TUS", Land, "PIE", "VEN", "ROM");
AddBorders("TUS", Water, "ROM", "TYS", "LYO", "PIE");
AddBorders("VEN", Land, "APU", "ROM", "TUS", "PIE", "TYR", "TRI");
AddBorders("VEN", Water, "TRI", "ADR", "APU");
AddBorders("BEL", Land, "HOL", "RUH", "BUR", "PIC");
AddBorders("BEL", Water, "PIC", "ENC", "NTH", "HOL");
AddBorders("HOL", Land, "BEL", "RUH", "KIE");
AddBorders("HOL", Water, "NTH", "HEL");
AddBorders("FIN", Land, "SWE", "NWY", "STP");
AddBorders("FIN", Water, "SWE", "BOT");
Water("FIN").AddBorder(Coast("STP", "wc"));
AddBorders("LVN", Land, "STP", "MOS", "WAR", "PRU");
AddBorders("LVN", Water, "PRU", "BAL", "BOT");
Water("LVN").AddBorder(Coast("STP", "wc"));
AddBorders("MOS", Land, "SEV", "UKR", "WAR", "LVN", "STP");
AddBorders("SEV", Land, "RUM", "UKR", "MOS", "ARM");
AddBorders("SEV", Water, "ARM", "BLA", "RUM");
AddBorders("STP", Land, "MOS", "LVN", "FIN");
AddBordersTo(Coast("STP", "nc"), Water, "BAR", "NWY");
AddBordersTo(Coast("STP", "wc"), Water, "LVN", "BOT", "FIN");
AddBorders("UKR", Land, "MOS", "SEV", "RUM", "GAL", "WAR");
AddBorders("WAR", Land, "PRU", "LVN", "MOS", "UKR", "GAL", "SIL");
AddBorders("DEN", Land, "KIE", "SWE");
AddBorders("DEN", Water, "KIE", "HEL", "NTH", "SKA", "BAL", "SWE");
AddBorders("NWY", Land, "STP", "FIN", "SWE");
AddBorders("NWY", Water, "BAR", "NWG", "NTH", "SKA", "SWE");
Water("NWY").AddBorder(Coast("STP", "nc"));
AddBorders("SWE", Land, "NWY", "FIN", "DEN");
AddBorders("SWE", Water, "FIN", "BOT", "BAL", "DEN", "SKA", "NWY");
AddBorders("ANK", Land, "ARM", "SMY", "CON");
AddBorders("ANK", Water, "CON", "BLA", "ARM");
AddBorders("ARM", Land, "SEV", "SYR", "SMY", "ANK");
AddBorders("ARM", Water, "ANK", "BLA", "SEV");
AddBorders("CON", Land, "BUL", "ANK", "SMY");
AddBorders("CON", Water, "BLA", "ANK", "SMY", "AEG");
Water("CON").AddBorder(Coast("BUL", "ec"));
Water("CON").AddBorder(Coast("BUL", "sc"));
AddBorders("SMY", Land, "CON", "ANK", "ARM", "SYR");
AddBorders("SMY", Water, "SYR", "EAS", "AEG", "CON");
AddBorders("SYR", Land, "SMY", "ARM");
AddBorders("SYR", Water, "EAS", "SMY");
AddBorders("BAR", Water, "NWG", "NWY");
Water("BAR").AddBorder(Coast("STP", "nc"));
AddBorders("ENC", Water, "LON", "NTH", "BEL", "PIC", "BRE", "MAO", "IRS", "WAL");
AddBorders("HEL", Water, "NTH", "DEN", "BAL", "KIE", "HOL");
AddBorders("IRS", Water, "NAO", "LVP", "WAL", "ENC", "MAO");
AddBorders("MAO", Water, "NAO", "IRS", "ENC", "BRE", "GAS", "POR", "NAF");
Water("MAO").AddBorder(Coast("SPA", "nc"));
Water("MAO").AddBorder(Coast("SPA", "sc"));
AddBorders("NAO", Water, "NWG", "CLY", "LVP", "IRS", "MAO");
AddBorders("NTH", Water, "NWG", "NWY", "SKA", "DEN", "HEL", "HOL", "BEL", "ENC", "LON", "YOR", "EDI");
AddBorders("NWG", Water, "BAR", "NWY", "NTH", "EDI", "CLY", "NAO");
AddBorders("SKA", Water, "NWY", "SWE", "BAL", "DEN", "NTH");
AddBorders("BAL", Water, "BOT", "LVN", "PRU", "BER", "KIE", "HEL", "DEN", "SWE");
AddBorders("BOT", Water, "LVN", "BAL", "SWE", "FIN");
Water("BOT").AddBorder(Coast("STP", "wc"));
AddBorders("ADR", Water, "IOS", "APU", "VEN", "TRI", "ALB");
AddBorders("AEG", Water, "CON", "SMY", "EAS", "IOS", "GRE");
Water("AEG").AddBorder(Coast("BUL", "sc"));
AddBorders("BLA", Water, "RUM", "SEV", "ARM", "ANK", "CON");
Water("BLA").AddBorder(Coast("BUL", "ec"));
AddBorders("EAS", Water, "IOS", "AEG", "SMY", "SYR");
AddBorders("LYO", Water, "MAR", "PIE", "TUS", "TYS", "WES");
Water("LYO").AddBorder(Coast("SPA", "sc"));
AddBorders("IOS", Water, "TUN", "TYS", "NAP", "APU", "ADR", "ALB", "GRE", "AEG");
AddBorders("TYS", Water, "LYO", "TUS", "ROM", "NAP", "IOS", "TUN", "WES");
AddBorders("WES", Water, "LYO", "TYS", "TUN", "NAF", "MAO");
Water("WES").AddBorder(Coast("SPA", "sc"));
#endregion
List<Power> powers =
[
new("Austria"),
new("England"),
new("France"),
new("Germany"),
new("Italy"),
new("Russia"),
new("Turkey"),
];
return new(MapType.Classical, provinces, powers);
});
}

View File

@ -0,0 +1,16 @@
using System.Text.Json.Serialization;
namespace MultiversalDiplomacy.Model;
[JsonConverter(typeof(JsonStringEnumConverter<MapType>))]
public enum MapType {
/// <summary>
/// A minimal test map.
/// </summary>
Test,
/// <summary>
/// The standard Diplomacy map.
/// </summary>
Classical,
}

View File

@ -20,4 +20,10 @@ public static class ModelExtensions
{ {
return $"{coord.season.Timeline}-{coord.province.Abbreviations[0]}@{coord.season.Turn}"; return $"{coord.season.Timeline}-{coord.province.Abbreviations[0]}@{coord.season.Turn}";
} }
public static World ContinueOrFork(this World world, Season season, out Season future)
{
future = world.ContinueOrFork(season);
return world.Update(seasons: world.Seasons.Append(future));
}
} }

View File

@ -13,7 +13,7 @@ public class OrderHistory
public Dictionary<MoveOrder, bool> DoesMoveOutcomes; public Dictionary<MoveOrder, bool> DoesMoveOutcomes;
public OrderHistory() public OrderHistory()
: this(new(), new(), new()) : this([], [], [])
{} {}
public OrderHistory( public OrderHistory(

View File

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

View File

@ -1,185 +1,41 @@
using System.Text.Json.Serialization;
namespace MultiversalDiplomacy.Model; namespace MultiversalDiplomacy.Model;
/// <summary> /// <summary>
/// Represents a state of the map produced by a set of move orders on a previous season. /// Represents a state of the map produced by a set of move orders on a previous season.
/// </summary> /// </summary>
public class Season public class Season(string? past, int turn, string timeline)
{ {
/// <summary> /// <summary>
/// A shared counter for handing out new timeline numbers. /// The first turn number. This is defined to reduce confusion about whether the first turn is 0 or 1.
/// </summary>
private class TimelineFactory
{
private int nextTimeline = 0;
public int NextTimeline() => nextTimeline++;
}
/// <summary>
/// The first turn number.
/// </summary> /// </summary>
public const int FIRST_TURN = 0; public const int FIRST_TURN = 0;
/// <summary> /// <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. /// 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 Season? Past { get; } public string? Past { get; } = past;
/// <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.
/// Phases that only occur after the fall phase occur when Turn % 2 == 1. /// Phases that only occur after the fall phase occur when Turn % 2 == 1.
/// The current year is (Turn / 2) + 1901. /// The current year is (Turn / 2) + 1901.
/// </summary> /// </summary>
public int Turn { get; } public int Turn { get; } = turn;
/// <summary> /// <summary>
/// The timeline to which this season belongs. /// The timeline to which this season belongs.
/// </summary> /// </summary>
public int Timeline { get; } public string Timeline { get; } = timeline;
/// <summary> /// <summary>
/// The season's spatial location as a turn-timeline tuple. /// The multiversal designation of this season.
/// </summary> /// </summary>
public (int Turn, int Timeline) Coord => (this.Turn, this.Timeline); [JsonIgnore]
public string Designation => $"{this.Timeline}{this.Turn}";
/// <summary> public override string ToString() => Designation;
/// The shared timeline number generator.
/// </summary>
private TimelineFactory Timelines { get; }
/// <summary>
/// Future seasons created directly from this season.
/// </summary>
public IEnumerable<Season> Futures => this.FutureList;
private List<Season> FutureList { get; }
private Season(Season? past, int turn, int timeline, TimelineFactory factory)
{
this.Past = past;
this.Turn = turn;
this.Timeline = timeline;
this.Timelines = factory;
this.FutureList = new();
if (past != null)
{
past.FutureList.Add(this);
}
}
public override string ToString()
{
return $"{this.Timeline}@{this.Turn}";
}
/// <summary>
/// Create a root season at the beginning of time.
/// </summary>
public static Season MakeRoot()
{
TimelineFactory factory = new TimelineFactory();
return new Season(
past: null,
turn: FIRST_TURN,
timeline: factory.NextTimeline(),
factory: factory);
}
/// <summary>
/// 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);
/// <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;
}
} }

View File

@ -0,0 +1,55 @@
using System.Text.Json.Serialization;
namespace MultiversalDiplomacy.Model;
/// <summary>
/// A shared counter for handing out new timeline designations.
/// </summary>
[JsonConverter(typeof(TimelineFactoryJsonConverter))]
public class TimelineFactory(int nextTimeline)
{
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());
}
public TimelineFactory() : this(0) { }
public int nextTimeline = nextTimeline;
public string NextTimeline() => IntToString(nextTimeline++);
}

View File

@ -0,0 +1,13 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MultiversalDiplomacy.Model;
internal class TimelineFactoryJsonConverter : JsonConverter<TimelineFactory>
{
public override TimelineFactory? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> new(reader.GetInt32());
public override void Write(Utf8JsonWriter writer, TimelineFactory value, JsonSerializerOptions options)
=> writer.WriteNumberValue(value.nextTimeline);
}

View File

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

View File

@ -1,6 +1,4 @@
using System.Collections.ObjectModel; using System.Text.Json.Serialization;
using MultiversalDiplomacy.Orders;
namespace MultiversalDiplomacy.Model; namespace MultiversalDiplomacy.Model;
@ -9,67 +7,116 @@ namespace MultiversalDiplomacy.Model;
/// </summary> /// </summary>
public class World public class World
{ {
/// <summary>
/// The map variant of the game.
/// </summary>
[JsonIgnore]
public Map Map { get; }
/// <summary>
/// The map variant of the game.
/// </summary>
/// <remarks>
/// While this is serialized to JSON, deserialization uses it to populate <see cref="Map"/>
/// </remarks>
public MapType MapType => this.Map.Type;
/// <summary> /// <summary>
/// The game map. /// The game map.
/// </summary> /// </summary>
public ReadOnlyCollection<Province> Provinces { get; } [JsonIgnore]
public IReadOnlyCollection<Province> Provinces => this.Map.Provinces;
/// <summary> /// <summary>
/// The game powers. /// The game powers.
/// </summary> /// </summary>
public ReadOnlyCollection<Power> Powers { get; } [JsonIgnore]
public IReadOnlyCollection<Power> Powers => this.Map.Powers;
/// <summary> /// <summary>
/// The state of the multiverse. /// The state of the multiverse.
/// </summary> /// </summary>
public ReadOnlyCollection<Season> Seasons { get; } public List<Season> Seasons { get; }
/// <summary>
/// Lookup for seasons by designation.
/// </summary>
[JsonIgnore]
public Dictionary<string, Season> SeasonLookup { get; }
/// <summary> /// <summary>
/// The first season of the game. /// The first season of the game.
/// </summary> /// </summary>
public Season RootSeason { get; } [JsonIgnore]
public Season RootSeason => GetSeason("a0");
/// <summary> /// <summary>
/// All units in the multiverse. /// All units in the multiverse.
/// </summary> /// </summary>
public ReadOnlyCollection<Unit> Units { get; } public List<Unit> Units { get; }
/// <summary> /// <summary>
/// All retreating units in the multiverse. /// All retreating units in the multiverse.
/// </summary> /// </summary>
public ReadOnlyCollection<RetreatingUnit> RetreatingUnits { get; } public List<RetreatingUnit> RetreatingUnits { get; }
/// <summary> /// <summary>
/// Orders given to units in each season. /// Orders given to units in each season.
/// </summary> /// </summary>
public ReadOnlyDictionary<Season, OrderHistory> OrderHistory { get; } public Dictionary<string, OrderHistory> OrderHistory { get; }
/// <summary>
/// The shared timeline number generator.
/// </summary>
public TimelineFactory Timelines { get; }
/// <summary> /// <summary>
/// Immutable game options. /// Immutable game options.
/// </summary> /// </summary>
public Options Options { get; } public Options Options { get; }
[JsonConstructor]
public World(
MapType mapType,
List<Season> seasons,
List<Unit> units,
List<RetreatingUnit> retreatingUnits,
Dictionary<string, OrderHistory> orderHistory,
TimelineFactory timelines,
Options options)
{
this.Map = Map.FromType(mapType);
this.Seasons = seasons;
this.Units = units;
this.RetreatingUnits = retreatingUnits;
this.OrderHistory = orderHistory;
this.Timelines = timelines;
this.Options = options;
this.SeasonLookup = new(Seasons.ToDictionary(season => $"{season.Timeline}{season.Turn}"));
}
/// <summary> /// <summary>
/// Create a new World, providing all state data. /// Create a new World, providing all state data.
/// </summary> /// </summary>
private World( private World(
ReadOnlyCollection<Province> provinces, Map map,
ReadOnlyCollection<Power> powers, List<Season> seasons,
ReadOnlyCollection<Season> seasons, List<Unit> units,
Season rootSeason, List<RetreatingUnit> retreatingUnits,
ReadOnlyCollection<Unit> units, Dictionary<string, OrderHistory> orderHistory,
ReadOnlyCollection<RetreatingUnit> retreatingUnits, TimelineFactory timelines,
ReadOnlyDictionary<Season, OrderHistory> orderHistory,
Options options) Options options)
{ {
this.Provinces = provinces; this.Map = map;
this.Powers = powers;
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.Timelines = timelines;
this.Options = options; this.Options = options;
this.SeasonLookup = new(Seasons.ToDictionary(season => $"{season.Timeline}{season.Turn}"));
} }
/// <summary> /// <summary>
@ -77,21 +124,18 @@ public class World
/// </summary> /// </summary>
private World( private World(
World previous, World previous,
ReadOnlyCollection<Province>? provinces = null, List<Season>? seasons = null,
ReadOnlyCollection<Power>? powers = null, List<Unit>? units = null,
ReadOnlyCollection<Season>? seasons = null, List<RetreatingUnit>? retreatingUnits = null,
ReadOnlyCollection<Unit>? units = null, Dictionary<string, OrderHistory>? orderHistory = null,
ReadOnlyCollection<RetreatingUnit>? retreatingUnits = null,
ReadOnlyDictionary<Season, OrderHistory>? orderHistory = null,
Options? options = null) Options? options = null)
: this( : this(
provinces ?? previous.Provinces, previous.Map,
powers ?? previous.Powers,
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,
previous.Timelines,
options ?? previous.Options) options ?? previous.Options)
{ {
} }
@ -99,17 +143,16 @@ public class World
/// <summary> /// <summary>
/// Create a new world with specified provinces and powers and an initial season. /// Create a new world with specified provinces and powers and an initial season.
/// </summary> /// </summary>
public static World WithMap(IEnumerable<Province> provinces, IEnumerable<Power> powers) public static World WithMap(Map map)
{ {
Season root = Season.MakeRoot(); TimelineFactory timelines = new();
return new World( return new World(
new(provinces.ToList()), map,
new(powers.ToList()), new([new(past: null, Season.FIRST_TURN, timelines.NextTimeline())]),
new(new List<Season> { root }), new([]),
root, new([]),
new(new List<Unit>()), new(new Dictionary<string, OrderHistory>()),
new(new List<RetreatingUnit>()), timelines,
new(new Dictionary<Season, OrderHistory>()),
new Options()); new Options());
} }
@ -117,14 +160,14 @@ public class World
/// Create a new world with the standard Diplomacy provinces and powers. /// Create a new world with the standard Diplomacy provinces and powers.
/// </summary> /// </summary>
public static World WithStandardMap() public static World WithStandardMap()
=> WithMap(StandardProvinces, StandardPowers); => WithMap(Map.Classical);
public World Update( public World Update(
IEnumerable<Season>? seasons = null, IEnumerable<Season>? seasons = null,
IEnumerable<Unit>? units = null, IEnumerable<Unit>? units = null,
IEnumerable<RetreatingUnit>? retreats = null, IEnumerable<RetreatingUnit>? retreats = null,
IEnumerable<KeyValuePair<Season, OrderHistory>>? orders = null) IEnumerable<KeyValuePair<string, OrderHistory>>? orders = null)
=> new World( => new(
previous: this, previous: this,
seasons: seasons == null seasons: seasons == null
? this.Seasons ? this.Seasons
@ -149,7 +192,7 @@ public class World
IEnumerable<Unit> units = unitSpecs.Select(spec => IEnumerable<Unit> units = unitSpecs.Select(spec =>
{ {
string[] splits = spec.Split(' ', 4); string[] splits = spec.Split(' ', 4);
Power power = this.GetPower(splits[0]); Power power = Map.GetPower(splits[0]);
UnitType type = splits[1] switch UnitType type = splits[1] switch
{ {
"A" => UnitType.Army, "A" => UnitType.Army,
@ -157,11 +200,11 @@ public class World
_ => throw new ApplicationException($"Unknown unit type {splits[1]}") _ => throw new ApplicationException($"Unknown unit type {splits[1]}")
}; };
Location location = type == UnitType.Army Location location = type == UnitType.Army
? this.GetLand(splits[2]) ? Map.GetLand(splits[2])
: splits.Length == 3 : splits.Length == 3
? this.GetWater(splits[2]) ? Map.GetWater(splits[2])
: this.GetWater(splits[2], splits[3]); : Map.GetWater(splits[2], splits[3]);
Unit unit = Unit.Build(location, this.RootSeason, power, type); Unit unit = Unit.Build(location.Designation, this.RootSeason, power, type);
return unit; return unit;
}); });
return this.Update(units: units); return this.Update(units: units);
@ -198,562 +241,99 @@ public class World
); );
} }
/// <summary>
/// Create a continuation of this season if it has no futures, otherwise create a fork.
/// </summary>
public Season ContinueOrFork(Season season)
=> GetFutures(season).Any()
? new(season.Designation, season.Turn + 1, Timelines.NextTimeline())
: new(season.Designation, season.Turn + 1, season.Timeline);
/// <summary> /// <summary>
/// A standard Diplomacy game setup. /// A standard Diplomacy game setup.
/// </summary> /// </summary>
public static World Standard => World public static World Standard => WithStandardMap().AddStandardUnits();
.WithStandardMap()
.AddStandardUnits();
/// <summary>
/// Get a province by name. Throws if the province is not found.
/// </summary>
private Province GetProvince(string provinceName)
=> GetProvince(provinceName, this.Provinces);
/// <summary>
/// Get a province by name. Throws if the province is not found.
/// </summary>
private static Province GetProvince(string provinceName, IEnumerable<Province> provinces)
{
string provinceNameUpper = provinceName.ToUpperInvariant();
Province? foundProvince = provinces.SingleOrDefault(
p => p!.Name.ToUpperInvariant() == provinceNameUpper
|| p.Abbreviations.Any(a => a.ToUpperInvariant() == provinceNameUpper),
null);
if (foundProvince == null) throw new KeyNotFoundException(
$"Province {provinceName} not found");
return foundProvince;
}
/// <summary>
/// Get the location in a province matching a predicate. Throws if there is not exactly one
/// such location.
/// </summary>
private Location GetLocation(string provinceName, Func<Location, bool> predicate)
{
Location? foundLocation = GetProvince(provinceName).Locations.SingleOrDefault(
l => l != null && predicate(l), null);
if (foundLocation == null) throw new KeyNotFoundException(
$"No such location in {provinceName}");
return foundLocation;
}
/// <summary>
/// Get the sole land location of a province.
/// </summary>
public Location GetLand(string provinceName)
=> GetLocation(provinceName, l => l.Type == LocationType.Land);
/// <summary>
/// Get the sole water location of a province, optionally specifying a named coast.
/// </summary>
public Location GetWater(string provinceName, string? coastName = null)
=> coastName == null
? GetLocation(provinceName, l => l.Type == LocationType.Water)
: GetLocation(provinceName, l => l.Name == coastName || l.Abbreviation == coastName);
/// <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(int turn, int timeline) public Season GetSeason(string timeline, int turn)
=> GetSeason($"{timeline}{turn}");
/// <summary>
/// Get a season by designation.
/// </summary>
public Season GetSeason(string designation)
=> SeasonLookup[designation];
/// <summary>
/// Get all seasons that are immediate futures of a season.
/// </summary>
/// <param name="present">A season designation.</param>
/// <returns>The immediate futures of the designated season.</returns>
public IEnumerable<Season> GetFutures(string present)
=> Seasons.Where(future => future.Past == present);
/// <summary>
/// Get all seasons that are immediate futures of a season.
/// </summary>
/// <param name="present">A season.</param>
/// <returns>The immediate futures of the season.</returns>
public IEnumerable<Season> GetFutures(Season present) => GetFutures(present.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( if (season.Past is null) {
s => s!.Turn == turn && s.Timeline == timeline, return season;
null); }
if (foundSeason == null) throw new KeyNotFoundException( Season past = SeasonLookup[season.Past];
$"Season {turn}:{timeline} not found"); return season.Timeline == past.Timeline
return foundSeason; ? GetTimelineRoot(past)
: season;
} }
/// <summary> /// <summary>
/// Get a power by name. Throws if there is not exactly one such power. /// 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> /// </summary>
public Power GetPower(string powerName) public bool InAdjacentTimeline(Season one, Season two)
{ {
Power? foundPower = this.Powers.SingleOrDefault( // Timelines are adjacent to themselves. Early out in that case.
p => p!.Name == powerName || p.Name.StartsWith(powerName), if (one == two) return true;
null);
if (foundPower == null) throw new KeyNotFoundException( // If the timelines aren't identical, one of them isn't the initial trunk.
$"Power {powerName} not found"); // They can still be adjacent if one of them branched off of the other, or
return foundPower; // 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, (int turn, int timeline)? seasonCoord = null) public Unit GetUnitAt(string provinceName, Season? season = null)
{ {
Province province = GetProvince(provinceName); Province province = Map.GetProvince(provinceName);
seasonCoord ??= (this.RootSeason.Turn, this.RootSeason.Timeline); season ??= RootSeason;
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 => Map.GetLocation(u!).Province == province && u!.Season == season,
null); null)
if (foundUnit == null) throw new KeyNotFoundException( ?? throw new KeyNotFoundException($"Unit at {province} at {season} not found");
$"Unit at {province} at {season} not found");
return foundUnit; return foundUnit;
} }
/// <summary> public Unit GetUnitByDesignation(string designation)
/// The standard Diplomacy provinces. => Units.SingleOrDefault(u => u!.Designation == designation, null)
/// </summary> ?? throw new KeyNotFoundException($"Unit {designation} not found");
public static ReadOnlyCollection<Province> StandardProvinces
{
get
{
// Define the provinces of the standard world map.
List<Province> standardProvinces = new List<Province>
{
Province.Empty("North Africa", "NAF")
.AddLandLocation()
.AddCoastLocation(),
Province.Supply("Tunis", "TUN")
.AddLandLocation()
.AddCoastLocation(),
Province.Empty("Bohemia", "BOH")
.AddLandLocation(),
Province.Supply("Budapest", "BUD")
.AddLandLocation(),
Province.Empty("Galacia", "GAL")
.AddLandLocation(),
Province.Supply("Trieste", "TRI")
.AddLandLocation()
.AddCoastLocation(),
Province.Empty("Tyrolia", "TYR")
.AddLandLocation(),
Province.Time("Vienna", "VIE")
.AddLandLocation(),
Province.Empty("Albania", "ALB")
.AddLandLocation()
.AddCoastLocation(),
Province.Supply("Bulgaria", "BUL")
.AddLandLocation()
.AddCoastLocation("east coast", "ec")
.AddCoastLocation("south coast", "sc"),
Province.Supply("Greece", "GRE")
.AddLandLocation()
.AddCoastLocation(),
Province.Supply("Rumania", "RUM", "RMA")
.AddLandLocation()
.AddCoastLocation(),
Province.Supply("Serbia", "SER")
.AddLandLocation(),
Province.Empty("Clyde", "CLY")
.AddLandLocation()
.AddCoastLocation(),
Province.Supply("Edinburgh", "EDI")
.AddLandLocation()
.AddCoastLocation(),
Province.Supply("Liverpool", "LVP", "LPL")
.AddLandLocation()
.AddCoastLocation(),
Province.Time("London", "LON")
.AddLandLocation()
.AddCoastLocation(),
Province.Empty("Wales", "WAL")
.AddLandLocation()
.AddCoastLocation(),
Province.Empty("Yorkshire", "YOR")
.AddLandLocation()
.AddCoastLocation(),
Province.Supply("Brest", "BRE")
.AddLandLocation()
.AddCoastLocation(),
Province.Empty("Burgundy", "BUR")
.AddLandLocation(),
Province.Empty("Gascony", "GAS")
.AddLandLocation()
.AddCoastLocation(),
Province.Supply("Marseilles", "MAR")
.AddLandLocation()
.AddCoastLocation(),
Province.Time("Paris", "PAR")
.AddLandLocation(),
Province.Empty("Picardy", "PIC")
.AddLandLocation()
.AddCoastLocation(),
Province.Time("Berlin", "BER")
.AddLandLocation()
.AddCoastLocation(),
Province.Supply("Kiel", "KIE")
.AddLandLocation()
.AddCoastLocation(),
Province.Supply("Munich", "MUN")
.AddLandLocation(),
Province.Empty("Prussia", "PRU")
.AddLandLocation()
.AddCoastLocation(),
Province.Empty("Ruhr", "RUH", "RHR")
.AddLandLocation(),
Province.Empty("Silesia", "SIL")
.AddLandLocation(),
Province.Supply("Spain", "SPA")
.AddLandLocation()
.AddCoastLocation("north coast", "nc")
.AddCoastLocation("south coast", "sc"),
Province.Supply("Portugal", "POR")
.AddLandLocation()
.AddCoastLocation(),
Province.Empty("Apulia", "APU")
.AddLandLocation()
.AddCoastLocation(),
Province.Supply("Naples", "NAP")
.AddLandLocation()
.AddCoastLocation(),
Province.Empty("Piedmont", "PIE")
.AddLandLocation()
.AddCoastLocation(),
Province.Time("Rome", "ROM", "RME")
.AddLandLocation()
.AddCoastLocation(),
Province.Empty("Tuscany", "TUS")
.AddLandLocation()
.AddCoastLocation(),
Province.Supply("Venice", "VEN")
.AddLandLocation()
.AddCoastLocation(),
Province.Supply("Belgium", "BEL")
.AddLandLocation()
.AddCoastLocation(),
Province.Supply("Holland", "HOL")
.AddLandLocation()
.AddCoastLocation(),
Province.Empty("Finland", "FIN")
.AddLandLocation()
.AddCoastLocation(),
Province.Empty("Livonia", "LVN", "LVA")
.AddLandLocation()
.AddCoastLocation(),
Province.Time("Moscow", "MOS")
.AddLandLocation()
.AddCoastLocation(),
Province.Supply("Sevastopol", "SEV")
.AddLandLocation()
.AddCoastLocation(),
Province.Supply("Saint Petersburg", "STP")
.AddLandLocation()
.AddCoastLocation("north coast", "nc")
.AddCoastLocation("west coast", "wc"),
Province.Empty("Ukraine", "UKR")
.AddLandLocation(),
Province.Supply("Warsaw", "WAR")
.AddLandLocation(),
Province.Supply("Denmark", "DEN")
.AddLandLocation()
.AddCoastLocation(),
Province.Supply("Norway", "NWY")
.AddLandLocation()
.AddCoastLocation(),
Province.Supply("Sweden", "SWE")
.AddLandLocation()
.AddCoastLocation(),
Province.Supply("Ankara", "ANK")
.AddLandLocation()
.AddCoastLocation(),
Province.Empty("Armenia", "ARM")
.AddLandLocation()
.AddCoastLocation(),
Province.Time("Constantinople", "CON")
.AddLandLocation()
.AddCoastLocation(),
Province.Supply("Smyrna", "SMY")
.AddLandLocation()
.AddCoastLocation(),
Province.Empty("Syria", "SYR")
.AddLandLocation()
.AddCoastLocation(),
Province.Empty("Barents Sea", "BAR")
.AddOceanLocation(),
Province.Empty("English Channel", "ENC", "ECH")
.AddOceanLocation(),
Province.Empty("Heligoland Bight", "HEL", "HGB")
.AddOceanLocation(),
Province.Empty("Irish Sea", "IRS", "IRI")
.AddOceanLocation(),
Province.Empty("Mid-Atlantic Ocean", "MAO", "MID")
.AddOceanLocation(),
Province.Empty("North Atlantic Ocean", "NAO", "NAT")
.AddOceanLocation(),
Province.Empty("North Sea", "NTH", "NTS")
.AddOceanLocation(),
Province.Empty("Norwegian Sea", "NWS", "NWG")
.AddOceanLocation(),
Province.Empty("Skagerrak", "SKA", "SKG")
.AddOceanLocation(),
Province.Empty("Baltic Sea", "BAL")
.AddOceanLocation(),
Province.Empty("Guld of Bothnia", "GOB", "BOT")
.AddOceanLocation(),
Province.Empty("Adriatic Sea", "ADS", "ADR")
.AddOceanLocation(),
Province.Empty("Aegean Sea", "AEG")
.AddOceanLocation(),
Province.Empty("Black Sea", "BLA")
.AddOceanLocation(),
Province.Empty("Eastern Mediterranean Sea", "EMS", "EAS")
.AddOceanLocation(),
Province.Empty("Gulf of Lyons", "GOL", "LYO")
.AddOceanLocation(),
Province.Empty("Ionian Sea", "IOS", "ION", "INS")
.AddOceanLocation(),
Province.Empty("Tyrrhenian Sea", "TYS", "TYN")
.AddOceanLocation(),
Province.Empty("Western Mediterranean Sea", "WMS", "WES")
.AddOceanLocation(),
};
// Declare some helpers for border definitions
Location Land(string provinceName) => GetProvince(provinceName, standardProvinces)
.Locations.Single(l => l.Type == LocationType.Land);
Location Water(string provinceName) => GetProvince(provinceName, standardProvinces)
.Locations.Single(l => l.Type == LocationType.Water);
Location Coast(string provinceName, string coastName)
=> GetProvince(provinceName, standardProvinces)
.Locations.Single(l => l.Name == coastName || l.Abbreviation == coastName);
static void AddBordersTo(Location location, Func<string, Location> LocationType, params string[] borders)
{
foreach (string bordering in borders)
{
location.AddBorder(LocationType(bordering));
}
}
void AddBorders(string provinceName, Func<string, Location> LocationType, params string[] borders)
=> AddBordersTo(LocationType(provinceName), LocationType, borders);
AddBorders("NAF", Land, "TUN");
AddBorders("NAF", Water, "MAO", "WES", "TUN");
AddBorders("TUN", Land, "NAF");
AddBorders("TUN", Water, "NAF", "WES", "TYS", "ION");
AddBorders("BOH", Land, "MUN", "SIL", "GAL", "VIE", "TYR");
AddBorders("BUD", Land, "VIE", "GAL", "RUM", "SER", "TRI");
AddBorders("GAL", Land, "BOH", "SIL", "WAR", "UKR", "RUM", "BUD", "VIE");
AddBorders("TRI", Land, "TYR", "VIE", "BUD", "SER", "ALB");
AddBorders("TRI", Water, "ALB", "ADR", "VEN");
AddBorders("TYR", Land, "MUN", "BOH", "VIE", "TRI", "VEN", "PIE");
AddBorders("VIE", Land, "TYR", "BOH", "GAL", "BUD", "TRI");
AddBorders("ALB", Land, "TRI", "SER", "GRE");
AddBorders("ALB", Water, "TRI", "ADR", "ION", "GRE");
AddBorders("BUL", Land, "GRE", "SER", "RUM", "CON");
AddBordersTo(Coast("BUL", "ec"), Water, "BLA", "CON");
AddBordersTo(Coast("BUL", "sc"), Water, "CON", "AEG", "GRE");
AddBorders("GRE", Land, "ALB", "SER", "BUL");
AddBorders("GRE", Water, "ALB", "ION", "AEG");
Water("GRE").AddBorder(Coast("BUL", "sc"));
AddBorders("RUM", Land, "BUL", "SER", "BUD", "GAL", "UKR", "SEV");
AddBorders("RUM", Water, "SEV", "BLA");
Water("RUM").AddBorder(Coast("BUL", "ec"));
AddBorders("SER", Land, "BUD", "RUM", "BUL", "GRE", "ALB", "TRI");
AddBorders("CLY", Land, "EDI", "LVP");
AddBorders("CLY", Water, "LVP", "NAO", "NWG", "EDI");
AddBorders("EDI", Land, "YOR", "LVP", "CLY");
AddBorders("EDI", Water, "CLY", "NWG", "NTH", "YOR");
AddBorders("LVP", Land, "CLY", "EDI", "YOR", "WAL");
AddBorders("LVP", Water, "WAL", "IRS", "NAO", "CLY");
AddBorders("LON", Land, "WAL", "YOR");
AddBorders("LON", Water, "WAL", "ENC", "NTH", "YOR");
AddBorders("WAL", Land, "LVP", "YOR", "LON");
AddBorders("WAL", Water, "LON", "ENC", "IRS", "LVP");
AddBorders("YOR", Land, "LON", "WAL", "LVP", "EDI");
AddBorders("YOR", Water, "EDI", "NTH", "LON");
AddBorders("BRE", Land, "PIC", "PAR", "GAS");
AddBorders("BRE", Water, "GAS", "MAO", "ENC", "PIC");
AddBorders("BUR", Land, "BEL", "RUH", "MUN", "MAR", "GAS", "PAR", "PIC");
AddBorders("GAS", Land, "BRE", "PAR", "BUR", "MAR", "SPA");
AddBorders("GAS", Water, "MAO", "BRE");
Water("GAS").AddBorder(Coast("SPA", "nc"));
AddBorders("MAR", Land, "SPA", "GAS", "BUR", "PIE");
AddBorders("MAR", Water, "LYO", "PIE");
Water("MAR").AddBorder(Coast("SPA", "sc"));
AddBorders("PAR", Land, "PIC", "BUR", "GAS", "BRE");
AddBorders("PIC", Land, "BEL", "BUR", "PAR", "BRE");
AddBorders("PIC", Water, "BRE", "ENC", "BEL");
AddBorders("BER", Land, "PRU", "SIL", "MUN", "KIE");
AddBorders("BER", Water, "KIE", "BAL", "PRU");
AddBorders("KIE", Land, "BER", "MUN", "RUH", "HOL", "DEN");
AddBorders("KIE", Water, "HOL", "HEL", "DEN", "BAL", "BER");
AddBorders("MUN", Land, "BUR", "RUH", "KIE", "BER", "SIL", "BOH", "TYR");
AddBorders("PRU", Land, "LVN", "WAR", "SIL", "BER");
AddBorders("PRU", Water, "BER", "BAL", "LVN");
AddBorders("RUH", Land, "KIE", "MUN", "BUR", "BEL", "HOL");
AddBorders("SIL", Land, "PRU", "WAR", "GAL", "BOH", "MUN", "BER");
AddBorders("SPA", Land, "POR", "GAS", "MAR");
AddBordersTo(Coast("SPA", "nc"), Water, "POR", "MAO", "GAS");
AddBordersTo(Coast("SPA", "sc"), Water, "POR", "MAO", "WES", "LYO", "MAR");
AddBorders("POR", Land, "SPA");
AddBorders("POR", Water, "MAO");
Water("POR").AddBorder(Coast("SPA", "nc"));
Water("POR").AddBorder(Coast("SPA", "sc"));
AddBorders("APU", Land, "NAP", "ROM", "VEN");
AddBorders("APU", Water, "VEN", "ADR", "IOS", "NAP");
AddBorders("NAP", Land, "ROM", "APU");
AddBorders("NAP", Water, "APU", "IOS", "TYS", "ROM");
AddBorders("PIE", Land, "MAR", "TYR", "VEN", "TUS");
AddBorders("PIE", Water, "TUS", "LYO", "MAR");
AddBorders("ROM", Land, "TUS", "VEN", "APU", "NAP");
AddBorders("ROM", Water, "NAP", "TYS", "TUS");
AddBorders("TUS", Land, "PIE", "VEN", "ROM");
AddBorders("TUS", Water, "ROM", "TYS", "LYO", "PIE");
AddBorders("VEN", Land, "APU", "ROM", "TUS", "PIE", "TYR", "TRI");
AddBorders("VEN", Water, "TRI", "ADR", "APU");
AddBorders("BEL", Land, "HOL", "RUH", "BUR", "PIC");
AddBorders("BEL", Water, "PIC", "ENC", "NTH", "HOL");
AddBorders("HOL", Land, "BEL", "RUH", "KIE");
AddBorders("HOL", Water, "NTH", "HEL");
AddBorders("FIN", Land, "SWE", "NWY", "STP");
AddBorders("FIN", Water, "SWE", "BOT");
Water("FIN").AddBorder(Coast("STP", "wc"));
AddBorders("LVN", Land, "STP", "MOS", "WAR", "PRU");
AddBorders("LVN", Water, "PRU", "BAL", "BOT");
Water("LVN").AddBorder(Coast("STP", "wc"));
AddBorders("MOS", Land, "SEV", "UKR", "WAR", "LVN", "STP");
AddBorders("SEV", Land, "RUM", "UKR", "MOS", "ARM");
AddBorders("SEV", Water, "ARM", "BLA", "RUM");
AddBorders("STP", Land, "MOS", "LVN", "FIN");
AddBordersTo(Coast("STP", "nc"), Water, "BAR", "NWY");
AddBordersTo(Coast("STP", "wc"), Water, "LVN", "BOT", "FIN");
AddBorders("UKR", Land, "MOS", "SEV", "RUM", "GAL", "WAR");
AddBorders("WAR", Land, "PRU", "LVN", "MOS", "UKR", "GAL", "SIL");
AddBorders("DEN", Land, "KIE", "SWE");
AddBorders("DEN", Water, "KIE", "HEL", "NTH", "SKA", "BAL", "SWE");
AddBorders("NWY", Land, "STP", "FIN", "SWE");
AddBorders("NWY", Water, "BAR", "NWG", "NTH", "SKA", "SWE");
Water("NWY").AddBorder(Coast("STP", "nc"));
AddBorders("SWE", Land, "NWY", "FIN", "DEN");
AddBorders("SWE", Water, "FIN", "BOT", "BAL", "DEN", "SKA", "NWY");
AddBorders("ANK", Land, "ARM", "SMY", "CON");
AddBorders("ANK", Water, "CON", "BLA", "ARM");
AddBorders("ARM", Land, "SEV", "SYR", "SMY", "ANK");
AddBorders("ARM", Water, "ANK", "BLA", "SEV");
AddBorders("CON", Land, "BUL", "ANK", "SMY");
AddBorders("CON", Water, "BLA", "ANK", "SMY", "AEG");
Water("CON").AddBorder(Coast("BUL", "ec"));
Water("CON").AddBorder(Coast("BUL", "sc"));
AddBorders("SMY", Land, "CON", "ANK", "ARM", "SYR");
AddBorders("SMY", Water, "SYR", "EAS", "AEG", "CON");
AddBorders("SYR", Land, "SMY", "ARM");
AddBorders("SYR", Water, "EAS", "SMY");
AddBorders("BAR", Water, "NWG", "NWY");
Water("BAR").AddBorder(Coast("STP", "nc"));
AddBorders("ENC", Water, "LON", "NTH", "BEL", "PIC", "BRE", "MAO", "IRS", "WAL");
AddBorders("HEL", Water, "NTH", "DEN", "BAL", "KIE", "HOL");
AddBorders("IRS", Water, "NAO", "LVP", "WAL", "ENC", "MAO");
AddBorders("MAO", Water, "NAO", "IRS", "ENC", "BRE", "GAS", "POR", "NAF");
Water("MAO").AddBorder(Coast("SPA", "nc"));
Water("MAO").AddBorder(Coast("SPA", "sc"));
AddBorders("NAO", Water, "NWG", "CLY", "LVP", "IRS", "MAO");
AddBorders("NTH", Water, "NWG", "NWY", "SKA", "DEN", "HEL", "HOL", "BEL", "ENC", "LON", "YOR", "EDI");
AddBorders("NWG", Water, "BAR", "NWY", "NTH", "EDI", "CLY", "NAO");
AddBorders("SKA", Water, "NWY", "SWE", "BAL", "DEN", "NTH");
AddBorders("BAL", Water, "BOT", "LVN", "PRU", "BER", "KIE", "HEL", "DEN", "SWE");
AddBorders("BOT", Water, "LVN", "BAL", "SWE", "FIN");
Water("BOT").AddBorder(Coast("STP", "wc"));
AddBorders("ADR", Water, "IOS", "APU", "VEN", "TRI", "ALB");
AddBorders("AEG", Water, "CON", "SMY", "EAS", "IOS", "GRE");
Water("AEG").AddBorder(Coast("BUL", "sc"));
AddBorders("BLA", Water, "RUM", "SEV", "ARM", "ANK", "CON");
Water("BLA").AddBorder(Coast("BUL", "ec"));
AddBorders("EAS", Water, "IOS", "AEG", "SMY", "SYR");
AddBorders("LYO", Water, "MAR", "PIE", "TUS", "TYS", "WES");
Water("LYO").AddBorder(Coast("SPA", "sc"));
AddBorders("IOS", Water, "TUN", "TYS", "NAP", "APU", "ADR", "ALB", "GRE", "AEG");
AddBorders("TYS", Water, "LYO", "TUS", "ROM", "NAP", "IOS", "TUN", "WES");
AddBorders("WES", Water, "LYO", "TYS", "TUN", "NAF", "MAO");
Water("WES").AddBorder(Coast("SPA", "sc"));
return new(standardProvinces);
}
}
/// <summary>
/// The standard Diplomacy powers.
/// </summary>
public static ReadOnlyCollection<Power> StandardPowers
{
get => new(new List<Power>
{
new Power("Austria"),
new Power("England"),
new Power("France"),
new Power("Germany"),
new Power("Italy"),
new Power("Russia"),
new Power("Turkey"),
});
}
} }

View File

@ -7,4 +7,10 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>MultiversalDiplomacyTests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
</Project> </Project>

View File

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

View File

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

View File

@ -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[(0, 0)] setup[("a", 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()
[(1, 0)] [("a", 1)]
.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,17 +41,17 @@ 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 1:1 originating from Mun 1:0 // Confirm that there is a unit in Tyr b1 originating from Mun a1
Season fork = world.GetSeason(1, 1); Season fork = world.GetSeason("b1");
Unit originalUnit = world.GetUnitAt("Mun", s0.Coord); Unit originalUnit = world.GetUnitAt("Mun", s0);
Unit aMun0 = world.GetUnitAt("Mun", s1.Coord); Unit aMun0 = world.GetUnitAt("Mun", s1);
Unit aTyr = world.GetUnitAt("Tyr", fork.Coord); Unit aTyr = world.GetUnitAt("Tyr", fork);
Assert.That(aTyr.Past, Is.EqualTo(mun1.Order.Unit)); Assert.That(aTyr.Past, Is.EqualTo(mun1.Order.Unit.Designation));
Assert.That(aTyr.Past?.Past, Is.EqualTo(mun0.Order.Unit)); Assert.That(world.GetUnitByDesignation(aTyr.Past!).Past, Is.EqualTo(mun0.Order.Unit.Designation));
// 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); Unit aMun1 = world.GetUnitAt("Mun", fork);
Assert.That(aMun1.Past, Is.EqualTo(originalUnit)); Assert.That(aMun1.Past, Is.EqualTo(originalUnit.Designation));
} }
[Test] [Test]
@ -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[(0, 0)] setup[("a", 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[(1, 0)] setup[("a", 1)]
["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(1, 1); Season fork = world.GetSeason("b1");
Unit tyr1 = world.GetUnitAt("Tyr", fork.Coord); Unit tyr1 = world.GetUnitAt("Tyr", fork);
Assert.That( Assert.That(
tyr1.Past, tyr1.Past,
Is.EqualTo(mun0.Order.Unit), Is.EqualTo(mun0.Order.Unit.Designation),
"Expected A Mun 0:0 to advance to Tyr 1:1"); "Expected A Mun a0 to advance to Tyr b1");
Assert.That( Assert.That(
world.RetreatingUnits.Count, world.RetreatingUnits.Count,
Is.EqualTo(1), 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)); 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[(0, 0)] setup[("a", 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()
[(1, 0)] [("a", 1)]
.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 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. // should not throw when accessing it.
Assert.That(tyr0, Is.NotDislodged); Assert.That(tyr0, Is.NotDislodged);
@ -136,12 +136,12 @@ public class TimeTravelTest
// change the past and therefore did not create a new timeline. // change the past and therefore did not create a new timeline.
World world = setup.UpdateWorld(); World world = setup.UpdateWorld();
Assert.That( Assert.That(
s0.Futures.Count(), world.GetFutures(s0).Count(),
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(world.GetFutures(s1).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)); Assert.That(world.GetFutures(s2).Count(), Is.EqualTo(0));
} }
[Test] [Test]
@ -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[(0, 0)] setup[("a", 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[(1, 0)] setup[("a", 1)]
.GetReference(out Season s1) .GetReference(out Season s1)
["Germany"] ["Germany"]
.Army("Tyr").Holds() .Army("Tyr").Holds()
@ -178,12 +178,12 @@ public class TimeTravelTest
// ...since it succeeded anyway, no fork is created. // ...since it succeeded anyway, no fork is created.
World world = setup.UpdateWorld(); World world = setup.UpdateWorld();
Assert.That( Assert.That(
s0.Futures.Count(), world.GetFutures(s0).Count(),
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(world.GetFutures(s1).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)); Assert.That(world.GetFutures(s2).Count(), Is.EqualTo(0));
} }
[Test] [Test]
@ -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[(0, 0)] setup[("a", 0)]
.GetReference(out var s0_0) .GetReference(out var a0)
["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()
[(1, 0)] [("a", 1)]
["England"].Army("Lon").MovesTo("Yor", s0_0) ["England"].Army("Lon").MovesTo("Yor", a0)
.Execute() .Execute()
// Head seasons: 2:0 1:1 // Head seasons: a2 b1
// 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.
[(2, 0)] [("a", 2)]
.GetReference(out var s2_0) .GetReference(out var a2)
["Germany"].Army("Mun").MovesTo("Tyr").GetReference(out var mun2_0) ["Germany"].Army("Mun").MovesTo("Tyr").GetReference(out var mun_a2)
["Austria"].Army("Tyr").Holds().GetReference(out var tyr2_0) ["Austria"].Army("Tyr").Holds().GetReference(out var tyr_a2)
[(1, 1)] [("b", 1)]
.GetReference(out var s1_1) .GetReference(out var b1)
["Germany"].Army("Mun").Supports.Army("Mun", s2_0).MoveTo("Tyr").GetReference(out var mun1_1) ["Germany"].Army("Mun").Supports.Army("Mun", a2).MoveTo("Tyr").GetReference(out var mun_b1)
["Austria"].Army("Tyr").Holds(); ["Austria"].Army("Tyr").Holds();
// The attack against Tyr 2:0 succeeds. // The attack against Tyr a2 succeeds.
setup.ValidateOrders(); setup.ValidateOrders();
Assert.That(mun2_0, Is.Valid); Assert.That(mun_a2, Is.Valid);
Assert.That(tyr2_0, Is.Valid); Assert.That(tyr_a2, Is.Valid);
Assert.That(mun1_1, Is.Valid); Assert.That(mun_b1, Is.Valid);
setup.AdjudicateOrders(); setup.AdjudicateOrders();
Assert.That(mun2_0, Is.Victorious); Assert.That(mun_a2, Is.Victorious);
Assert.That(tyr2_0, Is.Dislodged); Assert.That(tyr_a2, Is.Dislodged);
Assert.That(mun1_1, Is.NotCut); Assert.That(mun_b1, 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(
s2_0.Futures.Count(), world.GetFutures(a2).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(
s1_1.Futures.Count(), world.GetFutures(b1).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 s3_0 = world.GetSeason(s2_0.Turn + 1, s2_0.Timeline); Season a3 = world.GetSeason(a2.Timeline, a2.Turn + 1);
Assert.That(s3_0.Futures.Count(), Is.EqualTo(0)); Assert.That(world.GetFutures(a3).Count(), Is.EqualTo(0));
Season s2_1 = world.GetSeason(s1_1.Turn + 1, s1_1.Timeline); Season b2 = world.GetSeason(b1.Timeline, b1.Turn + 1);
Assert.That(s2_1.Futures.Count(), Is.EqualTo(0)); Assert.That(world.GetFutures(b2).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[(0, 0)] setup[("a", 0)]
.GetReference(out var s0_0) .GetReference(out var a0)
["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()
[(1, 0)] [("a", 1)]
["England"].Army("Lon").MovesTo("Yor", s0_0) ["England"].Army("Lon").MovesTo("Yor", a0)
.Execute() .Execute()
// Head seasons: 2:0 1:1 // Head seasons: a2, b1
[(2, 0)] [("a", 2)]
[(1, 1)] [("b", 1)]
["England"].Army("Yor").MovesTo("Edi", s0_0) ["England"].Army("Yor").MovesTo("Edi", a0)
.Execute() .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. // 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. // As above, Germany dislodges Austria in Tyr a3 by supporting the move from b2.
[(3, 0)] [("a", 3)]
.GetReference(out var s3_0) .GetReference(out var a3)
["Germany"].Army("Mun").MovesTo("Tyr") ["Germany"].Army("Mun").MovesTo("Tyr")
["Austria"].Army("Tyr").Holds() ["Austria"].Army("Tyr").Holds()
[(2, 1)] [("b", 2)]
.GetReference(out Season s2_1) .GetReference(out Season b2)
["Germany"].Army("Mun").Supports.Army("Mun", s3_0).MoveTo("Tyr").GetReference(out var mun2_1) ["Germany"].Army("Mun").Supports.Army("Mun", a3).MoveTo("Tyr").GetReference(out var mun_b2)
["Austria"].Army("Tyr").Holds() ["Austria"].Army("Tyr").Holds()
[(1, 2)] [("c", 1)]
["Germany"].Army("Mun").Holds() ["Germany"].Army("Mun").Holds()
["Austria"].Army("Tyr").Holds() ["Austria"].Army("Tyr").Holds()
.Execute() .Execute()
// Head seasons: 4:0 3:1 2:2 // Head seasons: a4, b3, c2
// Now Austria cuts the support in 2:1 by attacking from 2:2. // Now Austria cuts the support in b2 by attacking from c2.
[(4, 0)] [("a", 4)]
["Germany"].Army("Tyr").Holds() ["Germany"].Army("Tyr").Holds()
[(3, 1)] [("b", 3)]
["Germany"].Army("Mun").Holds() ["Germany"].Army("Mun").Holds()
["Austria"].Army("Tyr").Holds() ["Austria"].Army("Tyr").Holds()
[(2, 2)] [("c", 2)]
["Germany"].Army("Mun").Holds() ["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(); setup.ValidateOrders();
Assert.That(tyr2_2, Is.Valid); Assert.That(tyr_c2, Is.Valid);
setup.AdjudicateOrders(); setup.AdjudicateOrders();
Assert.That(tyr2_2, Is.Repelled); Assert.That(tyr_c2, Is.Repelled);
Assert.That(mun2_1, Is.NotDislodged); Assert.That(mun_b2, Is.NotDislodged);
Assert.That(mun2_1, Is.Cut); Assert.That(mun_b2, 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(
s3_0.Futures.Count(), world.GetFutures(a3).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(
s2_1.Futures.Count(), world.GetFutures(b2).Count(),
Is.EqualTo(1), Is.EqualTo(1),
"A cross-timeline support cut incorrectly forked the timeline"); "A cross-timeline support cut incorrectly forked the timeline");
} }

View File

@ -6,10 +6,10 @@ namespace MultiversalDiplomacyTests;
public class MapTests public class MapTests
{ {
IEnumerable<Location> LocationClosure(Location location) static IEnumerable<Location> LocationClosure(Location location)
{ {
IEnumerable<Location> visited = new List<Location>(); IEnumerable<Location> visited = [];
IEnumerable<Location> toVisit = new List<Location>() { location }; IEnumerable<Location> toVisit = [location];
while (toVisit.Any()) while (toVisit.Any())
{ {
@ -50,7 +50,7 @@ public class MapTests
[Test] [Test]
public void LandAndSeaBorders() public void LandAndSeaBorders()
{ {
World map = World.WithStandardMap(); Map map = Map.Classical;
Assert.That( Assert.That(
map.GetLand("NAF").Adjacents.Count(), map.GetLand("NAF").Adjacents.Count(),
Is.EqualTo(1), Is.EqualTo(1),

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

View File

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

View File

@ -9,57 +9,54 @@ public class SeasonTests
[Test] [Test]
public void TimelineForking() public void TimelineForking()
{ {
Season a0 = Season.MakeRoot(); World world = World.WithMap(Map.Test);
Season a1 = a0.MakeNext(); Season a0 = world.GetSeason("a0");
Season a2 = a1.MakeNext(); world = world
Season a3 = a2.MakeNext(); .ContinueOrFork(a0, out Season a1)
Season b1 = a1.MakeFork(); .ContinueOrFork(a1, out Season a2)
Season b2 = b1.MakeNext(); .ContinueOrFork(a2, out Season a3)
Season c1 = a1.MakeFork(); .ContinueOrFork(a1, out Season b2)
Season d1 = a2.MakeFork(); .ContinueOrFork(b2, out Season b3)
.ContinueOrFork(a1, out Season c2)
.ContinueOrFork(a2, out Season d3);
Assert.That(a0.Timeline, Is.EqualTo(0), "Unexpected trunk timeline number"); Assert.That(
Assert.That(a1.Timeline, Is.EqualTo(0), "Unexpected trunk timeline number"); world.Seasons.Select(season => season.ToString()),
Assert.That(a2.Timeline, Is.EqualTo(0), "Unexpected trunk timeline number"); Is.EquivalentTo(new List<string> { "a0", "a1", "a2", "a3", "b2", "b3", "c2", "d3" }),
Assert.That(a3.Timeline, Is.EqualTo(0), "Unexpected trunk timeline number"); "Unexpected seasons");
Assert.That(b1.Timeline, Is.EqualTo(1), "Unexpected first alt number");
Assert.That(b2.Timeline, Is.EqualTo(1), "Unexpected first alt number"); Assert.That(a0.Timeline, Is.EqualTo("a"), "Unexpected trunk timeline");
Assert.That(c1.Timeline, Is.EqualTo(2), "Unexpected second alt number"); Assert.That(a1.Timeline, Is.EqualTo("a"), "Unexpected trunk timeline");
Assert.That(d1.Timeline, Is.EqualTo(3), "Unexpected third alt number"); 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(b1.Turn, Is.EqualTo(Season.FIRST_TURN + 2), "Unexpected fork turn number"); Assert.That(b2.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(b3.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(c2.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(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(world.GetTimelineRoot(a0), Is.EqualTo(a0), "Expected timeline root to be reflexive");
Assert.That(a3.TimelineRoot(), Is.EqualTo(a0), "Expected trunk timeline to have root"); Assert.That(world.GetTimelineRoot(a3), 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(world.GetTimelineRoot(b2), Is.EqualTo(b2), "Expected alt timeline root to be reflexive");
Assert.That(b2.TimelineRoot(), Is.EqualTo(b1), "Expected alt timeline to root at first fork"); Assert.That(world.GetTimelineRoot(b3), Is.EqualTo(b2), "Expected alt timeline to root at first fork");
Assert.That(c1.TimelineRoot(), Is.EqualTo(c1), "Expected alt timeline root to be reflexive"); Assert.That(world.GetTimelineRoot(c2), Is.EqualTo(c2), "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(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(world.InAdjacentTimeline(b3, 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(world.InAdjacentTimeline(b3, c2), 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, d3), Is.False, "Expected alts from different origins not to be adjacent");
}
[Test] Assert.That(world.GetFutures(a0), Is.EquivalentTo(new List<Season> { a1 }), "Unexpected futures");
public void LookupTest() Assert.That(world.GetFutures(a1), Is.EquivalentTo(new List<Season> { a2, b2, c2 }), "Unexpected futures");
{ Assert.That(world.GetFutures(a2), Is.EquivalentTo(new List<Season> { a3, d3 }), "Unexpected futures");
World world = World.WithStandardMap(); Assert.That(world.GetFutures(b2), Is.EquivalentTo(new List<Season> { b3 }), "Unexpected futures");
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

@ -0,0 +1,105 @@
using System.Text.Json;
using MultiversalDiplomacy.Adjudicate;
using MultiversalDiplomacy.Model;
using NUnit.Framework;
namespace MultiversalDiplomacyTests;
public class SerializationTest
{
private JsonSerializerOptions Options = new() {
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
[Test]
public void SerializeRoundTrip_NewGame()
{
World world = World.WithStandardMap();
string serialized = JsonSerializer.Serialize(world, Options);
World? deserialized = JsonSerializer.Deserialize<World>(serialized, Options);
Assert.That(deserialized, Is.Not.Null, "Failed to deserialize");
Assert.That(deserialized!.Map, Is.Not.Null, "Failed to deserialize map");
Assert.That(deserialized!.Seasons, Is.Not.Null, "Failed to deserialize seasons");
Assert.That(deserialized!.Units, Is.Not.Null, "Failed to deserialize units");
Assert.That(deserialized!.RetreatingUnits, Is.Not.Null, "Failed to deserialize retreats");
Assert.That(deserialized!.OrderHistory, Is.Not.Null, "Failed to deserialize history");
Assert.That(deserialized!.Timelines, Is.Not.Null, "Failed to deserialize timelines");
Assert.That(deserialized!.Options, Is.Not.Null, "Failed to deserialize options");
Assert.That(deserialized.Timelines.nextTimeline, Is.EqualTo(world.Timelines.nextTimeline));
JsonElement document = JsonSerializer.SerializeToDocument(world, Options).RootElement;
Assert.That(
document.EnumerateObject().Select(prop => prop.Name),
Is.EquivalentTo(new List<string> {
"mapType",
"seasons",
"units",
"retreatingUnits",
"orderHistory",
"options",
"timelines",
}));
}
[Test]
public void SerializeRoundTrip_MDATC_3_A_2()
{
// Set up MDATC 3.A.2
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance);
setup[("a", 0)]
.GetReference(out Season s0)
["Germany"]
.Army("Mun").MovesTo("Tyr").GetReference(out var mun0)
["Austria"]
.Army("Tyr").Holds().GetReference(out var tyr0);
setup.ValidateOrders();
Assert.That(mun0, Is.Valid);
Assert.That(tyr0, Is.Valid);
setup.AdjudicateOrders();
Assert.That(mun0, Is.Repelled);
Assert.That(tyr0, Is.NotDislodged);
setup.UpdateWorld();
// Serialize the world
string serialized = JsonSerializer.Serialize(setup.World, Options);
// Deserialize the world
Console.WriteLine(serialized);
World reserialized = JsonSerializer.Deserialize<World>(serialized, Options)
?? throw new AssertionException("Failed to reserialize world");
// Resume the test case
setup = new(reserialized, MovementPhaseAdjudicator.Instance);
setup[("a", 1)]
["Germany"]
.Army("Mun").Supports.Army("Mun", season: reserialized.GetSeason("a0")).MoveTo("Tyr").GetReference(out var mun1)
["Austria"]
.Army("Tyr").Holds();
setup.ValidateOrders();
Assert.That(mun1, Is.Valid);
setup.AdjudicateOrders();
Assert.That(mun1, Is.NotCut);
Assert.That(mun0, Is.Victorious);
Assert.That(tyr0, Is.Dislodged);
// Confirm that an alternate future is created.
World world = setup.UpdateWorld();
Season fork = world.GetSeason("b1");
Unit tyr1 = world.GetUnitAt("Tyr", fork);
Assert.That(
tyr1.Past,
Is.EqualTo(mun0.Order.Unit.Designation),
"Expected A Mun a0 to advance to Tyr b1");
Assert.That(
world.RetreatingUnits.Count,
Is.EqualTo(1),
"Expected A Tyr a0 to be in retreat");
Assert.That(world.RetreatingUnits.First().Unit, Is.EqualTo(tyr0.Order.Unit));
}
}

View File

@ -19,7 +19,7 @@ public class TestCaseBuilder
/// <summary> /// <summary>
/// Choose a new season to define orders for. /// Choose a new season to define orders for.
/// </summary> /// </summary>
public ISeasonContext this[(int turn, int timeline) seasonCoord] { get; } public ISeasonContext this[(string timeline, int turn) 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[(int turn, int timeline) seasonCoord] { get; } public ISeasonContext this[(string timeline, int turn) 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[(int turn, int timeline) seasonCoord] { get; } public ISeasonContext this[(string timeline, int turn) 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[(0, 0)][powerName]; public IPowerContext this[string powerName] => this[("a", 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[(int turn, int timeline) seasonCoord] public ISeasonContext this[(string timeline, int turn) seasonCoord]
=> new SeasonContext(this, this.World.GetSeason(seasonCoord.turn, seasonCoord.timeline)); => new SeasonContext(this, this.World.GetSeason(seasonCoord.timeline, seasonCoord.turn));
/// <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
@ -262,7 +262,7 @@ public class TestCaseBuilder
foreach (Unit unit in this.World.Units) foreach (Unit unit in this.World.Units)
{ {
if (unit.Power == power if (unit.Power == power
&& unit.Province == location.Province && World.Map.GetLocation(unit).Province == location.Province
&& unit.Season == season) && unit.Season == season)
{ {
return unit; return unit;
@ -270,7 +270,7 @@ public class TestCaseBuilder
} }
// Not found // Not found
Unit newUnit = Unit.Build(location, season, power, type); Unit newUnit = Unit.Build(location.Designation, season, power, type);
this.World = this.World.Update(units: this.World.Units.Append(newUnit)); this.World = this.World.Update(units: this.World.Units.Append(newUnit));
return newUnit; return newUnit;
} }
@ -327,11 +327,11 @@ public class TestCaseBuilder
this.Season = season; this.Season = season;
} }
public ISeasonContext this[(int turn, int timeline) seasonCoord] public ISeasonContext this[(string timeline, int turn) seasonCoord]
=> this.Builder[(seasonCoord.turn, seasonCoord.timeline)]; => this.Builder[(seasonCoord.timeline, seasonCoord.turn)];
public IPowerContext this[string powerName] public IPowerContext this[string powerName]
=> new PowerContext(this, this.Builder.World.GetPower(powerName)); => new PowerContext(this, this.Builder.World.Map.GetPower(powerName));
public ISeasonContext GetReference(out Season season) public ISeasonContext GetReference(out Season season)
{ {
@ -353,7 +353,7 @@ public class TestCaseBuilder
this.Power = Power; this.Power = Power;
} }
public ISeasonContext this[(int turn, int timeline) seasonCoord] public ISeasonContext this[(string timeline, int turn) seasonCoord]
=> this.SeasonContext[seasonCoord]; => this.SeasonContext[seasonCoord];
public IPowerContext this[string powerName] public IPowerContext this[string powerName]
@ -363,8 +363,8 @@ public class TestCaseBuilder
{ {
Power power = powerName == null Power power = powerName == null
? this.Power ? this.Power
: this.Builder.World.GetPower(powerName); : this.Builder.World.Map.GetPower(powerName);
Location location = this.Builder.World.GetLand(provinceName); Location location = this.Builder.World.Map.GetLand(provinceName);
Unit unit = this.Builder.GetOrBuildUnit( Unit unit = this.Builder.GetOrBuildUnit(
power, location, this.SeasonContext.Season, UnitType.Army); power, location, this.SeasonContext.Season, UnitType.Army);
return new UnitContext(this, unit); return new UnitContext(this, unit);
@ -374,8 +374,8 @@ public class TestCaseBuilder
{ {
Power power = powerName == null Power power = powerName == null
? this.Power ? this.Power
: this.Builder.World.GetPower(powerName); : this.Builder.World.Map.GetPower(powerName);
Location location = this.Builder.World.GetWater(provinceName, coast); Location location = this.Builder.World.Map.GetWater(provinceName, coast);
Unit unit = this.Builder.GetOrBuildUnit( Unit unit = this.Builder.GetOrBuildUnit(
power, location, this.SeasonContext.Season, UnitType.Fleet); power, location, this.SeasonContext.Season, UnitType.Fleet);
return new UnitContext(this, unit); return new UnitContext(this, unit);
@ -413,8 +413,8 @@ public class TestCaseBuilder
string? coast = null) string? coast = null)
{ {
Location destination = this.Unit.Type == UnitType.Army Location destination = this.Unit.Type == UnitType.Army
? this.Builder.World.GetLand(provinceName) ? this.Builder.World.Map.GetLand(provinceName)
: this.Builder.World.GetWater(provinceName, coast); : this.Builder.World.Map.GetWater(provinceName, coast);
Season destSeason = season ?? this.SeasonContext.Season; Season destSeason = season ?? this.SeasonContext.Season;
MoveOrder moveOrder = new MoveOrder( MoveOrder moveOrder = new MoveOrder(
this.PowerContext.Power, this.PowerContext.Power,
@ -451,8 +451,8 @@ public class TestCaseBuilder
{ {
Power power = powerName == null Power power = powerName == null
? this.PowerContext.Power ? this.PowerContext.Power
: this.Builder.World.GetPower(powerName); : this.Builder.World.Map.GetPower(powerName);
Location location = this.Builder.World.GetLand(provinceName); Location location = this.Builder.World.Map.GetLand(provinceName);
Unit unit = this.Builder.GetOrBuildUnit( Unit unit = this.Builder.GetOrBuildUnit(
power, location, this.SeasonContext.Season, UnitType.Army); power, location, this.SeasonContext.Season, UnitType.Army);
return new ConvoyDestinationContext(this, unit); return new ConvoyDestinationContext(this, unit);
@ -465,8 +465,8 @@ public class TestCaseBuilder
{ {
Power power = powerName == null Power power = powerName == null
? this.PowerContext.Power ? this.PowerContext.Power
: this.Builder.World.GetPower(powerName); : this.Builder.World.Map.GetPower(powerName);
Location location = this.Builder.World.GetWater(provinceName, coast); Location location = this.Builder.World.Map.GetWater(provinceName, coast);
Unit unit = this.Builder.GetOrBuildUnit( Unit unit = this.Builder.GetOrBuildUnit(
power, location, this.SeasonContext.Season, UnitType.Fleet); power, location, this.SeasonContext.Season, UnitType.Fleet);
return new ConvoyDestinationContext(this, unit); return new ConvoyDestinationContext(this, unit);
@ -492,7 +492,7 @@ public class TestCaseBuilder
public IOrderDefinedContext<ConvoyOrder> To(string provinceName) public IOrderDefinedContext<ConvoyOrder> To(string provinceName)
{ {
Location location = this.Builder.World.GetLand(provinceName); Location location = this.Builder.World.Map.GetLand(provinceName);
ConvoyOrder order = new ConvoyOrder( ConvoyOrder order = new ConvoyOrder(
this.PowerContext.Power, this.PowerContext.Power,
this.UnitContext.Unit, this.UnitContext.Unit,
@ -526,8 +526,8 @@ public class TestCaseBuilder
{ {
Power power = powerName == null Power power = powerName == null
? this.PowerContext.Power ? this.PowerContext.Power
: this.Builder.World.GetPower(powerName); : this.Builder.World.Map.GetPower(powerName);
Location location = this.Builder.World.GetLand(provinceName); Location location = this.Builder.World.Map.GetLand(provinceName);
Season destSeason = season ?? this.SeasonContext.Season; Season destSeason = season ?? this.SeasonContext.Season;
Unit unit = this.Builder.GetOrBuildUnit( Unit unit = this.Builder.GetOrBuildUnit(
power, location, destSeason, UnitType.Army); power, location, destSeason, UnitType.Army);
@ -541,8 +541,8 @@ public class TestCaseBuilder
{ {
Power power = powerName == null Power power = powerName == null
? this.PowerContext.Power ? this.PowerContext.Power
: this.Builder.World.GetPower(powerName); : this.Builder.World.Map.GetPower(powerName);
Location location = this.Builder.World.GetWater(provinceName, coast); Location location = this.Builder.World.Map.GetWater(provinceName, coast);
Unit unit = this.Builder.GetOrBuildUnit( Unit unit = this.Builder.GetOrBuildUnit(
power, location, this.SeasonContext.Season, UnitType.Fleet); power, location, this.SeasonContext.Season, UnitType.Fleet);
return new SupportTypeContext(this, unit); return new SupportTypeContext(this, unit);
@ -582,8 +582,8 @@ public class TestCaseBuilder
string? coast = null) string? coast = null)
{ {
Location destination = this.Target.Type == UnitType.Army Location destination = this.Target.Type == UnitType.Army
? this.Builder.World.GetLand(provinceName) ? this.Builder.World.Map.GetLand(provinceName)
: this.Builder.World.GetWater(provinceName, coast); : this.Builder.World.Map.GetWater(provinceName, coast);
Season targetDestSeason = season ?? this.Target.Season; Season targetDestSeason = season ?? this.Target.Season;
SupportMoveOrder order = new SupportMoveOrder( SupportMoveOrder order = new SupportMoveOrder(
this.PowerContext.Power, this.PowerContext.Power,
@ -623,7 +623,7 @@ public class TestCaseBuilder
return this.Builder; return this.Builder;
} }
public ISeasonContext this[(int turn, int timeline) seasonCoord] public ISeasonContext this[(string timeline, int turn) seasonCoord]
=> this.SeasonContext[seasonCoord]; => this.SeasonContext[seasonCoord];
public IPowerContext this[string powerName] public IPowerContext this[string powerName]

View File

@ -14,7 +14,7 @@ class TestCaseBuilderTest
{ {
TestCaseBuilder setup = new(World.WithStandardMap()); TestCaseBuilder setup = new(World.WithStandardMap());
Assert.That(setup.World.Powers.Count(), Is.EqualTo(7), "Unexpected power count"); Assert.That(setup.World.Powers.Count, Is.EqualTo(7), "Unexpected power count");
Assert.That(setup.World.Units, Is.Empty, "Expected no units to be created yet"); Assert.That(setup.World.Units, Is.Empty, "Expected no units to be created yet");
setup setup
@ -40,7 +40,7 @@ class TestCaseBuilderTest
Assert.That(fleetSTP.Type, Is.EqualTo(UnitType.Fleet), "Unit created with wrong type"); Assert.That(fleetSTP.Type, Is.EqualTo(UnitType.Fleet), "Unit created with wrong type");
Assert.That( Assert.That(
fleetSTP.Location, fleetSTP.Location,
Is.EqualTo(setup.World.GetWater("STP", "wc")), Is.EqualTo(setup.World.Map.GetWater("STP", "wc").Designation),
"Unit created on wrong coast"); "Unit created on wrong coast");
} }
@ -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");
@ -68,13 +68,13 @@ class TestCaseBuilderTest
List<UnitOrder> orders = setup.Orders.OfType<UnitOrder>().ToList(); List<UnitOrder> orders = setup.Orders.OfType<UnitOrder>().ToList();
Func<UnitOrder, bool> OrderForProvince(string name) Func<UnitOrder, bool> OrderForProvince(string name)
=> order => order.Unit.Province.Name == name; => order => setup.World.Map.GetLocation(order.Unit).Province.Name == name;
UnitOrder orderBer = orders.Single(OrderForProvince("Berlin")); UnitOrder orderBer = orders.Single(OrderForProvince("Berlin"));
Assert.That(orderBer, Is.InstanceOf<MoveOrder>(), "Unexpected order type"); Assert.That(orderBer, Is.InstanceOf<MoveOrder>(), "Unexpected order type");
Assert.That( Assert.That(
(orderBer as MoveOrder)?.Location, (orderBer as MoveOrder)?.Location,
Is.EqualTo(setup.World.GetLand("Kiel")), Is.EqualTo(setup.World.Map.GetLand("Kiel")),
"Unexpected move order destination"); "Unexpected move order destination");
UnitOrder orderPru = orders.Single(OrderForProvince("Prussia")); UnitOrder orderPru = orders.Single(OrderForProvince("Prussia"));
@ -88,7 +88,7 @@ class TestCaseBuilderTest
"Unexpected convoy order target"); "Unexpected convoy order target");
Assert.That( Assert.That(
(orderNth as ConvoyOrder)?.Location, (orderNth as ConvoyOrder)?.Location,
Is.EqualTo(setup.World.GetLand("Holland")), Is.EqualTo(setup.World.Map.GetLand("Holland")),
"Unexpected convoy order destination"); "Unexpected convoy order destination");
UnitOrder orderKie = orders.Single(OrderForProvince("Kiel")); UnitOrder orderKie = orders.Single(OrderForProvince("Kiel"));
@ -99,7 +99,7 @@ class TestCaseBuilderTest
"Unexpected convoy order target"); "Unexpected convoy order target");
Assert.That( Assert.That(
(orderKie as SupportMoveOrder)?.Location, (orderKie as SupportMoveOrder)?.Location,
Is.EqualTo(setup.World.GetLand("Holland")), Is.EqualTo(setup.World.Map.GetLand("Holland")),
"Unexpected convoy order destination"); "Unexpected convoy order destination");
UnitOrder orderMun = orders.Single(OrderForProvince("Munich")); UnitOrder orderMun = orders.Single(OrderForProvince("Munich"));
@ -124,11 +124,11 @@ class TestCaseBuilderTest
Assert.That(orderMun, Is.Not.Null, "Expected order reference"); Assert.That(orderMun, Is.Not.Null, "Expected order reference");
Assert.That( Assert.That(
orderMun.Order.Power, orderMun.Order.Power,
Is.EqualTo(setup.World.GetPower("Germany")), Is.EqualTo(setup.World.Map.GetPower("Germany")),
"Wrong power"); "Wrong power");
Assert.That( Assert.That(
orderMun.Order.Unit.Location, orderMun.Order.Unit.Location,
Is.EqualTo(setup.World.GetLand("Mun")), Is.EqualTo(setup.World.Map.GetLand("Mun").Designation),
"Wrong unit"); "Wrong unit");
Assert.That( Assert.That(

View File

@ -10,29 +10,29 @@ public class UnitTests
public void MovementTest() public void MovementTest()
{ {
World world = World.WithStandardMap(); World world = World.WithStandardMap();
Location Mun = world.GetLand("Mun"), Location Mun = world.Map.GetLand("Mun"),
Boh = world.GetLand("Boh"), Boh = world.Map.GetLand("Boh"),
Tyr = world.GetLand("Tyr"); Tyr = world.Map.GetLand("Tyr");
Power pw1 = world.GetPower("Austria"); Power pw1 = world.Map.GetPower("Austria");
Season s1 = world.RootSeason; Season a0 = world.RootSeason;
Unit u1 = Unit.Build(Mun, s1, pw1, UnitType.Army); Unit u1 = Unit.Build(Mun.Designation, a0, pw1, UnitType.Army);
Season s2 = s1.MakeNext(); world = world.ContinueOrFork(a0, out Season a1);
Unit u2 = u1.Next(Boh, s2); Unit u2 = u1.Next(Boh.Designation, a1);
Season s3 = s2.MakeNext(); _ = world.ContinueOrFork(a1, out Season a2);
Unit u3 = u2.Next(Tyr, s3); Unit u3 = u2.Next(Tyr.Designation, a2);
Assert.That(u3.Past, Is.EqualTo(u2), "Missing unit past"); Assert.That(u3.Past, Is.EqualTo(u2.Designation), "Missing unit past");
Assert.That(u2.Past, Is.EqualTo(u1), "Missing unit past"); Assert.That(u2.Past, Is.EqualTo(u1.Designation), "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(s1), "Unexpected unit season"); Assert.That(u1.Season, Is.EqualTo(a0), "Unexpected unit season");
Assert.That(u2.Season, Is.EqualTo(s2), "Unexpected unit season"); Assert.That(u2.Season, Is.EqualTo(a1), "Unexpected unit season");
Assert.That(u3.Season, Is.EqualTo(s3), "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(u1.Location, Is.EqualTo(Mun.Designation), "Unexpected unit location");
Assert.That(u2.Location, Is.EqualTo(Boh), "Unexpected unit location"); Assert.That(u2.Location, Is.EqualTo(Boh.Designation), "Unexpected unit location");
Assert.That(u3.Location, Is.EqualTo(Tyr), "Unexpected unit location"); Assert.That(u3.Location, Is.EqualTo(Tyr.Designation), "Unexpected unit location");
} }
} }