Compare commits

...

24 Commits

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

View File

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

10
Makefile Normal file
View File

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

View File

@ -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)
{

View File

@ -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;

View File

@ -30,13 +30,13 @@ public static class PathFinder
// also have coasts, and between those coasts there is a path of adjacent sea provinces
// (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;
}
}

View File

@ -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}]";
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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;
}

View File

@ -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;
}

View File

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

View File

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

View File

@ -8,17 +8,12 @@ public class Unit
/// <summary>
/// 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);
}

View File

@ -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");
}

View File

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

View File

@ -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>

View File

@ -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;
}

View File

@ -14,12 +14,12 @@ public class TimeTravelTest
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance);
// Hold to move into the future, then move back into the past.
setup[(0, 0)]
setup[("a", 0)]
.GetReference(out Season s0)
["Germany"]
.Army("Mun").Holds().GetReference(out var mun0)
.Execute()
[(1, 0)]
[("a", 1)]
.GetReference(out Season s1)
["Germany"]
.Army("Mun").MovesTo("Tyr", season: s0).GetReference(out var mun1);
@ -41,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");
}

View File

@ -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),

View File

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

View File

@ -171,8 +171,8 @@ public class MovementAdjudicatorTest
// Confirm the future was created
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));
}
}

View File

@ -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;

View File

@ -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");
}
}

View File

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

View File

@ -19,7 +19,7 @@ public class TestCaseBuilder
/// <summary>
/// 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]

View File

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

View File

@ -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");
}
}