Compare commits
26 Commits
5ad57465d8
...
7749d8df4e
Author | SHA1 | Date |
---|---|---|
Tim Van Baak | 7749d8df4e | |
Tim Van Baak | 53e208ec31 | |
Tim Van Baak | ce25329d27 | |
Tim Van Baak | 4df5ef84dc | |
Tim Van Baak | 7400334a3d | |
Tim Van Baak | b2ff8896b2 | |
Tim Van Baak | c9bd8c8194 | |
Tim Van Baak | 5989970c42 | |
Tim Van Baak | cc2c29980a | |
Tim Van Baak | 984676f587 | |
Tim Van Baak | fd8c725286 | |
Tim Van Baak | 0dec1e1eec | |
Tim Van Baak | 27ffaccd20 | |
Tim Van Baak | b17ce9485a | |
Tim Van Baak | 3242186208 | |
Tim Van Baak | 11dfa403e4 | |
Tim Van Baak | ae77c3c708 | |
Tim Van Baak | f5afb4105b | |
Tim Van Baak | 7d4f7760be | |
Tim Van Baak | 5dfe9a5bb5 | |
Tim Van Baak | 5472dda931 | |
Tim Van Baak | 9696919773 | |
Tim Van Baak | 3d48a7d6f6 | |
Tim Van Baak | d7c07d1ff1 | |
Tim Van Baak | dab37c239b | |
Tim Van Baak | 135997a7cd |
92
MDATC.html
92
MDATC.html
|
@ -236,17 +236,13 @@
|
|||
<a name="2.B"><h3>2.B. ORDER NOTATION</h3></a>
|
||||
<p>The order notation in this document is as in DATC, with the following additions for multiversal time travel.</p>
|
||||
<ul>
|
||||
<li>A season within a particular timeline is designated in the format X:Y, where X is the turn (starting from 0 and advancing with each movement phase) and Y is the timeline number (starting from 0 and advancing with each timeline fork).</li>
|
||||
<li>Adjudication is implied to be done between successive seasons. For example, if orders are listed for 0:0 and then for 1:0, it is implied that the orders for 0:0 were adjudicated.</li>
|
||||
<li>Units are designated by unit type, province, and season, e.g. "A Munich 1:0". A destination for a move order or support-to-move order is designated by province and season, e.g. "Munich 1:0".
|
||||
<li>Timelines are designated by letters, e.g. "a", "b". Turns are designated by numbers, e.g. 0, 1, 2. A board in a timeline X at turn N is designated XN. A location LOC on that board is designated X-LOC@N.</li>
|
||||
<li>In examples that cover multiple turns, orders are given in sets. Each order set is adjudicated before moving on to the next set.</li>
|
||||
<li>Units are fully designated by unit type and multiversal location, e.g. "A b-Munich@3". Destinations for move orders or support-to-move orders are fully designated by multiversal location, e.g. <code>a-Munich@1</code>. Where orders are not fully designated, the full designations are implied according to these rules:</li>
|
||||
<ul>
|
||||
<li>If season of the ordered unit is not specified, the season is the season to which the orders are being given.</li>
|
||||
<li>If the season of a unit supported to hold is not specified, the season is the same season as the supporting unit.</li>
|
||||
<li>If the season of the destination of a move order or the season of the destination of a supported move order is not specified, the season is the season of the moving unit.</li>
|
||||
<li>For example:
|
||||
<pre>Germany 2:0
|
||||
A Munich supports A Munich 1:1 - Tyrolia</pre>
|
||||
The order here is for Army Munich in 2:0. The move being supported is for Army Munich in 1:1 to move to Tyrolia in 1:1.</li>
|
||||
<li>If only the timeline of a location is specified, the turn is the latest turn in that timeline. E.g. if timeline "a" is at turn 2, <code>a-Munich</code> is interpreted as <code>a-Munich@2</code>.</li>
|
||||
<li>If the timeline or turn are unspecified for the target of a move or support-hold order, the timeline and turn are those of the ordered unit. E.g. if timeline "b" is at turn 1, <code>A b-Tyrolia - Munich</code> is interpreted as <code>b-Tyrolia@1 - b-Munich@1</code>.</li>
|
||||
<li>If only the province is specified for the target of a support-move order, the timeline and turn are those of the supported unit. E.g. if timeline "a" is at turn 2 and "b" at turn 1, <code>A a-Munich supports A b-Tyrolia - Munich</code> is interpreted as <code>A a-Munich@2 supports A b-Tyrolia@1 - b-Munich@1</code>.</li>
|
||||
</ul>
|
||||
</ul>
|
||||
|
||||
|
@ -258,13 +254,15 @@ The order here is for Army Munich in 2:0. The move being supported is for Army M
|
|||
<summary><h4><a href="#3.A.1">3.A.1</a>. TEST CASE, MOVE INTO OWN PAST FORKS TIMELINE</h4></summary>
|
||||
<p>A unit that moves into its own immediate past causes the timeline to fork.</p>
|
||||
<pre>
|
||||
Germany 0:0
|
||||
A Munich hold
|
||||
Germany:
|
||||
A a-Munich hold
|
||||
|
||||
Germany 1:0
|
||||
A Munich - Tyrolia 0:0
|
||||
---
|
||||
|
||||
Germany:
|
||||
A a-Munich - a-Tyrolia@0
|
||||
</pre>
|
||||
<p>A Munich 1:0 moves to Tyrolia 0:0. The main timeline advances to 2:0 with an empty board. A forked timeline advances to 1:1 with armies in Munich and Tyrolia.</p>
|
||||
<p>A a-Munich@1 moves to a-Tyrolia@0. The main timeline advances to a2 with an empty board. A forked timeline advances to b1 with armies in Munich and Tyrolia.</p>
|
||||
<div class="figures">
|
||||
<canvas id="canvas-3-A-1-before" width="0" height="0"></canvas>
|
||||
<script>
|
||||
|
@ -311,19 +309,21 @@ The order here is for Army Munich in 2:0. The move being supported is for Army M
|
|||
<summary><h4><a href="#3.A.2">3.A.2</a>. TEST CASE, SUPPORT TO REPELLED PAST MOVE FORKS TIMELINE</h4></summary>
|
||||
<p>A unit that supports a move that previously failed in the past, such that it now succeeds, causes the timeline to fork.</p>
|
||||
<pre>
|
||||
Austria 0:0
|
||||
Austria:
|
||||
A Tyrolia hold
|
||||
|
||||
Germany 0:0
|
||||
Germany:
|
||||
A Munich - Tyrolia
|
||||
|
||||
Austria 1:0
|
||||
---
|
||||
|
||||
Austria:
|
||||
A Tyrolia hold
|
||||
|
||||
Germany 1:0
|
||||
A Munich supports A Munich 0:0 - Tyrolia 0:0
|
||||
Germany:
|
||||
A Munich supports A a-Munich@0 - Tyrolia
|
||||
</pre>
|
||||
<p>With the support from A Munich 1:0, A Munich 0:0 dislodges A Tyrolia 0:0. A forked timeline advances to 1:1 where A Tyrolia 0:0 has been dislodged. The main timeline advances to 2:0 where A Munich and A Tyrolia are in their initial positions.</p>
|
||||
<p>With the support from A a-Munich@1, A a-Munich@0 dislodges A a-Tyrolia@0. A forked timeline advances to b1 where A Tyrolia has been dislodged. The main timeline advances to a2 where A Munich and A Tyrolia are in their initial positions.</p>
|
||||
<div class="figures">
|
||||
<canvas id="canvas-3-A-2-before" width="0" height="0"></canvas>
|
||||
<script>
|
||||
|
@ -379,19 +379,21 @@ The order here is for Army Munich in 2:0. The move being supported is for Army M
|
|||
<summary><h4><a href="#3.A.3">3.A.3</a>. TEST CASE, FAILED MOVE DOES NOT FORK TIMELINE</h4></summary>
|
||||
<p>A unit that attempts to move into the past, but is repelled, does not cause the timeline to fork.</p>
|
||||
<pre>
|
||||
Austria 0:0
|
||||
Austria:
|
||||
A Tyrolia hold
|
||||
|
||||
Germany 0:0
|
||||
Germany:
|
||||
A Munich hold
|
||||
|
||||
Austria 1:0
|
||||
---
|
||||
|
||||
Austria:
|
||||
A Tyrolia hold
|
||||
|
||||
Germany 1:0
|
||||
A Munich - Tyrolia 0:0
|
||||
Germany:
|
||||
A Munich - a-Tyrolia@0
|
||||
</pre>
|
||||
<p>The move by A Munich 1:0 fails. The main timeline advances to 2:0 with both armies in their initial positions. No alternate timeline is created.</p>
|
||||
<p>The move by A a-Munich@1 fails. The main timeline advances to a2 with both armies in their initial positions. No alternate timeline is created.</p>
|
||||
<div class="figures">
|
||||
<canvas id="canvas-3-A-3-before" width="0" height="0"></canvas>
|
||||
<script>
|
||||
|
@ -441,15 +443,17 @@ The order here is for Army Munich in 2:0. The move being supported is for Army M
|
|||
<summary><h4><a href="#3.A.4">3.A.4</a>. TEST CASE, SUPERFLUOUS SUPPORT DOES NOT FORK TIMELINE</h4></summary>
|
||||
<p>A unit that supports a move that succeeded in the past and still succeeds after the additional future support does not cause the timeline to fork.</p>
|
||||
<pre>
|
||||
Germany 0:0
|
||||
Germany:
|
||||
A Munich - Tyrolia
|
||||
A Bohemia hold
|
||||
|
||||
Germany 1:0
|
||||
---
|
||||
|
||||
Germany:
|
||||
A Tyrolia hold
|
||||
A Bohemia supports A Munich 0:0 - Tyrolia
|
||||
A Bohemia supports A a-Munich@0 - Tyrolia
|
||||
</pre>
|
||||
<p>Both units in 1:0 continue to 2:0. No alternate timeline is created.</p>
|
||||
<p>Both units in a1 continue to a2. No alternate timeline is created.</p>
|
||||
<div class="figures">
|
||||
<canvas id="canvas-3-A-4-before" width="0" height="0"></canvas>
|
||||
<script>
|
||||
|
@ -501,15 +505,15 @@ The order here is for Army Munich in 2:0. The move being supported is for Army M
|
|||
<summary><h4><a href="#3.A.5">3.A.5</a>. TEST CASE, CROSS-TIMELINE SUPPORT DOES NOT FORK HEAD</h4></summary>
|
||||
<p>In this test case, a unit elsewhere on the map moves into the past to cause a timeline fork. Once there are two parallel timelines, a support from one to the head of the other should not cause any forking, since timeline forks only occur when the past changes, not the present.</p>
|
||||
<pre>
|
||||
Austria
|
||||
A Tyrolia 2:0 hold
|
||||
A Tyrolia 1:1 hold
|
||||
Austria:
|
||||
A a-Tyrolia hold
|
||||
A b-Tyrolia hold
|
||||
|
||||
Germany
|
||||
A Munich 2:0 - Tyrolia
|
||||
A Munich 1:1 supports A Munich 2:0 - Tyrolia
|
||||
A a-Munich - Tyrolia
|
||||
A b-Munich supports A a-Munich - Tyrolia
|
||||
</pre>
|
||||
<p>A Munich 2:0 dislodges A Tyrolia 2:0. No alternate timeline is created.</p>
|
||||
<p>A a-Munich dislodges A a-Tyrolia. No alternate timeline is created.</p>
|
||||
<div class="figures">
|
||||
<canvas id="canvas-3-A-5-before" width="0" height="0"></canvas>
|
||||
<script>
|
||||
|
@ -567,19 +571,11 @@ The order here is for Army Munich in 2:0. The move being supported is for Army M
|
|||
<p>Following <a href="#3.A.5">3.A.5</a>, a cross-timeline support that previously succeeded is cut.</p>
|
||||
<pre>
|
||||
Germany
|
||||
A Munich 2:0 - Tyrolia
|
||||
A Munich 1:1 supports A Munich 2:0 - Tyrolia
|
||||
A a-Tyrolia holds
|
||||
A b-Munich holds
|
||||
|
||||
Austria
|
||||
A Tyrolia 2:0 holds
|
||||
A Tyrolia 1:1 holds
|
||||
|
||||
Germany
|
||||
A Tyrolia 3:0 holds
|
||||
A Munich 2:1 holds
|
||||
|
||||
Austria
|
||||
A Tyrolia 2:1 - Munich 1:1
|
||||
A b-Tyrolia@2 - b-Munich@1
|
||||
</pre>
|
||||
<p>Cutting the support does not change the past or cause a timeline fork.</p>
|
||||
<div class="figures">
|
||||
|
|
|
@ -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)
|
|
@ -61,25 +61,25 @@ public class MovementDecisions
|
|||
case MoveOrder move:
|
||||
AdvanceTimeline.Ensure(
|
||||
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);
|
||||
break;
|
||||
|
||||
case SupportHoldOrder supportHold:
|
||||
AdvanceTimeline.Ensure(
|
||||
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);
|
||||
break;
|
||||
|
||||
case SupportMoveOrder supportMove:
|
||||
AdvanceTimeline.Ensure(
|
||||
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.Ensure(
|
||||
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);
|
||||
break;
|
||||
}
|
||||
|
@ -91,19 +91,33 @@ public class MovementDecisions
|
|||
.Distinct()
|
||||
.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.
|
||||
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.
|
||||
foreach (UnitOrder order in relevantOrders)
|
||||
{
|
||||
// Create a dislodge decision for this unit.
|
||||
List<MoveOrder> incoming = relevantOrders
|
||||
.OfType<MoveOrder>()
|
||||
.Where(order.IsIncoming)
|
||||
.Where(other => IsIncoming(order, other))
|
||||
.ToList();
|
||||
IsDislodged[order.Unit] = new(order, incoming);
|
||||
|
||||
|
@ -118,7 +132,7 @@ public class MovementDecisions
|
|||
// Determine if this move is a head-to-head battle.
|
||||
MoveOrder? opposingMove = relevantOrders
|
||||
.OfType<MoveOrder>()
|
||||
.FirstOrDefault(other => other!.IsOpposing(move), null);
|
||||
.FirstOrDefault(other => AreOpposing(move, other!), null);
|
||||
|
||||
// Find competing moves.
|
||||
List<MoveOrder> competing = relevantOrders
|
||||
|
@ -142,11 +156,11 @@ public class MovementDecisions
|
|||
GivesSupport[support] = new(support, incoming);
|
||||
|
||||
// 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)
|
||||
{
|
||||
HoldStrength[support.Target.Point].Supports.Add(supportHold);
|
||||
HoldStrength[Point(support.Target)].Supports.Add(supportHold);
|
||||
}
|
||||
else if (support is SupportMoveOrder supportMove)
|
||||
{
|
||||
|
|
|
@ -50,7 +50,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
|
||||
// Invalidate any order given to a unit in the past.
|
||||
AdjudicatorHelpers.InvalidateIfNotMatching(
|
||||
order => !order.Unit.Season.Futures.Any(),
|
||||
order => !world.GetFutures(order.Unit.Season).Any(),
|
||||
ValidationReason.IneligibleForOrder,
|
||||
ref unitOrders,
|
||||
ref validationResults);
|
||||
|
@ -77,7 +77,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
|
||||
// Trivial check: a unit cannot move to where it already is.
|
||||
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,
|
||||
ref moveOrders,
|
||||
ref validationResults);
|
||||
|
@ -90,11 +90,11 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
ILookup<bool, MoveOrder> moveOrdersByAdjacency = moveOrders
|
||||
.ToLookup(order =>
|
||||
// Map adjacency
|
||||
order.Unit.Location.Adjacents.Contains(order.Location)
|
||||
world.Map.GetLocation(order.Unit).Adjacents.Contains(order.Location)
|
||||
// Turn adjacency
|
||||
&& Math.Abs(order.Unit.Season.Turn - order.Season.Turn) <= 1
|
||||
// Timeline adjacency
|
||||
&& order.Unit.Season.InAdjacentTimeline(order.Season));
|
||||
&& world.InAdjacentTimeline(order.Unit.Season, order.Season));
|
||||
List<MoveOrder> adjacentMoveOrders = moveOrdersByAdjacency[true].ToList();
|
||||
List<MoveOrder> nonAdjacentMoveOrders = moveOrdersByAdjacency[false].ToList();
|
||||
|
||||
|
@ -138,7 +138,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
// Trivial check: cannot convoy a unit to its own location
|
||||
AdjudicatorHelpers.InvalidateIfNotMatching(
|
||||
order => !(
|
||||
order.Location == order.Target.Location
|
||||
order.Location.Designation == order.Target.Location
|
||||
&& order.Season == order.Target.Season),
|
||||
ValidationReason.DestinationMatchesOrigin,
|
||||
ref convoyOrders,
|
||||
|
@ -175,12 +175,12 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
AdjudicatorHelpers.InvalidateIfNotMatching(
|
||||
order =>
|
||||
// Map adjacency with respect to province
|
||||
order.Unit.Location.Adjacents.Any(
|
||||
adjLocation => adjLocation.Province == order.Target.Province)
|
||||
world.Map.GetLocation(order.Unit).Adjacents.Any(
|
||||
adjLocation => adjLocation.Province == world.Map.GetLocation(order.Target).Province)
|
||||
// Turn adjacency
|
||||
&& Math.Abs(order.Unit.Season.Turn - order.Target.Season.Turn) <= 1
|
||||
// Timeline adjacency
|
||||
&& order.Unit.Season.InAdjacentTimeline(order.Target.Season),
|
||||
&& world.InAdjacentTimeline(order.Unit.Season, order.Target.Season),
|
||||
ValidationReason.UnreachableSupport,
|
||||
ref supportHoldOrders,
|
||||
ref validationResults);
|
||||
|
@ -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
|
||||
// province.
|
||||
AdjudicatorHelpers.InvalidateIfNotMatching(
|
||||
order => order.Unit.Province != order.Province,
|
||||
order => world.Map.GetLocation(order.Unit).Province != order.Province,
|
||||
ValidationReason.NoSupportMoveAgainstSelf,
|
||||
ref supportMoveOrders,
|
||||
ref validationResults);
|
||||
|
@ -207,12 +207,12 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
AdjudicatorHelpers.InvalidateIfNotMatching(
|
||||
order =>
|
||||
// Map adjacency with respect to province
|
||||
order.Unit.Location.Adjacents.Any(
|
||||
world.Map.GetLocation(order.Unit).Adjacents.Any(
|
||||
adjLocation => adjLocation.Province == order.Province)
|
||||
// Turn adjacency
|
||||
&& Math.Abs(order.Unit.Season.Turn - order.Season.Turn) <= 1
|
||||
// Timeline adjacency
|
||||
&& order.Unit.Season.InAdjacentTimeline(order.Season),
|
||||
&& world.InAdjacentTimeline(order.Unit.Season, order.Season),
|
||||
ValidationReason.UnreachableSupport,
|
||||
ref supportMoveOrders,
|
||||
ref validationResults);
|
||||
|
@ -255,7 +255,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
|
||||
// Finally, add implicit hold orders for units without legal orders.
|
||||
List<Unit> allOrderableUnits = world.Units
|
||||
.Where(unit => !unit.Season.Futures.Any())
|
||||
.Where(unit => !world.GetFutures(unit.Season).Any())
|
||||
.ToList();
|
||||
HashSet<Unit> orderedUnits = validOrders.Select(order => order.Unit).ToHashSet();
|
||||
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
|
||||
// record of when a future season has been created.
|
||||
Dictionary<Season, Season> createdFutures = new();
|
||||
List<Unit> createdUnits = new();
|
||||
List<RetreatingUnit> retreats = new();
|
||||
Dictionary<Season, Season> createdFutures = [];
|
||||
List<Unit> createdUnits = [];
|
||||
List<RetreatingUnit> retreats = [];
|
||||
|
||||
// Populate createdFutures with the timeline fork decisions
|
||||
logger.Log(1, "Processing AdvanceTimeline decisions");
|
||||
|
@ -324,9 +324,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
if (advanceTimeline.Outcome == true)
|
||||
{
|
||||
// A timeline that doesn't have a future yet simply continues. Otherwise, it forks.
|
||||
createdFutures[advanceTimeline.Season] = !advanceTimeline.Season.Futures.Any()
|
||||
? advanceTimeline.Season.MakeNext()
|
||||
: advanceTimeline.Season.MakeFork();
|
||||
createdFutures[advanceTimeline.Season] = world.ContinueOrFork(advanceTimeline.Season);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -339,7 +337,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
Season moveSeason = doesMove.Order.Season;
|
||||
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);
|
||||
createdUnits.Add(next);
|
||||
}
|
||||
|
@ -368,7 +366,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
if (isDislodged.Outcome == false)
|
||||
{
|
||||
// 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);
|
||||
createdUnits.Add(next);
|
||||
}
|
||||
|
@ -377,7 +375,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
// Create a retreat for each dislodged unit.
|
||||
// TODO check valid retreats and disbands
|
||||
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))
|
||||
.ToList();
|
||||
RetreatingUnit retreat = new(order.Unit, validRetreats);
|
||||
|
@ -386,11 +384,11 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
}
|
||||
|
||||
// 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))
|
||||
{
|
||||
newHistory.Ensure(unitOrder.Unit.Season, () => new());
|
||||
OrderHistory history = newHistory[unitOrder.Unit.Season];
|
||||
newHistory.Ensure(unitOrder.Unit.Season.Designation, () => new());
|
||||
OrderHistory history = newHistory[unitOrder.Unit.Season.Designation];
|
||||
// TODO does this add every order to every season??
|
||||
history.Orders.Add(unitOrder);
|
||||
history.IsDislodgedOutcomes[unitOrder.Unit] = dislodges[unitOrder.Unit].Outcome == true;
|
||||
|
@ -401,7 +399,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
}
|
||||
|
||||
// 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";
|
||||
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))
|
||||
.Concat(newHistory);
|
||||
|
||||
|
@ -486,7 +484,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
bool progress = false;
|
||||
|
||||
// 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");
|
||||
return progress;
|
||||
|
@ -496,7 +494,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
IEnumerable<MoveOrder> newIncomingMoves = decision.Orders
|
||||
.OfType<MoveOrder>()
|
||||
.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)
|
||||
{
|
||||
DoesMove doesMove = decisions.DoesMove[moveOrder];
|
||||
|
@ -513,7 +511,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
// 1. The outcome of a dislodge decision is changed,
|
||||
// 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.
|
||||
OrderHistory history = world.OrderHistory[decision.Season];
|
||||
OrderHistory history = world.OrderHistory[decision.Season.Designation];
|
||||
bool anyUnresolved = false;
|
||||
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 (// Map adjacency
|
||||
decision.Order.Unit.Location.Adjacents.Contains(decision.Order.Location)
|
||||
world.Map.GetLocation(decision.Order.Unit).Adjacents.Contains(decision.Order.Location)
|
||||
// Turn adjacency
|
||||
&& Math.Abs(decision.Order.Unit.Season.Turn - decision.Order.Season.Turn) <= 1
|
||||
// Timeline adjacency
|
||||
&& decision.Order.Unit.Season.InAdjacentTimeline(decision.Order.Season))
|
||||
&& world.InAdjacentTimeline(decision.Order.Unit.Season, decision.Order.Season))
|
||||
{
|
||||
bool update = LoggedUpdate(decision, true, depth, "Adjacent move");
|
||||
return progress | update;
|
||||
|
|
|
@ -30,13 +30,13 @@ public static class PathFinder
|
|||
// 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
|
||||
// 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)
|
||||
.ToDictionary(unit => (unit.Location, unit.Season));
|
||||
|
||||
// Verify that the origin is a coastal province.
|
||||
if (movingUnit.Location.Type != LocationType.Land) return false;
|
||||
IEnumerable<Location> originCoasts = movingUnit.Province.Locations
|
||||
if (world.Map.GetLocation(movingUnit).Type != LocationType.Land) return false;
|
||||
IEnumerable<Location> originCoasts = world.Map.GetLocation(movingUnit).Province.Locations
|
||||
.Where(location => location.Type == LocationType.Water);
|
||||
if (!originCoasts.Any()) return false;
|
||||
|
||||
|
@ -60,7 +60,7 @@ public static class PathFinder
|
|||
(Location currentLocation, Season currentSeason) = toVisit.Dequeue();
|
||||
visited.Add((currentLocation, currentSeason));
|
||||
|
||||
var adjacents = GetAdjacentPoints(currentLocation, currentSeason);
|
||||
var adjacents = GetAdjacentPoints(world, currentLocation, currentSeason);
|
||||
foreach ((Location adjLocation, Season adjSeason) in adjacents)
|
||||
{
|
||||
// If the destination is adjacent, then a path exists.
|
||||
|
@ -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,
|
||||
// and hasn't already been visited.
|
||||
if (!adjLocation.Province.Locations.Any(l => l.Type == LocationType.Land)
|
||||
&& fleets.ContainsKey((adjLocation, adjSeason))
|
||||
&& fleets.ContainsKey((adjLocation.Designation, adjSeason))
|
||||
&& !visited.Contains((adjLocation, adjSeason)))
|
||||
{
|
||||
toVisit.Enqueue((adjLocation, adjSeason));
|
||||
|
@ -81,11 +81,11 @@ public static class PathFinder
|
|||
return false;
|
||||
}
|
||||
|
||||
private static List<(Location, Season)> GetAdjacentPoints(Location location, Season season)
|
||||
private static List<(Location, Season)> GetAdjacentPoints(World world, Location location, Season season)
|
||||
{
|
||||
List<(Location, Season)> adjacentPoints = new();
|
||||
List<(Location, Season)> adjacentPoints = [];
|
||||
List<Location> adjacentLocations = location.Adjacents.ToList();
|
||||
List<Season> adjacentSeasons = season.GetAdjacentSeasons().ToList();
|
||||
List<Season> adjacentSeasons = GetAdjacentSeasons(world, season).ToList();
|
||||
|
||||
foreach (Location adjacentLocation in adjacentLocations)
|
||||
{
|
||||
|
@ -105,4 +105,56 @@ public static class PathFinder
|
|||
|
||||
return adjacentPoints;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all seasons that are adjacent to a season.
|
||||
/// </summary>
|
||||
public static IEnumerable<Season> GetAdjacentSeasons(World world, Season season)
|
||||
{
|
||||
List<Season> adjacents = [];
|
||||
|
||||
// The immediate past and all immediate futures are adjacent.
|
||||
if (season.Past != null) adjacents.Add(world.GetSeason(season.Past));
|
||||
adjacents.AddRange(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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
using CommandLine;
|
||||
|
||||
namespace MultiversalDiplomacy.CommandLine;
|
||||
|
||||
[Verb("adjudicate", HelpText = "Adjudicate a Multiversal Diplomacy game state.")]
|
||||
public class AdjudicateOptions
|
||||
{
|
||||
[Value(0, HelpText = "Input file describing the game state to adjudicate, or - to read from stdin.")]
|
||||
public string? InputFile { get; set; }
|
||||
|
||||
public static void Execute(AdjudicateOptions args)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
using CommandLine;
|
||||
|
||||
namespace MultiversalDiplomacy.CommandLine;
|
||||
|
||||
[Verb("image", HelpText = "Generate an image of a game state.")]
|
||||
public class ImageOptions
|
||||
{
|
||||
[Value(0, HelpText = "Input file describing the game state to visualize, or - to read from stdin.")]
|
||||
public string? InputFile { get; set; }
|
||||
|
||||
public static void Execute(ImageOptions args)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
using CommandLine;
|
||||
|
||||
namespace MultiversalDiplomacy.CommandLine;
|
||||
|
||||
[Verb("repl", HelpText = "Begin an interactive 5dplomacy session.")]
|
||||
public class ReplOptions
|
||||
{
|
||||
[Option('i', "input", HelpText = "Begin the repl session by executing the commands in this file.")]
|
||||
public string? InputFile { get; set; }
|
||||
|
||||
[Option('o', "output", HelpText = "Echo the repl session to this file. Specify a directory to autogenerate a filename.")]
|
||||
public string? OutputFile { get; set; }
|
||||
|
||||
public static void Execute(ReplOptions args)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MultiversalDiplomacy.Model;
|
||||
|
||||
/// <summary>
|
||||
|
@ -9,17 +11,21 @@ public class Location
|
|||
{
|
||||
/// <summary>
|
||||
/// The province to which this location belongs.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public Province Province { get; }
|
||||
|
||||
public string ProvinceName => Province.Name;
|
||||
|
||||
/// <summary>
|
||||
/// The location's full human-readable name.
|
||||
/// </summary>
|
||||
public string? Name { get; }
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The location's shorthand abbreviation.
|
||||
/// </summary>
|
||||
public string? Abbreviation { get; }
|
||||
public string Abbreviation { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The location's type.
|
||||
|
@ -29,10 +35,16 @@ public class Location
|
|||
/// <summary>
|
||||
/// The locations that border this location.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public IEnumerable<Location> Adjacents => this.AdjacentList;
|
||||
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.Name = name;
|
||||
|
@ -43,7 +55,7 @@ public class Location
|
|||
|
||||
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.Name}]";
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -20,4 +20,10 @@ public static class ModelExtensions
|
|||
{
|
||||
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));
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@ public class OrderHistory
|
|||
public Dictionary<MoveOrder, bool> DoesMoveOutcomes;
|
||||
|
||||
public OrderHistory()
|
||||
: this(new(), new(), new())
|
||||
: this([], [], [])
|
||||
{}
|
||||
|
||||
public OrderHistory(
|
||||
|
|
|
@ -37,7 +37,7 @@ public class Province
|
|||
this.Abbreviations = abbreviations;
|
||||
this.IsSupplyCenter = isSupply;
|
||||
this.IsTimeCenter = isTime;
|
||||
this.LocationList = new List<Location>();
|
||||
this.LocationList = [];
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
|
@ -49,26 +49,26 @@ public class Province
|
|||
/// Create a new province with no supply center.
|
||||
/// </summary>
|
||||
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>
|
||||
/// Create a new province with a supply center.
|
||||
/// </summary>
|
||||
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>
|
||||
/// Create a new province with a time center.
|
||||
/// </summary>
|
||||
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>
|
||||
/// Create a new land location in this province.
|
||||
/// </summary>
|
||||
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);
|
||||
return this;
|
||||
}
|
||||
|
@ -78,19 +78,7 @@ public class Province
|
|||
/// </summary>
|
||||
public Province AddOceanLocation()
|
||||
{
|
||||
Location location = new Location(this, name: null, abbreviation: null, 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);
|
||||
Location location = new(this, "water", "w", LocationType.Water);
|
||||
this.LocationList.Add(location);
|
||||
return this;
|
||||
}
|
||||
|
@ -101,7 +89,7 @@ public class Province
|
|||
/// </summary>
|
||||
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);
|
||||
return this;
|
||||
}
|
||||
|
|
|
@ -1,185 +1,41 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MultiversalDiplomacy.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a state of the map produced by a set of move orders on a previous season.
|
||||
/// </summary>
|
||||
public class Season
|
||||
public class Season(string? past, int turn, string timeline)
|
||||
{
|
||||
/// <summary>
|
||||
/// A shared counter for handing out new timeline numbers.
|
||||
/// </summary>
|
||||
private class TimelineFactory
|
||||
{
|
||||
private int nextTimeline = 0;
|
||||
|
||||
public int NextTimeline() => nextTimeline++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The first turn number.
|
||||
/// The first turn number. This is defined to reduce confusion about whether the first turn is 0 or 1.
|
||||
/// </summary>
|
||||
public const int FIRST_TURN = 0;
|
||||
|
||||
/// <summary>
|
||||
/// The season immediately preceding this season.
|
||||
/// The designation of the season immediately preceding this season.
|
||||
/// If this season is an alternate timeline root, the past is from the origin timeline.
|
||||
/// The initial season does not have a past.
|
||||
/// </summary>
|
||||
public Season? Past { get; }
|
||||
public string? Past { get; } = past;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// The current year is (Turn / 2) + 1901.
|
||||
/// </summary>
|
||||
public int Turn { get; }
|
||||
public int Turn { get; } = turn;
|
||||
|
||||
/// <summary>
|
||||
/// The timeline to which this season belongs.
|
||||
/// </summary>
|
||||
public int Timeline { get; }
|
||||
public string Timeline { get; } = timeline;
|
||||
|
||||
/// <summary>
|
||||
/// The season's spatial location as a turn-timeline tuple.
|
||||
/// The multiversal designation of this season.
|
||||
/// </summary>
|
||||
public (int Turn, int Timeline) Coord => (this.Turn, this.Timeline);
|
||||
[JsonIgnore]
|
||||
public string Designation => $"{this.Timeline}{this.Turn}";
|
||||
|
||||
/// <summary>
|
||||
/// 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;
|
||||
}
|
||||
public override string ToString() => Designation;
|
||||
}
|
||||
|
|
|
@ -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++);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -8,17 +8,12 @@ public class Unit
|
|||
/// <summary>
|
||||
/// The previous iteration of a unit. This is null if the unit was just built.
|
||||
/// </summary>
|
||||
public Unit? Past { get; }
|
||||
public string? Past { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The location on the map where the unit is.
|
||||
/// </summary>
|
||||
public Location Location { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The province where the unit is.
|
||||
/// </summary>
|
||||
public Province Province => this.Location.Province;
|
||||
public string Location { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The season in time when the unit is.
|
||||
|
@ -36,11 +31,11 @@ public class Unit
|
|||
public UnitType Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The unit's spatiotemporal location as a province-season tuple.
|
||||
/// A unique designation for this unit.
|
||||
/// </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.Location = location;
|
||||
|
@ -50,20 +45,18 @@ public class Unit
|
|||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{this.Power.Name[0]} {this.Type.ToShort()} {(this.Province, this.Season).ToShort()}";
|
||||
}
|
||||
=> $"{Power.Name[0]} {Type.ToShort()} {Season.Timeline}-{Location}@{Season.Turn}";
|
||||
|
||||
/// <summary>
|
||||
/// Create a new unit. No validation is performed; the adjudicator should only call this
|
||||
/// method after accepting a build order.
|
||||
/// </summary>
|
||||
public static Unit Build(Location location, Season season, Power power, UnitType type)
|
||||
=> new Unit(past: null, location, season, power, type);
|
||||
public static Unit Build(string location, Season season, Power power, UnitType type)
|
||||
=> new(past: null, location, season, power, type);
|
||||
|
||||
/// <summary>
|
||||
/// Advance this unit's timeline to a new location and season.
|
||||
/// </summary>
|
||||
public Unit Next(Location location, Season season)
|
||||
=> new Unit(past: this, location, season, this.Power, this.Type);
|
||||
public Unit Next(string location, Season season)
|
||||
=> new(past: this.Designation, location, season, this.Power, this.Type);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
using System.Collections.ObjectModel;
|
||||
|
||||
using MultiversalDiplomacy.Orders;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MultiversalDiplomacy.Model;
|
||||
|
||||
|
@ -9,67 +7,116 @@ namespace MultiversalDiplomacy.Model;
|
|||
/// </summary>
|
||||
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>
|
||||
/// The game map.
|
||||
/// </summary>
|
||||
public ReadOnlyCollection<Province> Provinces { get; }
|
||||
[JsonIgnore]
|
||||
public IReadOnlyCollection<Province> Provinces => this.Map.Provinces;
|
||||
|
||||
/// <summary>
|
||||
/// The game powers.
|
||||
/// </summary>
|
||||
public ReadOnlyCollection<Power> Powers { get; }
|
||||
[JsonIgnore]
|
||||
public IReadOnlyCollection<Power> Powers => this.Map.Powers;
|
||||
|
||||
/// <summary>
|
||||
/// The state of the multiverse.
|
||||
/// </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>
|
||||
/// The first season of the game.
|
||||
/// </summary>
|
||||
public Season RootSeason { get; }
|
||||
[JsonIgnore]
|
||||
public Season RootSeason => GetSeason("a0");
|
||||
|
||||
/// <summary>
|
||||
/// All units in the multiverse.
|
||||
/// </summary>
|
||||
public ReadOnlyCollection<Unit> Units { get; }
|
||||
public List<Unit> Units { get; }
|
||||
|
||||
/// <summary>
|
||||
/// All retreating units in the multiverse.
|
||||
/// </summary>
|
||||
public ReadOnlyCollection<RetreatingUnit> RetreatingUnits { get; }
|
||||
public List<RetreatingUnit> RetreatingUnits { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Orders given to units in each season.
|
||||
/// </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>
|
||||
/// Immutable game options.
|
||||
/// </summary>
|
||||
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>
|
||||
/// Create a new World, providing all state data.
|
||||
/// </summary>
|
||||
private World(
|
||||
ReadOnlyCollection<Province> provinces,
|
||||
ReadOnlyCollection<Power> powers,
|
||||
ReadOnlyCollection<Season> seasons,
|
||||
Season rootSeason,
|
||||
ReadOnlyCollection<Unit> units,
|
||||
ReadOnlyCollection<RetreatingUnit> retreatingUnits,
|
||||
ReadOnlyDictionary<Season, OrderHistory> orderHistory,
|
||||
Map map,
|
||||
List<Season> seasons,
|
||||
List<Unit> units,
|
||||
List<RetreatingUnit> retreatingUnits,
|
||||
Dictionary<string, OrderHistory> orderHistory,
|
||||
TimelineFactory timelines,
|
||||
Options options)
|
||||
{
|
||||
this.Provinces = provinces;
|
||||
this.Powers = powers;
|
||||
this.Map = map;
|
||||
this.Seasons = seasons;
|
||||
this.RootSeason = rootSeason;
|
||||
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>
|
||||
|
@ -77,21 +124,18 @@ public class World
|
|||
/// </summary>
|
||||
private World(
|
||||
World previous,
|
||||
ReadOnlyCollection<Province>? provinces = null,
|
||||
ReadOnlyCollection<Power>? powers = null,
|
||||
ReadOnlyCollection<Season>? seasons = null,
|
||||
ReadOnlyCollection<Unit>? units = null,
|
||||
ReadOnlyCollection<RetreatingUnit>? retreatingUnits = null,
|
||||
ReadOnlyDictionary<Season, OrderHistory>? orderHistory = null,
|
||||
List<Season>? seasons = null,
|
||||
List<Unit>? units = null,
|
||||
List<RetreatingUnit>? retreatingUnits = null,
|
||||
Dictionary<string, OrderHistory>? orderHistory = null,
|
||||
Options? options = null)
|
||||
: this(
|
||||
provinces ?? previous.Provinces,
|
||||
powers ?? previous.Powers,
|
||||
previous.Map,
|
||||
seasons ?? previous.Seasons,
|
||||
previous.RootSeason, // Can't change the root season
|
||||
units ?? previous.Units,
|
||||
retreatingUnits ?? previous.RetreatingUnits,
|
||||
orderHistory ?? previous.OrderHistory,
|
||||
previous.Timelines,
|
||||
options ?? previous.Options)
|
||||
{
|
||||
}
|
||||
|
@ -99,17 +143,16 @@ public class World
|
|||
/// <summary>
|
||||
/// Create a new world with specified provinces and powers and an initial season.
|
||||
/// </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(
|
||||
new(provinces.ToList()),
|
||||
new(powers.ToList()),
|
||||
new(new List<Season> { root }),
|
||||
root,
|
||||
new(new List<Unit>()),
|
||||
new(new List<RetreatingUnit>()),
|
||||
new(new Dictionary<Season, OrderHistory>()),
|
||||
map,
|
||||
new([new(past: null, Season.FIRST_TURN, timelines.NextTimeline())]),
|
||||
new([]),
|
||||
new([]),
|
||||
new(new Dictionary<string, OrderHistory>()),
|
||||
timelines,
|
||||
new Options());
|
||||
}
|
||||
|
||||
|
@ -117,14 +160,14 @@ public class World
|
|||
/// Create a new world with the standard Diplomacy provinces and powers.
|
||||
/// </summary>
|
||||
public static World WithStandardMap()
|
||||
=> WithMap(StandardProvinces, StandardPowers);
|
||||
=> WithMap(Map.Classical);
|
||||
|
||||
public World Update(
|
||||
IEnumerable<Season>? seasons = null,
|
||||
IEnumerable<Unit>? units = null,
|
||||
IEnumerable<RetreatingUnit>? retreats = null,
|
||||
IEnumerable<KeyValuePair<Season, OrderHistory>>? orders = null)
|
||||
=> new World(
|
||||
IEnumerable<KeyValuePair<string, OrderHistory>>? orders = null)
|
||||
=> new(
|
||||
previous: this,
|
||||
seasons: seasons == null
|
||||
? this.Seasons
|
||||
|
@ -149,7 +192,7 @@ public class World
|
|||
IEnumerable<Unit> units = unitSpecs.Select(spec =>
|
||||
{
|
||||
string[] splits = spec.Split(' ', 4);
|
||||
Power power = this.GetPower(splits[0]);
|
||||
Power power = Map.GetPower(splits[0]);
|
||||
UnitType type = splits[1] switch
|
||||
{
|
||||
"A" => UnitType.Army,
|
||||
|
@ -157,11 +200,11 @@ public class World
|
|||
_ => throw new ApplicationException($"Unknown unit type {splits[1]}")
|
||||
};
|
||||
Location location = type == UnitType.Army
|
||||
? this.GetLand(splits[2])
|
||||
? Map.GetLand(splits[2])
|
||||
: splits.Length == 3
|
||||
? this.GetWater(splits[2])
|
||||
: this.GetWater(splits[2], splits[3]);
|
||||
Unit unit = Unit.Build(location, this.RootSeason, power, type);
|
||||
? Map.GetWater(splits[2])
|
||||
: Map.GetWater(splits[2], splits[3]);
|
||||
Unit unit = Unit.Build(location.Designation, this.RootSeason, power, type);
|
||||
return unit;
|
||||
});
|
||||
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>
|
||||
/// A standard Diplomacy game setup.
|
||||
/// </summary>
|
||||
public static World Standard => World
|
||||
.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);
|
||||
public static World Standard => WithStandardMap().AddStandardUnits();
|
||||
|
||||
/// <summary>
|
||||
/// Get a season by coordinate. Throws if the season is not found.
|
||||
/// </summary>
|
||||
public Season GetSeason(int turn, int timeline)
|
||||
public Season GetSeason(string timeline, int turn)
|
||||
=> GetSeason($"{timeline}{turn}");
|
||||
|
||||
/// <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(
|
||||
s => s!.Turn == turn && s.Timeline == timeline,
|
||||
null);
|
||||
if (foundSeason == null) throw new KeyNotFoundException(
|
||||
$"Season {turn}:{timeline} not found");
|
||||
return foundSeason;
|
||||
if (season.Past is null) {
|
||||
return season;
|
||||
}
|
||||
Season past = SeasonLookup[season.Past];
|
||||
return season.Timeline == past.Timeline
|
||||
? GetTimelineRoot(past)
|
||||
: season;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
public Power GetPower(string powerName)
|
||||
public bool InAdjacentTimeline(Season one, Season two)
|
||||
{
|
||||
Power? foundPower = this.Powers.SingleOrDefault(
|
||||
p => p!.Name == powerName || p.Name.StartsWith(powerName),
|
||||
null);
|
||||
if (foundPower == null) throw new KeyNotFoundException(
|
||||
$"Power {powerName} not found");
|
||||
return foundPower;
|
||||
// Timelines are adjacent to themselves. Early out in that case.
|
||||
if (one == two) return true;
|
||||
|
||||
// If the timelines aren't identical, one of them isn't the initial trunk.
|
||||
// They can still be adjacent if one of them branched off of the other, or
|
||||
// if they both branched off of the same point.
|
||||
Season rootOne = GetTimelineRoot(one);
|
||||
Season rootTwo = GetTimelineRoot(two);
|
||||
bool oneForked = rootOne.Past != null && GetSeason(rootOne.Past).Timeline == two.Timeline;
|
||||
bool twoForked = rootTwo.Past != null && GetSeason(rootTwo.Past).Timeline == one.Timeline;
|
||||
bool bothForked = rootOne.Past == rootTwo.Past;
|
||||
return oneForked || twoForked || bothForked;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a unit in a province. Throws if there are duplicate units.
|
||||
/// </summary>
|
||||
public Unit GetUnitAt(string provinceName, (int turn, int timeline)? seasonCoord = null)
|
||||
public Unit GetUnitAt(string provinceName, Season? season = null)
|
||||
{
|
||||
Province province = GetProvince(provinceName);
|
||||
seasonCoord ??= (this.RootSeason.Turn, this.RootSeason.Timeline);
|
||||
Season season = GetSeason(seasonCoord.Value.turn, seasonCoord.Value.timeline);
|
||||
Province province = Map.GetProvince(provinceName);
|
||||
season ??= RootSeason;
|
||||
Unit? foundUnit = this.Units.SingleOrDefault(
|
||||
u => u!.Province == province && u.Season == season,
|
||||
null);
|
||||
if (foundUnit == null) throw new KeyNotFoundException(
|
||||
$"Unit at {province} at {season} not found");
|
||||
u => Map.GetLocation(u!).Province == province && u!.Season == season,
|
||||
null)
|
||||
?? throw new KeyNotFoundException($"Unit at {province} at {season} not found");
|
||||
return foundUnit;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The standard Diplomacy provinces.
|
||||
/// </summary>
|
||||
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"),
|
||||
});
|
||||
}
|
||||
public Unit GetUnitByDesignation(string designation)
|
||||
=> Units.SingleOrDefault(u => u!.Designation == designation, null)
|
||||
?? throw new KeyNotFoundException($"Unit {designation} not found");
|
||||
}
|
|
@ -7,4 +7,14 @@
|
|||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>MultiversalDiplomacyTests</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -39,15 +39,6 @@ public class MoveOrder : UnitOrder
|
|||
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>
|
||||
/// Returns whether another move order has the same destination as this order.
|
||||
/// </summary>
|
||||
|
|
|
@ -16,12 +16,4 @@ public abstract class UnitOrder : Order
|
|||
{
|
||||
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;
|
||||
}
|
|
@ -1,12 +1,23 @@
|
|||
using System;
|
||||
using CommandLine;
|
||||
|
||||
using MultiversalDiplomacy.CommandLine;
|
||||
|
||||
namespace MultiversalDiplomacy;
|
||||
|
||||
namespace MultiversalDiplomacy
|
||||
{
|
||||
internal class Program
|
||||
{
|
||||
static void Main(string[] args)
|
||||
{
|
||||
Console.WriteLine("stab");
|
||||
}
|
||||
var parser = Parser.Default;
|
||||
var parseResult = parser.ParseArguments(
|
||||
args,
|
||||
typeof(AdjudicateOptions),
|
||||
typeof(ImageOptions),
|
||||
typeof(ReplOptions));
|
||||
|
||||
parseResult
|
||||
.WithParsed<AdjudicateOptions>(AdjudicateOptions.Execute)
|
||||
.WithParsed<ImageOptions>(ImageOptions.Execute)
|
||||
.WithParsed<ReplOptions>(ReplOptions.Execute);
|
||||
}
|
||||
}
|
|
@ -14,12 +14,12 @@ public class TimeTravelTest
|
|||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance);
|
||||
|
||||
// Hold to move into the future, then move back into the past.
|
||||
setup[(0, 0)]
|
||||
setup[("a", 0)]
|
||||
.GetReference(out Season s0)
|
||||
["Germany"]
|
||||
.Army("Mun").Holds().GetReference(out var mun0)
|
||||
.Execute()
|
||||
[(1, 0)]
|
||||
[("a", 1)]
|
||||
.GetReference(out Season s1)
|
||||
["Germany"]
|
||||
.Army("Mun").MovesTo("Tyr", season: s0).GetReference(out var mun1);
|
||||
|
@ -41,17 +41,17 @@ public class TimeTravelTest
|
|||
Is.EqualTo(1),
|
||||
"Failed to fork timeline when unit moved in");
|
||||
|
||||
// Confirm that there is a unit in Tyr 1:1 originating from Mun 1:0
|
||||
Season fork = world.GetSeason(1, 1);
|
||||
Unit originalUnit = world.GetUnitAt("Mun", s0.Coord);
|
||||
Unit aMun0 = world.GetUnitAt("Mun", s1.Coord);
|
||||
Unit aTyr = world.GetUnitAt("Tyr", fork.Coord);
|
||||
Assert.That(aTyr.Past, Is.EqualTo(mun1.Order.Unit));
|
||||
Assert.That(aTyr.Past?.Past, Is.EqualTo(mun0.Order.Unit));
|
||||
// Confirm that there is a unit in Tyr b1 originating from Mun a1
|
||||
Season fork = world.GetSeason("b1");
|
||||
Unit originalUnit = world.GetUnitAt("Mun", s0);
|
||||
Unit aMun0 = world.GetUnitAt("Mun", s1);
|
||||
Unit aTyr = world.GetUnitAt("Tyr", fork);
|
||||
Assert.That(aTyr.Past, Is.EqualTo(mun1.Order.Unit.Designation));
|
||||
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
|
||||
Unit aMun1 = world.GetUnitAt("Mun", fork.Coord);
|
||||
Assert.That(aMun1.Past, Is.EqualTo(originalUnit));
|
||||
// Confirm that there is a unit in Mun b1 originating from Mun a0
|
||||
Unit aMun1 = world.GetUnitAt("Mun", fork);
|
||||
Assert.That(aMun1.Past, Is.EqualTo(originalUnit.Designation));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -60,7 +60,7 @@ public class TimeTravelTest
|
|||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance);
|
||||
|
||||
// Fail to dislodge on the first turn, then support the move so it succeeds.
|
||||
setup[(0, 0)]
|
||||
setup[("a", 0)]
|
||||
.GetReference(out Season s0)
|
||||
["Germany"]
|
||||
.Army("Mun").MovesTo("Tyr").GetReference(out var mun0)
|
||||
|
@ -75,7 +75,7 @@ public class TimeTravelTest
|
|||
Assert.That(tyr0, Is.NotDislodged);
|
||||
setup.UpdateWorld();
|
||||
|
||||
setup[(1, 0)]
|
||||
setup[("a", 1)]
|
||||
["Germany"]
|
||||
.Army("Mun").Supports.Army("Mun", season: s0).MoveTo("Tyr").GetReference(out var mun1)
|
||||
["Austria"]
|
||||
|
@ -91,16 +91,16 @@ public class TimeTravelTest
|
|||
|
||||
// Confirm that an alternate future is created.
|
||||
World world = setup.UpdateWorld();
|
||||
Season fork = world.GetSeason(1, 1);
|
||||
Unit tyr1 = world.GetUnitAt("Tyr", fork.Coord);
|
||||
Season fork = world.GetSeason("b1");
|
||||
Unit tyr1 = world.GetUnitAt("Tyr", fork);
|
||||
Assert.That(
|
||||
tyr1.Past,
|
||||
Is.EqualTo(mun0.Order.Unit),
|
||||
"Expected A Mun 0:0 to advance to Tyr 1:1");
|
||||
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 0:0 to be in retreat");
|
||||
"Expected A Tyr a0 to be in retreat");
|
||||
Assert.That(world.RetreatingUnits.First().Unit, Is.EqualTo(tyr0.Order.Unit));
|
||||
}
|
||||
|
||||
|
@ -110,14 +110,14 @@ public class TimeTravelTest
|
|||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance);
|
||||
|
||||
// Hold to create a future, then attempt to attack in the past.
|
||||
setup[(0, 0)]
|
||||
setup[("a", 0)]
|
||||
.GetReference(out Season s0)
|
||||
["Germany"]
|
||||
.Army("Mun").Holds()
|
||||
["Austria"]
|
||||
.Army("Tyr").Holds().GetReference(out var tyr0)
|
||||
.Execute()
|
||||
[(1, 0)]
|
||||
[("a", 1)]
|
||||
.GetReference(out Season s1)
|
||||
["Germany"]
|
||||
.Army("Mun").MovesTo("Tyr", season: s0).GetReference(out var mun1)
|
||||
|
@ -128,7 +128,7 @@ public class TimeTravelTest
|
|||
Assert.That(mun1, Is.Valid);
|
||||
setup.AdjudicateOrders();
|
||||
Assert.That(mun1, Is.Repelled);
|
||||
// The order to Tyr 0:0 should have been pulled into the adjudication set, so the reference
|
||||
// The order to Tyr a0 should have been pulled into the adjudication set, so the reference
|
||||
// should not throw when accessing it.
|
||||
Assert.That(tyr0, Is.NotDislodged);
|
||||
|
||||
|
@ -136,12 +136,12 @@ public class TimeTravelTest
|
|||
// change the past and therefore did not create a new timeline.
|
||||
World world = setup.UpdateWorld();
|
||||
Assert.That(
|
||||
s0.Futures.Count(),
|
||||
world.GetFutures(s0).Count(),
|
||||
Is.EqualTo(1),
|
||||
"A failed move incorrectly forked the timeline");
|
||||
Assert.That(s1.Futures.Count(), Is.EqualTo(1));
|
||||
Season s2 = world.GetSeason(s1.Turn + 1, s1.Timeline);
|
||||
Assert.That(s2.Futures.Count(), Is.EqualTo(0));
|
||||
Assert.That(world.GetFutures(s1).Count(), Is.EqualTo(1));
|
||||
Season s2 = world.GetSeason(s1.Timeline, s1.Turn + 1);
|
||||
Assert.That(world.GetFutures(s2).Count(), Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -150,7 +150,7 @@ public class TimeTravelTest
|
|||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance);
|
||||
|
||||
// Move, then support the past move even though it succeeded already.
|
||||
setup[(0, 0)]
|
||||
setup[("a", 0)]
|
||||
.GetReference(out Season s0)
|
||||
["Germany"]
|
||||
.Army("Mun").MovesTo("Tyr").GetReference(out var mun0)
|
||||
|
@ -162,7 +162,7 @@ public class TimeTravelTest
|
|||
Assert.That(mun0, Is.Victorious);
|
||||
setup.UpdateWorld();
|
||||
|
||||
setup[(1, 0)]
|
||||
setup[("a", 1)]
|
||||
.GetReference(out Season s1)
|
||||
["Germany"]
|
||||
.Army("Tyr").Holds()
|
||||
|
@ -178,12 +178,12 @@ public class TimeTravelTest
|
|||
// ...since it succeeded anyway, no fork is created.
|
||||
World world = setup.UpdateWorld();
|
||||
Assert.That(
|
||||
s0.Futures.Count(),
|
||||
world.GetFutures(s0).Count(),
|
||||
Is.EqualTo(1),
|
||||
"A superfluous support incorrectly forked the timeline");
|
||||
Assert.That(s1.Futures.Count(), Is.EqualTo(1));
|
||||
Season s2 = world.GetSeason(s1.Turn + 1, s1.Timeline);
|
||||
Assert.That(s2.Futures.Count(), Is.EqualTo(0));
|
||||
Assert.That(world.GetFutures(s1).Count(), Is.EqualTo(1));
|
||||
Season s2 = world.GetSeason(s1.Timeline, s1.Turn + 1);
|
||||
Assert.That(world.GetFutures(s2).Count(), Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -192,51 +192,51 @@ public class TimeTravelTest
|
|||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance);
|
||||
|
||||
// London creates two timelines by moving into the past.
|
||||
setup[(0, 0)]
|
||||
.GetReference(out var s0_0)
|
||||
setup[("a", 0)]
|
||||
.GetReference(out var a0)
|
||||
["England"].Army("Lon").Holds()
|
||||
["Austria"].Army("Tyr").Holds()
|
||||
["Germany"].Army("Mun").Holds()
|
||||
.Execute()
|
||||
[(1, 0)]
|
||||
["England"].Army("Lon").MovesTo("Yor", s0_0)
|
||||
[("a", 1)]
|
||||
["England"].Army("Lon").MovesTo("Yor", a0)
|
||||
.Execute()
|
||||
// Head seasons: 2:0 1:1
|
||||
// Head seasons: a2 b1
|
||||
// Both contain copies of the armies in Mun and Tyr.
|
||||
// Now Germany dislodges Austria in Tyr by supporting the move across timelines.
|
||||
[(2, 0)]
|
||||
.GetReference(out var s2_0)
|
||||
["Germany"].Army("Mun").MovesTo("Tyr").GetReference(out var mun2_0)
|
||||
["Austria"].Army("Tyr").Holds().GetReference(out var tyr2_0)
|
||||
[(1, 1)]
|
||||
.GetReference(out var s1_1)
|
||||
["Germany"].Army("Mun").Supports.Army("Mun", s2_0).MoveTo("Tyr").GetReference(out var mun1_1)
|
||||
[("a", 2)]
|
||||
.GetReference(out var a2)
|
||||
["Germany"].Army("Mun").MovesTo("Tyr").GetReference(out var mun_a2)
|
||||
["Austria"].Army("Tyr").Holds().GetReference(out var tyr_a2)
|
||||
[("b", 1)]
|
||||
.GetReference(out var b1)
|
||||
["Germany"].Army("Mun").Supports.Army("Mun", a2).MoveTo("Tyr").GetReference(out var mun_b1)
|
||||
["Austria"].Army("Tyr").Holds();
|
||||
|
||||
// The attack against Tyr 2:0 succeeds.
|
||||
// The attack against Tyr a2 succeeds.
|
||||
setup.ValidateOrders();
|
||||
Assert.That(mun2_0, Is.Valid);
|
||||
Assert.That(tyr2_0, Is.Valid);
|
||||
Assert.That(mun1_1, Is.Valid);
|
||||
Assert.That(mun_a2, Is.Valid);
|
||||
Assert.That(tyr_a2, Is.Valid);
|
||||
Assert.That(mun_b1, Is.Valid);
|
||||
setup.AdjudicateOrders();
|
||||
Assert.That(mun2_0, Is.Victorious);
|
||||
Assert.That(tyr2_0, Is.Dislodged);
|
||||
Assert.That(mun1_1, Is.NotCut);
|
||||
Assert.That(mun_a2, Is.Victorious);
|
||||
Assert.That(tyr_a2, Is.Dislodged);
|
||||
Assert.That(mun_b1, Is.NotCut);
|
||||
|
||||
// Since both seasons were at the head of their timelines, there should be no forking.
|
||||
World world = setup.UpdateWorld();
|
||||
Assert.That(
|
||||
s2_0.Futures.Count(),
|
||||
world.GetFutures(a2).Count(),
|
||||
Is.EqualTo(1),
|
||||
"A cross-timeline support incorrectly forked the head of the timeline");
|
||||
Assert.That(
|
||||
s1_1.Futures.Count(),
|
||||
world.GetFutures(b1).Count(),
|
||||
Is.EqualTo(1),
|
||||
"A cross-timeline support incorrectly forked the head of the timeline");
|
||||
Season s3_0 = world.GetSeason(s2_0.Turn + 1, s2_0.Timeline);
|
||||
Assert.That(s3_0.Futures.Count(), Is.EqualTo(0));
|
||||
Season s2_1 = world.GetSeason(s1_1.Turn + 1, s1_1.Timeline);
|
||||
Assert.That(s2_1.Futures.Count(), Is.EqualTo(0));
|
||||
Season a3 = world.GetSeason(a2.Timeline, a2.Turn + 1);
|
||||
Assert.That(world.GetFutures(a3).Count(), Is.EqualTo(0));
|
||||
Season b2 = world.GetSeason(b1.Timeline, b1.Turn + 1);
|
||||
Assert.That(world.GetFutures(b2).Count(), Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -245,63 +245,63 @@ public class TimeTravelTest
|
|||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance);
|
||||
|
||||
// As above, only now London creates three timelines.
|
||||
setup[(0, 0)]
|
||||
.GetReference(out var s0_0)
|
||||
setup[("a", 0)]
|
||||
.GetReference(out var a0)
|
||||
["England"].Army("Lon").Holds()
|
||||
["Austria"].Army("Boh").Holds()
|
||||
["Germany"].Army("Mun").Holds()
|
||||
.Execute()
|
||||
[(1, 0)]
|
||||
["England"].Army("Lon").MovesTo("Yor", s0_0)
|
||||
[("a", 1)]
|
||||
["England"].Army("Lon").MovesTo("Yor", a0)
|
||||
.Execute()
|
||||
// Head seasons: 2:0 1:1
|
||||
[(2, 0)]
|
||||
[(1, 1)]
|
||||
["England"].Army("Yor").MovesTo("Edi", s0_0)
|
||||
// Head seasons: a2, b1
|
||||
[("a", 2)]
|
||||
[("b", 1)]
|
||||
["England"].Army("Yor").MovesTo("Edi", a0)
|
||||
.Execute()
|
||||
// Head seasons: 3:0 2:1 1:2
|
||||
// Head seasons: a3, b2, c1
|
||||
// All three of these contain copies of the armies in Mun and Tyr.
|
||||
// As above, Germany dislodges Austria in Tyr 3:0 by supporting the move from 2:1.
|
||||
[(3, 0)]
|
||||
.GetReference(out var s3_0)
|
||||
// As above, Germany dislodges Austria in Tyr a3 by supporting the move from b2.
|
||||
[("a", 3)]
|
||||
.GetReference(out var a3)
|
||||
["Germany"].Army("Mun").MovesTo("Tyr")
|
||||
["Austria"].Army("Tyr").Holds()
|
||||
[(2, 1)]
|
||||
.GetReference(out Season s2_1)
|
||||
["Germany"].Army("Mun").Supports.Army("Mun", s3_0).MoveTo("Tyr").GetReference(out var mun2_1)
|
||||
[("b", 2)]
|
||||
.GetReference(out Season b2)
|
||||
["Germany"].Army("Mun").Supports.Army("Mun", a3).MoveTo("Tyr").GetReference(out var mun_b2)
|
||||
["Austria"].Army("Tyr").Holds()
|
||||
[(1, 2)]
|
||||
[("c", 1)]
|
||||
["Germany"].Army("Mun").Holds()
|
||||
["Austria"].Army("Tyr").Holds()
|
||||
.Execute()
|
||||
// Head seasons: 4:0 3:1 2:2
|
||||
// Now Austria cuts the support in 2:1 by attacking from 2:2.
|
||||
[(4, 0)]
|
||||
// Head seasons: a4, b3, c2
|
||||
// Now Austria cuts the support in b2 by attacking from c2.
|
||||
[("a", 4)]
|
||||
["Germany"].Army("Tyr").Holds()
|
||||
[(3, 1)]
|
||||
[("b", 3)]
|
||||
["Germany"].Army("Mun").Holds()
|
||||
["Austria"].Army("Tyr").Holds()
|
||||
[(2, 2)]
|
||||
[("c", 2)]
|
||||
["Germany"].Army("Mun").Holds()
|
||||
["Austria"].Army("Tyr").MovesTo("Mun", s2_1).GetReference(out var tyr2_2);
|
||||
["Austria"].Army("Tyr").MovesTo("Mun", b2).GetReference(out var tyr_c2);
|
||||
|
||||
// The attack on Mun 2:1 is repelled, but the support is cut.
|
||||
// The attack on Mun b2 is repelled, but the support is cut.
|
||||
setup.ValidateOrders();
|
||||
Assert.That(tyr2_2, Is.Valid);
|
||||
Assert.That(tyr_c2, Is.Valid);
|
||||
setup.AdjudicateOrders();
|
||||
Assert.That(tyr2_2, Is.Repelled);
|
||||
Assert.That(mun2_1, Is.NotDislodged);
|
||||
Assert.That(mun2_1, Is.Cut);
|
||||
Assert.That(tyr_c2, Is.Repelled);
|
||||
Assert.That(mun_b2, Is.NotDislodged);
|
||||
Assert.That(mun_b2, Is.Cut);
|
||||
|
||||
// Though the support was cut, the timeline doesn't fork because the outcome of a battle
|
||||
// wasn't changed in this timeline.
|
||||
World world = setup.UpdateWorld();
|
||||
Assert.That(
|
||||
s3_0.Futures.Count(),
|
||||
world.GetFutures(a3).Count(),
|
||||
Is.EqualTo(1),
|
||||
"A cross-timeline support cut incorrectly forked the timeline");
|
||||
Assert.That(
|
||||
s2_1.Futures.Count(),
|
||||
world.GetFutures(b2).Count(),
|
||||
Is.EqualTo(1),
|
||||
"A cross-timeline support cut incorrectly forked the timeline");
|
||||
}
|
||||
|
|
|
@ -6,10 +6,10 @@ namespace MultiversalDiplomacyTests;
|
|||
|
||||
public class MapTests
|
||||
{
|
||||
IEnumerable<Location> LocationClosure(Location location)
|
||||
static IEnumerable<Location> LocationClosure(Location location)
|
||||
{
|
||||
IEnumerable<Location> visited = new List<Location>();
|
||||
IEnumerable<Location> toVisit = new List<Location>() { location };
|
||||
IEnumerable<Location> visited = [];
|
||||
IEnumerable<Location> toVisit = [location];
|
||||
|
||||
while (toVisit.Any())
|
||||
{
|
||||
|
@ -50,7 +50,7 @@ public class MapTests
|
|||
[Test]
|
||||
public void LandAndSeaBorders()
|
||||
{
|
||||
World map = World.WithStandardMap();
|
||||
Map map = Map.Classical;
|
||||
Assert.That(
|
||||
map.GetLand("NAF").Adjacents.Count(),
|
||||
Is.EqualTo(1),
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
}
|
|
@ -171,8 +171,8 @@ public class MovementAdjudicatorTest
|
|||
// Confirm the future was created
|
||||
Assert.That(updated.Seasons.Count, Is.EqualTo(2));
|
||||
Season future = updated.Seasons.Single(s => s != updated.RootSeason);
|
||||
Assert.That(future.Past, Is.EqualTo(updated.RootSeason));
|
||||
Assert.That(future.Futures, Is.Empty);
|
||||
Assert.That(future.Past, Is.EqualTo(updated.RootSeason.ToString()));
|
||||
Assert.That(updated.GetFutures(future), Is.Empty);
|
||||
Assert.That(future.Timeline, Is.EqualTo(updated.RootSeason.Timeline));
|
||||
Assert.That(future.Turn, Is.EqualTo(Season.FIRST_TURN + 1));
|
||||
|
||||
|
@ -186,7 +186,7 @@ public class MovementAdjudicatorTest
|
|||
public void Update_DoubleHold()
|
||||
{
|
||||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance);
|
||||
setup[(0, 0)]
|
||||
setup[("a", 0)]
|
||||
.GetReference(out Season s1)
|
||||
["Germany"]
|
||||
.Army("Mun").Holds().GetReference(out var mun1);
|
||||
|
@ -199,20 +199,20 @@ public class MovementAdjudicatorTest
|
|||
World updated = setup.UpdateWorld();
|
||||
|
||||
// Confirm the future was created
|
||||
Season s2 = updated.GetSeason(1, 0);
|
||||
Assert.That(s2.Past, Is.EqualTo(s1));
|
||||
Assert.That(s2.Futures, Is.Empty);
|
||||
Season s2 = updated.GetSeason("a1");
|
||||
Assert.That(s2.Past, Is.EqualTo(s1.ToString()));
|
||||
Assert.That(updated.GetFutures(s2), Is.Empty);
|
||||
Assert.That(s2.Timeline, Is.EqualTo(s1.Timeline));
|
||||
Assert.That(s2.Turn, Is.EqualTo(s1.Turn + 1));
|
||||
|
||||
// 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(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));
|
||||
|
||||
setup[(1, 0)]
|
||||
setup[("a", 1)]
|
||||
["Germany"]
|
||||
.Army("Mun").Holds().GetReference(out var mun2);
|
||||
|
||||
|
@ -227,16 +227,16 @@ public class MovementAdjudicatorTest
|
|||
|
||||
// Update the world again
|
||||
updated = setup.UpdateWorld();
|
||||
Season s3 = updated.GetSeason(s2.Turn + 1, s2.Timeline);
|
||||
Unit u3 = updated.GetUnitAt("Mun", s3.Coord);
|
||||
Assert.That(u3.Past, Is.EqualTo(mun2.Order.Unit));
|
||||
Season s3 = updated.GetSeason(s2.Timeline, s2.Turn + 1);
|
||||
Unit u3 = updated.GetUnitAt("Mun", s3);
|
||||
Assert.That(u3.Past, Is.EqualTo(mun2.Order.Unit.Designation));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Update_DoubleMove()
|
||||
{
|
||||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance);
|
||||
setup[(0, 0)]
|
||||
setup[("a", 0)]
|
||||
.GetReference(out Season s1)
|
||||
["Germany"]
|
||||
.Army("Mun").MovesTo("Tyr").GetReference(out var mun1);
|
||||
|
@ -249,20 +249,20 @@ public class MovementAdjudicatorTest
|
|||
World updated = setup.UpdateWorld();
|
||||
|
||||
// Confirm the future was created
|
||||
Season s2 = updated.GetSeason(s1.Turn + 1, s1.Timeline);
|
||||
Assert.That(s2.Past, Is.EqualTo(s1));
|
||||
Assert.That(s2.Futures, Is.Empty);
|
||||
Season s2 = updated.GetSeason(s1.Timeline, s1.Turn + 1);
|
||||
Assert.That(s2.Past, Is.EqualTo(s1.ToString()));
|
||||
Assert.That(updated.GetFutures(s2), Is.Empty);
|
||||
Assert.That(s2.Timeline, Is.EqualTo(s1.Timeline));
|
||||
Assert.That(s2.Turn, Is.EqualTo(s1.Turn + 1));
|
||||
|
||||
// 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(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));
|
||||
|
||||
setup[(1, 0)]
|
||||
setup[("a", 1)]
|
||||
["Germany"]
|
||||
.Army("Tyr").MovesTo("Mun").GetReference(out var tyr2);
|
||||
|
||||
|
@ -277,8 +277,8 @@ public class MovementAdjudicatorTest
|
|||
|
||||
// Update the world again
|
||||
updated = setup.UpdateWorld();
|
||||
Season s3 = updated.GetSeason(s2.Turn + 1, s2.Timeline);
|
||||
Unit u3 = updated.GetUnitAt("Mun", s3.Coord);
|
||||
Assert.That(u3.Past, Is.EqualTo(u2));
|
||||
Season s3 = updated.GetSeason(s2.Timeline, s2.Turn + 1);
|
||||
Unit u3 = updated.GetUnitAt("Mun", s3);
|
||||
Assert.That(u3.Past, Is.EqualTo(u2.Designation));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -108,8 +108,7 @@ public abstract class OrderReference
|
|||
DefendStrength defend => defend.Order == this.Order,
|
||||
PreventStrength prevent => prevent.Order == this.Order,
|
||||
HoldStrength hold => this.Order is UnitOrder unitOrder
|
||||
? hold.Province == unitOrder.Unit.Province
|
||||
: false,
|
||||
&& hold.Province == Builder.World.Map.GetLocation(unitOrder.Unit).Province,
|
||||
_ => false,
|
||||
}).ToList();
|
||||
return adjudications;
|
||||
|
|
|
@ -9,57 +9,54 @@ public class SeasonTests
|
|||
[Test]
|
||||
public void TimelineForking()
|
||||
{
|
||||
Season a0 = Season.MakeRoot();
|
||||
Season a1 = a0.MakeNext();
|
||||
Season a2 = a1.MakeNext();
|
||||
Season a3 = a2.MakeNext();
|
||||
Season b1 = a1.MakeFork();
|
||||
Season b2 = b1.MakeNext();
|
||||
Season c1 = a1.MakeFork();
|
||||
Season d1 = a2.MakeFork();
|
||||
World world = World.WithMap(Map.Test);
|
||||
Season a0 = world.GetSeason("a0");
|
||||
world = world
|
||||
.ContinueOrFork(a0, out Season a1)
|
||||
.ContinueOrFork(a1, out Season a2)
|
||||
.ContinueOrFork(a2, out Season a3)
|
||||
.ContinueOrFork(a1, out Season b2)
|
||||
.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(a1.Timeline, Is.EqualTo(0), "Unexpected trunk timeline number");
|
||||
Assert.That(a2.Timeline, Is.EqualTo(0), "Unexpected trunk timeline number");
|
||||
Assert.That(a3.Timeline, Is.EqualTo(0), "Unexpected trunk timeline number");
|
||||
Assert.That(b1.Timeline, Is.EqualTo(1), "Unexpected first alt number");
|
||||
Assert.That(b2.Timeline, Is.EqualTo(1), "Unexpected first alt number");
|
||||
Assert.That(c1.Timeline, Is.EqualTo(2), "Unexpected second alt number");
|
||||
Assert.That(d1.Timeline, Is.EqualTo(3), "Unexpected third alt number");
|
||||
Assert.That(
|
||||
world.Seasons.Select(season => season.ToString()),
|
||||
Is.EquivalentTo(new List<string> { "a0", "a1", "a2", "a3", "b2", "b3", "c2", "d3" }),
|
||||
"Unexpected seasons");
|
||||
|
||||
Assert.That(a0.Timeline, Is.EqualTo("a"), "Unexpected trunk timeline");
|
||||
Assert.That(a1.Timeline, Is.EqualTo("a"), "Unexpected trunk timeline");
|
||||
Assert.That(a2.Timeline, Is.EqualTo("a"), "Unexpected trunk timeline");
|
||||
Assert.That(a3.Timeline, Is.EqualTo("a"), "Unexpected trunk timeline");
|
||||
Assert.That(b2.Timeline, Is.EqualTo("b"), "Unexpected first alt");
|
||||
Assert.That(b3.Timeline, Is.EqualTo("b"), "Unexpected first alt");
|
||||
Assert.That(c2.Timeline, Is.EqualTo("c"), "Unexpected second alt");
|
||||
Assert.That(d3.Timeline, Is.EqualTo("d"), "Unexpected third alt");
|
||||
|
||||
Assert.That(a0.Turn, Is.EqualTo(Season.FIRST_TURN + 0), "Unexpected first turn number");
|
||||
Assert.That(a1.Turn, Is.EqualTo(Season.FIRST_TURN + 1), "Unexpected next turn number");
|
||||
Assert.That(a2.Turn, Is.EqualTo(Season.FIRST_TURN + 2), "Unexpected next turn number");
|
||||
Assert.That(a3.Turn, Is.EqualTo(Season.FIRST_TURN + 3), "Unexpected next turn number");
|
||||
Assert.That(b1.Turn, Is.EqualTo(Season.FIRST_TURN + 2), "Unexpected fork turn number");
|
||||
Assert.That(b2.Turn, Is.EqualTo(Season.FIRST_TURN + 3), "Unexpected fork turn number");
|
||||
Assert.That(c1.Turn, Is.EqualTo(Season.FIRST_TURN + 2), "Unexpected fork turn number");
|
||||
Assert.That(d1.Turn, Is.EqualTo(Season.FIRST_TURN + 3), "Unexpected fork turn number");
|
||||
Assert.That(b2.Turn, Is.EqualTo(Season.FIRST_TURN + 2), "Unexpected fork turn number");
|
||||
Assert.That(b3.Turn, Is.EqualTo(Season.FIRST_TURN + 3), "Unexpected fork turn number");
|
||||
Assert.That(c2.Turn, Is.EqualTo(Season.FIRST_TURN + 2), "Unexpected fork turn number");
|
||||
Assert.That(d3.Turn, Is.EqualTo(Season.FIRST_TURN + 3), "Unexpected fork turn number");
|
||||
|
||||
Assert.That(a0.TimelineRoot(), Is.EqualTo(a0), "Expected timeline root to be reflexive");
|
||||
Assert.That(a3.TimelineRoot(), Is.EqualTo(a0), "Expected trunk timeline to have root");
|
||||
Assert.That(b1.TimelineRoot(), Is.EqualTo(b1), "Expected alt timeline root to be reflexive");
|
||||
Assert.That(b2.TimelineRoot(), Is.EqualTo(b1), "Expected alt timeline to root at first fork");
|
||||
Assert.That(c1.TimelineRoot(), Is.EqualTo(c1), "Expected alt timeline root to be reflexive");
|
||||
Assert.That(d1.TimelineRoot(), Is.EqualTo(d1), "Expected alt timeline root to be reflexive");
|
||||
Assert.That(world.GetTimelineRoot(a0), Is.EqualTo(a0), "Expected timeline root to be reflexive");
|
||||
Assert.That(world.GetTimelineRoot(a3), Is.EqualTo(a0), "Expected trunk timeline to have root");
|
||||
Assert.That(world.GetTimelineRoot(b2), Is.EqualTo(b2), "Expected alt timeline root to be reflexive");
|
||||
Assert.That(world.GetTimelineRoot(b3), Is.EqualTo(b2), "Expected alt timeline to root at first fork");
|
||||
Assert.That(world.GetTimelineRoot(c2), Is.EqualTo(c2), "Expected alt timeline root to be reflexive");
|
||||
Assert.That(world.GetTimelineRoot(d3), Is.EqualTo(d3), "Expected alt timeline root to be reflexive");
|
||||
|
||||
Assert.That(b2.InAdjacentTimeline(a3), Is.True, "Expected alts to be adjacent to origin");
|
||||
Assert.That(b2.InAdjacentTimeline(c1), Is.True, "Expected alts with common origin to be adjacent");
|
||||
Assert.That(b2.InAdjacentTimeline(d1), Is.False, "Expected alts from different origins not to be adjacent");
|
||||
}
|
||||
Assert.That(world.InAdjacentTimeline(b3, a3), Is.True, "Expected alts to be adjacent to origin");
|
||||
Assert.That(world.InAdjacentTimeline(b3, c2), Is.True, "Expected alts with common origin to be adjacent");
|
||||
Assert.That(world.InAdjacentTimeline(b3, d3), Is.False, "Expected alts from different origins not to be adjacent");
|
||||
|
||||
[Test]
|
||||
public void LookupTest()
|
||||
{
|
||||
World world = World.WithStandardMap();
|
||||
Season s2 = world.RootSeason.MakeNext();
|
||||
Season s3 = s2.MakeNext();
|
||||
Season s4 = s2.MakeFork();
|
||||
World updated = world.Update(seasons: world.Seasons.Append(s2).Append(s3).Append(s4));
|
||||
|
||||
Assert.That(updated.GetSeason(Season.FIRST_TURN, 0), Is.EqualTo(updated.RootSeason));
|
||||
Assert.That(updated.GetSeason(Season.FIRST_TURN + 1, 0), Is.EqualTo(s2));
|
||||
Assert.That(updated.GetSeason(Season.FIRST_TURN + 2, 0), Is.EqualTo(s3));
|
||||
Assert.That(updated.GetSeason(Season.FIRST_TURN + 2, 1), Is.EqualTo(s4));
|
||||
Assert.That(world.GetFutures(a0), Is.EquivalentTo(new List<Season> { a1 }), "Unexpected futures");
|
||||
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");
|
||||
Assert.That(world.GetFutures(b2), Is.EquivalentTo(new List<Season> { b3 }), "Unexpected futures");
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -19,7 +19,7 @@ public class TestCaseBuilder
|
|||
/// <summary>
|
||||
/// Choose a new season to define orders for.
|
||||
/// </summary>
|
||||
public ISeasonContext this[(int turn, int timeline) seasonCoord] { get; }
|
||||
public ISeasonContext this[(string timeline, int turn) seasonCoord] { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get the context for defining the orders for a power.
|
||||
|
@ -40,7 +40,7 @@ public class TestCaseBuilder
|
|||
/// <summary>
|
||||
/// Choose a new season to define orders for.
|
||||
/// </summary>
|
||||
public ISeasonContext this[(int turn, int timeline) seasonCoord] { get; }
|
||||
public ISeasonContext this[(string timeline, int turn) seasonCoord] { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get the context for defining the orders for another power.
|
||||
|
@ -188,7 +188,7 @@ public class TestCaseBuilder
|
|||
/// <summary>
|
||||
/// Choose a new season to define orders for.
|
||||
/// </summary>
|
||||
public ISeasonContext this[(int turn, int timeline) seasonCoord] { get; }
|
||||
public ISeasonContext this[(string timeline, int turn) seasonCoord] { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get the context for defining the orders for another power.
|
||||
|
@ -234,13 +234,13 @@ public class TestCaseBuilder
|
|||
/// <summary>
|
||||
/// Get the context for defining the orders for a power. Defaults to the root season.
|
||||
/// </summary>
|
||||
public IPowerContext this[string powerName] => this[(0, 0)][powerName];
|
||||
public IPowerContext this[string powerName] => this[("a", 0)][powerName];
|
||||
|
||||
/// <summary>
|
||||
/// Get the context for defining the orders for a season.
|
||||
/// </summary>
|
||||
public ISeasonContext this[(int turn, int timeline) seasonCoord]
|
||||
=> new SeasonContext(this, this.World.GetSeason(seasonCoord.turn, seasonCoord.timeline));
|
||||
public ISeasonContext this[(string timeline, int turn) seasonCoord]
|
||||
=> new SeasonContext(this, this.World.GetSeason(seasonCoord.timeline, seasonCoord.turn));
|
||||
|
||||
/// <summary>
|
||||
/// Get a unit matching a description. If no such unit exists, one is created and added to the
|
||||
|
@ -262,7 +262,7 @@ public class TestCaseBuilder
|
|||
foreach (Unit unit in this.World.Units)
|
||||
{
|
||||
if (unit.Power == power
|
||||
&& unit.Province == location.Province
|
||||
&& World.Map.GetLocation(unit).Province == location.Province
|
||||
&& unit.Season == season)
|
||||
{
|
||||
return unit;
|
||||
|
@ -270,7 +270,7 @@ public class TestCaseBuilder
|
|||
}
|
||||
|
||||
// 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));
|
||||
return newUnit;
|
||||
}
|
||||
|
@ -327,11 +327,11 @@ public class TestCaseBuilder
|
|||
this.Season = season;
|
||||
}
|
||||
|
||||
public ISeasonContext this[(int turn, int timeline) seasonCoord]
|
||||
=> this.Builder[(seasonCoord.turn, seasonCoord.timeline)];
|
||||
public ISeasonContext this[(string timeline, int turn) seasonCoord]
|
||||
=> this.Builder[(seasonCoord.timeline, seasonCoord.turn)];
|
||||
|
||||
public IPowerContext this[string powerName]
|
||||
=> new PowerContext(this, this.Builder.World.GetPower(powerName));
|
||||
=> new PowerContext(this, this.Builder.World.Map.GetPower(powerName));
|
||||
|
||||
public ISeasonContext GetReference(out Season season)
|
||||
{
|
||||
|
@ -353,7 +353,7 @@ public class TestCaseBuilder
|
|||
this.Power = Power;
|
||||
}
|
||||
|
||||
public ISeasonContext this[(int turn, int timeline) seasonCoord]
|
||||
public ISeasonContext this[(string timeline, int turn) seasonCoord]
|
||||
=> this.SeasonContext[seasonCoord];
|
||||
|
||||
public IPowerContext this[string powerName]
|
||||
|
@ -363,8 +363,8 @@ public class TestCaseBuilder
|
|||
{
|
||||
Power power = powerName == null
|
||||
? this.Power
|
||||
: this.Builder.World.GetPower(powerName);
|
||||
Location location = this.Builder.World.GetLand(provinceName);
|
||||
: this.Builder.World.Map.GetPower(powerName);
|
||||
Location location = this.Builder.World.Map.GetLand(provinceName);
|
||||
Unit unit = this.Builder.GetOrBuildUnit(
|
||||
power, location, this.SeasonContext.Season, UnitType.Army);
|
||||
return new UnitContext(this, unit);
|
||||
|
@ -374,8 +374,8 @@ public class TestCaseBuilder
|
|||
{
|
||||
Power power = powerName == null
|
||||
? this.Power
|
||||
: this.Builder.World.GetPower(powerName);
|
||||
Location location = this.Builder.World.GetWater(provinceName, coast);
|
||||
: this.Builder.World.Map.GetPower(powerName);
|
||||
Location location = this.Builder.World.Map.GetWater(provinceName, coast);
|
||||
Unit unit = this.Builder.GetOrBuildUnit(
|
||||
power, location, this.SeasonContext.Season, UnitType.Fleet);
|
||||
return new UnitContext(this, unit);
|
||||
|
@ -413,8 +413,8 @@ public class TestCaseBuilder
|
|||
string? coast = null)
|
||||
{
|
||||
Location destination = this.Unit.Type == UnitType.Army
|
||||
? this.Builder.World.GetLand(provinceName)
|
||||
: this.Builder.World.GetWater(provinceName, coast);
|
||||
? this.Builder.World.Map.GetLand(provinceName)
|
||||
: this.Builder.World.Map.GetWater(provinceName, coast);
|
||||
Season destSeason = season ?? this.SeasonContext.Season;
|
||||
MoveOrder moveOrder = new MoveOrder(
|
||||
this.PowerContext.Power,
|
||||
|
@ -451,8 +451,8 @@ public class TestCaseBuilder
|
|||
{
|
||||
Power power = powerName == null
|
||||
? this.PowerContext.Power
|
||||
: this.Builder.World.GetPower(powerName);
|
||||
Location location = this.Builder.World.GetLand(provinceName);
|
||||
: this.Builder.World.Map.GetPower(powerName);
|
||||
Location location = this.Builder.World.Map.GetLand(provinceName);
|
||||
Unit unit = this.Builder.GetOrBuildUnit(
|
||||
power, location, this.SeasonContext.Season, UnitType.Army);
|
||||
return new ConvoyDestinationContext(this, unit);
|
||||
|
@ -465,8 +465,8 @@ public class TestCaseBuilder
|
|||
{
|
||||
Power power = powerName == null
|
||||
? this.PowerContext.Power
|
||||
: this.Builder.World.GetPower(powerName);
|
||||
Location location = this.Builder.World.GetWater(provinceName, coast);
|
||||
: this.Builder.World.Map.GetPower(powerName);
|
||||
Location location = this.Builder.World.Map.GetWater(provinceName, coast);
|
||||
Unit unit = this.Builder.GetOrBuildUnit(
|
||||
power, location, this.SeasonContext.Season, UnitType.Fleet);
|
||||
return new ConvoyDestinationContext(this, unit);
|
||||
|
@ -492,7 +492,7 @@ public class TestCaseBuilder
|
|||
|
||||
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(
|
||||
this.PowerContext.Power,
|
||||
this.UnitContext.Unit,
|
||||
|
@ -526,8 +526,8 @@ public class TestCaseBuilder
|
|||
{
|
||||
Power power = powerName == null
|
||||
? this.PowerContext.Power
|
||||
: this.Builder.World.GetPower(powerName);
|
||||
Location location = this.Builder.World.GetLand(provinceName);
|
||||
: this.Builder.World.Map.GetPower(powerName);
|
||||
Location location = this.Builder.World.Map.GetLand(provinceName);
|
||||
Season destSeason = season ?? this.SeasonContext.Season;
|
||||
Unit unit = this.Builder.GetOrBuildUnit(
|
||||
power, location, destSeason, UnitType.Army);
|
||||
|
@ -541,8 +541,8 @@ public class TestCaseBuilder
|
|||
{
|
||||
Power power = powerName == null
|
||||
? this.PowerContext.Power
|
||||
: this.Builder.World.GetPower(powerName);
|
||||
Location location = this.Builder.World.GetWater(provinceName, coast);
|
||||
: this.Builder.World.Map.GetPower(powerName);
|
||||
Location location = this.Builder.World.Map.GetWater(provinceName, coast);
|
||||
Unit unit = this.Builder.GetOrBuildUnit(
|
||||
power, location, this.SeasonContext.Season, UnitType.Fleet);
|
||||
return new SupportTypeContext(this, unit);
|
||||
|
@ -582,8 +582,8 @@ public class TestCaseBuilder
|
|||
string? coast = null)
|
||||
{
|
||||
Location destination = this.Target.Type == UnitType.Army
|
||||
? this.Builder.World.GetLand(provinceName)
|
||||
: this.Builder.World.GetWater(provinceName, coast);
|
||||
? this.Builder.World.Map.GetLand(provinceName)
|
||||
: this.Builder.World.Map.GetWater(provinceName, coast);
|
||||
Season targetDestSeason = season ?? this.Target.Season;
|
||||
SupportMoveOrder order = new SupportMoveOrder(
|
||||
this.PowerContext.Power,
|
||||
|
@ -623,7 +623,7 @@ public class TestCaseBuilder
|
|||
return this.Builder;
|
||||
}
|
||||
|
||||
public ISeasonContext this[(int turn, int timeline) seasonCoord]
|
||||
public ISeasonContext this[(string timeline, int turn) seasonCoord]
|
||||
=> this.SeasonContext[seasonCoord];
|
||||
|
||||
public IPowerContext this[string powerName]
|
||||
|
|
|
@ -14,7 +14,7 @@ class TestCaseBuilderTest
|
|||
{
|
||||
TestCaseBuilder setup = new(World.WithStandardMap());
|
||||
|
||||
Assert.That(setup.World.Powers.Count(), Is.EqualTo(7), "Unexpected power count");
|
||||
Assert.That(setup.World.Powers.Count, Is.EqualTo(7), "Unexpected power count");
|
||||
Assert.That(setup.World.Units, Is.Empty, "Expected no units to be created yet");
|
||||
|
||||
setup
|
||||
|
@ -40,7 +40,7 @@ class TestCaseBuilderTest
|
|||
Assert.That(fleetSTP.Type, Is.EqualTo(UnitType.Fleet), "Unit created with wrong type");
|
||||
Assert.That(
|
||||
fleetSTP.Location,
|
||||
Is.EqualTo(setup.World.GetWater("STP", "wc")),
|
||||
Is.EqualTo(setup.World.Map.GetWater("STP", "wc").Designation),
|
||||
"Unit created on wrong coast");
|
||||
}
|
||||
|
||||
|
@ -49,7 +49,7 @@ class TestCaseBuilderTest
|
|||
{
|
||||
TestCaseBuilder setup = new(World.WithStandardMap());
|
||||
|
||||
Assert.That(setup.World.Powers.Count(), Is.EqualTo(7), "Unexpected power count");
|
||||
Assert.That(setup.World.Powers.Count, Is.EqualTo(7), "Unexpected power count");
|
||||
Assert.That(setup.World.Units, Is.Empty, "Expected no units to be created yet");
|
||||
Assert.That(setup.Orders, Is.Empty, "Expected no orders to be created yet");
|
||||
|
||||
|
@ -68,13 +68,13 @@ class TestCaseBuilderTest
|
|||
List<UnitOrder> orders = setup.Orders.OfType<UnitOrder>().ToList();
|
||||
|
||||
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"));
|
||||
Assert.That(orderBer, Is.InstanceOf<MoveOrder>(), "Unexpected order type");
|
||||
Assert.That(
|
||||
(orderBer as MoveOrder)?.Location,
|
||||
Is.EqualTo(setup.World.GetLand("Kiel")),
|
||||
Is.EqualTo(setup.World.Map.GetLand("Kiel")),
|
||||
"Unexpected move order destination");
|
||||
|
||||
UnitOrder orderPru = orders.Single(OrderForProvince("Prussia"));
|
||||
|
@ -88,7 +88,7 @@ class TestCaseBuilderTest
|
|||
"Unexpected convoy order target");
|
||||
Assert.That(
|
||||
(orderNth as ConvoyOrder)?.Location,
|
||||
Is.EqualTo(setup.World.GetLand("Holland")),
|
||||
Is.EqualTo(setup.World.Map.GetLand("Holland")),
|
||||
"Unexpected convoy order destination");
|
||||
|
||||
UnitOrder orderKie = orders.Single(OrderForProvince("Kiel"));
|
||||
|
@ -99,7 +99,7 @@ class TestCaseBuilderTest
|
|||
"Unexpected convoy order target");
|
||||
Assert.That(
|
||||
(orderKie as SupportMoveOrder)?.Location,
|
||||
Is.EqualTo(setup.World.GetLand("Holland")),
|
||||
Is.EqualTo(setup.World.Map.GetLand("Holland")),
|
||||
"Unexpected convoy order destination");
|
||||
|
||||
UnitOrder orderMun = orders.Single(OrderForProvince("Munich"));
|
||||
|
@ -124,11 +124,11 @@ class TestCaseBuilderTest
|
|||
Assert.That(orderMun, Is.Not.Null, "Expected order reference");
|
||||
Assert.That(
|
||||
orderMun.Order.Power,
|
||||
Is.EqualTo(setup.World.GetPower("Germany")),
|
||||
Is.EqualTo(setup.World.Map.GetPower("Germany")),
|
||||
"Wrong power");
|
||||
Assert.That(
|
||||
orderMun.Order.Unit.Location,
|
||||
Is.EqualTo(setup.World.GetLand("Mun")),
|
||||
Is.EqualTo(setup.World.Map.GetLand("Mun").Designation),
|
||||
"Wrong unit");
|
||||
|
||||
Assert.That(
|
||||
|
|
|
@ -10,29 +10,29 @@ public class UnitTests
|
|||
public void MovementTest()
|
||||
{
|
||||
World world = World.WithStandardMap();
|
||||
Location Mun = world.GetLand("Mun"),
|
||||
Boh = world.GetLand("Boh"),
|
||||
Tyr = world.GetLand("Tyr");
|
||||
Power pw1 = world.GetPower("Austria");
|
||||
Season s1 = world.RootSeason;
|
||||
Unit u1 = Unit.Build(Mun, s1, pw1, UnitType.Army);
|
||||
Location Mun = world.Map.GetLand("Mun"),
|
||||
Boh = world.Map.GetLand("Boh"),
|
||||
Tyr = world.Map.GetLand("Tyr");
|
||||
Power pw1 = world.Map.GetPower("Austria");
|
||||
Season a0 = world.RootSeason;
|
||||
Unit u1 = Unit.Build(Mun.Designation, a0, pw1, UnitType.Army);
|
||||
|
||||
Season s2 = s1.MakeNext();
|
||||
Unit u2 = u1.Next(Boh, s2);
|
||||
world = world.ContinueOrFork(a0, out Season a1);
|
||||
Unit u2 = u1.Next(Boh.Designation, a1);
|
||||
|
||||
Season s3 = s2.MakeNext();
|
||||
Unit u3 = u2.Next(Tyr, s3);
|
||||
_ = world.ContinueOrFork(a1, out Season a2);
|
||||
Unit u3 = u2.Next(Tyr.Designation, a2);
|
||||
|
||||
Assert.That(u3.Past, Is.EqualTo(u2), "Missing unit past");
|
||||
Assert.That(u2.Past, Is.EqualTo(u1), "Missing unit past");
|
||||
Assert.That(u3.Past, Is.EqualTo(u2.Designation), "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.Season, Is.EqualTo(s1), "Unexpected unit season");
|
||||
Assert.That(u2.Season, Is.EqualTo(s2), "Unexpected unit season");
|
||||
Assert.That(u3.Season, Is.EqualTo(s3), "Unexpected unit season");
|
||||
Assert.That(u1.Season, Is.EqualTo(a0), "Unexpected unit season");
|
||||
Assert.That(u2.Season, Is.EqualTo(a1), "Unexpected unit season");
|
||||
Assert.That(u3.Season, Is.EqualTo(a2), "Unexpected unit season");
|
||||
|
||||
Assert.That(u1.Location, Is.EqualTo(Mun), "Unexpected unit location");
|
||||
Assert.That(u2.Location, Is.EqualTo(Boh), "Unexpected unit location");
|
||||
Assert.That(u3.Location, Is.EqualTo(Tyr), "Unexpected unit location");
|
||||
Assert.That(u1.Location, Is.EqualTo(Mun.Designation), "Unexpected unit location");
|
||||
Assert.That(u2.Location, Is.EqualTo(Boh.Designation), "Unexpected unit location");
|
||||
Assert.That(u3.Location, Is.EqualTo(Tyr.Designation), "Unexpected unit location");
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue