Compare commits
105 Commits
order-pars
...
develop
Author | SHA1 | Date | |
---|---|---|---|
ddf951c17e | |||
9fc298adda | |||
7c0cdb0a21 | |||
5e2d495fa5 | |||
7773c571e3 | |||
4096e4d517 | |||
aaf2e78730 | |||
864a933ba0 | |||
c6f10868ae | |||
5b32786904 | |||
ae5eb22010 | |||
26f7cee070 | |||
80f340c0b2 | |||
e9c9999268 | |||
4fee854c4c | |||
569c9021e6 | |||
3984b814ca | |||
f18147f666 | |||
9f52c78b40 | |||
7b890046b6 | |||
720ccc4329 | |||
d2a46aa02d | |||
f02e71d4f9 | |||
f77cc60185 | |||
14a493d95c | |||
44f2c25a2c | |||
43a2517a95 | |||
512c91d2de | |||
416f2aa919 | |||
4f276df6c1 | |||
24e80af7ef | |||
e25191548e | |||
33aecf876a | |||
b4f8f621ca | |||
ffe164975b | |||
ebeb178984 | |||
93b106da1e | |||
973f8ea0d7 | |||
868138b988 | |||
55dfe0ca99 | |||
e9c4d3d2d3 | |||
32a7ddd3b5 | |||
5167978f8c | |||
aaf3320cf8 | |||
2745d12d29 | |||
8e976433c8 | |||
bfafb66603 | |||
1689d2e9b1 | |||
ea366220eb | |||
f9f8ea2b5a | |||
b2461b3736 | |||
92506ac6ed | |||
114379de59 | |||
7ad6f3a3d3 | |||
6bb6c0695f | |||
eaafdeb5a9 | |||
9a64609605 | |||
889e9d173b | |||
f21b1e500c | |||
ff9e6196ad | |||
a02e8121eb | |||
3d664208b5 | |||
25d903d91a | |||
566d29e539 | |||
4b2712e4bc | |||
bfdf2d5636 | |||
2e6e6c55b8 | |||
161e0a1ddb | |||
abbe929122 | |||
601ce2d297 | |||
64f48064fc | |||
9185534f70 | |||
f2d3d5a583 | |||
868022d34f | |||
2484d4f0fd | |||
a4002a1081 | |||
8f5dc63833 | |||
5b5320b3e2 | |||
73d849e117 | |||
31bd6a45cb | |||
5ad57465d8 | |||
885628900b | |||
e1772ce60b | |||
abaa7f7a92 | |||
442015b942 | |||
228ad53cca | |||
f1563b8f5f | |||
345d54f960 | |||
58f877425a | |||
2f4c8b2a38 | |||
ef4e130dbb | |||
9606307e12 | |||
87685ec744 | |||
752a898123 | |||
400263ea0b | |||
5e5483367d | |||
81c9aa4859 | |||
fca8b77a21 | |||
b756959b0a | |||
b887e01334 | |||
421e84b559 | |||
780ae8b948 | |||
40254b0fca | |||
bd8e0da6b6 | |||
9fd63f4317 |
22
LICENSE
Normal file
22
LICENSE
Normal file
@ -0,0 +1,22 @@
|
||||
5dplomacy: A Diplomacy adjudicator with multiversal time travel
|
||||
Copyright (C) 2024 Tim Van Baak
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program, under docs/GPL3.txt. If not, see
|
||||
<https://www.gnu.org/licenses/>
|
||||
|
||||
The Diplomacy Adjudicator Test Cases are copyright (C) 2001-2024 Lucas B. Kruijswijk.
|
||||
|
||||
_Diplomacy_, a game of international intrigue, is trademarked by Avalon Hill Game Company and copyright (C) 1976 Avalon Hill. Avalon Hill belongs to Hasbro.
|
||||
|
||||
_5D Chess with Multiversal Time Travel_ was created by Conor Peterson and is copyright (C) 2020 Thunkspace, LLC.
|
92
MDATC.html
92
MDATC.html
@ -236,17 +236,13 @@
|
||||
<a name="2.B"><h3>2.B. ORDER NOTATION</h3></a>
|
||||
<p>The order notation in this document is as in DATC, with the following additions for multiversal time travel.</p>
|
||||
<ul>
|
||||
<li>A season within a particular timeline is designated in the format X:Y, where X is the turn (starting from 0 and advancing with each movement phase) and Y is the timeline number (starting from 0 and advancing with each timeline fork).</li>
|
||||
<li>Adjudication is implied to be done between successive seasons. For example, if orders are listed for 0:0 and then for 1:0, it is implied that the orders for 0:0 were adjudicated.</li>
|
||||
<li>Units are designated by unit type, province, and season, e.g. "A Munich 1:0". A destination for a move order or support-to-move order is designated by province and season, e.g. "Munich 1:0".
|
||||
<li>Timelines are designated by letters, e.g. "a", "b". Turns are designated by numbers, e.g. 0, 1, 2. A board in a timeline X at turn N is designated XN. A location LOC on that board is designated X-LOC@N.</li>
|
||||
<li>In examples that cover multiple turns, orders are given in sets. Each order set is adjudicated before moving on to the next set.</li>
|
||||
<li>Units are fully designated by unit type and multiversal location, e.g. "A b-Munich@3". Destinations for move orders or support-to-move orders are fully designated by multiversal location, e.g. <code>a-Munich@1</code>. Where orders are not fully designated, the full designations are implied according to these rules:</li>
|
||||
<ul>
|
||||
<li>If season of the ordered unit is not specified, the season is the season to which the orders are being given.</li>
|
||||
<li>If the season of a unit supported to hold is not specified, the season is the same season as the supporting unit.</li>
|
||||
<li>If the season of the destination of a move order or the season of the destination of a supported move order is not specified, the season is the season of the moving unit.</li>
|
||||
<li>For example:
|
||||
<pre>Germany 2:0
|
||||
A Munich supports A Munich 1:1 - Tyrolia</pre>
|
||||
The order here is for Army Munich in 2:0. The move being supported is for Army Munich in 1:1 to move to Tyrolia in 1:1.</li>
|
||||
<li>If only the timeline of a location is specified, the turn is the latest turn in that timeline. E.g. if timeline "a" is at turn 2, <code>a-Munich</code> is interpreted as <code>a-Munich@2</code>.</li>
|
||||
<li>If the timeline or turn are unspecified for the target of a move or support-hold order, the timeline and turn are those of the ordered unit. E.g. if timeline "b" is at turn 1, <code>A b-Tyrolia - Munich</code> is interpreted as <code>b-Tyrolia@1 - b-Munich@1</code>.</li>
|
||||
<li>If only the province is specified for the target of a support-move order, the timeline and turn are those of the supported unit. E.g. if timeline "a" is at turn 2 and "b" at turn 1, <code>A a-Munich supports A b-Tyrolia - Munich</code> is interpreted as <code>A a-Munich@2 supports A b-Tyrolia@1 - b-Munich@1</code>.</li>
|
||||
</ul>
|
||||
</ul>
|
||||
|
||||
@ -258,13 +254,15 @@ The order here is for Army Munich in 2:0. The move being supported is for Army M
|
||||
<summary><h4><a href="#3.A.1">3.A.1</a>. TEST CASE, MOVE INTO OWN PAST FORKS TIMELINE</h4></summary>
|
||||
<p>A unit that moves into its own immediate past causes the timeline to fork.</p>
|
||||
<pre>
|
||||
Germany 0:0
|
||||
A Munich hold
|
||||
Germany:
|
||||
A a-Munich hold
|
||||
|
||||
Germany 1:0
|
||||
A Munich - Tyrolia 0:0
|
||||
---
|
||||
|
||||
Germany:
|
||||
A a-Munich - a-Tyrolia@0
|
||||
</pre>
|
||||
<p>A Munich 1:0 moves to Tyrolia 0:0. The main timeline advances to 2:0 with an empty board. A forked timeline advances to 1:1 with armies in Munich and Tyrolia.</p>
|
||||
<p>A a-Munich@1 moves to a-Tyrolia@0. The main timeline advances to a2 with an empty board. A forked timeline advances to b1 with armies in Munich and Tyrolia.</p>
|
||||
<div class="figures">
|
||||
<canvas id="canvas-3-A-1-before" width="0" height="0"></canvas>
|
||||
<script>
|
||||
@ -311,19 +309,21 @@ The order here is for Army Munich in 2:0. The move being supported is for Army M
|
||||
<summary><h4><a href="#3.A.2">3.A.2</a>. TEST CASE, SUPPORT TO REPELLED PAST MOVE FORKS TIMELINE</h4></summary>
|
||||
<p>A unit that supports a move that previously failed in the past, such that it now succeeds, causes the timeline to fork.</p>
|
||||
<pre>
|
||||
Austria 0:0
|
||||
Austria:
|
||||
A Tyrolia hold
|
||||
|
||||
Germany 0:0
|
||||
Germany:
|
||||
A Munich - Tyrolia
|
||||
|
||||
Austria 1:0
|
||||
---
|
||||
|
||||
Austria:
|
||||
A Tyrolia hold
|
||||
|
||||
Germany 1:0
|
||||
A Munich supports A Munich 0:0 - Tyrolia 0:0
|
||||
Germany:
|
||||
A Munich supports A a-Munich@0 - Tyrolia
|
||||
</pre>
|
||||
<p>With the support from A Munich 1:0, A Munich 0:0 dislodges A Tyrolia 0:0. A forked timeline advances to 1:1 where A Tyrolia 0:0 has been dislodged. The main timeline advances to 2:0 where A Munich and A Tyrolia are in their initial positions.</p>
|
||||
<p>With the support from A a-Munich@1, A a-Munich@0 dislodges A a-Tyrolia@0. A forked timeline advances to b1 where A Tyrolia has been dislodged. The main timeline advances to a2 where A Munich and A Tyrolia are in their initial positions.</p>
|
||||
<div class="figures">
|
||||
<canvas id="canvas-3-A-2-before" width="0" height="0"></canvas>
|
||||
<script>
|
||||
@ -379,19 +379,21 @@ The order here is for Army Munich in 2:0. The move being supported is for Army M
|
||||
<summary><h4><a href="#3.A.3">3.A.3</a>. TEST CASE, FAILED MOVE DOES NOT FORK TIMELINE</h4></summary>
|
||||
<p>A unit that attempts to move into the past, but is repelled, does not cause the timeline to fork.</p>
|
||||
<pre>
|
||||
Austria 0:0
|
||||
Austria:
|
||||
A Tyrolia hold
|
||||
|
||||
Germany 0:0
|
||||
Germany:
|
||||
A Munich hold
|
||||
|
||||
Austria 1:0
|
||||
---
|
||||
|
||||
Austria:
|
||||
A Tyrolia hold
|
||||
|
||||
Germany 1:0
|
||||
A Munich - Tyrolia 0:0
|
||||
Germany:
|
||||
A Munich - a-Tyrolia@0
|
||||
</pre>
|
||||
<p>The move by A Munich 1:0 fails. The main timeline advances to 2:0 with both armies in their initial positions. No alternate timeline is created.</p>
|
||||
<p>The move by A a-Munich@1 fails. The main timeline advances to a2 with both armies in their initial positions. No alternate timeline is created.</p>
|
||||
<div class="figures">
|
||||
<canvas id="canvas-3-A-3-before" width="0" height="0"></canvas>
|
||||
<script>
|
||||
@ -441,15 +443,17 @@ The order here is for Army Munich in 2:0. The move being supported is for Army M
|
||||
<summary><h4><a href="#3.A.4">3.A.4</a>. TEST CASE, SUPERFLUOUS SUPPORT DOES NOT FORK TIMELINE</h4></summary>
|
||||
<p>A unit that supports a move that succeeded in the past and still succeeds after the additional future support does not cause the timeline to fork.</p>
|
||||
<pre>
|
||||
Germany 0:0
|
||||
Germany:
|
||||
A Munich - Tyrolia
|
||||
A Bohemia hold
|
||||
|
||||
Germany 1:0
|
||||
---
|
||||
|
||||
Germany:
|
||||
A Tyrolia hold
|
||||
A Bohemia supports A Munich 0:0 - Tyrolia
|
||||
A Bohemia supports A a-Munich@0 - Tyrolia
|
||||
</pre>
|
||||
<p>Both units in 1:0 continue to 2:0. No alternate timeline is created.</p>
|
||||
<p>Both units in a1 continue to a2. No alternate timeline is created.</p>
|
||||
<div class="figures">
|
||||
<canvas id="canvas-3-A-4-before" width="0" height="0"></canvas>
|
||||
<script>
|
||||
@ -501,15 +505,15 @@ The order here is for Army Munich in 2:0. The move being supported is for Army M
|
||||
<summary><h4><a href="#3.A.5">3.A.5</a>. TEST CASE, CROSS-TIMELINE SUPPORT DOES NOT FORK HEAD</h4></summary>
|
||||
<p>In this test case, a unit elsewhere on the map moves into the past to cause a timeline fork. Once there are two parallel timelines, a support from one to the head of the other should not cause any forking, since timeline forks only occur when the past changes, not the present.</p>
|
||||
<pre>
|
||||
Austria
|
||||
A Tyrolia 2:0 hold
|
||||
A Tyrolia 1:1 hold
|
||||
Austria:
|
||||
A a-Tyrolia hold
|
||||
A b-Tyrolia hold
|
||||
|
||||
Germany
|
||||
A Munich 2:0 - Tyrolia
|
||||
A Munich 1:1 supports A Munich 2:0 - Tyrolia
|
||||
A a-Munich - Tyrolia
|
||||
A b-Munich supports A a-Munich - Tyrolia
|
||||
</pre>
|
||||
<p>A Munich 2:0 dislodges A Tyrolia 2:0. No alternate timeline is created.</p>
|
||||
<p>A a-Munich dislodges A a-Tyrolia. No alternate timeline is created.</p>
|
||||
<div class="figures">
|
||||
<canvas id="canvas-3-A-5-before" width="0" height="0"></canvas>
|
||||
<script>
|
||||
@ -567,19 +571,11 @@ The order here is for Army Munich in 2:0. The move being supported is for Army M
|
||||
<p>Following <a href="#3.A.5">3.A.5</a>, a cross-timeline support that previously succeeded is cut.</p>
|
||||
<pre>
|
||||
Germany
|
||||
A Munich 2:0 - Tyrolia
|
||||
A Munich 1:1 supports A Munich 2:0 - Tyrolia
|
||||
A a-Tyrolia holds
|
||||
A b-Munich holds
|
||||
|
||||
Austria
|
||||
A Tyrolia 2:0 holds
|
||||
A Tyrolia 1:1 holds
|
||||
|
||||
Germany
|
||||
A Tyrolia 3:0 holds
|
||||
A Munich 2:1 holds
|
||||
|
||||
Austria
|
||||
A Tyrolia 2:1 - Munich 1:1
|
||||
A b-Tyrolia@2 - b-Munich@1
|
||||
</pre>
|
||||
<p>Cutting the support does not change the past or cause a timeline fork.</p>
|
||||
<div class="figures">
|
||||
|
13
Makefile
Normal file
13
Makefile
Normal file
@ -0,0 +1,13 @@
|
||||
.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)
|
||||
|
||||
repl: ## execute the repl
|
||||
dotnet run --project MultiversalDiplomacy repl
|
@ -1,19 +1,21 @@
|
||||
using MultiversalDiplomacy.Model;
|
||||
using MultiversalDiplomacy.Orders;
|
||||
|
||||
using static MultiversalDiplomacy.Model.Location;
|
||||
|
||||
namespace MultiversalDiplomacy.Adjudicate.Decision;
|
||||
|
||||
public class MovementDecisions
|
||||
{
|
||||
public Dictionary<Unit, IsDislodged> IsDislodged { get; }
|
||||
public Dictionary<string, IsDislodged> IsDislodged { get; }
|
||||
public Dictionary<MoveOrder, HasPath> HasPath { get; }
|
||||
public Dictionary<SupportOrder, GivesSupport> GivesSupport { get; }
|
||||
public Dictionary<(Province, Season), HoldStrength> HoldStrength { get; }
|
||||
public Dictionary<(string, string), HoldStrength> HoldStrength { get; }
|
||||
public Dictionary<MoveOrder, AttackStrength> AttackStrength { get; }
|
||||
public Dictionary<MoveOrder, DefendStrength> DefendStrength { get; }
|
||||
public Dictionary<MoveOrder, PreventStrength> PreventStrength { get; }
|
||||
public Dictionary<MoveOrder, DoesMove> DoesMove { get; }
|
||||
public Dictionary<Season, AdvanceTimeline> AdvanceTimeline { get; }
|
||||
public Dictionary<string, AdvanceTimeline> AdvanceTimeline { get; }
|
||||
|
||||
public IEnumerable<AdjudicationDecision> Values =>
|
||||
IsDislodged.Values.Cast<AdjudicationDecision>()
|
||||
@ -47,7 +49,7 @@ public class MovementDecisions
|
||||
var submittedOrdersBySeason = orders.Cast<UnitOrder>().ToLookup(order => order.Unit.Season);
|
||||
foreach (var group in submittedOrdersBySeason)
|
||||
{
|
||||
AdvanceTimeline[group.Key] = new(group.Key, group);
|
||||
AdvanceTimeline[group.Key.Key] = new(group.Key, group);
|
||||
}
|
||||
|
||||
// Create timeline decisions for each season potentially affected by the submitted orders.
|
||||
@ -60,27 +62,27 @@ public class MovementDecisions
|
||||
{
|
||||
case MoveOrder move:
|
||||
AdvanceTimeline.Ensure(
|
||||
move.Season,
|
||||
() => new(move.Season, world.OrderHistory[move.Season].Orders));
|
||||
AdvanceTimeline[move.Season].Orders.Add(move);
|
||||
move.Season.Key,
|
||||
() => new(move.Season, world.OrderHistory[move.Season.Key].Orders));
|
||||
AdvanceTimeline[move.Season.Key].Orders.Add(move);
|
||||
break;
|
||||
|
||||
case SupportHoldOrder supportHold:
|
||||
AdvanceTimeline.Ensure(
|
||||
supportHold.Target.Season,
|
||||
() => new(supportHold.Target.Season, world.OrderHistory[supportHold.Target.Season].Orders));
|
||||
AdvanceTimeline[supportHold.Target.Season].Orders.Add(supportHold);
|
||||
supportHold.Target.Season.Key,
|
||||
() => new(supportHold.Target.Season, world.OrderHistory[supportHold.Target.Season.Key].Orders));
|
||||
AdvanceTimeline[supportHold.Target.Season.Key].Orders.Add(supportHold);
|
||||
break;
|
||||
|
||||
case SupportMoveOrder supportMove:
|
||||
AdvanceTimeline.Ensure(
|
||||
supportMove.Target.Season,
|
||||
() => new(supportMove.Target.Season, world.OrderHistory[supportMove.Target.Season].Orders));
|
||||
AdvanceTimeline[supportMove.Target.Season].Orders.Add(supportMove);
|
||||
supportMove.Target.Season.Key,
|
||||
() => new(supportMove.Target.Season, world.OrderHistory[supportMove.Target.Season.Key].Orders));
|
||||
AdvanceTimeline[supportMove.Target.Season.Key].Orders.Add(supportMove);
|
||||
AdvanceTimeline.Ensure(
|
||||
supportMove.Season,
|
||||
() => new(supportMove.Season, world.OrderHistory[supportMove.Season].Orders));
|
||||
AdvanceTimeline[supportMove.Season].Orders.Add(supportMove);
|
||||
supportMove.Season.Key,
|
||||
() => new(supportMove.Season, world.OrderHistory[supportMove.Season.Key].Orders));
|
||||
AdvanceTimeline[supportMove.Season.Key].Orders.Add(supportMove);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -91,39 +93,68 @@ public class MovementDecisions
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
(string province, string season) UnitPoint(Unit unit)
|
||||
=> (world.Map.GetLocation(unit.Location).Province.Name, unit.Season.Key);
|
||||
(string province, string season) MovePoint(MoveOrder move)
|
||||
=> (SplitKey(move.Location).province, move.Season.Key);
|
||||
|
||||
// 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[UnitPoint(order.Unit)] = new(
|
||||
world.Map.GetLocation(order.Unit.Location).Province,
|
||||
order.Unit.Season,
|
||||
order);
|
||||
}
|
||||
|
||||
bool IsIncoming(UnitOrder me, MoveOrder other)
|
||||
=> me != other
|
||||
&& other.Season == me.Unit.Season
|
||||
&& SplitKey(other.Location).province == world.Map.GetLocation(me.Unit).Province.Name;
|
||||
|
||||
bool IsSupportFor(SupportMoveOrder me, MoveOrder move)
|
||||
=> me.Target.Key == move.Unit.Key
|
||||
&& me.Season == move.Season
|
||||
&& me.Location.Key == move.Location;
|
||||
|
||||
bool AreOpposing(MoveOrder one, MoveOrder two)
|
||||
=> one.Season == two.Unit.Season
|
||||
&& two.Season == one.Unit.Season
|
||||
&& SplitKey(one.Location).province == world.Map.GetLocation(two.Unit).Province.Name
|
||||
&& SplitKey(two.Location).province == world.Map.GetLocation(one.Unit).Province.Name;
|
||||
|
||||
bool AreCompeting(MoveOrder one, MoveOrder two)
|
||||
=> one != two
|
||||
&& one.Season == two.Season
|
||||
&& SplitKey(one.Location).province == SplitKey(two.Location).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);
|
||||
IsDislodged[order.Unit.Key] = new(order, incoming);
|
||||
|
||||
if (order is MoveOrder move)
|
||||
{
|
||||
// Find supports corresponding to this move.
|
||||
List<SupportMoveOrder> supports = relevantOrders
|
||||
.OfType<SupportMoveOrder>()
|
||||
.Where(support => support.IsSupportFor(move))
|
||||
.Where(support => IsSupportFor(support, move))
|
||||
.ToList();
|
||||
|
||||
// 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
|
||||
.OfType<MoveOrder>()
|
||||
.Where(move.IsCompeting)
|
||||
.Where(other => AreCompeting(move, other))
|
||||
.ToList();
|
||||
|
||||
// Create the move-related decisions.
|
||||
@ -134,7 +165,7 @@ public class MovementDecisions
|
||||
DoesMove[move] = new(move, opposingMove, competing);
|
||||
|
||||
// Ensure a hold strength decision exists for the destination.
|
||||
HoldStrength.Ensure(move.Point, () => new(move.Point));
|
||||
HoldStrength.Ensure(MovePoint(move), () => new(world.Map.GetLocation(move.Location).Province, move.Season));
|
||||
}
|
||||
else if (order is SupportOrder support)
|
||||
{
|
||||
@ -142,16 +173,20 @@ 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(UnitPoint(support.Target), () => new(
|
||||
world.Map.GetLocation(support.Target.Location).Province,
|
||||
support.Target.Season));
|
||||
|
||||
if (support is SupportHoldOrder supportHold)
|
||||
{
|
||||
HoldStrength[support.Target.Point].Supports.Add(supportHold);
|
||||
HoldStrength[UnitPoint(support.Target)].Supports.Add(supportHold);
|
||||
}
|
||||
else if (support is SupportMoveOrder supportMove)
|
||||
{
|
||||
// Ensure a hold strength decision exists for the target's destination.
|
||||
HoldStrength.Ensure(supportMove.Point, () => new(supportMove.Point));
|
||||
HoldStrength.Ensure(
|
||||
(supportMove.Province.Name, supportMove.Season.Key),
|
||||
() => new(supportMove.Province, supportMove.Season));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,8 @@ using MultiversalDiplomacy.Adjudicate.Logging;
|
||||
using MultiversalDiplomacy.Model;
|
||||
using MultiversalDiplomacy.Orders;
|
||||
|
||||
using static MultiversalDiplomacy.Model.Location;
|
||||
|
||||
namespace MultiversalDiplomacy.Adjudicate;
|
||||
|
||||
/// <summary>
|
||||
@ -50,7 +52,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.Timelines.GetFutures(order.Unit.Season).Any(),
|
||||
ValidationReason.IneligibleForOrder,
|
||||
ref unitOrders,
|
||||
ref validationResults);
|
||||
@ -69,8 +71,8 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||
|
||||
// Trivial check: armies cannot move to water and fleets cannot move to land.
|
||||
AdjudicatorHelpers.InvalidateIfNotMatching(
|
||||
order => (order.Unit.Type == UnitType.Army && order.Location.Type == LocationType.Land)
|
||||
|| (order.Unit.Type == UnitType.Fleet && order.Location.Type == LocationType.Water),
|
||||
order => (order.Unit.Type == UnitType.Army && world.Map.GetLocation(order.Location).Type == LocationType.Land)
|
||||
|| (order.Unit.Type == UnitType.Fleet && world.Map.GetLocation(order.Location).Type == LocationType.Water),
|
||||
ValidationReason.IllegalDestinationType,
|
||||
ref moveOrders,
|
||||
ref validationResults);
|
||||
@ -90,11 +92,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.Select(loc => loc.Key).Contains(order.Location)
|
||||
// Turn adjacency
|
||||
&& Math.Abs(order.Unit.Season.Turn - order.Season.Turn) <= 1
|
||||
// Timeline adjacency
|
||||
&& order.Unit.Season.InAdjacentTimeline(order.Season));
|
||||
&& world.Timelines.InAdjacentTimeline(order.Unit.Season, order.Season));
|
||||
List<MoveOrder> adjacentMoveOrders = moveOrdersByAdjacency[true].ToList();
|
||||
List<MoveOrder> nonAdjacentMoveOrders = moveOrdersByAdjacency[false].ToList();
|
||||
|
||||
@ -138,7 +140,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.Key == order.Target.Location
|
||||
&& order.Season == order.Target.Season),
|
||||
ValidationReason.DestinationMatchesOrigin,
|
||||
ref convoyOrders,
|
||||
@ -161,7 +163,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||
|
||||
// Support-hold orders are invalid if the unit supports itself.
|
||||
AdjudicatorHelpers.InvalidateIfNotMatching(
|
||||
order => order.Unit != order.Target,
|
||||
order => order.Unit.Key != order.Target.Key,
|
||||
ValidationReason.NoSelfSupport,
|
||||
ref supportHoldOrders,
|
||||
ref validationResults);
|
||||
@ -175,12 +177,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.Timelines.InAdjacentTimeline(order.Unit.Season, order.Target.Season),
|
||||
ValidationReason.UnreachableSupport,
|
||||
ref supportHoldOrders,
|
||||
ref validationResults);
|
||||
@ -195,7 +197,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 +209,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.Timelines.InAdjacentTimeline(order.Unit.Season, order.Season),
|
||||
ValidationReason.UnreachableSupport,
|
||||
ref supportMoveOrders,
|
||||
ref validationResults);
|
||||
@ -239,13 +241,13 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||
// were not addressed by 4.D.1-2 and will be handled according to 4.D.3, i.e. replaced with
|
||||
// hold orders. Note that this happens last, after all other invalidations have been
|
||||
// applied in order to comply with what 4.D.3 specifies about illegal orders.
|
||||
List<Unit> duplicateOrderedUnits = unitOrders
|
||||
.GroupBy(o => o.Unit)
|
||||
List<string> duplicateOrderedUnits = unitOrders
|
||||
.GroupBy(o => o.Unit.Key)
|
||||
.Where(orderGroup => orderGroup.Count() > 1)
|
||||
.Select(orderGroup => orderGroup.Key)
|
||||
.ToList();
|
||||
List<UnitOrder> duplicateOrders = unitOrders
|
||||
.Where(o => duplicateOrderedUnits.Contains(o.Unit))
|
||||
.Where(o => duplicateOrderedUnits.Contains(o.Unit.Key))
|
||||
.ToList();
|
||||
List<UnitOrder> validOrders = unitOrders.Except(duplicateOrders).ToList();
|
||||
validationResults = validationResults
|
||||
@ -255,11 +257,11 @@ 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.Timelines.GetFutures(unit.Season).Any())
|
||||
.ToList();
|
||||
HashSet<Unit> orderedUnits = validOrders.Select(order => order.Unit).ToHashSet();
|
||||
HashSet<string> orderedUnits = validOrders.Select(order => order.Unit.Key).ToHashSet();
|
||||
List<Unit> unorderedUnits = allOrderableUnits
|
||||
.Where(unit => !orderedUnits.Contains(unit))
|
||||
.Where(unit => !orderedUnits.Contains(unit.Key))
|
||||
.ToList();
|
||||
List<HoldOrder> implicitHolds = unorderedUnits
|
||||
.Select(unit => new HoldOrder(unit.Power, unit))
|
||||
@ -306,15 +308,16 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||
Dictionary<MoveOrder, DoesMove> moves = decisions
|
||||
.OfType<DoesMove>()
|
||||
.ToDictionary(dm => dm.Order);
|
||||
Dictionary<Unit, IsDislodged> dislodges = decisions
|
||||
Dictionary<string, IsDislodged> dislodges = decisions
|
||||
.OfType<IsDislodged>()
|
||||
.ToDictionary(dm => dm.Order.Unit);
|
||||
.ToDictionary(dm => dm.Order.Unit.Key);
|
||||
|
||||
// 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 = [];
|
||||
Timelines newTimelines = world.Timelines;
|
||||
|
||||
// Populate createdFutures with the timeline fork decisions
|
||||
logger.Log(1, "Processing AdvanceTimeline decisions");
|
||||
@ -324,9 +327,8 @@ 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();
|
||||
newTimelines = newTimelines.WithNewSeason(advanceTimeline.Season, out var newFuture);
|
||||
createdFutures[advanceTimeline.Season] = newFuture;
|
||||
}
|
||||
}
|
||||
|
||||
@ -337,9 +339,9 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||
{
|
||||
logger.Log(2, "{0} = {1}", doesMove, doesMove.Outcome?.ToString() ?? "?");
|
||||
Season moveSeason = doesMove.Order.Season;
|
||||
if (doesMove.Outcome == true && createdFutures.ContainsKey(moveSeason))
|
||||
if (doesMove.Outcome == true && createdFutures.TryGetValue(moveSeason, out Season future))
|
||||
{
|
||||
Unit next = doesMove.Order.Unit.Next(doesMove.Order.Location, createdFutures[moveSeason]);
|
||||
Unit next = doesMove.Order.Unit.Next(doesMove.Order.Location, future);
|
||||
logger.Log(3, "Advancing unit to {0}", next);
|
||||
createdUnits.Add(next);
|
||||
}
|
||||
@ -368,7 +370,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).Key, future);
|
||||
logger.Log(3, "Advancing unit to {0}", next);
|
||||
createdUnits.Add(next);
|
||||
}
|
||||
@ -377,7 +379,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,22 +388,22 @@ 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.Key, () => new([], [], []));
|
||||
OrderHistory history = newHistory[unitOrder.Unit.Season.Key];
|
||||
// TODO does this add every order to every season??
|
||||
history.Orders.Add(unitOrder);
|
||||
history.IsDislodgedOutcomes[unitOrder.Unit] = dislodges[unitOrder.Unit].Outcome == true;
|
||||
history.IsDislodgedOutcomes[unitOrder.Unit.Key] = dislodges[unitOrder.Unit.Key].Outcome == true;
|
||||
if (unitOrder is MoveOrder moveOrder)
|
||||
{
|
||||
history.DoesMoveOutcomes[moveOrder] = moves[moveOrder].Outcome == true;
|
||||
history.DoesMoveOutcomes[moveOrder.Unit.Key] = moves[moveOrder].Outcome == true;
|
||||
}
|
||||
}
|
||||
|
||||
// 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,17 +413,17 @@ 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);
|
||||
|
||||
// TODO provide more structured information about order outcomes
|
||||
|
||||
World updated = world.Update(
|
||||
seasons: world.Seasons.Concat(createdFutures.Values),
|
||||
units: world.Units.Concat(createdUnits),
|
||||
retreats: retreats,
|
||||
orders: updatedHistory);
|
||||
orders: updatedHistory,
|
||||
timelines: newTimelines);
|
||||
|
||||
logger.Log(0, "Completed update");
|
||||
|
||||
@ -486,7 +488,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.Timelines.GetFutures(decision.Season).Any())
|
||||
{
|
||||
progress |= LoggedUpdate(decision, true, depth, "A timeline head always advances");
|
||||
return progress;
|
||||
@ -496,7 +498,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.Key].DoesMoveOutcomes.ContainsKey(order.Unit.Key));
|
||||
foreach (MoveOrder moveOrder in newIncomingMoves)
|
||||
{
|
||||
DoesMove doesMove = decisions.DoesMove[moveOrder];
|
||||
@ -513,14 +515,14 @@ 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.Key];
|
||||
bool anyUnresolved = false;
|
||||
foreach (UnitOrder order in decision.Orders)
|
||||
{
|
||||
// TODO these aren't timeline-specific
|
||||
IsDislodged dislodged = decisions.IsDislodged[order.Unit];
|
||||
IsDislodged dislodged = decisions.IsDislodged[order.Unit.Key];
|
||||
progress |= ResolveDecision(dislodged, world, decisions, depth + 1);
|
||||
if (history.IsDislodgedOutcomes.TryGetValue(order.Unit, out bool previous)
|
||||
if (history.IsDislodgedOutcomes.TryGetValue(order.Unit.Key, out bool previous)
|
||||
&& dislodged.Resolved
|
||||
&& dislodged.Outcome != previous)
|
||||
{
|
||||
@ -537,7 +539,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||
{
|
||||
DoesMove moves = decisions.DoesMove[moveOrder];
|
||||
progress |= ResolveDecision(moves, world, decisions, depth + 1);
|
||||
if (history.DoesMoveOutcomes.TryGetValue(moveOrder, out bool previousMove)
|
||||
if (history.DoesMoveOutcomes.TryGetValue(moveOrder.Unit.Key, out bool previousMove)
|
||||
&& moves.Resolved
|
||||
&& moves.Outcome != previousMove)
|
||||
if (moves.Resolved && moves.Outcome != previousMove)
|
||||
@ -618,7 +620,10 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||
|
||||
if (!potentialDislodger)
|
||||
{
|
||||
progress |= LoggedUpdate(decision, false, depth, "No invader can move");
|
||||
string reason = decision.Incoming.Count == 0
|
||||
? "No unit is attacking"
|
||||
: "All attacks failed";
|
||||
progress |= LoggedUpdate(decision, false, depth, reason);
|
||||
}
|
||||
|
||||
return progress;
|
||||
@ -635,11 +640,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.Select(loc => loc.Key).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.Timelines.InAdjacentTimeline(decision.Order.Unit.Season, decision.Order.Season))
|
||||
{
|
||||
bool update = LoggedUpdate(decision, true, depth, "Adjacent move");
|
||||
return progress | update;
|
||||
@ -689,7 +694,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||
}
|
||||
|
||||
// Support is also cut if the unit is dislodged.
|
||||
IsDislodged dislodge = decisions.IsDislodged[decision.Order.Unit];
|
||||
IsDislodged dislodge = decisions.IsDislodged[decision.Order.Unit.Key];
|
||||
progress |= ResolveDecision(dislodge, world, decisions, depth + 1);
|
||||
if (dislodge.Outcome == true)
|
||||
{
|
||||
@ -750,7 +755,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||
if (givesSupport.Outcome == true) min += 1;
|
||||
if (givesSupport.Outcome != false) max += 1;
|
||||
}
|
||||
progress |= LoggedUpdate(decision, min, max, depth, "Updated based on unit's supports");
|
||||
progress |= LoggedUpdate(decision, min, max, depth, $"Updated based on {decision.Supports.Count} hold supports");
|
||||
return progress;
|
||||
}
|
||||
}
|
||||
@ -776,7 +781,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||
// If there is a head to head battle, a unit at the destination that isn't moving away, or
|
||||
// a unit at the destination that will fail to move away, then the attacking unit will have
|
||||
// to dislodge it.
|
||||
UnitOrder? destOrder = decisions.HoldStrength[decision.Order.Point].Order;
|
||||
UnitOrder? destOrder = decisions.HoldStrength[(SplitKey(decision.Order.Location).province, decision.Order.Season.Key)].Order;
|
||||
DoesMove? destMoveAway = destOrder is MoveOrder moveAway
|
||||
? decisions.DoesMove[moveAway]
|
||||
: null;
|
||||
@ -786,7 +791,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||
}
|
||||
if (// In any case here, there will have to be a unit at the destination with an order,
|
||||
// which means that destOrder will have to be populated. Including this in the if
|
||||
//condition lets the compiler know it won't be null in the if block.
|
||||
// condition lets the compiler know it won't be null in the if block.
|
||||
destOrder != null
|
||||
&& (// Is head to head
|
||||
decision.OpposingMove != null
|
||||
@ -795,7 +800,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||
// Is failing to move away
|
||||
|| destMoveAway.Outcome == false))
|
||||
{
|
||||
Power destPower = destOrder.Unit.Power;
|
||||
string destPower = destOrder.Unit.Power;
|
||||
if (decision.Order.Unit.Power == destPower)
|
||||
{
|
||||
// Cannot dislodge own unit.
|
||||
@ -815,7 +820,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||
if (givesSupport.Outcome == true) min += 1;
|
||||
if (givesSupport.Outcome != false) max += 1;
|
||||
}
|
||||
progress |= LoggedUpdate(decision, min, max, depth, "Updated with supports from other powers");
|
||||
progress |= LoggedUpdate(decision, min, max, depth, $"Updated with {decision.Supports.Count} (?) move supports from third parties");
|
||||
return progress;
|
||||
}
|
||||
}
|
||||
@ -825,7 +830,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||
// the case where it doesn't move and the attack strength is mitigated by supports not
|
||||
// helping to dislodge units of the same power as the support. The maximum tracks the
|
||||
// case where it does move and the attack strength is unmitigated.
|
||||
Power destPower = destMoveAway.Order.Unit.Power;
|
||||
string destPower = destMoveAway.Order.Unit.Power;
|
||||
int min = 1;
|
||||
int max = 1;
|
||||
foreach (SupportMoveOrder support in decision.Supports)
|
||||
@ -837,7 +842,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||
}
|
||||
// Force min to zero in case of an attempt to disloge a unit of the same power.
|
||||
if (decision.Order.Unit.Power == destPower) min = 0;
|
||||
progress |= LoggedUpdate(decision, min, max, depth, "Updated with supports");
|
||||
progress |= LoggedUpdate(decision, min, max, depth, $"Updated with {decision.Supports.Count} (?) move supports");
|
||||
return progress;
|
||||
}
|
||||
else
|
||||
@ -853,7 +858,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||
if (givesSupport.Outcome == true) min += 1;
|
||||
if (givesSupport.Outcome != false) max += 1;
|
||||
}
|
||||
progress |= LoggedUpdate(decision, min, max, depth, "Updated with supports from all powers");
|
||||
progress |= LoggedUpdate(decision, min, max, depth, $"Updated with {decision.Supports.Count} move supports from all powers");
|
||||
return progress;
|
||||
}
|
||||
}
|
||||
@ -878,7 +883,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||
if (givesSupport.Outcome == true) min += 1;
|
||||
if (givesSupport.Outcome != false) max += 1;
|
||||
}
|
||||
progress |= LoggedUpdate(decision, min, max, depth, "Updated based on unit's supports");
|
||||
progress |= LoggedUpdate(decision, min, max, depth, $"Updated based on {decision.Supports.Count} supports");
|
||||
|
||||
return progress;
|
||||
}
|
||||
@ -932,7 +937,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||
min = 0;
|
||||
}
|
||||
|
||||
progress |= LoggedUpdate(decision, min, max, depth, "Updated based on unit's supports");
|
||||
progress |= LoggedUpdate(decision, min, max, depth, $"Updated based on {decision.Supports.Count} supports");
|
||||
|
||||
return progress;
|
||||
}
|
||||
@ -955,7 +960,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||
// strength.
|
||||
NumericAdjudicationDecision defense = decision.OpposingMove != null
|
||||
? decisions.DefendStrength[decision.OpposingMove]
|
||||
: decisions.HoldStrength[decision.Order.Point];
|
||||
: decisions.HoldStrength[(SplitKey(decision.Order.Location).province, decision.Order.Season.Key)];
|
||||
progress |= ResolveDecision(defense, world, decisions, depth + 1);
|
||||
|
||||
// If the attack doesn't beat the defense, resolve the move to false.
|
||||
@ -990,7 +995,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||
decision,
|
||||
attack.MinValue > defense.MaxValue && beatsAllCompetingMoves,
|
||||
depth,
|
||||
"Updated based on competing moves");
|
||||
$"Updated based on {decision.Competing.Count} competing moves");
|
||||
return progress;
|
||||
}
|
||||
}
|
||||
|
@ -12,37 +12,41 @@ public static class PathFinder
|
||||
/// Determines if a convoy path exists for a move in a convoy order.
|
||||
/// </summary>
|
||||
public static bool ConvoyPathExists(World world, ConvoyOrder order)
|
||||
=> ConvoyPathExists(world, order.Target, order.Location, order.Season);
|
||||
=> ConvoyPathExists(world, world.Map.GetLocation(order.Target), order.Location, order.Season);
|
||||
|
||||
/// <summary>
|
||||
/// Determines if a convoy path exists for a move order.
|
||||
/// </summary>
|
||||
public static bool ConvoyPathExists(World world, MoveOrder order)
|
||||
=> ConvoyPathExists(world, order.Unit, order.Location, order.Season);
|
||||
=> ConvoyPathExists(
|
||||
world,
|
||||
world.Map.GetLocation(order.Unit),
|
||||
world.Map.GetLocation(order.Location),
|
||||
order.Season);
|
||||
|
||||
private static bool ConvoyPathExists(
|
||||
World world,
|
||||
Unit movingUnit,
|
||||
Location unitLocation,
|
||||
Season unitSeason)
|
||||
Location destLocation,
|
||||
Season destSeason)
|
||||
{
|
||||
// A convoy path exists between two locations if both are land locations in provinces that
|
||||
// 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 (unitLocation.Type != LocationType.Land) return false;
|
||||
IEnumerable<Location> originCoasts = unitLocation.Province.Locations
|
||||
.Where(location => location.Type == LocationType.Water);
|
||||
if (!originCoasts.Any()) return false;
|
||||
|
||||
// Verify that the destination is a coastal province.
|
||||
if (unitLocation.Type != LocationType.Land) return false;
|
||||
IEnumerable<Location> destCoasts = unitLocation.Province.Locations
|
||||
if (destLocation.Type != LocationType.Land) return false;
|
||||
IEnumerable<Location> destCoasts = destLocation.Province.Locations
|
||||
.Where(location => location.Type == LocationType.Water);
|
||||
if (!destCoasts.Any()) return false;
|
||||
|
||||
@ -50,7 +54,7 @@ public static class PathFinder
|
||||
// locations added to the to-visit set, but the logic will still work with these as
|
||||
// starting points.
|
||||
Queue<(Location location, Season season)> toVisit = new(
|
||||
originCoasts.Select(location => (location, unitSeason)));
|
||||
originCoasts.Select(location => (location, destSeason)));
|
||||
HashSet<(Location, Season)> visited = new();
|
||||
|
||||
// Begin pathfinding.
|
||||
@ -60,16 +64,16 @@ 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.
|
||||
if (destCoasts.Contains(adjLocation) && unitSeason == adjSeason) return true;
|
||||
if (destCoasts.Contains(adjLocation) && destSeason == adjSeason) return true;
|
||||
|
||||
// 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.Key, adjSeason))
|
||||
&& !visited.Contains((adjLocation, adjSeason)))
|
||||
{
|
||||
toVisit.Enqueue((adjLocation, adjSeason));
|
||||
@ -81,11 +85,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 +109,59 @@ 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)
|
||||
{
|
||||
var pasts = world.Timelines.Pasts;
|
||||
List<Season> adjacents = [];
|
||||
|
||||
// The immediate past and all immediate futures are adjacent.
|
||||
if (pasts[season.Key] is Season immediatePast) adjacents.Add(immediatePast);
|
||||
adjacents.AddRange(world.Timelines.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 = season;
|
||||
for (;
|
||||
pasts[current?.Key!] is Season currentPast && currentPast.Timeline == current?.Timeline;
|
||||
current = pasts[current?.Key!])
|
||||
{
|
||||
adjacentTimelineRoots.AddRange(
|
||||
world.Timelines.GetFutures(current.Value).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). Check for co-branches if this season is in a branched timeline, since
|
||||
// the first timeline by definition cannot have co-branches.
|
||||
if (pasts[current?.Key!] is Season rootPast)
|
||||
{
|
||||
IEnumerable<Season> cobranchRoots = world.Timelines
|
||||
.GetFutures(rootPast)
|
||||
.Where(s => s.Timeline != current?.Timeline && s.Timeline != rootPast.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 is Season branch && branch.Turn <= season.Turn + 1;
|
||||
branchSeason = world.Timelines
|
||||
.GetFutures(branchSeason!.Value)
|
||||
.Cast<Season?>()
|
||||
.FirstOrDefault(s => s?.Timeline == branchSeason?.Timeline, null))
|
||||
{
|
||||
if (branchSeason?.Turn >= season.Turn - 1) adjacents.Add(branchSeason.Value);
|
||||
}
|
||||
}
|
||||
|
||||
return adjacents;
|
||||
}
|
||||
}
|
15
MultiversalDiplomacy/CommandLine/AdjudicateOptions.cs
Normal file
15
MultiversalDiplomacy/CommandLine/AdjudicateOptions.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using CommandLine;
|
||||
|
||||
namespace MultiversalDiplomacy.CommandLine;
|
||||
|
||||
[Verb("adjudicate", HelpText = "Adjudicate a Multiversal Diplomacy game state.")]
|
||||
public class AdjudicateOptions
|
||||
{
|
||||
[Value(0, HelpText = "Input file describing the game state to adjudicate, or - to read from stdin.")]
|
||||
public string? InputFile { get; set; }
|
||||
|
||||
public static void Execute(AdjudicateOptions args)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
15
MultiversalDiplomacy/CommandLine/ImageOptions.cs
Normal file
15
MultiversalDiplomacy/CommandLine/ImageOptions.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using CommandLine;
|
||||
|
||||
namespace MultiversalDiplomacy.CommandLine;
|
||||
|
||||
[Verb("image", HelpText = "Generate an image of a game state.")]
|
||||
public class ImageOptions
|
||||
{
|
||||
[Value(0, HelpText = "Input file describing the game state to visualize, or - to read from stdin.")]
|
||||
public string? InputFile { get; set; }
|
||||
|
||||
public static void Execute(ImageOptions args)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
92
MultiversalDiplomacy/CommandLine/ReplOptions.cs
Normal file
92
MultiversalDiplomacy/CommandLine/ReplOptions.cs
Normal file
@ -0,0 +1,92 @@
|
||||
using CommandLine;
|
||||
|
||||
using MultiversalDiplomacy.Script;
|
||||
|
||||
namespace MultiversalDiplomacy.CommandLine;
|
||||
|
||||
[Verb("repl", HelpText = "Begin an interactive 5dplomacy session.")]
|
||||
public class ReplOptions
|
||||
{
|
||||
[Option('i', "input", HelpText = "Begin the repl session by executing the commands in this file.")]
|
||||
public string? InputFile { get; set; }
|
||||
|
||||
[Option('o', "output", HelpText = "Echo the repl session to this file. Specify a directory to autogenerate a filename.")]
|
||||
public string? OutputFile { get; set; }
|
||||
|
||||
public static void Execute(ReplOptions args)
|
||||
{
|
||||
IEnumerable<string>? inputFileLines = null;
|
||||
if (args.InputFile is not null) {
|
||||
var fullPath = Path.GetFullPath(args.InputFile);
|
||||
inputFileLines = File.ReadAllLines(fullPath);
|
||||
Console.WriteLine($"Reading from {fullPath}");
|
||||
}
|
||||
|
||||
// Create a writer to the output file, if specified.
|
||||
StreamWriter? outputWriter = null;
|
||||
if (args.OutputFile is not null)
|
||||
{
|
||||
string fullPath = Path.GetFullPath(args.OutputFile);
|
||||
string outputPath = Directory.Exists(fullPath)
|
||||
? Path.Combine(fullPath, $"{DateTime.UtcNow:yyyyMMddHHmmss}.log")
|
||||
: fullPath;
|
||||
Console.WriteLine($"Echoing to {outputPath}");
|
||||
outputWriter = File.CreateText(outputPath);
|
||||
}
|
||||
|
||||
IEnumerable<string?> GetInputs()
|
||||
{
|
||||
foreach (string line in inputFileLines ?? [])
|
||||
{
|
||||
var trimmed = line.Trim();
|
||||
// File inputs weren't echoed to the terminal so they need to be echoed here
|
||||
Console.WriteLine($"{trimmed}");
|
||||
yield return trimmed;
|
||||
}
|
||||
|
||||
string? input;
|
||||
do
|
||||
{
|
||||
input = Console.ReadLine();
|
||||
yield return input;
|
||||
}
|
||||
while (input is not null);
|
||||
// The last null is returned because an EOF means we should quit the repl.
|
||||
}
|
||||
|
||||
IScriptHandler? handler = new ReplScriptHandler(Console.WriteLine);
|
||||
|
||||
Console.Write(handler.Prompt);
|
||||
foreach (string? nextInput in GetInputs())
|
||||
{
|
||||
// Handle quitting directly.
|
||||
if (nextInput is null || nextInput == "quit" || nextInput == "exit")
|
||||
{
|
||||
break;
|
||||
}
|
||||
string input = nextInput.Trim();
|
||||
outputWriter?.WriteLine(input);
|
||||
outputWriter?.Flush();
|
||||
|
||||
// Delegate all other command parsing to the handler.
|
||||
var result = handler.HandleInput(input);
|
||||
|
||||
// Report errors if they occured.
|
||||
if (!result.Success)
|
||||
{
|
||||
Console.WriteLine($"Error: {result.Message}");
|
||||
}
|
||||
|
||||
// Quit if the handler didn't continue processing.
|
||||
if (result.NextHandler is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Otherwise prompt for the next command.
|
||||
Console.Write(handler.Prompt);
|
||||
}
|
||||
|
||||
Console.WriteLine("exiting");
|
||||
}
|
||||
}
|
@ -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 Key => $"{this.ProvinceName}/{this.Abbreviation}";
|
||||
|
||||
public Location(Province province, string name, string abbreviation, LocationType type)
|
||||
{
|
||||
this.Province = province;
|
||||
this.Name = name;
|
||||
@ -41,9 +53,21 @@ public class Location
|
||||
this.AdjacentList = new List<Location>();
|
||||
}
|
||||
|
||||
public static (string province, string location) SplitKey(string locationKey)
|
||||
{
|
||||
var split = locationKey.Split(['/'], 2);
|
||||
return (split[0], split[1]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether a name is the name or abbreviation of this location.
|
||||
/// </summary>
|
||||
public bool Is(string name)
|
||||
=> name.EqualsAnyCase(Name) || name.EqualsAnyCase(Abbreviation);
|
||||
|
||||
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}]";
|
||||
}
|
||||
|
576
MultiversalDiplomacy/Model/Map.cs
Normal file
576
MultiversalDiplomacy/Model/Map.cs
Normal file
@ -0,0 +1,576 @@
|
||||
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<string> Powers => _Powers.AsReadOnly();
|
||||
|
||||
private List<string> _Powers { get; }
|
||||
|
||||
private Map(MapType type, IEnumerable<Province> provinces, IEnumerable<string> powers)
|
||||
{
|
||||
Type = type;
|
||||
_Provinces = provinces.ToList();
|
||||
_Powers = powers.ToList();
|
||||
|
||||
LocationLookup = Provinces
|
||||
.SelectMany(province => province.Locations)
|
||||
.ToDictionary(location => location.Key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A regex that matches any of the power names for this variant.
|
||||
/// </summary>
|
||||
public string PowerRegex => $"({string.Join("|", Powers)})";
|
||||
|
||||
/// <summary>
|
||||
/// A regex that matches any of the province names or abbreviations for this variant.
|
||||
/// </summary>
|
||||
public string ProvinceRegex => $"({string.Join("|", Provinces.SelectMany(p => p.AllNames))})";
|
||||
|
||||
/// <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(province => province!.Is(provinceName), 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 full or partial name. Throws if there is not exactly one such power.
|
||||
/// </summary>
|
||||
public string GetPower(string powerName)
|
||||
=> Powers.SingleOrDefault(p => p!.EqualsAnyCase(powerName) || p!.StartsWithAnyCase(powerName), null)
|
||||
?? throw new KeyNotFoundException($"Power {powerName} not found (powers: {string.Join(", ", Powers)})");
|
||||
|
||||
public static Map FromType(MapType type)
|
||||
=> type switch {
|
||||
MapType.Test => Test,
|
||||
MapType.Classical => Classical,
|
||||
_ => 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());
|
||||
|
||||
return new(MapType.Test, [lef, cen, rig], ["Alpha", "Beta"]);
|
||||
});
|
||||
|
||||
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<string> powers =
|
||||
[
|
||||
"Austria",
|
||||
"England",
|
||||
"France",
|
||||
"Germany",
|
||||
"Italy",
|
||||
"Russia",
|
||||
"Turkey",
|
||||
];
|
||||
|
||||
return new(MapType.Classical, provinces, powers);
|
||||
});
|
||||
}
|
16
MultiversalDiplomacy/Model/MapType.cs
Normal file
16
MultiversalDiplomacy/Model/MapType.cs
Normal 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,
|
||||
}
|
@ -20,4 +20,7 @@ public static class ModelExtensions
|
||||
{
|
||||
return $"{coord.season.Timeline}-{coord.province.Abbreviations[0]}@{coord.season.Turn}";
|
||||
}
|
||||
}
|
||||
|
||||
public static World WithNewSeason(this World world, Season season, out Season future)
|
||||
=> world.Update(timelines: world.Timelines.WithNewSeason(season, out future));
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using MultiversalDiplomacy.Orders;
|
||||
|
||||
@ -6,20 +6,23 @@ namespace MultiversalDiplomacy.Model;
|
||||
|
||||
public class OrderHistory
|
||||
{
|
||||
public List<UnitOrder> Orders;
|
||||
public List<UnitOrder> Orders { get; }
|
||||
|
||||
public Dictionary<Unit, bool> IsDislodgedOutcomes;
|
||||
/// <summary>
|
||||
/// Map from unit designation to dislodge outcome.
|
||||
/// </summary>
|
||||
public Dictionary<string, bool> IsDislodgedOutcomes { get; }
|
||||
|
||||
public Dictionary<MoveOrder, bool> DoesMoveOutcomes;
|
||||
|
||||
public OrderHistory()
|
||||
: this(new(), new(), new())
|
||||
{}
|
||||
/// <summary>
|
||||
/// Map from designation of the ordered unit to move outcome.
|
||||
/// </summary>
|
||||
public Dictionary<string, bool> DoesMoveOutcomes { get; }
|
||||
|
||||
[JsonConstructor]
|
||||
public OrderHistory(
|
||||
List<UnitOrder> orders,
|
||||
Dictionary<Unit, bool> isDislodgedOutcomes,
|
||||
Dictionary<MoveOrder, bool> doesMoveOutcomes)
|
||||
Dictionary<string, bool> isDislodgedOutcomes,
|
||||
Dictionary<string, bool> doesMoveOutcomes)
|
||||
{
|
||||
this.Orders = new(orders);
|
||||
this.IsDislodgedOutcomes = new(isDislodgedOutcomes);
|
||||
|
451
MultiversalDiplomacy/Model/OrderParser.cs
Normal file
451
MultiversalDiplomacy/Model/OrderParser.cs
Normal file
@ -0,0 +1,451 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using MultiversalDiplomacy.Orders;
|
||||
|
||||
namespace MultiversalDiplomacy.Model;
|
||||
|
||||
/// <summary>
|
||||
/// This class defines the regular expressions that are used to build up larger expressions for matching orders
|
||||
/// and other script inputs. It also provides helper functions to extract the captured order elements as tuples,
|
||||
/// which function as the structured intermediate representation between raw user input and full Order objects.
|
||||
/// </summary>
|
||||
public class OrderParser(World world)
|
||||
{
|
||||
public const string Type = "(A|F|Army|Fleet)";
|
||||
|
||||
public const string Timeline = "([A-Za-z]+)";
|
||||
|
||||
public const string Turn = "([0-9]+)";
|
||||
|
||||
public const string SlashLocation = "(?:/([A-Za-z]+))";
|
||||
|
||||
public const string ParenLocation = "(?:\\(([A-Za-z ]+)\\))";
|
||||
|
||||
public string FullLocation => $"(?:{Timeline}-)?{world.Map.ProvinceRegex}(?:{SlashLocation}|{ParenLocation})?(?:@{Turn})?";
|
||||
|
||||
public string UnitSpec => $"(?:{Type} )?{FullLocation}";
|
||||
|
||||
public const string HoldVerb = "(h|hold|holds)";
|
||||
|
||||
public const string MoveVerb = "(-|(?:->)|(?:=>)|(?:attack(?:s)?)|(?:move(?:s)?(?: to)?))";
|
||||
|
||||
public const string SupportVerb = "(s|support|supports)";
|
||||
|
||||
public const string ViaConvoy = "(convoy|via convoy|by convoy)";
|
||||
|
||||
public Regex UnitDeclaration = new(
|
||||
$"^{world.Map.PowerRegex} {Type} {world.Map.ProvinceRegex}(?:{SlashLocation}|{ParenLocation})?$",
|
||||
RegexOptions.IgnoreCase);
|
||||
|
||||
public static (
|
||||
string power,
|
||||
string type,
|
||||
string province,
|
||||
string location)
|
||||
ParseUnitDeclaration(Match match) => (
|
||||
match.Groups[1].Value,
|
||||
match.Groups[2].Value,
|
||||
match.Groups[3].Value,
|
||||
match.Groups[4].Value.Length > 0
|
||||
? match.Groups[4].Value
|
||||
: match.Groups[5].Value);
|
||||
|
||||
public Regex Hold => new(
|
||||
$"^{UnitSpec} {HoldVerb}$",
|
||||
RegexOptions.IgnoreCase);
|
||||
|
||||
public static (
|
||||
string type,
|
||||
string timeline,
|
||||
string province,
|
||||
string location,
|
||||
string turn,
|
||||
string holdVerb)
|
||||
ParseHold(Match match) => (
|
||||
match.Groups[1].Value,
|
||||
match.Groups[2].Value,
|
||||
match.Groups[3].Value,
|
||||
match.Groups[4].Length > 0
|
||||
? match.Groups[4].Value
|
||||
: match.Groups[5].Value,
|
||||
match.Groups[6].Value,
|
||||
match.Groups[7].Value);
|
||||
|
||||
public Regex Move => new(
|
||||
$"^{UnitSpec} {MoveVerb} {FullLocation}(?: {ViaConvoy})?$",
|
||||
RegexOptions.IgnoreCase);
|
||||
|
||||
public static (
|
||||
string type,
|
||||
string timeline,
|
||||
string province,
|
||||
string location,
|
||||
string turn,
|
||||
string moveVerb,
|
||||
string destTimeline,
|
||||
string destProvince,
|
||||
string destLocation,
|
||||
string destTurn,
|
||||
string viaConvoy)
|
||||
ParseMove(Match match) => (
|
||||
match.Groups[1].Value,
|
||||
match.Groups[2].Value,
|
||||
match.Groups[3].Value,
|
||||
match.Groups[4].Length > 0
|
||||
? match.Groups[4].Value
|
||||
: match.Groups[5].Value,
|
||||
match.Groups[6].Value,
|
||||
match.Groups[7].Value,
|
||||
match.Groups[8].Value,
|
||||
match.Groups[9].Value,
|
||||
match.Groups[10].Length > 0
|
||||
? match.Groups[10].Value
|
||||
: match.Groups[11].Value,
|
||||
match.Groups[12].Value,
|
||||
match.Groups[13].Value);
|
||||
|
||||
public Regex SupportHold => new(
|
||||
$"^{UnitSpec} {SupportVerb} {UnitSpec}$",
|
||||
RegexOptions.IgnoreCase);
|
||||
|
||||
public static (
|
||||
string type,
|
||||
string timeline,
|
||||
string province,
|
||||
string location,
|
||||
string turn,
|
||||
string supportVerb,
|
||||
string targetType,
|
||||
string targetTimeline,
|
||||
string targetProvince,
|
||||
string targetLocation,
|
||||
string targetTurn)
|
||||
ParseSupportHold(Match match) => (
|
||||
match.Groups[1].Value,
|
||||
match.Groups[2].Value,
|
||||
match.Groups[3].Value,
|
||||
match.Groups[4].Length > 0
|
||||
? match.Groups[4].Value
|
||||
: match.Groups[5].Value,
|
||||
match.Groups[6].Value,
|
||||
match.Groups[7].Value,
|
||||
match.Groups[8].Value,
|
||||
match.Groups[9].Value,
|
||||
match.Groups[10].Value,
|
||||
match.Groups[11].Length > 0
|
||||
? match.Groups[11].Value
|
||||
: match.Groups[12].Value,
|
||||
match.Groups[13].Value);
|
||||
|
||||
public Regex SupportMove => new(
|
||||
$"{UnitSpec} {SupportVerb} {UnitSpec} {MoveVerb} {FullLocation}$",
|
||||
RegexOptions.IgnoreCase);
|
||||
|
||||
public static (
|
||||
string type,
|
||||
string timeline,
|
||||
string province,
|
||||
string location,
|
||||
string turn,
|
||||
string supportVerb,
|
||||
string targetType,
|
||||
string targetTimeline,
|
||||
string targetProvince,
|
||||
string targetLocation,
|
||||
string targetTurn,
|
||||
string moveVerb,
|
||||
string destTimeline,
|
||||
string destProvince,
|
||||
string destLocation,
|
||||
string destTurn)
|
||||
ParseSupportMove(Match match) => (
|
||||
match.Groups[1].Value,
|
||||
match.Groups[2].Value,
|
||||
match.Groups[3].Value,
|
||||
match.Groups[4].Length > 0
|
||||
? match.Groups[4].Value
|
||||
: match.Groups[5].Value,
|
||||
match.Groups[6].Value,
|
||||
match.Groups[7].Value,
|
||||
match.Groups[8].Value,
|
||||
match.Groups[9].Value,
|
||||
match.Groups[10].Value,
|
||||
match.Groups[11].Length > 0
|
||||
? match.Groups[11].Value
|
||||
: match.Groups[12].Value,
|
||||
match.Groups[13].Value,
|
||||
match.Groups[14].Value,
|
||||
match.Groups[15].Value,
|
||||
match.Groups[16].Value,
|
||||
match.Groups[17].Length > 0
|
||||
? match.Groups[17].Value
|
||||
: match.Groups[18].Value,
|
||||
match.Groups[19].Value);
|
||||
|
||||
public static bool TryParseUnit(World world, string unitSpec, [NotNullWhen(true)] out Unit? newUnit)
|
||||
{
|
||||
newUnit = null;
|
||||
OrderParser re = new(world);
|
||||
|
||||
Match match = re.UnitDeclaration.Match(unitSpec);
|
||||
if (!match.Success) return false;
|
||||
var unit = ParseUnitDeclaration(match);
|
||||
|
||||
string power = world.Map.Powers.First(p => p.EqualsAnyCase(unit.power));
|
||||
|
||||
string typeName = Enum.GetNames<UnitType>().First(name => name.StartsWithAnyCase(unit.type));
|
||||
UnitType type = Enum.Parse<UnitType>(typeName);
|
||||
|
||||
Province province = world.Map.Provinces.First(prov => prov.Is(unit.province));
|
||||
Location? location;
|
||||
if (unit.location.Length > 0) {
|
||||
location = province.Locations.FirstOrDefault(loc => loc!.Is(unit.location), null);
|
||||
} else {
|
||||
location = type switch {
|
||||
UnitType.Army => province.Locations.FirstOrDefault(loc => loc.Type == LocationType.Land),
|
||||
UnitType.Fleet => province.Locations.FirstOrDefault(loc => loc.Type == LocationType.Water),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
if (location is null) return false;
|
||||
|
||||
newUnit = Unit.Build(location.Key, Season.First, power, type);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryParseOrder(World world, string power, string command, [NotNullWhen(true)] out Order? order)
|
||||
{
|
||||
order = null;
|
||||
OrderParser re = new(world);
|
||||
|
||||
if (re.Hold.Match(command) is Match holdMatch && holdMatch.Success) {
|
||||
return TryParseHoldOrder(world, power, holdMatch, out order);
|
||||
} else if (re.Move.Match(command) is Match moveMatch && moveMatch.Success) {
|
||||
return TryParseMoveOrder(world, power, moveMatch, out order);
|
||||
} else if (re.SupportHold.Match(command) is Match sholdMatch && sholdMatch.Success) {
|
||||
return TryParseSupportHoldOrder(world, power, sholdMatch, out order);
|
||||
} else if (re.SupportMove.Match(command) is Match smoveMatch && smoveMatch.Success) {
|
||||
return TryParseSupportMoveOrder(world, power, smoveMatch, out order);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TryParseOrderSubject(
|
||||
World world,
|
||||
string parsedTimeline,
|
||||
string parsedTurn,
|
||||
string parsedProvince,
|
||||
[NotNullWhen(true)] out Unit? subject)
|
||||
{
|
||||
subject = null;
|
||||
|
||||
string timeline = parsedTimeline.Length > 0
|
||||
? parsedTimeline
|
||||
// If timeline is unspecified, use the root timeline
|
||||
: Season.First.Timeline;
|
||||
var seasonsInTimeline = world.Timelines.Seasons.Where(season => season.Timeline == timeline);
|
||||
if (!seasonsInTimeline.Any()) return false;
|
||||
|
||||
int turn = parsedTurn.Length > 0
|
||||
? int.Parse(parsedTurn)
|
||||
// If turn is unspecified, use the latest turn in the timeline
|
||||
: seasonsInTimeline.Max(season => season.Turn);
|
||||
|
||||
Province province = world.Map.Provinces.Single(province => province.Is(parsedProvince));
|
||||
|
||||
// Because only one unit can be in a province at a time, the province is sufficient to identify the subject
|
||||
// and the location is ignored. This also satisfies DATC 4.B.5, which requires that a wrong coast for the
|
||||
// subject be ignored.
|
||||
subject = world.Units.FirstOrDefault(unit
|
||||
=> world.Map.GetLocation(unit!.Location).ProvinceName == province.Name
|
||||
&& unit!.Season.Timeline == timeline
|
||||
&& unit!.Season.Turn == turn,
|
||||
null);
|
||||
return subject is not null;
|
||||
}
|
||||
|
||||
public static bool TryParseHoldOrder(
|
||||
World world,
|
||||
string power,
|
||||
Match match,
|
||||
[NotNullWhen(true)] out Order? order)
|
||||
{
|
||||
order = null;
|
||||
var hold = ParseHold(match);
|
||||
|
||||
if (!TryParseOrderSubject(world, hold.timeline, hold.turn, hold.province, out Unit? subject)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
order = new HoldOrder(power, subject);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryParseMoveOrder(
|
||||
World world,
|
||||
string power,
|
||||
Match match,
|
||||
[NotNullWhen(true)] out Order? order)
|
||||
{
|
||||
order = null;
|
||||
var move = ParseMove(match);
|
||||
|
||||
if (!TryParseOrderSubject(world, move.timeline, move.turn, move.province, out Unit? subject)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
string destTimeline = move.destTimeline.Length > 0
|
||||
? move.destTimeline
|
||||
// If the destination is unspecified, use the unit's
|
||||
: subject.Season.Timeline;
|
||||
|
||||
int destTurn = move.destTurn.Length > 0
|
||||
? int.Parse(move.destTurn)
|
||||
// If the destination is unspecified, use the unit's
|
||||
: subject.Season.Turn;
|
||||
|
||||
var destProvince = world.Map.Provinces.Single(province => province.Is(move.destProvince));
|
||||
string? destLocationKey = null;
|
||||
|
||||
// DATC 4.B specifies how to interpret orders with missing or incorrect locations. These issues arise because
|
||||
// of provinces with multiple locations of the same type, i.e. two-coast provinces in Classical. In general,
|
||||
// DATC's only concern is to disambiguate the order, failing the order only when it is ineluctably ambiguous
|
||||
// (4.B.1) or explicitly incorrect (4.B.3). Irrelevant or nonexistent locations can be ignored.
|
||||
|
||||
// If there is only one possible location for the moving unit, that location is used. The idea of land and
|
||||
// water locations is an implementation detail of 5dplomacy and not part of the Diplomacy rules, so they will
|
||||
// usually be omitted, and so moving an army to any land province or a fleet to a non-multi-coast province is
|
||||
// naturally unambiguous even without the location.
|
||||
var unitLocations = destProvince.Locations.Where(loc => loc.Type switch {
|
||||
LocationType.Land => subject.Type == UnitType.Army,
|
||||
LocationType.Water => subject.Type == UnitType.Fleet,
|
||||
_ => false,
|
||||
});
|
||||
if (!unitLocations.Any()) return false; // If *no* locations match, the move is illegal
|
||||
if (unitLocations.Count() == 1) destLocationKey ??= unitLocations.Single().Key;
|
||||
|
||||
// If more than one location is possible for the unit, the order must be disambiguated by the dest location
|
||||
// or the physical realities of which coast is accessible. DATC 4.B.3 makes an order illegal if the location
|
||||
// is specified but it isn't an accessible coast, so successfully specifying a location takes precedence over
|
||||
// there being one accessible coast.
|
||||
if (destLocationKey is null) {
|
||||
var matchingLocations = unitLocations.Where(loc => loc.Is(move.destLocation));
|
||||
if (matchingLocations.Any()) destLocationKey ??= matchingLocations.Single().Key;
|
||||
}
|
||||
|
||||
// If the order location didn't disambiguate the coasts, either because it's missing or it's nonsense, the
|
||||
// order can be disambiguated by there being one accessible coast from the order source.
|
||||
if (destLocationKey is null) {
|
||||
Location source = world.Map.GetLocation(subject.Location);
|
||||
var accessibleLocations = destProvince.Locations.Where(loc => loc.Adjacents.Contains(source));
|
||||
if (accessibleLocations.Count() == 1) destLocationKey ??= accessibleLocations.Single().Key;
|
||||
}
|
||||
|
||||
// If the order is still ambiguous, fail per DATC 4.B.1.
|
||||
if (destLocationKey is null) return false;
|
||||
|
||||
order = new MoveOrder(power, subject, new(destTimeline, destTurn), destLocationKey);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryParseSupportHoldOrder(
|
||||
World world,
|
||||
string power,
|
||||
Match match,
|
||||
[NotNullWhen(true)] out Order? order)
|
||||
{
|
||||
order = null;
|
||||
var support = ParseSupportHold(match);
|
||||
|
||||
if (!TryParseOrderSubject(world, support.timeline, support.turn, support.province, out Unit? subject)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryParseOrderSubject(
|
||||
world, support.targetTimeline, support.targetTurn, support.targetProvince, out Unit? target))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
order = new SupportHoldOrder(power, subject, target);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryParseSupportMoveOrder(
|
||||
World world,
|
||||
string power,
|
||||
Match match,
|
||||
[NotNullWhen(true)] out Order? order)
|
||||
{
|
||||
order = null;
|
||||
var support = ParseSupportMove(match);
|
||||
|
||||
if (!TryParseOrderSubject(world, support.timeline, support.turn, support.province, out Unit? subject)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryParseOrderSubject(
|
||||
world, support.targetTimeline, support.targetTurn, support.targetProvince, out Unit? target))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string destTimeline = support.destTimeline.Length > 0
|
||||
? support.destTimeline
|
||||
// If the destination is unspecified, use the target's
|
||||
: target.Season.Timeline;
|
||||
|
||||
int destTurn = support.destTurn.Length > 0
|
||||
? int.Parse(support.destTurn)
|
||||
// If the destination is unspecified, use the unit's
|
||||
: target.Season.Turn;
|
||||
|
||||
var destProvince = world.Map.Provinces.Single(province => province.Is(support.destProvince));
|
||||
string? destLocationKey = null;
|
||||
|
||||
// DATC 4.B specifies how to interpret orders with missing or incorrect locations. These issues arise because
|
||||
// of provinces with multiple locations of the same type, i.e. two-coast provinces in Classical. In general,
|
||||
// DATC's only concern is to disambiguate the order, failing the order only when it is ineluctably ambiguous
|
||||
// (4.B.1) or explicitly incorrect (4.B.3). Irrelevant or nonexistent locations can be ignored.
|
||||
|
||||
// If there is only one possible location for the moving unit, that location is used. The idea of land and
|
||||
// water locations is an implementation detail of 5dplomacy and not part of the Diplomacy rules, so they will
|
||||
// usually be omitted, and so moving an army to any land province or a fleet to a non-multi-coast province is
|
||||
// naturally unambiguous even without the location.
|
||||
var unitLocations = destProvince.Locations.Where(loc => loc.Type switch {
|
||||
LocationType.Land => target.Type == UnitType.Army,
|
||||
LocationType.Water => target.Type == UnitType.Fleet,
|
||||
_ => false,
|
||||
});
|
||||
if (!unitLocations.Any()) return false; // If *no* locations match, the move is illegal
|
||||
if (unitLocations.Count() == 1) destLocationKey ??= unitLocations.Single().Key;
|
||||
|
||||
// If more than one location is possible for the unit, the order must be disambiguated by the dest location
|
||||
// or the physical realities of which coast is accessible. DATC 4.B.3 makes an order illegal if the location
|
||||
// is specified but it isn't an accessible coast, so successfully specifying a location takes precedence over
|
||||
// there being one accessible coast.
|
||||
if (destLocationKey is null) {
|
||||
var matchingLocations = unitLocations.Where(loc => loc.Is(support.destLocation));
|
||||
if (matchingLocations.Any()) destLocationKey ??= matchingLocations.Single().Key;
|
||||
}
|
||||
|
||||
// If the order location didn't disambiguate the coasts, either because it's missing or it's nonsense, the
|
||||
// order can be disambiguated by there being one accessible coast from the order source.
|
||||
if (destLocationKey is null) {
|
||||
Location source = world.Map.GetLocation(target.Location);
|
||||
var accessibleLocations = destProvince.Locations.Where(loc => loc.Adjacents.Contains(source));
|
||||
if (accessibleLocations.Count() == 1) destLocationKey ??= accessibleLocations.Single().Key;
|
||||
}
|
||||
|
||||
// If the order is still ambiguous, fail per DATC 4.B.1. This also satisfies 4.B.4, which prefers for
|
||||
// programmatic adjudicators with order validation to require the coasts instead of interpreting the ambiguous
|
||||
// support by referring to the move order it supports.
|
||||
if (destLocationKey is null) return false;
|
||||
|
||||
var destLocation = world.Map.GetLocation(destLocationKey);
|
||||
order = new SupportMoveOrder(power, subject, target, new(destTimeline, destTurn), destLocation);
|
||||
return true;
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
namespace MultiversalDiplomacy.Model;
|
||||
|
||||
/// <summary>
|
||||
/// One of the rival nations vying for control of the map.
|
||||
/// </summary>
|
||||
public class Power
|
||||
{
|
||||
/// <summary>
|
||||
/// The power's name.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
public Power(string name)
|
||||
{
|
||||
this.Name = name;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return this.Name;
|
||||
}
|
||||
}
|
@ -31,13 +31,18 @@ public class Province
|
||||
public IEnumerable<Location> Locations => LocationList;
|
||||
private List<Location> LocationList { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The province's name and abbreviations as a single enumeration.
|
||||
/// </summary>
|
||||
public IEnumerable<string> AllNames => Abbreviations.Append(Name);
|
||||
|
||||
public Province(string name, string[] abbreviations, bool isSupply, bool isTime)
|
||||
{
|
||||
this.Name = name;
|
||||
this.Abbreviations = abbreviations;
|
||||
this.IsSupplyCenter = isSupply;
|
||||
this.IsTimeCenter = isTime;
|
||||
this.LocationList = new List<Location>();
|
||||
this.LocationList = [];
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
@ -45,30 +50,36 @@ public class Province
|
||||
return this.Name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether a name is the name or abbreviation of this province.
|
||||
/// </summary>
|
||||
public bool Is(string name)
|
||||
=> name.EqualsAnyCase(Name) || Abbreviations.Any(name.EqualsAnyCase);
|
||||
|
||||
/// <summary>
|
||||
/// 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 +89,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 +100,7 @@ public class Province
|
||||
/// </summary>
|
||||
public Province AddCoastLocation(string name, string abbreviation)
|
||||
{
|
||||
Location location = new Location(this, name, abbreviation, LocationType.Water);
|
||||
Location location = new(this, name, abbreviation, LocationType.Water);
|
||||
this.LocationList.Add(location);
|
||||
return this;
|
||||
}
|
||||
|
@ -1,185 +1,77 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
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.
|
||||
/// Represents a multiversal coordinate at which a state of the map exists.
|
||||
/// </summary>
|
||||
public class Season
|
||||
[JsonConverter(typeof(SeasonJsonConverter))]
|
||||
public struct Season(string timeline, int turn)
|
||||
{
|
||||
/// <summary>
|
||||
/// A shared counter for handing out new timeline numbers.
|
||||
/// The root season of every game. This is defined to avoid any confusion about what the first turn or timeline
|
||||
/// should be or what season to use to key into a fresh <see cref="Timelines"/>.
|
||||
/// </summary>
|
||||
private class TimelineFactory
|
||||
{
|
||||
private int nextTimeline = 0;
|
||||
|
||||
public int NextTimeline() => nextTimeline++;
|
||||
}
|
||||
public static readonly Season First = new(Timelines.IntToString(0), 0);
|
||||
|
||||
/// <summary>
|
||||
/// The first turn number.
|
||||
/// The timeline to which this season belongs.
|
||||
/// </summary>
|
||||
public const int FIRST_TURN = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 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 Timeline { get; } = timeline;
|
||||
|
||||
/// <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.
|
||||
/// The multiversal designation of this season.
|
||||
/// </summary>
|
||||
public int Timeline { get; }
|
||||
[JsonIgnore]
|
||||
public readonly string Key => $"{this.Timeline}{this.Turn}";
|
||||
|
||||
/// <summary>
|
||||
/// The season's spatial location as a turn-timeline tuple.
|
||||
/// Create a new season from a tuple coordinate.
|
||||
/// </summary>
|
||||
public (int Turn, int Timeline) Coord => (this.Turn, this.Timeline);
|
||||
public Season((string timeline, int turn) coord) : this(coord.timeline, coord.turn) { }
|
||||
|
||||
/// <summary>
|
||||
/// The shared timeline number generator.
|
||||
/// Create a new season from a combined string designation.
|
||||
/// </summary>
|
||||
private TimelineFactory Timelines { get; }
|
||||
/// <param name="designation"></param>
|
||||
public Season(string designation) : this(SplitKey(designation)) { }
|
||||
|
||||
/// <summary>
|
||||
/// Future seasons created directly from this season.
|
||||
/// Extract the timeline and turn components of a season designation.
|
||||
/// </summary>
|
||||
public IEnumerable<Season> Futures => this.FutureList;
|
||||
private List<Season> FutureList { get; }
|
||||
|
||||
private Season(Season? past, int turn, int timeline, TimelineFactory factory)
|
||||
/// <param name="seasonKey">A timeline-turn season designation.</param>
|
||||
/// <returns>The timeline and turn components.</returns>
|
||||
/// <exception cref="FormatException"></exception>
|
||||
public static (string timeline, int turn) SplitKey(string seasonKey)
|
||||
{
|
||||
this.Past = past;
|
||||
this.Turn = turn;
|
||||
this.Timeline = timeline;
|
||||
this.Timelines = factory;
|
||||
this.FutureList = new();
|
||||
|
||||
if (past != null)
|
||||
{
|
||||
past.FutureList.Add(this);
|
||||
}
|
||||
int i = 1;
|
||||
for (; !char.IsAsciiDigit(seasonKey[i]) && i < seasonKey.Length; i++);
|
||||
return int.TryParse(seasonKey.AsSpan(i), out int turn)
|
||||
? (seasonKey[..i], turn)
|
||||
: throw new FormatException($"Could not parse turn from {seasonKey}");
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{this.Timeline}@{this.Turn}";
|
||||
}
|
||||
public override readonly string ToString() => Key;
|
||||
|
||||
/// <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);
|
||||
}
|
||||
/// <remarks>
|
||||
/// Seasons are essentially 2D points, so they are equal when their components are equal.
|
||||
/// </remarks>
|
||||
public override readonly bool Equals([NotNullWhen(true)] object? obj)
|
||||
=> obj is Season season
|
||||
&& Timeline == season.Timeline
|
||||
&& Turn == season.Turn;
|
||||
|
||||
/// <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);
|
||||
public static bool operator ==(Season one, Season two) => one.Equals(two);
|
||||
|
||||
/// <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);
|
||||
public static bool operator !=(Season one, Season two) => !(one == two);
|
||||
|
||||
/// <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 readonly int GetHashCode() => (Timeline, Turn).GetHashCode();
|
||||
}
|
||||
|
16
MultiversalDiplomacy/Model/SeasonJsonConverter.cs
Normal file
16
MultiversalDiplomacy/Model/SeasonJsonConverter.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MultiversalDiplomacy.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Serializes a <see cref="Season"/> as its combined designation.
|
||||
/// </summary>
|
||||
internal class SeasonJsonConverter : JsonConverter<Season>
|
||||
{
|
||||
public override Season Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
=> new(reader.GetString()!);
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, Season value, JsonSerializerOptions options)
|
||||
=> writer.WriteStringValue(value.Key);
|
||||
}
|
158
MultiversalDiplomacy/Model/Timelines.cs
Normal file
158
MultiversalDiplomacy/Model/Timelines.cs
Normal file
@ -0,0 +1,158 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MultiversalDiplomacy.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Tracks the relations between seasons.
|
||||
/// </summary>
|
||||
public class Timelines(int next, Dictionary<string, Season?> pasts)
|
||||
{
|
||||
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="serial">Integer.</param>
|
||||
/// <returns>Timeline identifier.</returns>
|
||||
public static string IntToString(int serial) {
|
||||
static int downshift(int i ) => (i - (i % 26)) / 26;
|
||||
IEnumerable<char> result = [Letters[serial % 26]];
|
||||
for (int remainder = downshift(serial); 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());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract the timeline and turn components of a season designation.
|
||||
/// </summary>
|
||||
/// <param name="seasonKey">A timeline-turn season designation.</param>
|
||||
/// <returns>The timeline and turn components.</returns>
|
||||
/// <exception cref="FormatException"></exception>
|
||||
public static (string timeline, int turn) SplitKey(string seasonKey)
|
||||
{
|
||||
int i = 1;
|
||||
for (; !char.IsAsciiDigit(seasonKey[i]) && i < seasonKey.Length; i++);
|
||||
return int.TryParse(seasonKey.AsSpan(i), out int turn)
|
||||
? (seasonKey[..i], turn)
|
||||
: throw new FormatException($"Could not parse turn from {seasonKey}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The next timeline to be created.
|
||||
/// </summary>
|
||||
public int Next { get; private set; } = next;
|
||||
|
||||
/// <summary>
|
||||
/// Map of season designations to their parent seasons. Every season has an entry, so
|
||||
/// the set of keys is the set of existing seasons.
|
||||
/// </summary>
|
||||
public Dictionary<string, Season?> Pasts { get; } = pasts;
|
||||
|
||||
/// <summary>
|
||||
/// All seasons in the multiverse.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public IEnumerable<Season> Seasons => Pasts.Keys.Select(key => new Season(key));
|
||||
|
||||
/// <summary>
|
||||
/// Create a new multiverse with an initial season.
|
||||
/// </summary>
|
||||
public static Timelines Create()
|
||||
=> new(StringToInt(Season.First.Timeline) + 1, new() { {Season.First.Key, null} });
|
||||
|
||||
/// <summary>
|
||||
/// Create a continuation of a season if it has no futures, otherwise create a fork.
|
||||
/// </summary>
|
||||
public Timelines WithNewSeason(Season past, out Season future)
|
||||
{
|
||||
int next;
|
||||
(next, future) = GetFutureKeys(past).Any()
|
||||
? (Next + 1, new Season(IntToString(Next), past.Turn + 1))
|
||||
: (Next, new Season(past.Timeline, past.Turn + 1));
|
||||
return new Timelines(next, new(Pasts.Append(new KeyValuePair<string, Season?>(future.Key, past))));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a continuation of a season if it has no futures, otherwise create a fork.
|
||||
/// </summary>
|
||||
public Timelines WithNewSeason(string past, out Season future) => WithNewSeason(new Season(past), out future);
|
||||
|
||||
/// <summary>
|
||||
/// Get all seasons that are immediate futures of a season.
|
||||
/// </summary>
|
||||
/// <param name="season">A season.</param>
|
||||
/// <returns>The immediate futures of the season.</returns>
|
||||
public IEnumerable<string> GetFutureKeys(Season season)
|
||||
=> Pasts.Where(kvp => kvp.Value is Season future && future == season).Select(kvp => kvp.Key);
|
||||
|
||||
/// <summary>
|
||||
/// Get all seasons that are immediate futures of a season.
|
||||
/// </summary>
|
||||
/// <param name="season">A season.</param>
|
||||
/// <returns>The immediate futures of the season.</returns>
|
||||
public IEnumerable<Season> GetFutures(Season season) => GetFutureKeys(season).Select(key => new Season(key));
|
||||
|
||||
/// <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)
|
||||
{
|
||||
return Pasts[season.Key] is Season past && season.Timeline == past.Timeline
|
||||
? GetTimelineRoot(past)
|
||||
: season;
|
||||
}
|
||||
|
||||
/// <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(string season) => GetTimelineRoot(new Season(season));
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether a season is in an adjacent timeline to another season.
|
||||
/// Seasons are considered to be in adjacent timelines if they are in the same timeline,
|
||||
/// one is in a timeline that branched from the other's timeline, or both are in timelines
|
||||
/// that branched from the same point.
|
||||
/// </summary>
|
||||
public bool InAdjacentTimeline(Season one, Season two)
|
||||
{
|
||||
// Timelines are adjacent to themselves. Early out in that case.
|
||||
if (one == two) return true;
|
||||
|
||||
// If the timelines aren't identical, one of them isn't the initial trunk.
|
||||
// They can still be adjacent if one of them branched off of the other, or
|
||||
// if they both branched off of the same point.
|
||||
Season rootOne = GetTimelineRoot(one);
|
||||
Season rootTwo = GetTimelineRoot(two);
|
||||
bool oneForked = Pasts[rootOne.Key] is Season originOne && originOne.Timeline == two.Timeline;
|
||||
bool twoForked = Pasts[rootTwo.Key] is Season originTwo && originTwo.Timeline == one.Timeline;
|
||||
bool bothForked = Pasts[rootOne.Key] == Pasts[rootTwo.Key];
|
||||
return oneForked || twoForked || bothForked;
|
||||
}
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MultiversalDiplomacy.Model;
|
||||
|
||||
/// <summary>
|
||||
@ -8,17 +10,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.
|
||||
@ -28,7 +25,7 @@ public class Unit
|
||||
/// <summary>
|
||||
/// The allegiance of the unit.
|
||||
/// </summary>
|
||||
public Power Power { get; }
|
||||
public string Power { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of unit.
|
||||
@ -36,11 +33,12 @@ 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);
|
||||
[JsonIgnore]
|
||||
public string Key => $"{Type.ToShort()} {Season.Timeline}-{Location}@{Season.Turn}";
|
||||
|
||||
private Unit(Unit? past, Location location, Season season, Power power, UnitType type)
|
||||
public Unit(string? past, string location, Season season, string power, UnitType type)
|
||||
{
|
||||
this.Past = past;
|
||||
this.Location = location;
|
||||
@ -50,20 +48,18 @@ public class Unit
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{this.Power.Name[0]} {this.Type.ToShort()} {(this.Province, this.Season).ToShort()}";
|
||||
}
|
||||
=> $"{Power[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, string 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.Key, location, season, this.Power, this.Type);
|
||||
}
|
||||
|
@ -1,8 +1,11 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MultiversalDiplomacy.Model;
|
||||
|
||||
/// <summary>
|
||||
/// The type of a unit.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<UnitType>))]
|
||||
public enum UnitType
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -1,6 +1,5 @@
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
using MultiversalDiplomacy.Orders;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MultiversalDiplomacy.Model;
|
||||
|
||||
@ -9,66 +8,96 @@ namespace MultiversalDiplomacy.Model;
|
||||
/// </summary>
|
||||
public class World
|
||||
{
|
||||
public static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
};
|
||||
|
||||
/// <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; }
|
||||
|
||||
/// <summary>
|
||||
/// The state of the multiverse.
|
||||
/// </summary>
|
||||
public ReadOnlyCollection<Season> Seasons { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The first season of the game.
|
||||
/// </summary>
|
||||
public Season RootSeason { get; }
|
||||
[JsonIgnore]
|
||||
public IReadOnlyCollection<string> Powers => this.Map.Powers;
|
||||
|
||||
/// <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 state of the multiverse.
|
||||
/// </summary>
|
||||
public Timelines Timelines { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Immutable game options.
|
||||
/// </summary>
|
||||
public Options Options { get; }
|
||||
|
||||
[JsonConstructor]
|
||||
public World(
|
||||
MapType mapType,
|
||||
List<Unit> units,
|
||||
List<RetreatingUnit> retreatingUnits,
|
||||
Dictionary<string, OrderHistory> orderHistory,
|
||||
Timelines timelines,
|
||||
Options options)
|
||||
{
|
||||
this.Map = Map.FromType(mapType);
|
||||
this.Units = units;
|
||||
this.RetreatingUnits = retreatingUnits;
|
||||
this.OrderHistory = orderHistory;
|
||||
this.Timelines = timelines;
|
||||
this.Options = options;
|
||||
|
||||
}
|
||||
|
||||
/// <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<Unit> units,
|
||||
List<RetreatingUnit> retreatingUnits,
|
||||
Dictionary<string, OrderHistory> orderHistory,
|
||||
Timelines timelines,
|
||||
Options options)
|
||||
{
|
||||
this.Provinces = provinces;
|
||||
this.Powers = powers;
|
||||
this.Seasons = seasons;
|
||||
this.RootSeason = rootSeason;
|
||||
this.Map = map;
|
||||
this.Units = units;
|
||||
this.RetreatingUnits = retreatingUnits;
|
||||
this.OrderHistory = orderHistory;
|
||||
this.Timelines = timelines;
|
||||
this.Options = options;
|
||||
}
|
||||
|
||||
@ -77,21 +106,17 @@ 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<Unit>? units = null,
|
||||
List<RetreatingUnit>? retreatingUnits = null,
|
||||
Dictionary<string, OrderHistory>? orderHistory = null,
|
||||
Timelines? timelines = null,
|
||||
Options? options = null)
|
||||
: this(
|
||||
provinces ?? previous.Provinces,
|
||||
powers ?? previous.Powers,
|
||||
seasons ?? previous.Seasons,
|
||||
previous.RootSeason, // Can't change the root season
|
||||
previous.Map,
|
||||
units ?? previous.Units,
|
||||
retreatingUnits ?? previous.RetreatingUnits,
|
||||
orderHistory ?? previous.OrderHistory,
|
||||
timelines ?? previous.Timelines,
|
||||
options ?? previous.Options)
|
||||
{
|
||||
}
|
||||
@ -99,17 +124,14 @@ 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();
|
||||
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([]),
|
||||
new(new Dictionary<string, OrderHistory>()),
|
||||
Timelines.Create(),
|
||||
new Options());
|
||||
}
|
||||
|
||||
@ -117,18 +139,15 @@ 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,
|
||||
Timelines? timelines = null)
|
||||
=> new(
|
||||
previous: this,
|
||||
seasons: seasons == null
|
||||
? this.Seasons
|
||||
: new(seasons.ToList()),
|
||||
units: units == null
|
||||
? this.Units
|
||||
: new(units.ToList()),
|
||||
@ -137,7 +156,8 @@ public class World
|
||||
: new(retreats.ToList()),
|
||||
orderHistory: orders == null
|
||||
? this.OrderHistory
|
||||
: new(orders.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)));
|
||||
: new(orders.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)),
|
||||
timelines: timelines ?? this.Timelines);
|
||||
|
||||
/// <summary>
|
||||
/// Create a new world with new units created from unit specs. Units specs are in the format
|
||||
@ -149,7 +169,7 @@ public class World
|
||||
IEnumerable<Unit> units = unitSpecs.Select(spec =>
|
||||
{
|
||||
string[] splits = spec.Split(' ', 4);
|
||||
Power power = this.GetPower(splits[0]);
|
||||
string power = Map.GetPower(splits[0]);
|
||||
UnitType type = splits[1] switch
|
||||
{
|
||||
"A" => UnitType.Army,
|
||||
@ -157,11 +177,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.Key, Season.First, power, type);
|
||||
return unit;
|
||||
});
|
||||
return this.Update(units: units);
|
||||
@ -174,7 +194,7 @@ public class World
|
||||
{
|
||||
return this.AddUnits(
|
||||
"Austria A Bud",
|
||||
"Austria A Vir",
|
||||
"Austria A Vie",
|
||||
"Austria F Tri",
|
||||
"England A Lvp",
|
||||
"England F Edi",
|
||||
@ -201,559 +221,23 @@ public class World
|
||||
/// <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);
|
||||
|
||||
/// <summary>
|
||||
/// Get a season by coordinate. Throws if the season is not found.
|
||||
/// </summary>
|
||||
public Season GetSeason(int turn, int timeline)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a power by name. Throws if there is not exactly one such power.
|
||||
/// </summary>
|
||||
public Power GetPower(string powerName)
|
||||
{
|
||||
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;
|
||||
}
|
||||
public static World Standard => WithStandardMap().AddStandardUnits();
|
||||
|
||||
/// <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 ??= Season.First;
|
||||
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 GetUnitByKey(string designation)
|
||||
=> Units.SingleOrDefault(u => u!.Key == designation, null)
|
||||
?? throw new KeyNotFoundException($"Unit {designation} not found");
|
||||
}
|
||||
|
@ -1,10 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<VersionPrefix>0.0.2</VersionPrefix>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>MultiversalDiplomacyTests</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -17,7 +17,7 @@ public class BuildOrder : Order
|
||||
/// </summary>
|
||||
public UnitType Type { get; }
|
||||
|
||||
public BuildOrder(Power power, Location location, UnitType type)
|
||||
public BuildOrder(string power, Location location, UnitType type)
|
||||
: base (power)
|
||||
{
|
||||
this.Location = location;
|
||||
|
@ -27,7 +27,7 @@ public class ConvoyOrder : UnitOrder
|
||||
/// </summary>
|
||||
public Province Province => this.Location.Province;
|
||||
|
||||
public ConvoyOrder(Power power, Unit unit, Unit target, Season season, Location location)
|
||||
public ConvoyOrder(string power, Unit unit, Unit target, Season season, Location location)
|
||||
: base (power, unit)
|
||||
{
|
||||
this.Target = target;
|
||||
@ -37,6 +37,6 @@ public class ConvoyOrder : UnitOrder
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{this.Unit} C {this.Target} -> {(this.Province, this.Season).ToShort()}";
|
||||
return $"{this.Unit} con {this.Target} -> {(this.Province, this.Season).ToShort()}";
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,6 @@ namespace MultiversalDiplomacy.Orders;
|
||||
/// </summary>
|
||||
public class DisbandOrder : UnitOrder
|
||||
{
|
||||
public DisbandOrder(Power power, Unit unit)
|
||||
public DisbandOrder(string power, Unit unit)
|
||||
: base (power, unit) {}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ namespace MultiversalDiplomacy.Orders;
|
||||
/// </summary>
|
||||
public class HoldOrder : UnitOrder
|
||||
{
|
||||
public HoldOrder(Power power, Unit unit)
|
||||
public HoldOrder(string power, Unit unit)
|
||||
: base (power, unit) {}
|
||||
|
||||
public override string ToString()
|
||||
|
@ -1,5 +1,7 @@
|
||||
using MultiversalDiplomacy.Model;
|
||||
|
||||
using static MultiversalDiplomacy.Model.Location;
|
||||
|
||||
namespace MultiversalDiplomacy.Orders;
|
||||
|
||||
/// <summary>
|
||||
@ -13,21 +15,11 @@ public class MoveOrder : UnitOrder
|
||||
public Season Season { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The destination location to which the unit should move.
|
||||
/// The destination province/location to which the unit should move.
|
||||
/// </summary>
|
||||
public Location Location { get; }
|
||||
public string Location { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The destination province to which the unit should move.
|
||||
/// </summary>
|
||||
public Province Province => this.Location.Province;
|
||||
|
||||
/// <summary>
|
||||
/// The destination's spatiotemporal location as a province-season tuple.
|
||||
/// </summary>
|
||||
public (Province province, Season season) Point => (this.Province, this.Season);
|
||||
|
||||
public MoveOrder(Power power, Unit unit, Season season, Location location)
|
||||
public MoveOrder(string power, Unit unit, Season season, string location)
|
||||
: base (power, unit)
|
||||
{
|
||||
this.Season = season;
|
||||
@ -36,23 +28,6 @@ public class MoveOrder : UnitOrder
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{this.Unit} -> {(this.Province, this.Season).ToShort()}";
|
||||
return $"{this.Unit} -> {Season.Timeline}-{SplitKey(Location).province}@{Season.Turn}";
|
||||
}
|
||||
|
||||
/// <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>
|
||||
public bool IsCompeting(MoveOrder other)
|
||||
=> this != other
|
||||
&& this.Season == other.Season
|
||||
&& this.Province == other.Province;
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using MultiversalDiplomacy.Model;
|
||||
|
||||
namespace MultiversalDiplomacy.Orders;
|
||||
@ -5,14 +7,24 @@ namespace MultiversalDiplomacy.Orders;
|
||||
/// <summary>
|
||||
/// A submitted action by a power.
|
||||
/// </summary>
|
||||
[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")]
|
||||
[JsonDerivedType(typeof(BuildOrder), typeDiscriminator: "move")]
|
||||
[JsonDerivedType(typeof(ConvoyOrder), typeDiscriminator: "convoy")]
|
||||
[JsonDerivedType(typeof(DisbandOrder), typeDiscriminator: "disband")]
|
||||
[JsonDerivedType(typeof(HoldOrder), typeDiscriminator: "hold")]
|
||||
[JsonDerivedType(typeof(MoveOrder), typeDiscriminator: "move")]
|
||||
[JsonDerivedType(typeof(RetreatOrder), typeDiscriminator: "retreat")]
|
||||
[JsonDerivedType(typeof(SupportHoldOrder), typeDiscriminator: "supportHold")]
|
||||
[JsonDerivedType(typeof(SupportMoveOrder), typeDiscriminator: "supportMove")]
|
||||
[JsonDerivedType(typeof(SustainOrder), typeDiscriminator: "sustain")]
|
||||
public abstract class Order
|
||||
{
|
||||
/// <summary>
|
||||
/// The power that submitted this order.
|
||||
/// </summary>
|
||||
public Power Power { get; }
|
||||
public string Power { get; }
|
||||
|
||||
public Order(Power power)
|
||||
public Order(string power)
|
||||
{
|
||||
this.Power = power;
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ public class RetreatOrder : UnitOrder
|
||||
/// </summary>
|
||||
public Location Location { get; }
|
||||
|
||||
public RetreatOrder(Power power, Unit unit, Location location)
|
||||
public RetreatOrder(string power, Unit unit, Location location)
|
||||
: base (power, unit)
|
||||
{
|
||||
this.Location = location;
|
||||
|
@ -7,13 +7,13 @@ namespace MultiversalDiplomacy.Orders;
|
||||
/// </summary>
|
||||
public class SupportHoldOrder : SupportOrder
|
||||
{
|
||||
public SupportHoldOrder(Power power, Unit unit, Unit target)
|
||||
public SupportHoldOrder(string power, Unit unit, Unit target)
|
||||
: base (power, unit, target)
|
||||
{
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{this.Unit} S {this.Target}";
|
||||
return $"{this.Unit} sup {this.Target}";
|
||||
}
|
||||
}
|
@ -27,7 +27,7 @@ public class SupportMoveOrder : SupportOrder
|
||||
/// </summary>
|
||||
public (Province province, Season season) Point => (this.Province, this.Season);
|
||||
|
||||
public SupportMoveOrder(Power power, Unit unit, Unit target, Season season, Location location)
|
||||
public SupportMoveOrder(string power, Unit unit, Unit target, Season season, Location location)
|
||||
: base(power, unit, target)
|
||||
{
|
||||
this.Season = season;
|
||||
@ -36,11 +36,6 @@ public class SupportMoveOrder : SupportOrder
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{this.Unit} S {this.Target} -> {(this.Province, this.Season).ToShort()}";
|
||||
return $"{this.Unit} sup {this.Target} -> {(this.Province, this.Season).ToShort()}";
|
||||
}
|
||||
|
||||
public bool IsSupportFor(MoveOrder move)
|
||||
=> this.Target == move.Unit
|
||||
&& this.Season == move.Season
|
||||
&& this.Location == move.Location;
|
||||
}
|
@ -12,7 +12,7 @@ public abstract class SupportOrder : UnitOrder
|
||||
/// </summary>
|
||||
public Unit Target { get; }
|
||||
|
||||
public SupportOrder(Power power, Unit unit, Unit target)
|
||||
public SupportOrder(string power, Unit unit, Unit target)
|
||||
: base (power, unit)
|
||||
{
|
||||
this.Target = target;
|
||||
|
@ -17,7 +17,7 @@ public class SustainOrder : Order
|
||||
/// </summary>
|
||||
public int Timeline { get; }
|
||||
|
||||
public SustainOrder(Power power, Location timeCenter, int timeline)
|
||||
public SustainOrder(string power, Location timeCenter, int timeline)
|
||||
: base (power)
|
||||
{
|
||||
this.TimeCenter = timeCenter;
|
||||
|
@ -1,3 +1,5 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using MultiversalDiplomacy.Model;
|
||||
|
||||
namespace MultiversalDiplomacy.Orders;
|
||||
@ -5,6 +7,14 @@ namespace MultiversalDiplomacy.Orders;
|
||||
/// <summary>
|
||||
/// An order given to a specific unit.
|
||||
/// </summary>
|
||||
[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")]
|
||||
[JsonDerivedType(typeof(ConvoyOrder), typeDiscriminator: "convoy")]
|
||||
[JsonDerivedType(typeof(DisbandOrder), typeDiscriminator: "disband")]
|
||||
[JsonDerivedType(typeof(HoldOrder), typeDiscriminator: "hold")]
|
||||
[JsonDerivedType(typeof(MoveOrder), typeDiscriminator: "move")]
|
||||
[JsonDerivedType(typeof(RetreatOrder), typeDiscriminator: "retreat")]
|
||||
[JsonDerivedType(typeof(SupportHoldOrder), typeDiscriminator: "supportHold")]
|
||||
[JsonDerivedType(typeof(SupportMoveOrder), typeDiscriminator: "supportMove")]
|
||||
public abstract class UnitOrder : Order
|
||||
{
|
||||
/// <summary>
|
||||
@ -12,16 +22,8 @@ public abstract class UnitOrder : Order
|
||||
/// </summary>
|
||||
public Unit Unit { get; }
|
||||
|
||||
public UnitOrder(Power power, Unit unit) : base(power)
|
||||
public UnitOrder(string power, Unit unit) : base(power)
|
||||
{
|
||||
this.Unit = unit;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether a move order is moving into this order's unit's province.
|
||||
/// </summary>
|
||||
public bool IsIncoming(MoveOrder other)
|
||||
=> this != other
|
||||
&& other.Season == this.Unit.Season
|
||||
&& other.Province == this.Unit.Province;
|
||||
}
|
@ -1,12 +1,32 @@
|
||||
using System;
|
||||
using CommandLine;
|
||||
|
||||
namespace MultiversalDiplomacy
|
||||
using MultiversalDiplomacy.CommandLine;
|
||||
|
||||
namespace MultiversalDiplomacy;
|
||||
|
||||
internal class Program
|
||||
{
|
||||
internal class Program
|
||||
[Verb("stab", HelpText = "stab")]
|
||||
private class StabOptions
|
||||
{
|
||||
static void Main(string[] args)
|
||||
{
|
||||
Console.WriteLine("stab");
|
||||
}
|
||||
public static void Execute(StabOptions _)
|
||||
=> Console.WriteLine("stab");
|
||||
}
|
||||
}
|
||||
|
||||
static void Main(string[] args)
|
||||
{
|
||||
var parser = Parser.Default;
|
||||
var parseResult = parser.ParseArguments(
|
||||
args,
|
||||
typeof(AdjudicateOptions),
|
||||
typeof(ImageOptions),
|
||||
typeof(ReplOptions),
|
||||
typeof(StabOptions));
|
||||
|
||||
parseResult
|
||||
.WithParsed<AdjudicateOptions>(AdjudicateOptions.Execute)
|
||||
.WithParsed<ImageOptions>(ImageOptions.Execute)
|
||||
.WithParsed<ReplOptions>(ReplOptions.Execute)
|
||||
.WithParsed<StabOptions>(StabOptions.Execute);
|
||||
}
|
||||
}
|
||||
|
276
MultiversalDiplomacy/Script/AdjudicationQueryScriptHandler.cs
Normal file
276
MultiversalDiplomacy/Script/AdjudicationQueryScriptHandler.cs
Normal file
@ -0,0 +1,276 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using MultiversalDiplomacy.Adjudicate;
|
||||
using MultiversalDiplomacy.Adjudicate.Decision;
|
||||
using MultiversalDiplomacy.Model;
|
||||
using MultiversalDiplomacy.Orders;
|
||||
|
||||
namespace MultiversalDiplomacy.Script;
|
||||
|
||||
public class AdjudicationQueryScriptHandler(
|
||||
Action<string> WriteLine,
|
||||
List<OrderValidation> validations,
|
||||
List<AdjudicationDecision> adjudications,
|
||||
World world,
|
||||
IPhaseAdjudicator adjudicator)
|
||||
: IScriptHandler
|
||||
{
|
||||
public string Prompt => "valid> ";
|
||||
|
||||
public List<OrderValidation> Validations { get; } = validations;
|
||||
|
||||
public List<AdjudicationDecision> Adjudications { get; } = adjudications;
|
||||
|
||||
public World World { get; private set; } = world;
|
||||
|
||||
public ScriptResult HandleInput(string input)
|
||||
{
|
||||
var args = input.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (args.Length == 0 || input.StartsWith('#'))
|
||||
{
|
||||
return ScriptResult.Succeed(this);
|
||||
}
|
||||
|
||||
var command = args[0];
|
||||
switch (command)
|
||||
{
|
||||
case "---":
|
||||
WriteLine("Ready for orders");
|
||||
return ScriptResult.Succeed(new GameScriptHandler(WriteLine, World, adjudicator));
|
||||
|
||||
case "assert" when args.Length == 1:
|
||||
WriteLine("Usage:");
|
||||
break;
|
||||
|
||||
case "assert":
|
||||
return EvaluateAssertion(args[1]);
|
||||
|
||||
case "status":
|
||||
throw new NotImplementedException();
|
||||
|
||||
default:
|
||||
return ScriptResult.Fail($"Unrecognized command: \"{command}\"", this);
|
||||
}
|
||||
|
||||
return ScriptResult.Succeed(this);
|
||||
}
|
||||
|
||||
private ScriptResult EvaluateAssertion(string assertion)
|
||||
{
|
||||
var args = assertion.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
|
||||
OrderParser re = new(World);
|
||||
Regex prov = new($"^{re.FullLocation}$", RegexOptions.IgnoreCase);
|
||||
Match match;
|
||||
string timeline;
|
||||
IEnumerable<Season> seasonsInTimeline;
|
||||
int turn;
|
||||
Season season;
|
||||
Province province;
|
||||
|
||||
switch (args[0])
|
||||
{
|
||||
case "true":
|
||||
return ScriptResult.Succeed(this);
|
||||
|
||||
case "false":
|
||||
return ScriptResult.Fail("assert false", this);
|
||||
|
||||
case "hold-order":
|
||||
// The hold-order assertion primarily serves to verify that a unit's order was illegal in cases where
|
||||
// a written non-hold order was rejected before order validation and replaced with a hold order.
|
||||
match = prov.Match(args[1]);
|
||||
|
||||
timeline = match.Groups[1].Length > 0
|
||||
? match.Groups[1].Value
|
||||
: Season.First.Timeline;
|
||||
seasonsInTimeline = World.Timelines.Seasons.Where(season => season.Timeline == timeline);
|
||||
if (!seasonsInTimeline.Any()) return ScriptResult.Fail($"No seasons in timeline {timeline}", this);
|
||||
|
||||
turn = match.Groups[4].Length > 0
|
||||
? int.Parse(match.Groups[4].Value)
|
||||
// If turn is unspecified, use the second-latest turn in the timeline,
|
||||
// since we want to assert against the subjects of the orders just adjudicated,
|
||||
// and adjudication created a new set of seasons.
|
||||
: seasonsInTimeline.Max(season => season.Turn) - 1;
|
||||
season = new(timeline, turn);
|
||||
|
||||
province = World.Map.Provinces.Single(province => province.Is(match.Groups[2].Value));
|
||||
|
||||
var matchingHolds = Validations.Where(val
|
||||
=> val.Valid
|
||||
&& val.Order is HoldOrder hold
|
||||
&& hold.Unit.Season == season
|
||||
&& World.Map.GetLocation(hold.Unit.Location).ProvinceName == province.Name);
|
||||
if (!matchingHolds.Any()) return ScriptResult.Fail("No matching holds");
|
||||
|
||||
return ScriptResult.Succeed(this);
|
||||
|
||||
case "order-valid":
|
||||
case "order-invalid":
|
||||
match = prov.Match(args[1]);
|
||||
if (!match.Success) return ScriptResult.Fail($"Could not parse province from \"{args[1]}\"", this);
|
||||
|
||||
timeline = match.Groups[1].Length > 0
|
||||
? match.Groups[1].Value
|
||||
: Season.First.Timeline;
|
||||
seasonsInTimeline = World.Timelines.Seasons.Where(season => season.Timeline == timeline);
|
||||
if (!seasonsInTimeline.Any()) return ScriptResult.Fail($"No seasons in timeline {timeline}", this);
|
||||
|
||||
turn = match.Groups[4].Length > 0
|
||||
? int.Parse(match.Groups[4].Value)
|
||||
// If turn is unspecified, use the second-latest turn in the timeline,
|
||||
// since we want to assert against the subjects of the orders just adjudicated,
|
||||
// and adjudication created a new set of seasons.
|
||||
: seasonsInTimeline.Max(season => season.Turn) - 1;
|
||||
season = new(timeline, turn);
|
||||
|
||||
province = World.Map.Provinces.Single(province => province.Is(match.Groups[2].Value));
|
||||
|
||||
var matching = Validations.Where(val
|
||||
=> val.Order is UnitOrder order
|
||||
&& order.Unit.Season == season
|
||||
&& World.Map.GetLocation(order.Unit.Location).ProvinceName == province.Name);
|
||||
if (!matching.Any()) return ScriptResult.Fail("No matching validations");
|
||||
|
||||
if (args[0] == "order-valid" && !matching.First().Valid) {
|
||||
return ScriptResult.Fail($"Order \"{matching.First().Order} is invalid");
|
||||
}
|
||||
if (args[0] == "order-invalid" && matching.First().Valid) {
|
||||
return ScriptResult.Fail($"Order \"{matching.First().Order} is valid");
|
||||
}
|
||||
return ScriptResult.Succeed(this);
|
||||
|
||||
case "has-past":
|
||||
Regex hasPast = new($"^([a-z]+[0-9]+)>([a-z]+[0-9]+)$");
|
||||
match = hasPast.Match(args[1]);
|
||||
if (!match.Success) return ScriptResult.Fail("Expected format s1>s2", this);
|
||||
|
||||
Season future = new(match.Groups[1].Value);
|
||||
if (!World.Timelines.Pasts.TryGetValue(future.Key, out Season? actual)) {
|
||||
return ScriptResult.Fail($"No such season \"{future}\"");
|
||||
}
|
||||
|
||||
Season expected = new(match.Groups[2].Value);
|
||||
if (actual != expected) return ScriptResult.Fail(
|
||||
$"Expected past of {future} to be {expected}, but it was {actual}");
|
||||
return ScriptResult.Succeed(this);
|
||||
|
||||
case "not-dislodged":
|
||||
case "dislodged":
|
||||
re = new(World);
|
||||
prov = new($"^{re.FullLocation}$", RegexOptions.IgnoreCase);
|
||||
match = prov.Match(args[1]);
|
||||
if (!match.Success) return ScriptResult.Fail($"Could not parse province from \"{args[1]}\"", this);
|
||||
|
||||
timeline = match.Groups[1].Length > 0
|
||||
? match.Groups[1].Value
|
||||
: Season.First.Timeline;
|
||||
seasonsInTimeline = World.Timelines.Seasons.Where(season => season.Timeline == timeline);
|
||||
if (!seasonsInTimeline.Any()) return ScriptResult.Fail($"No seasons in timeline {timeline}", this);
|
||||
|
||||
turn = match.Groups[4].Length > 0
|
||||
? int.Parse(match.Groups[4].Value)
|
||||
// If turn is unspecified, use the second-latest turn in the timeline,
|
||||
// since we want to assert against the subjects of the orders just adjudicated,
|
||||
// and adjudication created a new set of seasons.
|
||||
: seasonsInTimeline.Max(season => season.Turn) - 1;
|
||||
season = new(timeline, turn);
|
||||
|
||||
province = World.Map.Provinces.Single(province => province.Is(match.Groups[2].Value));
|
||||
|
||||
var matchingDislodges = Adjudications.Where(adj
|
||||
=> adj is IsDislodged dislodge
|
||||
&& dislodge.Order.Unit.Season == season
|
||||
&& World.Map.GetLocation(dislodge.Order.Unit.Location).ProvinceName == province.Name);
|
||||
if (!matchingDislodges.Any()) return ScriptResult.Fail("No matching dislodge decisions");
|
||||
var isDislodged = matchingDislodges.Cast<IsDislodged>().First();
|
||||
|
||||
if (args[0] == "not-dislodged" && isDislodged.Outcome != false) {
|
||||
return ScriptResult.Fail($"Adjudication {isDislodged} is true");
|
||||
}
|
||||
if (args[0] == "dislodged" && isDislodged.Outcome != true) {
|
||||
return ScriptResult.Fail($"Adjudication {isDislodged} is false");
|
||||
}
|
||||
return ScriptResult.Succeed(this);
|
||||
|
||||
case "moves":
|
||||
case "no-move":
|
||||
re = new(World);
|
||||
prov = new($"^{re.FullLocation}$", RegexOptions.IgnoreCase);
|
||||
match = prov.Match(args[1]);
|
||||
if (!match.Success) return ScriptResult.Fail($"Could not parse province from \"{args[1]}\"", this);
|
||||
|
||||
timeline = match.Groups[1].Length > 0
|
||||
? match.Groups[1].Value
|
||||
: Season.First.Timeline;
|
||||
seasonsInTimeline = World.Timelines.Seasons.Where(season => season.Timeline == timeline);
|
||||
if (!seasonsInTimeline.Any()) return ScriptResult.Fail($"No seasons in timeline {timeline}", this);
|
||||
|
||||
turn = match.Groups[4].Length > 0
|
||||
? int.Parse(match.Groups[4].Value)
|
||||
// If turn is unspecified, use the second-latest turn in the timeline,
|
||||
// since we want to assert against the subjects of the orders just adjudicated,
|
||||
// and adjudication created a new set of seasons.
|
||||
: seasonsInTimeline.Max(season => season.Turn) - 1;
|
||||
season = new(timeline, turn);
|
||||
|
||||
province = World.Map.Provinces.Single(province => province.Is(match.Groups[2].Value));
|
||||
|
||||
var matchingMoves = Adjudications.Where(adj
|
||||
=> adj is DoesMove moves
|
||||
&& moves.Order.Unit.Season == season
|
||||
&& World.Map.GetLocation(moves.Order.Unit.Location).ProvinceName == province.Name);
|
||||
if (!matchingMoves.Any()) return ScriptResult.Fail("No matching movement decisions");
|
||||
var doesMove = matchingMoves.Cast<DoesMove>().First();
|
||||
|
||||
if (args[0] == "moves" && doesMove.Outcome != true) {
|
||||
return ScriptResult.Fail($"Adjudication {doesMove} is false");
|
||||
}
|
||||
if (args[0] == "no-move" && doesMove.Outcome != false) {
|
||||
return ScriptResult.Fail($"Adjudication {doesMove} is true");
|
||||
}
|
||||
return ScriptResult.Succeed(this);
|
||||
|
||||
case "support-given":
|
||||
case "support-cut":
|
||||
re = new(World);
|
||||
prov = new($"^{re.FullLocation}$", RegexOptions.IgnoreCase);
|
||||
match = prov.Match(args[1]);
|
||||
if (!match.Success) return ScriptResult.Fail($"Could not parse province from \"{args[1]}\"", this);
|
||||
|
||||
timeline = match.Groups[1].Length > 0
|
||||
? match.Groups[1].Value
|
||||
: Season.First.Timeline;
|
||||
seasonsInTimeline = World.Timelines.Seasons.Where(season => season.Timeline == timeline);
|
||||
if (!seasonsInTimeline.Any()) return ScriptResult.Fail($"No seasons in timeline {timeline}", this);
|
||||
|
||||
turn = match.Groups[4].Length > 0
|
||||
? int.Parse(match.Groups[4].Value)
|
||||
// If turn is unspecified, use the second-latest turn in the timeline,
|
||||
// since we want to assert against the subjects of the orders just adjudicated,
|
||||
// and adjudication created a new set of seasons.
|
||||
: seasonsInTimeline.Max(season => season.Turn) - 1;
|
||||
season = new(timeline, turn);
|
||||
|
||||
province = World.Map.Provinces.Single(province => province.Is(match.Groups[2].Value));
|
||||
|
||||
var matchingSupports = Adjudications.Where(adj
|
||||
=> adj is GivesSupport sup
|
||||
&& sup.Order.Unit.Season == season
|
||||
&& World.Map.GetLocation(sup.Order.Unit.Location).ProvinceName == province.Name);
|
||||
if (!matchingSupports.Any()) return ScriptResult.Fail("No matching support decisions");
|
||||
var supports = matchingSupports.Cast<GivesSupport>().First();
|
||||
|
||||
if (args[0] == "support-given" && supports.Outcome != true) {
|
||||
return ScriptResult.Fail($"Adjudication {supports} is false");
|
||||
}
|
||||
if (args[0] == "support-cut" && supports.Outcome != false) {
|
||||
return ScriptResult.Fail($"Adjudication {supports} is true");
|
||||
}
|
||||
return ScriptResult.Succeed(this);
|
||||
|
||||
default:
|
||||
return ScriptResult.Fail($"Unknown assertion \"{args[0]}\"", this);
|
||||
}
|
||||
}
|
||||
}
|
90
MultiversalDiplomacy/Script/GameScriptHandler.cs
Normal file
90
MultiversalDiplomacy/Script/GameScriptHandler.cs
Normal file
@ -0,0 +1,90 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using MultiversalDiplomacy.Adjudicate;
|
||||
using MultiversalDiplomacy.Model;
|
||||
using MultiversalDiplomacy.Orders;
|
||||
|
||||
namespace MultiversalDiplomacy.Script;
|
||||
|
||||
public class GameScriptHandler(
|
||||
Action<string> WriteLine,
|
||||
World world,
|
||||
IPhaseAdjudicator adjudicator)
|
||||
: IScriptHandler
|
||||
{
|
||||
public string Prompt => "orders> ";
|
||||
|
||||
public World World { get; private set; } = world;
|
||||
|
||||
private string? CurrentPower { get; set; } = null;
|
||||
|
||||
public List<Order> Orders { get; } = [];
|
||||
|
||||
public ScriptResult HandleInput(string input)
|
||||
{
|
||||
if (input == "") {
|
||||
CurrentPower = null;
|
||||
return ScriptResult.Succeed(this);
|
||||
}
|
||||
if (input.StartsWith('#')) return ScriptResult.Succeed(this);
|
||||
|
||||
// "---" submits the orders and allows queries about the outcome
|
||||
if (input == "---") {
|
||||
WriteLine("Submitting orders for adjudication");
|
||||
var validation = adjudicator.ValidateOrders(World, Orders);
|
||||
var validOrders = validation
|
||||
.Where(v => v.Valid)
|
||||
.Select(v => v.Order)
|
||||
.ToList();
|
||||
var adjudication = adjudicator.AdjudicateOrders(World, validOrders);
|
||||
var newWorld = adjudicator.UpdateWorld(World, adjudication);
|
||||
return ScriptResult.Succeed(new AdjudicationQueryScriptHandler(
|
||||
WriteLine, validation, adjudication, newWorld, adjudicator));
|
||||
}
|
||||
|
||||
// "===" submits the orders and moves immediately to taking the next set of orders
|
||||
// i.e. it's "---" twice
|
||||
if (input == "===") {
|
||||
WriteLine("Submitting orders for adjudication");
|
||||
var validation = adjudicator.ValidateOrders(World, Orders);
|
||||
var validOrders = validation
|
||||
.Where(v => v.Valid)
|
||||
.Select(v => v.Order)
|
||||
.ToList();
|
||||
var adjudication = adjudicator.AdjudicateOrders(World, validOrders);
|
||||
World = adjudicator.UpdateWorld(World, adjudication);
|
||||
WriteLine("Ready for orders");
|
||||
return ScriptResult.Succeed(this);
|
||||
}
|
||||
|
||||
// A block of orders for a single power beginning with "{name}:"
|
||||
if (World.Powers.FirstOrDefault(p => input.EqualsAnyCase($"{p}:"), null) is string power) {
|
||||
CurrentPower = power;
|
||||
return ScriptResult.Succeed(this);
|
||||
}
|
||||
|
||||
// If it's not a comment, submit, or order block, assume it's an order.
|
||||
string orderPower;
|
||||
string orderText;
|
||||
if (CurrentPower is not null) {
|
||||
// In a block of orders from a power, the power was specified at the top and each line is an order.
|
||||
orderPower = CurrentPower;
|
||||
orderText = input;
|
||||
} else {
|
||||
// Outside a power block, the power is prefixed to each order.
|
||||
Regex re = new($"^{World.Map.PowerRegex}(?:[:])? (.*)$", RegexOptions.IgnoreCase);
|
||||
var match = re.Match(input);
|
||||
if (!match.Success) return ScriptResult.Fail($"Could not determine ordering power in \"{input}\"", this);
|
||||
orderPower = match.Groups[1].Value;
|
||||
orderText = match.Groups[2].Value;
|
||||
}
|
||||
|
||||
if (OrderParser.TryParseOrder(World, orderPower, orderText, out Order? order)) {
|
||||
WriteLine($"Parsed {orderPower} order: {order}");
|
||||
Orders.Add(order);
|
||||
return ScriptResult.Succeed(this);
|
||||
}
|
||||
|
||||
return ScriptResult.Fail($"Failed to parse \"{orderText}\"", this);
|
||||
}
|
||||
}
|
18
MultiversalDiplomacy/Script/IScriptHandler.cs
Normal file
18
MultiversalDiplomacy/Script/IScriptHandler.cs
Normal file
@ -0,0 +1,18 @@
|
||||
namespace MultiversalDiplomacy.Script;
|
||||
|
||||
/// <summary>
|
||||
/// A handler that interprets and executes 5dp script commands. Script handlers may create additional script handlers
|
||||
/// and delegate handling to them, allowing a sort of recursive parsing of script commands.
|
||||
/// </summary>
|
||||
public interface IScriptHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// When used interactively, the prompt that should be displayed.
|
||||
/// </summary>
|
||||
public string Prompt { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Process a line of input.
|
||||
/// </summary>
|
||||
public ScriptResult HandleInput(string input);
|
||||
}
|
64
MultiversalDiplomacy/Script/ReplScriptHandler.cs
Normal file
64
MultiversalDiplomacy/Script/ReplScriptHandler.cs
Normal file
@ -0,0 +1,64 @@
|
||||
using MultiversalDiplomacy.Adjudicate;
|
||||
using MultiversalDiplomacy.Model;
|
||||
|
||||
namespace MultiversalDiplomacy.Script;
|
||||
|
||||
/// <summary>
|
||||
/// A script handler for the interactive repl.
|
||||
/// </summary>
|
||||
public class ReplScriptHandler(Action<string> WriteLine) : IScriptHandler
|
||||
{
|
||||
public string Prompt => "5dp> ";
|
||||
|
||||
public ScriptResult HandleInput(string input)
|
||||
{
|
||||
var args = input.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (args.Length == 0 || input.StartsWith('#'))
|
||||
{
|
||||
return ScriptResult.Succeed(this);
|
||||
}
|
||||
|
||||
var command = args[0];
|
||||
switch (command)
|
||||
{
|
||||
case "help":
|
||||
case "?":
|
||||
WriteLine("Commands:");
|
||||
WriteLine(" help, ?: print this message");
|
||||
WriteLine(" map <variant>: start a new game of the given variant");
|
||||
WriteLine(" stab: stab");
|
||||
break;
|
||||
|
||||
case "stab":
|
||||
WriteLine("stab");
|
||||
break;
|
||||
|
||||
case "map" when args.Length == 1:
|
||||
WriteLine("Usage:");
|
||||
WriteLine(" map <variant>");
|
||||
WriteLine("Available variants:");
|
||||
WriteLine($" {string.Join(", ", Enum.GetNames<MapType>().Select(s => s.ToLowerInvariant()))}");
|
||||
break;
|
||||
|
||||
case "map" when args.Length > 1:
|
||||
string mapType = args[1].Trim();
|
||||
if (!Enum.TryParse(mapType, ignoreCase: true, out MapType map)) {
|
||||
WriteLine($"Unknown variant \"{mapType}\"");
|
||||
WriteLine("Available variants:");
|
||||
WriteLine($" {string.Join(", ", Enum.GetNames<MapType>().Select(s => s.ToLowerInvariant()))}");
|
||||
break;
|
||||
}
|
||||
World world = World.WithMap(Map.FromType(map));
|
||||
WriteLine($"Created a new {map} game");
|
||||
return ScriptResult.Succeed(new SetupScriptHandler(
|
||||
WriteLine,
|
||||
world,
|
||||
MovementPhaseAdjudicator.Instance));
|
||||
|
||||
default:
|
||||
return ScriptResult.Fail($"Unrecognized command: \"{command}\"", this);
|
||||
}
|
||||
|
||||
return ScriptResult.Succeed(this);
|
||||
}
|
||||
}
|
36
MultiversalDiplomacy/Script/ScriptResult.cs
Normal file
36
MultiversalDiplomacy/Script/ScriptResult.cs
Normal file
@ -0,0 +1,36 @@
|
||||
namespace MultiversalDiplomacy.Script;
|
||||
|
||||
/// <summary>
|
||||
/// The result of an <see cref="IScriptHandler"/> processing a line of input.
|
||||
/// </summary>
|
||||
/// <param name="success">Whether processing was successful.</param>
|
||||
/// <param name="next">The handler to continue script processing with.</param>
|
||||
/// <param name="message">If processing failed, the error message.</param>
|
||||
public class ScriptResult(bool success, IScriptHandler? next, string message)
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether processing was successful.
|
||||
/// </summary>
|
||||
public bool Success { get; } = success;
|
||||
|
||||
/// <summary>
|
||||
/// The handler to continue script processing with.
|
||||
/// </summary>
|
||||
public IScriptHandler? NextHandler { get; } = next;
|
||||
|
||||
/// <summary>
|
||||
/// If processing failed, the error message.
|
||||
/// </summary>
|
||||
public string Message { get; } = message;
|
||||
|
||||
/// <summary>
|
||||
/// Mark the processing as successful and continue processing with the next handler.
|
||||
/// </summary>
|
||||
public static ScriptResult Succeed(IScriptHandler next) => new(true, next, "");
|
||||
|
||||
/// <summary>
|
||||
/// Mark the processing as a failure and optionally continue with the next handler.
|
||||
/// </summary>
|
||||
/// <param name="message">The reason for the processing failure.</param>
|
||||
public static ScriptResult Fail(string message, IScriptHandler? next = null) => new(false, next, message);
|
||||
}
|
91
MultiversalDiplomacy/Script/SetupScriptHandler.cs
Normal file
91
MultiversalDiplomacy/Script/SetupScriptHandler.cs
Normal file
@ -0,0 +1,91 @@
|
||||
using MultiversalDiplomacy.Adjudicate;
|
||||
using MultiversalDiplomacy.Model;
|
||||
|
||||
namespace MultiversalDiplomacy.Script;
|
||||
|
||||
/// <summary>
|
||||
/// A script handler for modifying a game before it begins.
|
||||
/// </summary>
|
||||
public class SetupScriptHandler(
|
||||
Action<string> WriteLine,
|
||||
World world,
|
||||
IPhaseAdjudicator adjudicator)
|
||||
: IScriptHandler
|
||||
{
|
||||
public string Prompt => "setup> ";
|
||||
|
||||
public World World { get; private set; } = world;
|
||||
|
||||
public ScriptResult HandleInput(string input)
|
||||
{
|
||||
var args = input.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (args.Length == 0 || input.StartsWith('#'))
|
||||
{
|
||||
return ScriptResult.Succeed(this);
|
||||
}
|
||||
|
||||
var command = args[0];
|
||||
switch (command)
|
||||
{
|
||||
case "help":
|
||||
case "?":
|
||||
WriteLine("commands:");
|
||||
WriteLine(" begin: complete setup and start the game (alias: ---)");
|
||||
WriteLine(" list <type>: list things in a game category");
|
||||
WriteLine(" option <name> <value>: set a game option");
|
||||
WriteLine(" unit <power> <type> <province> [location]: add a unit to the game");
|
||||
WriteLine(" <province> may be \"province/location\"");
|
||||
break;
|
||||
|
||||
case "begin":
|
||||
case "---":
|
||||
WriteLine("Starting game");
|
||||
WriteLine("Ready for orders");
|
||||
return ScriptResult.Succeed(new GameScriptHandler(WriteLine, World, adjudicator));
|
||||
|
||||
case "list" when args.Length == 1:
|
||||
WriteLine("usage:");
|
||||
WriteLine(" list powers: the powers in the game");
|
||||
WriteLine(" list units: units created so far");
|
||||
break;
|
||||
|
||||
case "list" when args[1] == "powers":
|
||||
WriteLine("Powers:");
|
||||
foreach (string powerName in World.Powers)
|
||||
{
|
||||
WriteLine($" {powerName}");
|
||||
}
|
||||
break;
|
||||
|
||||
case "list" when args[1] == "units":
|
||||
WriteLine("Units:");
|
||||
foreach (Unit unit in World.Units)
|
||||
{
|
||||
WriteLine($" {unit}");
|
||||
}
|
||||
break;
|
||||
|
||||
case "option" when args.Length < 3:
|
||||
throw new NotImplementedException("There are no supported options yet");
|
||||
|
||||
case "unit" when args.Length < 2:
|
||||
WriteLine("usage: unit [power] [type] [province]</location>");
|
||||
break;
|
||||
|
||||
case "unit":
|
||||
string unitSpec = input["unit ".Length..];
|
||||
if (OrderParser.TryParseUnit(World, unitSpec, out Unit? newUnit)) {
|
||||
World = World.Update(units: World.Units.Append(newUnit));
|
||||
WriteLine($"Created {newUnit}");
|
||||
return ScriptResult.Succeed(this);
|
||||
}
|
||||
return ScriptResult.Fail($"Could not match unit spec \"{unitSpec}\"", this);
|
||||
|
||||
default:
|
||||
ScriptResult.Fail($"Unrecognized command: \"{command}\"", this);
|
||||
break;
|
||||
}
|
||||
|
||||
return ScriptResult.Succeed(this);
|
||||
}
|
||||
}
|
10
MultiversalDiplomacy/StringExtensions.cs
Normal file
10
MultiversalDiplomacy/StringExtensions.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace System;
|
||||
|
||||
public static class StringExtensions
|
||||
{
|
||||
public static bool EqualsAnyCase(this string str, string? other)
|
||||
=> str.Equals(other, StringComparison.InvariantCultureIgnoreCase);
|
||||
|
||||
public static bool StartsWithAnyCase(this string str, string other)
|
||||
=> str.StartsWith(other, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
8
MultiversalDiplomacyTests/Adjudicator.cs
Normal file
8
MultiversalDiplomacyTests/Adjudicator.cs
Normal file
@ -0,0 +1,8 @@
|
||||
using MultiversalDiplomacy.Adjudicate;
|
||||
|
||||
namespace MultiversalDiplomacyTests;
|
||||
|
||||
public static class Adjudicator
|
||||
{
|
||||
public static MovementPhaseAdjudicator MovementPhase { get; } = new MovementPhaseAdjudicator(NullLogger.Instance);
|
||||
}
|
@ -1,223 +0,0 @@
|
||||
using MultiversalDiplomacy.Adjudicate;
|
||||
using MultiversalDiplomacy.Adjudicate.Decision;
|
||||
using MultiversalDiplomacy.Model;
|
||||
using MultiversalDiplomacy.Orders;
|
||||
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace MultiversalDiplomacyTests;
|
||||
|
||||
public class DATC_A
|
||||
{
|
||||
private World StandardEmpty { get; } = World.WithStandardMap();
|
||||
|
||||
[Test]
|
||||
public void DATC_6_A_1_MoveToAnAreaThatIsNotANeighbor()
|
||||
{
|
||||
TestCaseBuilder setup = new TestCaseBuilder(StandardEmpty);
|
||||
setup["England"]
|
||||
.Fleet("North Sea").MovesTo("Picardy").GetReference(out var order);
|
||||
|
||||
// Order should fail.
|
||||
setup.ValidateOrders(MovementPhaseAdjudicator.Instance);
|
||||
Assert.That(order, Is.Invalid(ValidationReason.UnreachableDestination));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DATC_6_A_2_MoveArmyToSea()
|
||||
{
|
||||
TestCaseBuilder setup = new TestCaseBuilder(StandardEmpty);
|
||||
|
||||
// Order should fail.
|
||||
Assert.That(
|
||||
() =>
|
||||
{
|
||||
setup["England"]
|
||||
.Army("Liverpool").MovesTo("Irish Sea");
|
||||
},
|
||||
Throws.TypeOf<KeyNotFoundException>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DATC_6_A_3_MoveFleetToLand()
|
||||
{
|
||||
TestCaseBuilder setup = new TestCaseBuilder(StandardEmpty);
|
||||
|
||||
// Order should fail.
|
||||
Assert.That(
|
||||
() =>
|
||||
{
|
||||
setup["Germany"]
|
||||
.Fleet("Kiel").MovesTo("Munich");
|
||||
},
|
||||
Throws.TypeOf<KeyNotFoundException>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DATC_6_A_4_MoveToOwnSector()
|
||||
{
|
||||
TestCaseBuilder setup = new TestCaseBuilder(StandardEmpty);
|
||||
setup["Germany"]
|
||||
.Fleet("Kiel").MovesTo("Kiel").GetReference(out var order);
|
||||
|
||||
// Program should not crash.
|
||||
setup.ValidateOrders(MovementPhaseAdjudicator.Instance);
|
||||
Assert.That(order, Is.Invalid(ValidationReason.DestinationMatchesOrigin));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DATC_6_A_5_MoveToOwnSectorWithConvoy()
|
||||
{
|
||||
TestCaseBuilder setup = new TestCaseBuilder(StandardEmpty);
|
||||
setup
|
||||
["England"]
|
||||
.Fleet("North Sea").Convoys.Army("Yorkshire").To("Yorkshire").GetReference(out var orderNth)
|
||||
.Army("Yorkshire").MovesTo("Yorkshire").GetReference(out var orderYor)
|
||||
.Army("Liverpool").Supports.Army("Yorkshire").MoveTo("Yorkshire")
|
||||
["Germany"]
|
||||
.Fleet("London").MovesTo("Yorkshire").GetReference(out var orderLon)
|
||||
.Army("Wales").Supports.Fleet("London").MoveTo("Yorkshire");
|
||||
|
||||
// The move of the army in Yorkshire is illegal. This makes the support of Liverpool also illegal.
|
||||
setup.ValidateOrders(MovementPhaseAdjudicator.Instance);
|
||||
Assert.That(orderLon, Is.Valid);
|
||||
Assert.That(orderNth, Is.Invalid(ValidationReason.DestinationMatchesOrigin));
|
||||
Assert.That(orderYor, Is.Invalid(ValidationReason.DestinationMatchesOrigin));
|
||||
var orderYorRepl = orderYor.GetReplacementReference<HoldOrder>();
|
||||
|
||||
// Without the support, the Germans have a stronger force. The army in London dislodges the army in Yorkshire.
|
||||
setup.AdjudicateOrders(MovementPhaseAdjudicator.Instance);
|
||||
Assert.That(orderLon, Is.Victorious);
|
||||
Assert.That(orderYorRepl, Is.Dislodged);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DATC_6_A_6_OrderingAUnitOfAnotherCountry()
|
||||
{
|
||||
TestCaseBuilder setup = new TestCaseBuilder(StandardEmpty);
|
||||
setup
|
||||
["Germany"]
|
||||
.Fleet("London", powerName: "England").MovesTo("North Sea").GetReference(out var order);
|
||||
|
||||
// Order should fail.
|
||||
setup.ValidateOrders(MovementPhaseAdjudicator.Instance);
|
||||
Assert.That(order, Is.Invalid(ValidationReason.InvalidUnitForPower));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DATC_6_A_7_OnlyArmiesCanBeConvoyed()
|
||||
{
|
||||
TestCaseBuilder setup = new TestCaseBuilder(StandardEmpty);
|
||||
setup
|
||||
["England"]
|
||||
.Fleet("London").MovesTo("Belgium")
|
||||
.Fleet("North Sea").Convoys.Army("London").To("Belgium").GetReference(out var order);
|
||||
|
||||
// Move from London to Belgium should fail.
|
||||
setup.ValidateOrders(MovementPhaseAdjudicator.Instance);
|
||||
Assert.That(order, Is.Invalid(ValidationReason.InvalidOrderTypeForUnit));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DATC_6_A_8_SupportToHoldYourselfIsNotPossible()
|
||||
{
|
||||
TestCaseBuilder setup = new TestCaseBuilder(StandardEmpty);
|
||||
setup
|
||||
["Italy"]
|
||||
.Army("Venice").MovesTo("Trieste")
|
||||
.Army("Tyrolia").Supports.Army("Venice").MoveTo("Trieste")
|
||||
["Austria"]
|
||||
.Fleet("Trieste").Supports.Fleet("Trieste").Hold().GetReference(out var orderTri);
|
||||
|
||||
setup.ValidateOrders(MovementPhaseAdjudicator.Instance);
|
||||
Assert.That(orderTri, Is.Invalid(ValidationReason.NoSelfSupport));
|
||||
var orderTriRepl = orderTri.GetReplacementReference<HoldOrder>();
|
||||
|
||||
// The army in Trieste should be dislodged.
|
||||
setup.AdjudicateOrders(MovementPhaseAdjudicator.Instance);
|
||||
Assert.That(orderTriRepl, Is.Dislodged);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DATC_6_A_9_FleetsMustFollowCoastIfNotOnSea()
|
||||
{
|
||||
TestCaseBuilder setup = new TestCaseBuilder(StandardEmpty);
|
||||
setup
|
||||
["Italy"]
|
||||
.Fleet("Rome").MovesTo("Venice").GetReference(out var order);
|
||||
|
||||
// Move fails. An army can go from Rome to Venice, but a fleet can not.
|
||||
setup.ValidateOrders(MovementPhaseAdjudicator.Instance);
|
||||
Assert.That(order, Is.Invalid(ValidationReason.UnreachableDestination));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DATC_6_A_10_SupportOnUnreachableDestinationNotPossible()
|
||||
{
|
||||
TestCaseBuilder setup = new TestCaseBuilder(StandardEmpty);
|
||||
setup
|
||||
["Austria"]
|
||||
.Army("Venice").Holds().GetReference(out var orderVen)
|
||||
["Italy"]
|
||||
.Army("Apulia").MovesTo("Venice")
|
||||
.Fleet("Rome").Supports.Army("Apulia").MoveTo("Venice").GetReference(out var orderRom);
|
||||
|
||||
setup.ValidateOrders(MovementPhaseAdjudicator.Instance);
|
||||
|
||||
// The support of Rome is illegal, because Venice can not be reached from Rome by a fleet.
|
||||
Assert.That(orderRom, Is.Invalid(ValidationReason.UnreachableSupport));
|
||||
|
||||
// Venice is not dislodged.
|
||||
setup.AdjudicateOrders(MovementPhaseAdjudicator.Instance);
|
||||
Assert.That(orderVen, Is.NotDislodged);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DATC_6_A_11_SimpleBounce()
|
||||
{
|
||||
TestCaseBuilder setup = new TestCaseBuilder(StandardEmpty);
|
||||
setup
|
||||
["Austria"]
|
||||
.Army("Vienna").MovesTo("Tyrolia").GetReference(out var orderVie)
|
||||
["Italy"]
|
||||
.Army("Venice").MovesTo("Tyrolia").GetReference(out var orderVen);
|
||||
|
||||
setup.ValidateOrders(MovementPhaseAdjudicator.Instance);
|
||||
Assert.That(orderVie, Is.Valid);
|
||||
Assert.That(orderVen, Is.Valid);
|
||||
|
||||
// The two units bounce.
|
||||
var adjudications = setup.AdjudicateOrders(MovementPhaseAdjudicator.Instance);
|
||||
Assert.That(orderVie, Is.Repelled);
|
||||
Assert.That(orderVie, Is.NotDislodged);
|
||||
Assert.That(orderVen, Is.Repelled);
|
||||
Assert.That(orderVen, Is.NotDislodged);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DATC_6_A_12_BounceOfThreeUnits()
|
||||
{
|
||||
TestCaseBuilder setup = new TestCaseBuilder(StandardEmpty);
|
||||
setup
|
||||
["Austria"]
|
||||
.Army("Vienna").MovesTo("Tyrolia").GetReference(out var orderVie)
|
||||
["Germany"]
|
||||
.Army("Munich").MovesTo("Tyrolia").GetReference(out var orderMun)
|
||||
["Italy"]
|
||||
.Army("Venice").MovesTo("Tyrolia").GetReference(out var orderVen);
|
||||
|
||||
var validations = setup.ValidateOrders(MovementPhaseAdjudicator.Instance);
|
||||
Assert.That(orderVie, Is.Valid);
|
||||
Assert.That(orderMun, Is.Valid);
|
||||
Assert.That(orderVen, Is.Valid);
|
||||
|
||||
var adjudications = setup.AdjudicateOrders(MovementPhaseAdjudicator.Instance);
|
||||
// The three units bounce.
|
||||
Assert.That(orderVie, Is.Repelled);
|
||||
Assert.That(orderVie, Is.NotDislodged);
|
||||
Assert.That(orderMun, Is.Repelled);
|
||||
Assert.That(orderMun, Is.NotDislodged);
|
||||
Assert.That(orderVen, Is.Repelled);
|
||||
Assert.That(orderVen, Is.NotDislodged);
|
||||
}
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
using MultiversalDiplomacy.Adjudicate;
|
||||
using MultiversalDiplomacy.Adjudicate.Decision;
|
||||
using MultiversalDiplomacy.Model;
|
||||
|
||||
using static MultiversalDiplomacyTests.Adjudicator;
|
||||
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace MultiversalDiplomacyTests;
|
||||
@ -11,15 +12,15 @@ public class TimeTravelTest
|
||||
[Test]
|
||||
public void MDATC_3_A_1_MoveIntoOwnPastForksTimeline()
|
||||
{
|
||||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance);
|
||||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhase);
|
||||
|
||||
// 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);
|
||||
@ -33,34 +34,34 @@ public class TimeTravelTest
|
||||
|
||||
// Confirm that there are now four seasons: three in the main timeline and one in a fork.
|
||||
Assert.That(
|
||||
world.Seasons.Where(s => s.Timeline == s0.Timeline).Count(),
|
||||
world.Timelines.Seasons.Where(s => s.Timeline == s0.Timeline).Count(),
|
||||
Is.EqualTo(3),
|
||||
"Failed to advance main timeline after last unit left");
|
||||
Assert.That(
|
||||
world.Seasons.Where(s => s.Timeline != s0.Timeline).Count(),
|
||||
world.Timelines.Seasons.Where(s => s.Timeline != s0.Timeline).Count(),
|
||||
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 = new("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.Key));
|
||||
Assert.That(world.GetUnitByKey(aTyr.Past!).Past, Is.EqualTo(mun0.Order.Unit.Key));
|
||||
|
||||
// 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.Key));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void MDATC_3_A_2_SupportToRepelledPastMoveForksTimeline()
|
||||
{
|
||||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance);
|
||||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhase);
|
||||
|
||||
// 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 +76,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,33 +92,33 @@ 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 = new("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.Key),
|
||||
"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");
|
||||
Assert.That(world.RetreatingUnits.First().Unit, Is.EqualTo(tyr0.Order.Unit));
|
||||
"Expected A Tyr a0 to be in retreat");
|
||||
Assert.That(world.RetreatingUnits.First().Unit.Key, Is.EqualTo(tyr0.Order.Unit.Key));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void MDATC_3_A_3_FailedMoveDoesNotForkTimeline()
|
||||
{
|
||||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance);
|
||||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhase);
|
||||
|
||||
// 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 +129,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,21 +137,21 @@ public class TimeTravelTest
|
||||
// change the past and therefore did not create a new timeline.
|
||||
World world = setup.UpdateWorld();
|
||||
Assert.That(
|
||||
s0.Futures.Count(),
|
||||
world.Timelines.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.Timelines.GetFutures(s1).Count(), Is.EqualTo(1));
|
||||
Season s2 = new(s1.Timeline, s1.Turn + 1);
|
||||
Assert.That(world.Timelines.GetFutures(s2).Count(), Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void MDATC_3_A_4_SuperfluousSupportDoesNotForkTimeline()
|
||||
{
|
||||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance);
|
||||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhase);
|
||||
|
||||
// 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 +163,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,130 +179,130 @@ public class TimeTravelTest
|
||||
// ...since it succeeded anyway, no fork is created.
|
||||
World world = setup.UpdateWorld();
|
||||
Assert.That(
|
||||
s0.Futures.Count(),
|
||||
world.Timelines.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.Timelines.GetFutures(s1).Count(), Is.EqualTo(1));
|
||||
Season s2 = new(s1.Timeline, s1.Turn + 1);
|
||||
Assert.That(world.Timelines.GetFutures(s2).Count(), Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void MDATC_3_A_5_CrossTimelineSupportDoesNotForkHead()
|
||||
{
|
||||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance);
|
||||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhase);
|
||||
|
||||
// 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.Timelines.GetFutures(a2).Count(),
|
||||
Is.EqualTo(1),
|
||||
"A cross-timeline support incorrectly forked the head of the timeline");
|
||||
Assert.That(
|
||||
s1_1.Futures.Count(),
|
||||
world.Timelines.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 = new(a2.Timeline, a2.Turn + 1);
|
||||
Assert.That(world.Timelines.GetFutures(a3).Count(), Is.EqualTo(0));
|
||||
Season b2 = new(b1.Timeline, b1.Turn + 1);
|
||||
Assert.That(world.Timelines.GetFutures(b2).Count(), Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void MDATC_3_A_6_CuttingCrossTimelineSupportDoesNotFork()
|
||||
{
|
||||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance);
|
||||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhase);
|
||||
|
||||
// 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.Timelines.GetFutures(a3).Count(),
|
||||
Is.EqualTo(1),
|
||||
"A cross-timeline support cut incorrectly forked the timeline");
|
||||
Assert.That(
|
||||
s2_1.Futures.Count(),
|
||||
world.Timelines.GetFutures(b2).Count(),
|
||||
Is.EqualTo(1),
|
||||
"A cross-timeline support cut incorrectly forked the timeline");
|
||||
}
|
||||
|
@ -6,10 +6,10 @@ namespace MultiversalDiplomacyTests;
|
||||
|
||||
public class MapTests
|
||||
{
|
||||
IEnumerable<Location> LocationClosure(Location location)
|
||||
static IEnumerable<Location> LocationClosure(Location location)
|
||||
{
|
||||
IEnumerable<Location> visited = new List<Location>();
|
||||
IEnumerable<Location> toVisit = new List<Location>() { location };
|
||||
IEnumerable<Location> visited = [];
|
||||
IEnumerable<Location> toVisit = [location];
|
||||
|
||||
while (toVisit.Any())
|
||||
{
|
||||
@ -50,7 +50,7 @@ public class MapTests
|
||||
[Test]
|
||||
public void LandAndSeaBorders()
|
||||
{
|
||||
World map = World.WithStandardMap();
|
||||
Map map = Map.Classical;
|
||||
Assert.That(
|
||||
map.GetLand("NAF").Adjacents.Count(),
|
||||
Is.EqualTo(1),
|
||||
|
@ -1,9 +1,10 @@
|
||||
using MultiversalDiplomacy.Adjudicate;
|
||||
using MultiversalDiplomacy.Adjudicate.Decision;
|
||||
using MultiversalDiplomacy.Model;
|
||||
|
||||
using NUnit.Framework;
|
||||
|
||||
using static MultiversalDiplomacyTests.Adjudicator;
|
||||
|
||||
namespace MultiversalDiplomacyTests;
|
||||
|
||||
public class MovementAdjudicatorTest
|
||||
@ -11,7 +12,7 @@ public class MovementAdjudicatorTest
|
||||
[Test]
|
||||
public void Validation_ValidHold()
|
||||
{
|
||||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance);
|
||||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhase);
|
||||
setup["Germany"]
|
||||
.Army("Mun").Holds().GetReference(out var order);
|
||||
|
||||
@ -24,7 +25,7 @@ public class MovementAdjudicatorTest
|
||||
[Test]
|
||||
public void Validation_ValidMove()
|
||||
{
|
||||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance);
|
||||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhase);
|
||||
setup["Germany"]
|
||||
.Army("Mun").MovesTo("Tyr").GetReference(out var order);
|
||||
|
||||
@ -37,7 +38,7 @@ public class MovementAdjudicatorTest
|
||||
[Test]
|
||||
public void Validation_ValidConvoy()
|
||||
{
|
||||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance);
|
||||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhase);
|
||||
setup["Germany"]
|
||||
.Fleet("Nth").Convoys.Army("Hol").To("Lon").GetReference(out var order);
|
||||
|
||||
@ -50,7 +51,7 @@ public class MovementAdjudicatorTest
|
||||
[Test]
|
||||
public void Validation_ValidSupportHold()
|
||||
{
|
||||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance);
|
||||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhase);
|
||||
setup["Germany"]
|
||||
.Army("Mun").Supports.Army("Kie").Hold().GetReference(out var order);
|
||||
|
||||
@ -63,7 +64,7 @@ public class MovementAdjudicatorTest
|
||||
[Test]
|
||||
public void Validation_ValidSupportMove()
|
||||
{
|
||||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance);
|
||||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhase);
|
||||
setup["Germany"]
|
||||
.Army("Mun").Supports.Army("Kie").MoveTo("Ber").GetReference(out var order);
|
||||
|
||||
@ -76,12 +77,12 @@ public class MovementAdjudicatorTest
|
||||
[Test]
|
||||
public void Adjudication_Hold()
|
||||
{
|
||||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance);
|
||||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhase);
|
||||
setup["Germany"]
|
||||
.Army("Mun").Holds().GetReference(out var order);
|
||||
|
||||
setup.ValidateOrders();
|
||||
setup.AdjudicateOrders(MovementPhaseAdjudicator.Instance);
|
||||
setup.AdjudicateOrders(MovementPhase);
|
||||
|
||||
var adjMun = order.Adjudications;
|
||||
Assert.That(adjMun.All(adj => adj.Resolved), Is.True);
|
||||
@ -96,7 +97,7 @@ public class MovementAdjudicatorTest
|
||||
[Test]
|
||||
public void Adjudication_Move()
|
||||
{
|
||||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance);
|
||||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhase);
|
||||
setup["Germany"]
|
||||
.Army("Mun").MovesTo("Tyr").GetReference(out var order);
|
||||
|
||||
@ -122,7 +123,7 @@ public class MovementAdjudicatorTest
|
||||
[Test]
|
||||
public void Adjudication_Support()
|
||||
{
|
||||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance);
|
||||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhase);
|
||||
setup["Germany"]
|
||||
.Army("Mun").MovesTo("Tyr").GetReference(out var move)
|
||||
.Army("Boh").Supports.Army("Mun").MoveTo("Tyr").GetReference(out var support);
|
||||
@ -156,7 +157,7 @@ public class MovementAdjudicatorTest
|
||||
[Test]
|
||||
public void Update_SingleHold()
|
||||
{
|
||||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance);
|
||||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhase);
|
||||
setup["Germany"]
|
||||
.Army("Mun").Holds().GetReference(out var mun);
|
||||
|
||||
@ -168,25 +169,23 @@ public class MovementAdjudicatorTest
|
||||
|
||||
World updated = setup.UpdateWorld();
|
||||
|
||||
// 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.Timeline, Is.EqualTo(updated.RootSeason.Timeline));
|
||||
Assert.That(future.Turn, Is.EqualTo(Season.FIRST_TURN + 1));
|
||||
|
||||
// Confirm the unit was created
|
||||
Assert.That(updated.Units.Count, Is.EqualTo(2));
|
||||
Unit second = updated.Units.Single(u => u.Past != null);
|
||||
Assert.That(second.Location, Is.EqualTo(mun.Order.Unit.Location));
|
||||
Assert.That(second.Season.Timeline, Is.EqualTo(mun.Order.Unit.Season.Timeline));
|
||||
|
||||
// Confirm that the unit's season exists
|
||||
CollectionAssert.Contains(updated.Timelines.Pasts.Keys, second.Season.Key, "Season was not added");
|
||||
CollectionAssert.DoesNotContain(updated.Timelines.Pasts.Values, second.Season.Key, "Season should not have a future");
|
||||
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Update_DoubleHold()
|
||||
{
|
||||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance);
|
||||
setup[(0, 0)]
|
||||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhase);
|
||||
setup[("a", 0)]
|
||||
.GetReference(out Season s1)
|
||||
["Germany"]
|
||||
.Army("Mun").Holds().GetReference(out var mun1);
|
||||
@ -199,20 +198,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 = new(s1.Timeline, s1.Turn + 1);
|
||||
Assert.That(updated.Timelines.Pasts[s2.Key], Is.EqualTo(s1));
|
||||
Assert.That(updated.Timelines.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.Key, Is.Not.EqualTo(mun1.Order.Unit.Key));
|
||||
Assert.That(u2.Past, Is.EqualTo(mun1.Order.Unit.Key));
|
||||
Assert.That(u2.Season, Is.EqualTo(s2));
|
||||
|
||||
setup[(1, 0)]
|
||||
setup[("a", 1)]
|
||||
["Germany"]
|
||||
.Army("Mun").Holds().GetReference(out var mun2);
|
||||
|
||||
@ -227,16 +226,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 = new(s2.Timeline, s2.Turn + 1);
|
||||
Unit u3 = updated.GetUnitAt("Mun", s3);
|
||||
Assert.That(u3.Past, Is.EqualTo(mun2.Order.Unit.Key));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Update_DoubleMove()
|
||||
{
|
||||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance);
|
||||
setup[(0, 0)]
|
||||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhase);
|
||||
setup[("a", 0)]
|
||||
.GetReference(out Season s1)
|
||||
["Germany"]
|
||||
.Army("Mun").MovesTo("Tyr").GetReference(out var mun1);
|
||||
@ -249,20 +248,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 = new(s1.Timeline, s1.Turn + 1);
|
||||
Assert.That(updated.Timelines.Pasts[s2.Key], Is.EqualTo(s1));
|
||||
Assert.That(updated.Timelines.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.Key, Is.Not.EqualTo(mun1.Order.Unit.Key));
|
||||
Assert.That(u2.Past, Is.EqualTo(mun1.Order.Unit.Key));
|
||||
Assert.That(u2.Season, Is.EqualTo(s2));
|
||||
|
||||
setup[(1, 0)]
|
||||
setup[("a", 1)]
|
||||
["Germany"]
|
||||
.Army("Tyr").MovesTo("Mun").GetReference(out var tyr2);
|
||||
|
||||
@ -277,8 +276,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 = new(s2.Timeline, s2.Turn + 1);
|
||||
Unit u3 = updated.GetUnitAt("Mun", s3);
|
||||
Assert.That(u3.Past, Is.EqualTo(u2.Key));
|
||||
}
|
||||
}
|
||||
|
@ -18,4 +18,10 @@
|
||||
<PackageReference Include="coverlet.collector" Version="3.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="Scripts/**/*.txt">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
10
MultiversalDiplomacyTests/NullLogger.cs
Normal file
10
MultiversalDiplomacyTests/NullLogger.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using MultiversalDiplomacy.Adjudicate.Logging;
|
||||
|
||||
namespace MultiversalDiplomacyTests;
|
||||
|
||||
public class NullLogger : IAdjudicatorLogger
|
||||
{
|
||||
public static NullLogger Instance { get; } = new();
|
||||
|
||||
public void Log(int contextLevel, string message, params object[] args) {}
|
||||
}
|
335
MultiversalDiplomacyTests/OrderParserTest.cs
Normal file
335
MultiversalDiplomacyTests/OrderParserTest.cs
Normal file
@ -0,0 +1,335 @@
|
||||
using NUnit.Framework;
|
||||
|
||||
using MultiversalDiplomacy.Model;
|
||||
using MultiversalDiplomacy.Orders;
|
||||
|
||||
namespace MultiversalDiplomacyTests;
|
||||
|
||||
public class OrderParserTest
|
||||
{
|
||||
private static TestCaseData Test(string order, params string[] expected)
|
||||
=> new TestCaseData(order, expected).SetName($"{{m}}(\"{order}\")");
|
||||
|
||||
static IEnumerable<TestCaseData> HoldRegexMatchesTestCases()
|
||||
{
|
||||
// Full specification
|
||||
yield return Test(
|
||||
"Army a-Munich/l@0 holds",
|
||||
"Army", "a", "Munich", "l", "0", "holds");
|
||||
// Case insensitivity
|
||||
yield return Test(
|
||||
"fleet B-lon/C@0 H",
|
||||
"fleet", "B", "lon", "C", "0", "H");
|
||||
// All optionals missing
|
||||
yield return Test(
|
||||
"ROM h",
|
||||
"", "", "ROM", "", "", "h");
|
||||
// No confusion of unit type and timeline
|
||||
yield return Test(
|
||||
"A F-STP hold",
|
||||
"A", "F", "STP", "", "", "hold");
|
||||
// Province with space in name
|
||||
yield return Test(
|
||||
"Fleet North Sea Hold",
|
||||
"Fleet", "", "North Sea", "", "", "Hold");
|
||||
// Parenthesis location
|
||||
yield return Test(
|
||||
"F Spain(nc) holds",
|
||||
"F", "", "Spain", "nc", "", "holds");
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(HoldRegexMatchesTestCases))]
|
||||
public void HoldRegexMatches(string order, string[] expected)
|
||||
{
|
||||
OrderParser re = new(World.WithStandardMap());
|
||||
var match = re.Hold.Match(order);
|
||||
Assert.True(match.Success, "Match failed");
|
||||
var (type, timeline, province, location, turn, holdVerb) = OrderParser.ParseHold(match);
|
||||
string[] actual = [type, timeline, province, location, turn, holdVerb];
|
||||
// Use EquivalentTo for more detailed error message
|
||||
Assert.That(actual, Is.EquivalentTo(expected), "Unexpected parse results");
|
||||
Assert.That(actual, Is.EqualTo(expected), "Unexpected parse results");
|
||||
}
|
||||
|
||||
static IEnumerable<TestCaseData> MoveRegexMatchesTestCases()
|
||||
{
|
||||
// Full specification
|
||||
yield return Test(
|
||||
"Army a-Munich/l@0 - a-Tyrolia/l@0",
|
||||
"Army", "a", "Munich", "l", "0", "-", "a", "Tyrolia", "l", "0", "");
|
||||
// Case insensitivity
|
||||
yield return Test(
|
||||
"fleet B-lon/C@0 - B-enc/W@0",
|
||||
"fleet", "B", "lon", "C", "0", "-", "B", "enc", "W", "0", "");
|
||||
// All optionals missing
|
||||
yield return Test(
|
||||
"ROM - VIE",
|
||||
"", "", "ROM", "", "", "-", "", "VIE", "", "", "");
|
||||
// No confusion of unit type and timeline
|
||||
yield return Test(
|
||||
"A F-STP - MOS",
|
||||
"A", "F", "STP", "", "", "-", "", "MOS", "", "", "");
|
||||
// No confusion of timeline and hold verb
|
||||
yield return Test(
|
||||
"A Mun - h-Tyr",
|
||||
"A", "", "Mun", "", "", "-", "h", "Tyr", "", "", "");
|
||||
// No confusion of timeline and support verb
|
||||
yield return Test(
|
||||
"A Mun - s-Tyr",
|
||||
"A", "", "Mun", "", "", "-", "s", "Tyr", "", "", "");
|
||||
// Elements with spaces
|
||||
yield return Test(
|
||||
"Western Mediterranean Sea moves to Gulf of Lyons via convoy",
|
||||
"", "", "Western Mediterranean Sea", "", "", "moves to", "", "Gulf of Lyons", "", "", "via convoy");
|
||||
// Parenthesis location
|
||||
yield return Test(
|
||||
"F Spain(nc) - Spain(sc)",
|
||||
"F", "", "Spain", "nc", "", "-", "", "Spain", "sc", "", "");
|
||||
// Timeline designation spells out a province
|
||||
yield return Test(
|
||||
"A tyr-MUN(vie) - mun-TYR/vie",
|
||||
"A", "tyr", "MUN", "vie", "", "-", "mun", "TYR", "vie", "", "");
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(MoveRegexMatchesTestCases))]
|
||||
public void MoveRegexMatches(string order, string[] expected)
|
||||
{
|
||||
OrderParser re = new(World.WithStandardMap());
|
||||
var match = re.Move.Match(order);
|
||||
Assert.True(match.Success, "Match failed");
|
||||
var (type, timeline, province, location, turn, moveVerb,
|
||||
destTimeline, destProvince, destLocation, destTurn, viaConvoy) = OrderParser.ParseMove(match);
|
||||
string[] actual = [type, timeline, province, location, turn, moveVerb,
|
||||
destTimeline, destProvince, destLocation, destTurn, viaConvoy];
|
||||
// Use EquivalentTo for more detailed error message
|
||||
Assert.That(actual, Is.EquivalentTo(expected), "Unexpected parse results");
|
||||
Assert.That(actual, Is.EqualTo(expected), "Unexpected parse results");
|
||||
}
|
||||
|
||||
static IEnumerable<TestCaseData> SupportHoldRegexMatchesTestCases()
|
||||
{
|
||||
// Full specification
|
||||
yield return Test(
|
||||
"Army a-Munich/l@0 s A a-Tyrolia/l@0",
|
||||
"Army", "a", "Munich", "l", "0", "s", "A", "a", "Tyrolia", "l", "0");
|
||||
// Case insensitivity
|
||||
yield return Test(
|
||||
"fleet B-lon/C@0 SUPPORTS B-enc/W@0",
|
||||
"fleet", "B", "lon", "C", "0", "SUPPORTS", "", "B", "enc", "W", "0");
|
||||
// All optionals missing
|
||||
yield return Test(
|
||||
"ROM s VIE",
|
||||
"", "", "ROM", "", "", "s", "", "", "VIE", "", "");
|
||||
// No confusion of unit type and timeline
|
||||
yield return Test(
|
||||
"A F-STP s MOS",
|
||||
"A", "F", "STP", "", "", "s", "", "", "MOS", "", "");
|
||||
// No confusion of timeline and support verb
|
||||
yield return Test(
|
||||
"A Mun s Tyr",
|
||||
"A", "", "Mun", "", "", "s", "", "", "Tyr", "", "");
|
||||
// Elements with spaces
|
||||
yield return Test(
|
||||
"Western Mediterranean Sea supports Gulf of Lyons",
|
||||
"", "", "Western Mediterranean Sea", "", "", "supports", "", "", "Gulf of Lyons", "", "");
|
||||
// Parenthesis location
|
||||
yield return Test(
|
||||
"F Spain(nc) s Spain(sc)",
|
||||
"F", "", "Spain", "nc", "", "s", "", "", "Spain", "sc", "");
|
||||
// Timeline designation spells out a province
|
||||
yield return Test(
|
||||
"A tyr-MUN(vie) s mun-TYR/vie",
|
||||
"A", "tyr", "MUN", "vie", "", "s", "", "mun", "TYR", "vie", "");
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(SupportHoldRegexMatchesTestCases))]
|
||||
public void SupportHoldRegexMatches(string order, string[] expected)
|
||||
{
|
||||
OrderParser re = new(World.WithStandardMap());
|
||||
var match = re.SupportHold.Match(order);
|
||||
Assert.True(match.Success, "Match failed");
|
||||
var (type, timeline, province, location, turn, supportVerb,
|
||||
targetType, targetTimeline, targetProvince, targetLocation, targetTurn) = OrderParser.ParseSupportHold(match);
|
||||
string[] actual = [type, timeline, province, location, turn, supportVerb,
|
||||
targetType, targetTimeline, targetProvince, targetLocation, targetTurn];
|
||||
// Use EquivalentTo for more detailed error message
|
||||
Assert.That(actual, Is.EquivalentTo(expected), "Unexpected parse results");
|
||||
Assert.That(actual, Is.EqualTo(expected), "Unexpected parse results");
|
||||
}
|
||||
|
||||
static IEnumerable<TestCaseData> SupportMoveRegexMatchesTestCases()
|
||||
{
|
||||
// Full specification
|
||||
yield return Test(
|
||||
"Army a-Munich/l@0 s A a-Tyrolia/l@0 - a-Vienna/l@0",
|
||||
"Army", "a", "Munich", "l", "0", "s", "A", "a", "Tyrolia", "l", "0", "-", "a", "Vienna", "l", "0");
|
||||
// Case insensitivity
|
||||
yield return Test(
|
||||
"fleet B-lon/C@0 SUPPORTS B-enc/W@0 MOVE TO B-nts/W@0",
|
||||
"fleet", "B", "lon", "C", "0", "SUPPORTS", "", "B", "enc", "W", "0", "MOVE TO", "B", "nts", "W", "0");
|
||||
// All optionals missing
|
||||
yield return Test(
|
||||
"ROM s VIE - TYR",
|
||||
"", "", "ROM", "", "", "s", "", "", "VIE", "", "", "-", "", "TYR", "", "");
|
||||
// No confusion of unit type and timeline
|
||||
yield return Test(
|
||||
"A F-STP S MOS - A-UKR",
|
||||
"A", "F", "STP", "", "", "S", "", "", "MOS", "", "", "-", "A", "UKR", "", "");
|
||||
// Elements with spaces
|
||||
yield return Test(
|
||||
"Western Mediterranean Sea supports Gulf of Lyons move to North Sea",
|
||||
"", "", "Western Mediterranean Sea", "", "", "supports", "", "", "Gulf of Lyons", "", "", "move to", "", "North Sea", "", "");
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(SupportMoveRegexMatchesTestCases))]
|
||||
public void SupportMoveRegexMatches(string order, string[] expected)
|
||||
{
|
||||
OrderParser re = new(World.WithStandardMap());
|
||||
var match = re.SupportMove.Match(order);
|
||||
Assert.True(match.Success, "Match failed");
|
||||
var (type, timeline, province, location, turn, supportVerb,
|
||||
targetType, targetTimeline, targetProvince, targetLocation, targetTurn, moveVerb,
|
||||
destTimeline, destProvince, destLocation, destTurn) = OrderParser.ParseSupportMove(match);
|
||||
string[] actual = [type, timeline, province, location, turn, supportVerb,
|
||||
targetType, targetTimeline, targetProvince, targetLocation, targetTurn, moveVerb,
|
||||
destTimeline, destProvince, destLocation, destTurn];
|
||||
// Use EquivalentTo for more detailed error message
|
||||
Assert.That(actual, Is.EquivalentTo(expected), "Unexpected parse results");
|
||||
Assert.That(actual, Is.EqualTo(expected), "Unexpected parse results");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OrderParsingTest()
|
||||
{
|
||||
World world = World.WithStandardMap().AddUnits("Germany A Mun");
|
||||
OrderParser re = new(world);
|
||||
|
||||
var match = re.Move.Match("A Mun - Tyr");
|
||||
var success = OrderParser.TryParseMoveOrder(world, "Germany", match, out Order? order);
|
||||
|
||||
Assert.That(success, Is.True);
|
||||
Assert.That(order, Is.TypeOf<MoveOrder>());
|
||||
MoveOrder move = (MoveOrder)order!;
|
||||
|
||||
Assert.That(move.Power, Is.EqualTo("Germany"));
|
||||
Assert.That(move.Unit.Key, Is.EqualTo("A a-Munich/l@0"));
|
||||
Assert.That(move.Location, Is.EqualTo("Tyrolia/l"));
|
||||
Assert.That(move.Season.Key, Is.EqualTo("a0"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void OrderDisambiguation()
|
||||
{
|
||||
World world = World.WithStandardMap().AddUnits("Germany A Mun");
|
||||
OrderParser.TryParseOrder(world, "Germany", "Mun h", out Order? parsed);
|
||||
Assert.That(parsed?.ToString(), Is.EqualTo("G A a-Munich/l@0 holds"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void UnitTypeDisambiguatesCoastalLocation()
|
||||
{
|
||||
World world = World.WithStandardMap().AddUnits("England F Nth", "Germany A Ruhr");
|
||||
|
||||
Assert.That(
|
||||
OrderParser.TryParseOrder(world, "England", "North Sea - Holland", out Order? fleetOrder),
|
||||
"Failed to parse fleet order");
|
||||
Assert.That(
|
||||
OrderParser.TryParseOrder(world, "Germany", "Ruhr - Holland", out Order? armyOrder),
|
||||
"Failed to parse army order");
|
||||
|
||||
Assert.That(fleetOrder, Is.TypeOf<MoveOrder>(), "Unexpected fleet order");
|
||||
Assert.That(armyOrder, Is.TypeOf<MoveOrder>(), "Unexpected army order");
|
||||
Location fleetDest = world.Map.GetLocation(((MoveOrder)fleetOrder!).Location);
|
||||
Location armyDest = world.Map.GetLocation(((MoveOrder)armyOrder!).Location);
|
||||
|
||||
Assert.That(fleetDest.ProvinceName, Is.EqualTo(armyDest.ProvinceName));
|
||||
Assert.That(fleetDest.Type, Is.EqualTo(LocationType.Water), "Unexpected fleet movement location");
|
||||
Assert.That(armyDest.Type, Is.EqualTo(LocationType.Land), "Unexpected army movement location");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void UnitTypeOverrulesNonsenseLocation()
|
||||
{
|
||||
World world = World.WithStandardMap().AddUnits("England F Nth", "Germany A Ruhr");
|
||||
|
||||
Assert.That(
|
||||
OrderParser.TryParseOrder(world, "England", "F North Sea - Holland/l", out Order? fleetOrder),
|
||||
"Failed to parse fleet order");
|
||||
Assert.That(
|
||||
OrderParser.TryParseOrder(world, "Germany", "A Ruhr - Holland/w", out Order? armyOrder),
|
||||
"Failed to parse army order");
|
||||
|
||||
Assert.That(fleetOrder, Is.TypeOf<MoveOrder>(), "Unexpected fleet order");
|
||||
Assert.That(armyOrder, Is.TypeOf<MoveOrder>(), "Unexpected army order");
|
||||
Location fleetDest = world.Map.GetLocation(((MoveOrder)fleetOrder!).Location);
|
||||
Location armyDest = world.Map.GetLocation(((MoveOrder)armyOrder!).Location);
|
||||
|
||||
Assert.That(fleetDest.ProvinceName, Is.EqualTo(armyDest.ProvinceName));
|
||||
Assert.That(fleetDest.Type, Is.EqualTo(LocationType.Water), "Unexpected fleet movement location");
|
||||
Assert.That(armyDest.Type, Is.EqualTo(LocationType.Land), "Unexpected army movement location");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DisambiguateSingleAccessibleCoast()
|
||||
{
|
||||
World world = World.WithStandardMap().AddUnits("France F Gascony", "France F Marseilles");
|
||||
|
||||
Assert.That(
|
||||
OrderParser.TryParseOrder(world, "France", "Gascony - Spain", out Order? northOrder),
|
||||
"Failed to parse north coast order");
|
||||
Assert.That(
|
||||
OrderParser.TryParseOrder(world, "France", "Marseilles - Spain", out Order? southOrder),
|
||||
"Failed to parse south coast order");
|
||||
|
||||
Assert.That(northOrder, Is.TypeOf<MoveOrder>(), "Unexpected north coast order");
|
||||
Assert.That(southOrder, Is.TypeOf<MoveOrder>(), "Unexpected south coast order");
|
||||
Location north = world.Map.GetLocation(((MoveOrder)northOrder!).Location);
|
||||
Location south = world.Map.GetLocation(((MoveOrder)southOrder!).Location);
|
||||
|
||||
Assert.That(north.Name, Is.EqualTo("north coast"), "Unexpected disambiguation");
|
||||
Assert.That(south.Name, Is.EqualTo("south coast"), "Unexpected disambiguation");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DisambiguateMultipleAccessibleCoasts()
|
||||
{
|
||||
World world = World.WithStandardMap().AddUnits("France F Portugal");
|
||||
|
||||
Assert.That(
|
||||
OrderParser.TryParseOrder(world, "France", "Portugal - Spain", out Order? _),
|
||||
Is.False,
|
||||
"Should not parse ambiguous coastal move");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DisambiguateSupportToSingleAccessibleCoast()
|
||||
{
|
||||
World world = World.WithStandardMap().AddUnits("France F Gascony", "France F Marseilles");
|
||||
|
||||
Assert.That(
|
||||
OrderParser.TryParseOrder(world, "France", "Gascony S Marseilles - Spain", out Order? northOrder),
|
||||
"Failed to parse north coast order");
|
||||
Assert.That(
|
||||
OrderParser.TryParseOrder(world, "France", "Marseilles S Gascony - Spain", out Order? southOrder),
|
||||
"Failed to parse south coast order");
|
||||
|
||||
Assert.That(northOrder, Is.TypeOf<SupportMoveOrder>(), "Unexpected north coast order");
|
||||
Assert.That(southOrder, Is.TypeOf<SupportMoveOrder>(), "Unexpected south coast order");
|
||||
Location northTarget = ((SupportMoveOrder)northOrder!).Location;
|
||||
Location southTarget = ((SupportMoveOrder)southOrder!).Location;
|
||||
|
||||
Assert.That(northTarget.Name, Is.EqualTo("south coast"), "Unexpected disambiguation");
|
||||
Assert.That(southTarget.Name, Is.EqualTo("north coast"), "Unexpected disambiguation");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DisambiguateSupportToMultipleAccessibleCoasts()
|
||||
{
|
||||
World world = World.WithStandardMap().AddUnits("France F Portugal", "France F Marseilles");
|
||||
|
||||
Assert.That(
|
||||
OrderParser.TryParseOrder(world, "France", "Marseilles S Portugal - Spain", out Order? _),
|
||||
Is.False,
|
||||
"Should not parse ambiguous coastal support");
|
||||
}
|
||||
}
|
@ -58,7 +58,7 @@ public abstract class OrderReference
|
||||
if (this.Order is UnitOrder unitOrder)
|
||||
{
|
||||
var replacementOrder = this.Builder.ValidationResults.Where(
|
||||
v => v.Order is UnitOrder uo && uo != unitOrder && uo.Unit == unitOrder.Unit);
|
||||
v => v.Order is UnitOrder uo && uo != unitOrder && uo.Unit.Key == unitOrder.Unit.Key);
|
||||
if (replacementOrder.Any())
|
||||
{
|
||||
return replacementOrder.Single();
|
||||
@ -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;
|
||||
@ -143,7 +142,7 @@ public abstract class OrderReference
|
||||
if (this.Order is UnitOrder unitOrder)
|
||||
{
|
||||
var retreat = this.Builder.World.RetreatingUnits.Where(
|
||||
ru => ru.Unit == unitOrder.Unit);
|
||||
ru => ru.Unit.Key == unitOrder.Unit.Key);
|
||||
if (retreat.Any())
|
||||
{
|
||||
return retreat.Single();
|
||||
|
48
MultiversalDiplomacyTests/ReplDriver.cs
Normal file
48
MultiversalDiplomacyTests/ReplDriver.cs
Normal file
@ -0,0 +1,48 @@
|
||||
using NUnit.Framework;
|
||||
|
||||
using MultiversalDiplomacy.Script;
|
||||
|
||||
namespace MultiversalDiplomacyTests;
|
||||
|
||||
public class ReplDriver(IScriptHandler initialHandler, bool echo = false)
|
||||
{
|
||||
public IScriptHandler? Handler { get; private set; } = initialHandler;
|
||||
|
||||
private string? LastInput { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Whether to print the inputs as they are executed. This is primarily a debugging aid.
|
||||
/// </summary>
|
||||
bool Echo { get; } = echo;
|
||||
|
||||
public ReplDriver ExecuteAll(string multiline)
|
||||
{
|
||||
var lines = multiline.Split('\n', StringSplitOptions.TrimEntries);
|
||||
return lines.Aggregate(this, (repl, line) => repl.Execute(line));
|
||||
}
|
||||
|
||||
public ReplDriver Execute(string inputLine)
|
||||
{
|
||||
if (Handler is null) throw new AssertionException(
|
||||
$"Cannot execute \"{inputLine}\", handler quit. Last input was \"{LastInput}\"");
|
||||
if (Echo) Console.WriteLine($"{Handler.Prompt}{inputLine}");
|
||||
|
||||
var result = Handler.HandleInput(inputLine);
|
||||
if (!result.Success) Assert.Fail($"Script failed at \"{inputLine}\": {result.Message}");
|
||||
|
||||
Handler = result.NextHandler;
|
||||
LastInput = inputLine;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public void AssertFails(string inputLine)
|
||||
{
|
||||
if (Handler is null) throw new AssertionException(
|
||||
$"Cannot execute \"{inputLine}\", handler quit. Last input was \"{LastInput}\"");
|
||||
if (Echo) Console.WriteLine($"{Handler.Prompt}{inputLine}");
|
||||
|
||||
var result = Handler.HandleInput(inputLine);
|
||||
if (result.Success) Assert.Fail($"Expected \"{inputLine}\" to fail, but it succeeded.");
|
||||
}
|
||||
}
|
276
MultiversalDiplomacyTests/ReplTest.cs
Normal file
276
MultiversalDiplomacyTests/ReplTest.cs
Normal file
@ -0,0 +1,276 @@
|
||||
using NUnit.Framework;
|
||||
|
||||
using MultiversalDiplomacy.Model;
|
||||
using MultiversalDiplomacy.Orders;
|
||||
using MultiversalDiplomacy.Script;
|
||||
|
||||
namespace MultiversalDiplomacyTests;
|
||||
|
||||
public class ReplTest
|
||||
{
|
||||
private static ReplDriver StandardRepl() => new(
|
||||
new SetupScriptHandler(
|
||||
(msg) => {/* discard */},
|
||||
World.WithStandardMap(),
|
||||
Adjudicator.MovementPhase));
|
||||
|
||||
[Test]
|
||||
public void SetupHandler()
|
||||
{
|
||||
var repl = StandardRepl();
|
||||
|
||||
repl.ExecuteAll("""
|
||||
unit Germany A Munich
|
||||
unit Austria Army Tyrolia
|
||||
unit England F Lon
|
||||
""");
|
||||
|
||||
Assert.That(repl.Handler, Is.TypeOf<SetupScriptHandler>());
|
||||
SetupScriptHandler handler = (SetupScriptHandler)repl.Handler!;
|
||||
Assert.That(handler.World.Units.Count, Is.EqualTo(3));
|
||||
Assert.That(handler.World.GetUnitAt("Mun"), Is.Not.Null);
|
||||
Assert.That(handler.World.GetUnitAt("Tyr"), Is.Not.Null);
|
||||
Assert.That(handler.World.GetUnitAt("Lon"), Is.Not.Null);
|
||||
|
||||
repl.Execute("---");
|
||||
|
||||
Assert.That(repl.Handler, Is.TypeOf<GameScriptHandler>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SubmitOrders()
|
||||
{
|
||||
var repl = StandardRepl();
|
||||
|
||||
repl.ExecuteAll("""
|
||||
unit Germany A Mun
|
||||
unit Austria A Tyr
|
||||
unit England F Lon
|
||||
---
|
||||
Germany A Mun hold
|
||||
Austria: Army Tyrolia - Vienna
|
||||
England:
|
||||
Lon h
|
||||
""");
|
||||
|
||||
Assert.That(repl.Handler, Is.TypeOf<GameScriptHandler>());
|
||||
GameScriptHandler handler = (GameScriptHandler)repl.Handler!;
|
||||
Assert.That(handler.Orders.Count, Is.EqualTo(3));
|
||||
Assert.That(handler.Orders.Single(o => o.Power == "Germany"), Is.TypeOf<HoldOrder>());
|
||||
Assert.That(handler.Orders.Single(o => o.Power == "Austria"), Is.TypeOf<MoveOrder>());
|
||||
Assert.That(handler.Orders.Single(o => o.Power == "England"), Is.TypeOf<HoldOrder>());
|
||||
Assert.That(handler.World.Timelines.Pasts.Count, Is.EqualTo(1));
|
||||
|
||||
World before = handler.World;
|
||||
|
||||
repl.Execute("---");
|
||||
|
||||
Assert.That(repl.Handler, Is.TypeOf<AdjudicationQueryScriptHandler>());
|
||||
var newHandler = (AdjudicationQueryScriptHandler)repl.Handler!;
|
||||
Assert.That(newHandler.World, Is.Not.EqualTo(before));
|
||||
Assert.That(newHandler.World.Timelines.Pasts.Count, Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AssertBasic()
|
||||
{
|
||||
var repl = StandardRepl();
|
||||
|
||||
repl.ExecuteAll("""
|
||||
unit Germany A Munich
|
||||
---
|
||||
---
|
||||
assert true
|
||||
""");
|
||||
|
||||
repl.AssertFails("assert false");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AssertOrderValidity()
|
||||
{
|
||||
var repl = StandardRepl();
|
||||
|
||||
repl.ExecuteAll("""
|
||||
unit Germany A Mun
|
||||
---
|
||||
Germany A Mun - Stp
|
||||
---
|
||||
""");
|
||||
|
||||
// Order should be invalid
|
||||
repl.Execute("assert order-invalid Mun");
|
||||
repl.AssertFails("assert order-valid Mun");
|
||||
|
||||
repl.ExecuteAll("""
|
||||
---
|
||||
Germany A Mun - Tyr
|
||||
---
|
||||
""");
|
||||
|
||||
// Order should be valid
|
||||
repl.Execute("assert order-valid Mun");
|
||||
repl.AssertFails("assert order-invalid Mun");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AssertSeasonPast()
|
||||
{
|
||||
var repl = StandardRepl();
|
||||
|
||||
repl.ExecuteAll("""
|
||||
unit England F London
|
||||
---
|
||||
---
|
||||
""");
|
||||
|
||||
// Expected past
|
||||
repl.Execute("assert has-past a1>a0");
|
||||
// Incorrect past
|
||||
repl.AssertFails("assert has-past a0>a1");
|
||||
repl.AssertFails("assert has-past a1>a1");
|
||||
// Missing season
|
||||
repl.AssertFails("assert has-past a2>a1");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AssertHoldOrder()
|
||||
{
|
||||
var repl = StandardRepl();
|
||||
|
||||
repl.ExecuteAll("""
|
||||
unit Germany A Mun
|
||||
---
|
||||
""");
|
||||
repl.AssertFails("Germany A Mun - The Sun");
|
||||
repl.Execute("---");
|
||||
|
||||
// Order is invalid
|
||||
repl.Execute("assert hold-order Mun");
|
||||
// order-invalid requires the order be parsable, which this isn't
|
||||
repl.AssertFails("assert order-invalid Mun");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AssertMovement()
|
||||
{
|
||||
var repl = StandardRepl();
|
||||
|
||||
repl.ExecuteAll("""
|
||||
unit Germany A Mun
|
||||
unit Austria A Tyr
|
||||
---
|
||||
Germany Mun - Tyr
|
||||
---
|
||||
""");
|
||||
|
||||
// Movement fails
|
||||
repl.Execute("assert no-move Mun");
|
||||
repl.AssertFails("assert moves Mun");
|
||||
|
||||
repl.ExecuteAll("""
|
||||
---
|
||||
Germany Mun - Boh
|
||||
---
|
||||
""");
|
||||
|
||||
// Movement succeeds
|
||||
repl.Execute("assert moves Mun");
|
||||
repl.AssertFails("assert no-move Mun");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AssertSupportHold()
|
||||
{
|
||||
var repl = StandardRepl();
|
||||
|
||||
repl.ExecuteAll("""
|
||||
unit Germany A Mun
|
||||
unit Germany A Boh
|
||||
unit Austria A Tyr
|
||||
---
|
||||
Germany Mun s Boh
|
||||
---
|
||||
""");
|
||||
|
||||
// Support is given
|
||||
repl.Execute("assert support-given Mun");
|
||||
repl.AssertFails("assert support-cut Mun");
|
||||
|
||||
repl.ExecuteAll("""
|
||||
---
|
||||
Germany Mun s Boh
|
||||
Austria Tyr - Mun
|
||||
---
|
||||
""");
|
||||
|
||||
// Support is cut
|
||||
repl.Execute("assert support-cut Mun");
|
||||
repl.AssertFails("assert support-given Mun");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AssertSupportMove()
|
||||
{
|
||||
var repl = StandardRepl();
|
||||
|
||||
repl.ExecuteAll("""
|
||||
unit Germany A Berlin
|
||||
unit Germany A Bohemia
|
||||
unit Austria A Tyrolia
|
||||
---
|
||||
Germany:
|
||||
Berlin - Silesia
|
||||
Bohemia s Berlin - Silesia
|
||||
---
|
||||
""");
|
||||
|
||||
// Support is given
|
||||
repl.Execute("assert support-given Boh");
|
||||
repl.AssertFails("assert support-cut Boh");
|
||||
|
||||
repl.ExecuteAll("""
|
||||
---
|
||||
Germany:
|
||||
Silesia - Munich
|
||||
Bohemia s Silesia - Munich
|
||||
|
||||
Austria Tyrolia - Bohemia
|
||||
---
|
||||
""");
|
||||
|
||||
// Support is cut
|
||||
repl.AssertFails("assert support-given Boh");
|
||||
repl.Execute("assert support-cut Boh");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AssertDislodged()
|
||||
{
|
||||
var repl = StandardRepl();
|
||||
|
||||
repl.ExecuteAll("""
|
||||
unit Germany A Mun
|
||||
unit Germany A Boh
|
||||
unit Austria A Tyr
|
||||
---
|
||||
Germany Mun - Tyr
|
||||
---
|
||||
""");
|
||||
|
||||
// Move repelled
|
||||
repl.Execute("assert not-dislodged Tyr");
|
||||
repl.AssertFails("assert dislodged Tyr");
|
||||
|
||||
repl.ExecuteAll("""
|
||||
---
|
||||
Germany Mun - Tyr
|
||||
Germany Boh s Mun - Tyr
|
||||
---
|
||||
""");
|
||||
|
||||
// Move succeeds
|
||||
repl.Execute("assert dislodged Tyr");
|
||||
repl.AssertFails("assert not-dislodged Tyr");
|
||||
}
|
||||
}
|
55
MultiversalDiplomacyTests/ScriptTests.cs
Normal file
55
MultiversalDiplomacyTests/ScriptTests.cs
Normal file
@ -0,0 +1,55 @@
|
||||
using NUnit.Framework;
|
||||
|
||||
using MultiversalDiplomacy.Model;
|
||||
using MultiversalDiplomacy.Script;
|
||||
|
||||
namespace MultiversalDiplomacyTests;
|
||||
|
||||
public class ScriptTests
|
||||
{
|
||||
static IEnumerable<TestCaseData> DatcTestCases()
|
||||
{
|
||||
foreach (var path in Directory.EnumerateFiles("Scripts/DATC"))
|
||||
{
|
||||
yield return new TestCaseData(path)
|
||||
.SetName($"{{m}}({Path.GetFileNameWithoutExtension(path)})");
|
||||
}
|
||||
}
|
||||
|
||||
[TestCaseSource(nameof(DatcTestCases))]
|
||||
public void Test_DATC(string testScriptPath)
|
||||
{
|
||||
string filename = Path.GetFileName(testScriptPath);
|
||||
int line = 0;
|
||||
bool expectFailure = false;
|
||||
|
||||
IScriptHandler handler = new SetupScriptHandler(
|
||||
(msg) => {/* discard */},
|
||||
World.WithStandardMap(),
|
||||
Adjudicator.MovementPhase);
|
||||
|
||||
foreach (string input in File.ReadAllLines(testScriptPath)) {
|
||||
line++;
|
||||
|
||||
// Handle test directives
|
||||
if (input == "#test:skip") {
|
||||
Assert.Ignore($"Script {filename} skipped at line {line}");
|
||||
}
|
||||
if (input == "#test:fails") {
|
||||
expectFailure = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
var result = handler.HandleInput(input);
|
||||
if (expectFailure && result.Success) throw new AssertionException(
|
||||
$"Script {filename} expected line {line} to fail, but it succeeded");
|
||||
if (!expectFailure && !result.Success) throw new AssertionException(
|
||||
$"Script {filename} error at line {line}: {result.Message}");
|
||||
if (result.NextHandler is null) throw new AssertionException(
|
||||
$"Script {filename} quit unexpectedly at line {line}: \"{input}\"");
|
||||
|
||||
handler = result.NextHandler;
|
||||
expectFailure = false;
|
||||
}
|
||||
}
|
||||
}
|
14
MultiversalDiplomacyTests/Scripts/DATC/6.A.1.txt
Normal file
14
MultiversalDiplomacyTests/Scripts/DATC/6.A.1.txt
Normal file
@ -0,0 +1,14 @@
|
||||
# 6.A.1. TEST CASE, MOVING TO AN AREA THAT IS NOT A NEIGHBOUR
|
||||
# Check if an illegal move (without convoy) will fail.
|
||||
|
||||
unit England F North Sea
|
||||
|
||||
---
|
||||
|
||||
England:
|
||||
F North Sea - Picardy
|
||||
|
||||
---
|
||||
|
||||
# Order should fail.
|
||||
assert hold-order North Sea
|
21
MultiversalDiplomacyTests/Scripts/DATC/6.A.10.txt
Normal file
21
MultiversalDiplomacyTests/Scripts/DATC/6.A.10.txt
Normal file
@ -0,0 +1,21 @@
|
||||
# 6.A.10. TEST CASE, SUPPORT ON UNREACHABLE DESTINATION NOT POSSIBLE
|
||||
# The destination of the move that is supported must be reachable by the supporting unit.
|
||||
|
||||
unit Austria A Venice
|
||||
unit Italy F Rome
|
||||
unit Italy A Apulia
|
||||
|
||||
---
|
||||
|
||||
Austria:
|
||||
A Venice Hold
|
||||
|
||||
Italy:
|
||||
F Rome Supports A Apulia - Venice
|
||||
A Apulia - Venice
|
||||
|
||||
---
|
||||
|
||||
# The support of Rome is illegal, because Venice cannot be reached from Rome by a fleet. Venice is not dislodged.
|
||||
assert hold-order Rome
|
||||
assert not-dislodged Venice
|
19
MultiversalDiplomacyTests/Scripts/DATC/6.A.11.txt
Normal file
19
MultiversalDiplomacyTests/Scripts/DATC/6.A.11.txt
Normal file
@ -0,0 +1,19 @@
|
||||
# 6.A.11. TEST CASE, SIMPLE BOUNCE
|
||||
# Two armies bouncing on each other.
|
||||
|
||||
unit Austria A Vienna
|
||||
unit Italy A Venice
|
||||
|
||||
---
|
||||
|
||||
Austria:
|
||||
A Vienna - Tyrolia
|
||||
|
||||
Italy:
|
||||
A Venice - Tyrolia
|
||||
|
||||
---
|
||||
|
||||
# The two units bounce.
|
||||
assert no-move Vienna
|
||||
assert no-move Venice
|
24
MultiversalDiplomacyTests/Scripts/DATC/6.A.12.txt
Normal file
24
MultiversalDiplomacyTests/Scripts/DATC/6.A.12.txt
Normal file
@ -0,0 +1,24 @@
|
||||
# 6.A.12. TEST CASE, BOUNCE OF THREE UNITS
|
||||
# If three units move to the same area, the adjudicator should not bounce the first two units and then let the third unit go to the now open area.
|
||||
|
||||
unit Austria A Vienna
|
||||
unit Germany A Munich
|
||||
unit Italy A Venice
|
||||
|
||||
---
|
||||
|
||||
Austria:
|
||||
A Vienna - Tyrolia
|
||||
|
||||
Germany:
|
||||
A Munich - Tyrolia
|
||||
|
||||
Italy:
|
||||
A Venice - Tyrolia
|
||||
|
||||
---
|
||||
|
||||
# The three units bounce.
|
||||
assert no-move Vienna
|
||||
assert no-move Munich
|
||||
assert no-move Venice
|
15
MultiversalDiplomacyTests/Scripts/DATC/6.A.2.txt
Normal file
15
MultiversalDiplomacyTests/Scripts/DATC/6.A.2.txt
Normal file
@ -0,0 +1,15 @@
|
||||
# 6.A.2. TEST CASE, MOVE ARMY TO SEA
|
||||
# Check if an army could not be moved to open sea.
|
||||
|
||||
unit England A Liverpool
|
||||
|
||||
---
|
||||
|
||||
England:
|
||||
#test:fails
|
||||
A Liverpool - Irish Sea
|
||||
|
||||
---
|
||||
|
||||
# Order should fail.
|
||||
assert hold-order Liverpool
|
15
MultiversalDiplomacyTests/Scripts/DATC/6.A.3.txt
Normal file
15
MultiversalDiplomacyTests/Scripts/DATC/6.A.3.txt
Normal file
@ -0,0 +1,15 @@
|
||||
# 6.A.3. TEST CASE, MOVE FLEET TO LAND
|
||||
# Check whether a fleet cannot move to land.
|
||||
|
||||
unit Germany F Kiel
|
||||
|
||||
---
|
||||
|
||||
Germany:
|
||||
#test:fails
|
||||
F Kiel - Munich
|
||||
|
||||
---
|
||||
|
||||
# Order should fail.
|
||||
assert hold-order Kiel
|
14
MultiversalDiplomacyTests/Scripts/DATC/6.A.4.txt
Normal file
14
MultiversalDiplomacyTests/Scripts/DATC/6.A.4.txt
Normal file
@ -0,0 +1,14 @@
|
||||
# 6.A.4. TEST CASE, MOVE TO OWN SECTOR
|
||||
# Moving to the same sector is an illegal move (2023 rulebook, page 7, "An Army can be ordered to move into an adjacent inland or coastal province.").
|
||||
|
||||
unit Germany F Kiel
|
||||
|
||||
---
|
||||
|
||||
Germany:
|
||||
F Kiel - Kiel
|
||||
|
||||
---
|
||||
|
||||
# Program should not crash.
|
||||
assert hold-order Kiel
|
34
MultiversalDiplomacyTests/Scripts/DATC/6.A.5.txt
Normal file
34
MultiversalDiplomacyTests/Scripts/DATC/6.A.5.txt
Normal file
@ -0,0 +1,34 @@
|
||||
# 6.A.5. TEST CASE, MOVE TO OWN SECTOR WITH CONVOY
|
||||
# Moving to the same sector is still illegal with convoy (2023 rulebook, page 7, "Note: An Army can move across water provinces from one coastal province to another...").
|
||||
|
||||
# TODO convoy order parsing
|
||||
#test:skip
|
||||
|
||||
unit England F North Sea
|
||||
unit England A Yorkshire
|
||||
unit England A Liverpool
|
||||
unit Germany F London
|
||||
unit Germany A Wales
|
||||
|
||||
---
|
||||
|
||||
England:
|
||||
F North Sea Convoys A Yorkshire - Yorkshire
|
||||
A Yorkshire - Yorkshire
|
||||
A Liverpool Supports A Yorkshire - Yorkshire
|
||||
|
||||
Germany:
|
||||
F London - Yorkshire
|
||||
A Wales Supports F London - Yorkshire
|
||||
|
||||
---
|
||||
|
||||
# The move of the army in Yorkshire is illegal.
|
||||
assert hold-order Yorkshire
|
||||
# This makes the support of Liverpool also illegal and without the support, the Germans have a stronger force.
|
||||
assert hold-order North Sea
|
||||
assert hold-order Liverpool
|
||||
assert moves London
|
||||
# The army in London dislodges the army in Yorkshire.
|
||||
assert support-given Wales
|
||||
assert dislodged Yorkshire
|
16
MultiversalDiplomacyTests/Scripts/DATC/6.A.6.txt
Normal file
16
MultiversalDiplomacyTests/Scripts/DATC/6.A.6.txt
Normal file
@ -0,0 +1,16 @@
|
||||
# 6.A.6. TEST CASE, ORDERING A UNIT OF ANOTHER COUNTRY
|
||||
# Check whether someone cannot order a unit that is not his own unit.
|
||||
|
||||
unit England F London
|
||||
# A German unit is included here so Germany isn't considered dead
|
||||
unit Germany A Munich
|
||||
|
||||
---
|
||||
|
||||
Germany:
|
||||
F London - North Sea
|
||||
|
||||
---
|
||||
|
||||
# Order should fail.
|
||||
assert hold-order London
|
19
MultiversalDiplomacyTests/Scripts/DATC/6.A.7.txt
Normal file
19
MultiversalDiplomacyTests/Scripts/DATC/6.A.7.txt
Normal file
@ -0,0 +1,19 @@
|
||||
# 6.A.7. TEST CASE, ONLY ARMIES CAN BE CONVOYED
|
||||
# A fleet cannot be convoyed.
|
||||
|
||||
# TODO convoy order parsing
|
||||
#test:skip
|
||||
|
||||
unit England F London
|
||||
unit England F North Sea
|
||||
|
||||
---
|
||||
|
||||
England:
|
||||
F London - Belgium
|
||||
F North Sea Convoys A London - Belgium
|
||||
|
||||
---
|
||||
|
||||
# Move from London to Belgium should fail.
|
||||
assert hold-order London
|
20
MultiversalDiplomacyTests/Scripts/DATC/6.A.8.txt
Normal file
20
MultiversalDiplomacyTests/Scripts/DATC/6.A.8.txt
Normal file
@ -0,0 +1,20 @@
|
||||
# 6.A.8. TEST CASE, SUPPORT TO HOLD YOURSELF IS NOT POSSIBLE
|
||||
# An army cannot get an additional hold power by supporting itself.
|
||||
|
||||
unit Italy A Venice
|
||||
unit Italy A Tyrolia
|
||||
unit Austria F Trieste
|
||||
|
||||
---
|
||||
|
||||
Italy:
|
||||
A Venice - Trieste
|
||||
A Tyrolia Supports A Venice - Trieste
|
||||
|
||||
Austria:
|
||||
F Trieste Supports F Trieste
|
||||
|
||||
---
|
||||
|
||||
# The army in Trieste should be dislodged.
|
||||
assert dislodged Trieste
|
14
MultiversalDiplomacyTests/Scripts/DATC/6.A.9.txt
Normal file
14
MultiversalDiplomacyTests/Scripts/DATC/6.A.9.txt
Normal file
@ -0,0 +1,14 @@
|
||||
# 6.A.9. TEST CASE, FLEETS MUST FOLLOW COAST IF NOT ON SEA
|
||||
# If two provinces are adjacent, that does not mean that a fleet can move between those two provinces. An implementation that only holds one list of adjacent provinces for each province is incorrect.
|
||||
|
||||
unit Italy F Rome
|
||||
|
||||
---
|
||||
|
||||
Italy:
|
||||
F Rome - Venice
|
||||
|
||||
---
|
||||
|
||||
# Move fails. An army can go from Rome to Venice, but a fleet cannot.
|
||||
assert hold-order Rome
|
3
MultiversalDiplomacyTests/Scripts/DATC/README.md
Normal file
3
MultiversalDiplomacyTests/Scripts/DATC/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# DATC test scripts
|
||||
|
||||
These test scripts are copied from DATC v3.1.
|
@ -1,65 +0,0 @@
|
||||
using MultiversalDiplomacy.Model;
|
||||
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace MultiversalDiplomacyTests;
|
||||
|
||||
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();
|
||||
|
||||
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(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(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(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");
|
||||
}
|
||||
|
||||
[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));
|
||||
}
|
||||
}
|
146
MultiversalDiplomacyTests/SerializationTest.cs
Normal file
146
MultiversalDiplomacyTests/SerializationTest.cs
Normal file
@ -0,0 +1,146 @@
|
||||
using System.Text.Json;
|
||||
|
||||
using MultiversalDiplomacy.Adjudicate.Decision;
|
||||
using MultiversalDiplomacy.Model;
|
||||
|
||||
using NUnit.Framework;
|
||||
|
||||
using static MultiversalDiplomacyTests.Adjudicator;
|
||||
|
||||
namespace MultiversalDiplomacyTests;
|
||||
|
||||
public class SerializationTest
|
||||
{
|
||||
private JsonSerializerOptions Options = new(World.JsonOptions) {
|
||||
WriteIndented = true,
|
||||
};
|
||||
|
||||
[Test]
|
||||
public void SerializeRoundTrip_Timelines()
|
||||
{
|
||||
Timelines one = Timelines.Create();
|
||||
|
||||
string serial1 = JsonSerializer.Serialize(one, Options);
|
||||
Timelines two = JsonSerializer.Deserialize<Timelines>(serial1, Options)
|
||||
?? throw new AssertionException("Failed to deserialize");
|
||||
|
||||
Assert.That(two.Next, Is.EqualTo(one.Next), "Failed to reserialize next timeline");
|
||||
Assert.That(two.Pasts, Is.EquivalentTo(one.Pasts), "Failed to reserialize pasts");
|
||||
|
||||
Timelines three = two
|
||||
.WithNewSeason(Season.First, out var a1)
|
||||
.WithNewSeason(a1, out var a2)
|
||||
.WithNewSeason(a1, out var b2);
|
||||
|
||||
string serial2 = JsonSerializer.Serialize(three, Options);
|
||||
Timelines four = JsonSerializer.Deserialize<Timelines>(serial2, Options)
|
||||
?? throw new AssertionException("Failed to deserialize");
|
||||
|
||||
Assert.That(four.Next, Is.EqualTo(three.Next), "Failed to reserialize next timeline");
|
||||
Assert.That(four.Pasts, Is.EquivalentTo(three.Pasts), "Failed to reserialize pasts");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SerializeRoundTrip_NewGame()
|
||||
{
|
||||
World world = World.WithStandardMap();
|
||||
|
||||
JsonElement document = JsonSerializer.SerializeToDocument(world, Options).RootElement;
|
||||
Assert.That(
|
||||
document.EnumerateObject().Select(prop => prop.Name),
|
||||
Is.EquivalentTo(new List<string> {
|
||||
"mapType",
|
||||
"units",
|
||||
"retreatingUnits",
|
||||
"orderHistory",
|
||||
"options",
|
||||
"timelines",
|
||||
}));
|
||||
|
||||
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!.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!.Timelines.Pasts, Is.Not.Null, "Failed to deserialize timeline pasts");
|
||||
Assert.That(deserialized!.Timelines.Next, Is.EqualTo(world.Timelines.Next));
|
||||
Assert.That(deserialized!.Options, Is.Not.Null, "Failed to deserialize options");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SerializeRoundTrip_MDATC_3_A_2()
|
||||
{
|
||||
// Set up MDATC 3.A.2
|
||||
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhase);
|
||||
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();
|
||||
|
||||
Assert.That(setup.World.OrderHistory[s0.Key].Orders.Count, Is.GreaterThan(0), "Missing orders");
|
||||
Assert.That(setup.World.OrderHistory[s0.Key].DoesMoveOutcomes.Count, Is.GreaterThan(0), "Missing moves");
|
||||
Assert.That(setup.World.OrderHistory[s0.Key].IsDislodgedOutcomes.Count, Is.GreaterThan(0), "Missing dislodges");
|
||||
|
||||
// Serialize and deserialize the world
|
||||
string serialized = JsonSerializer.Serialize(setup.World, Options);
|
||||
World reserialized = JsonSerializer.Deserialize<World>(serialized, Options)
|
||||
?? throw new AssertionException("Failed to reserialize world");
|
||||
|
||||
Assert.Multiple(() => {
|
||||
Assert.That(reserialized.OrderHistory[s0.Key].Orders.Count, Is.GreaterThan(0), "Missing orders");
|
||||
Assert.That(reserialized.OrderHistory[s0.Key].DoesMoveOutcomes.Count, Is.GreaterThan(0), "Missing moves");
|
||||
Assert.That(reserialized.OrderHistory[s0.Key].IsDislodgedOutcomes.Count, Is.GreaterThan(0), "Missing dislodges");
|
||||
Assert.That(reserialized.Timelines.Pasts, Is.Not.Empty, "Missing timeline history");
|
||||
});
|
||||
|
||||
// Resume the test case
|
||||
setup = new(reserialized, MovementPhase);
|
||||
setup[("a", 1)]
|
||||
["Germany"]
|
||||
.Army("Mun").Supports.Army("Mun", season: s0).MoveTo("Tyr").GetReference(out var mun1)
|
||||
["Austria"]
|
||||
.Army("Tyr").Holds();
|
||||
|
||||
setup.ValidateOrders();
|
||||
Assert.That(mun1, Is.Valid);
|
||||
var adjudications = setup.AdjudicateOrders();
|
||||
Assert.That(mun1, Is.NotCut);
|
||||
|
||||
AttackStrength mun0attack = adjudications.OfType<AttackStrength>().Single();
|
||||
Assert.That(mun0attack.Supports, Is.Not.Empty, "Support not tracked");
|
||||
|
||||
DoesMove mun0move = adjudications.OfType<DoesMove>().Single(move => move.Order.Unit.Key == mun0.Order.Unit.Key);
|
||||
Assert.That(mun0move.Outcome, Is.True);
|
||||
|
||||
IsDislodged tyr0dislodge = adjudications.OfType<IsDislodged>().Single(dis => dis.Order.Unit.Key == tyr0.Order.Unit.Key);
|
||||
Assert.That(tyr0dislodge.Outcome, Is.True);
|
||||
|
||||
// Confirm that an alternate future is created.
|
||||
World world = setup.UpdateWorld();
|
||||
Season fork = new("b1");
|
||||
Unit tyr1 = world.GetUnitAt("Tyr", fork);
|
||||
Assert.That(
|
||||
tyr1.Past,
|
||||
Is.EqualTo(mun0.Order.Unit.Key),
|
||||
"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.Key, Is.EqualTo(tyr0.Order.Unit.Key));
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ using System.Collections.ObjectModel;
|
||||
using MultiversalDiplomacy.Adjudicate;
|
||||
using MultiversalDiplomacy.Model;
|
||||
using MultiversalDiplomacy.Orders;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace MultiversalDiplomacyTests;
|
||||
|
||||
@ -19,7 +20,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 +41,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 +189,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 +235,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, new(seasonCoord));
|
||||
|
||||
/// <summary>
|
||||
/// Get a unit matching a description. If no such unit exists, one is created and added to the
|
||||
@ -254,7 +255,7 @@ public class TestCaseBuilder
|
||||
/// of this type.
|
||||
/// </param>
|
||||
private Unit GetOrBuildUnit(
|
||||
Power power,
|
||||
string power,
|
||||
Location location,
|
||||
Season season,
|
||||
UnitType type)
|
||||
@ -262,7 +263,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 +271,7 @@ public class TestCaseBuilder
|
||||
}
|
||||
|
||||
// Not found
|
||||
Unit newUnit = Unit.Build(location, season, power, type);
|
||||
Unit newUnit = Unit.Build(location.Key, season, power, type);
|
||||
this.World = this.World.Update(units: this.World.Units.Append(newUnit));
|
||||
return newUnit;
|
||||
}
|
||||
@ -327,11 +328,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)
|
||||
{
|
||||
@ -344,16 +345,18 @@ public class TestCaseBuilder
|
||||
{
|
||||
public TestCaseBuilder Builder;
|
||||
public SeasonContext SeasonContext;
|
||||
public Power Power;
|
||||
public string Power;
|
||||
|
||||
public PowerContext(SeasonContext seasonContext, Power Power)
|
||||
public PowerContext(SeasonContext seasonContext, string power)
|
||||
{
|
||||
Assert.That(power, Is.AnyOf([.. seasonContext.Builder.World.Map.Powers]), "Invalid power");
|
||||
|
||||
this.Builder = seasonContext.Builder;
|
||||
this.SeasonContext = seasonContext;
|
||||
this.Power = Power;
|
||||
this.Power = power;
|
||||
}
|
||||
|
||||
public ISeasonContext this[(int turn, int timeline) seasonCoord]
|
||||
public ISeasonContext this[(string timeline, int turn) seasonCoord]
|
||||
=> this.SeasonContext[seasonCoord];
|
||||
|
||||
public IPowerContext this[string powerName]
|
||||
@ -361,10 +364,10 @@ public class TestCaseBuilder
|
||||
|
||||
public IUnitContext Army(string provinceName, string? powerName = null)
|
||||
{
|
||||
Power power = powerName == null
|
||||
string 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);
|
||||
@ -372,10 +375,10 @@ public class TestCaseBuilder
|
||||
|
||||
public IUnitContext Fleet(string provinceName, string? coast = null, string? powerName = null)
|
||||
{
|
||||
Power power = powerName == null
|
||||
string 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,14 +416,14 @@ 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,
|
||||
this.Unit,
|
||||
destSeason,
|
||||
destination);
|
||||
destination.Key);
|
||||
this.Builder.OrderList.Add(moveOrder);
|
||||
return new OrderDefinedContext<MoveOrder>(this, moveOrder);
|
||||
}
|
||||
@ -449,10 +452,10 @@ public class TestCaseBuilder
|
||||
|
||||
public IConvoyDestinationContext Army(string provinceName, string? powerName = null)
|
||||
{
|
||||
Power power = powerName == null
|
||||
string 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);
|
||||
@ -463,10 +466,10 @@ public class TestCaseBuilder
|
||||
string? coast = null,
|
||||
string? powerName = null)
|
||||
{
|
||||
Power power = powerName == null
|
||||
string 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 +495,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,
|
||||
@ -524,10 +527,10 @@ public class TestCaseBuilder
|
||||
Season? season = null,
|
||||
string? powerName = null)
|
||||
{
|
||||
Power power = powerName == null
|
||||
string 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);
|
||||
@ -539,10 +542,10 @@ public class TestCaseBuilder
|
||||
string? coast = null,
|
||||
string? powerName = null)
|
||||
{
|
||||
Power power = powerName == null
|
||||
string 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 +585,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 +626,7 @@ public class TestCaseBuilder
|
||||
return this.Builder;
|
||||
}
|
||||
|
||||
public ISeasonContext this[(int turn, int timeline) seasonCoord]
|
||||
public ISeasonContext this[(string timeline, int turn) seasonCoord]
|
||||
=> this.SeasonContext[seasonCoord];
|
||||
|
||||
public IPowerContext this[string powerName]
|
||||
|
@ -14,7 +14,7 @@ class TestCaseBuilderTest
|
||||
{
|
||||
TestCaseBuilder setup = new(World.WithStandardMap());
|
||||
|
||||
Assert.That(setup.World.Powers.Count(), Is.EqualTo(7), "Unexpected power count");
|
||||
Assert.That(setup.World.Powers.Count, Is.EqualTo(7), "Unexpected power count");
|
||||
Assert.That(setup.World.Units, Is.Empty, "Expected no units to be created yet");
|
||||
|
||||
setup
|
||||
@ -28,19 +28,19 @@ class TestCaseBuilderTest
|
||||
Assert.That(setup.World.Units, Is.Not.Empty, "Expected units to be created");
|
||||
|
||||
Unit armyLON = setup.World.GetUnitAt("London");
|
||||
Assert.That(armyLON.Power.Name, Is.EqualTo("England"), "Unit created with wrong power");
|
||||
Assert.That(armyLON.Power, Is.EqualTo("England"), "Unit created with wrong power");
|
||||
Assert.That(armyLON.Type, Is.EqualTo(UnitType.Army), "Unit created with wrong type");
|
||||
|
||||
Unit fleetIRI = setup.World.GetUnitAt("Irish Sea");
|
||||
Assert.That(fleetIRI.Power.Name, Is.EqualTo("England"), "Unit created with wrong power");
|
||||
Assert.That(fleetIRI.Power, Is.EqualTo("England"), "Unit created with wrong power");
|
||||
Assert.That(fleetIRI.Type, Is.EqualTo(UnitType.Fleet), "Unit created with wrong type");
|
||||
|
||||
Unit fleetSTP = setup.World.GetUnitAt("Saint Petersburg");
|
||||
Assert.That(fleetSTP.Power.Name, Is.EqualTo("Russia"), "Unit created with wrong power");
|
||||
Assert.That(fleetSTP.Power, Is.EqualTo("Russia"), "Unit created with wrong power");
|
||||
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").Key),
|
||||
"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").Key),
|
||||
"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("Germany"),
|
||||
"Wrong power");
|
||||
Assert.That(
|
||||
orderMun.Order.Unit.Location,
|
||||
Is.EqualTo(setup.World.GetLand("Mun")),
|
||||
Is.EqualTo(setup.World.Map.GetLand("Mun").Key),
|
||||
"Wrong unit");
|
||||
|
||||
Assert.That(
|
||||
|
103
MultiversalDiplomacyTests/TimelinesTest.cs
Normal file
103
MultiversalDiplomacyTests/TimelinesTest.cs
Normal file
@ -0,0 +1,103 @@
|
||||
using MultiversalDiplomacy.Model;
|
||||
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace MultiversalDiplomacyTests;
|
||||
|
||||
public class TimelinesTest
|
||||
{
|
||||
[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 RoundTripTimelineKeys(int number, string designation)
|
||||
{
|
||||
Assert.That(Timelines.IntToString(number), Is.EqualTo(designation), "Incorrect string");
|
||||
Assert.That(Timelines.StringToInt(designation), Is.EqualTo(number), "Incorrect number");
|
||||
}
|
||||
|
||||
[TestCase("a0", "a", 0)]
|
||||
[TestCase("a1", "a", 1)]
|
||||
[TestCase("a10", "a", 10)]
|
||||
[TestCase("aa2", "aa", 2)]
|
||||
[TestCase("aa22", "aa", 22)]
|
||||
public void SeasonKeySplit(string key, string timeline, int turn)
|
||||
{
|
||||
Assert.That(Timelines.SplitKey(key), Is.EqualTo((timeline, turn)), "Failed to split key");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NoSharedFactoryState()
|
||||
{
|
||||
Timelines one = Timelines.Create()
|
||||
.WithNewSeason(Season.First, out var s1)
|
||||
.WithNewSeason(Season.First, out var s2)
|
||||
.WithNewSeason(Season.First, out var s3);
|
||||
Timelines two = Timelines.Create()
|
||||
.WithNewSeason(Season.First, out var s4)
|
||||
.WithNewSeason(Season.First, out var s5);
|
||||
|
||||
Assert.That(s1.Timeline, Is.EqualTo("a"));
|
||||
Assert.That(s2.Timeline, Is.EqualTo("b"));
|
||||
Assert.That(s3.Timeline, Is.EqualTo("c"));
|
||||
Assert.That(s4.Timeline, Is.EqualTo("a"), "Unexpected first timeline");
|
||||
Assert.That(s5.Timeline, Is.EqualTo("b"), "Unexpected second timeline");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TimelineForking()
|
||||
{
|
||||
Timelines timelines = Timelines.Create()
|
||||
.WithNewSeason(Season.First, out var a1)
|
||||
.WithNewSeason(a1, out var a2)
|
||||
.WithNewSeason(a2, out var a3)
|
||||
.WithNewSeason(a1, out var b2)
|
||||
.WithNewSeason(b2, out var b3)
|
||||
.WithNewSeason(a1, out var c2)
|
||||
.WithNewSeason(a2, out var d3);
|
||||
Season a0 = Season.First;
|
||||
|
||||
Assert.That(
|
||||
timelines.Pasts.Keys,
|
||||
Is.EquivalentTo(new List<string> { "a0", "a1", "a2", "a3", "b2", "b3", "c2", "d3" }),
|
||||
"Unexpected seasons");
|
||||
|
||||
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(a1.Turn, Is.EqualTo(Season.First.Turn + 1), "Unexpected a1 turn number");
|
||||
Assert.That(a2.Turn, Is.EqualTo(Season.First.Turn + 2), "Unexpected a2 turn number");
|
||||
Assert.That(a3.Turn, Is.EqualTo(Season.First.Turn + 3), "Unexpected a3 turn number");
|
||||
Assert.That(b2.Turn, Is.EqualTo(Season.First.Turn + 2), "Unexpected b2 turn number");
|
||||
Assert.That(b3.Turn, Is.EqualTo(Season.First.Turn + 3), "Unexpected b3 turn number");
|
||||
Assert.That(c2.Turn, Is.EqualTo(Season.First.Turn + 2), "Unexpected c2 turn number");
|
||||
Assert.That(d3.Turn, Is.EqualTo(Season.First.Turn + 3), "Unexpected d3 turn number");
|
||||
|
||||
Assert.That(timelines.GetTimelineRoot(a0), Is.EqualTo(a0), "Expected timeline root to be reflexive");
|
||||
Assert.That(timelines.GetTimelineRoot(a3), Is.EqualTo(a0), "Expected trunk timeline to have root");
|
||||
Assert.That(timelines.GetTimelineRoot(b2), Is.EqualTo(b2), "Expected alt timeline root to be reflexive");
|
||||
Assert.That(timelines.GetTimelineRoot(b3), Is.EqualTo(b2), "Expected alt timeline to root at first fork");
|
||||
Assert.That(timelines.GetTimelineRoot(c2), Is.EqualTo(c2), "Expected alt timeline root to be reflexive");
|
||||
Assert.That(timelines.GetTimelineRoot(d3), Is.EqualTo(d3), "Expected alt timeline root to be reflexive");
|
||||
|
||||
Assert.That(timelines.InAdjacentTimeline(b3, a3), Is.True, "Expected alts to be adjacent to origin");
|
||||
Assert.That(timelines.InAdjacentTimeline(b3, c2), Is.True, "Expected alts with common origin to be adjacent");
|
||||
Assert.That(timelines.InAdjacentTimeline(b3, d3), Is.False, "Expected alts from different origins not to be adjacent");
|
||||
|
||||
Assert.That(timelines.GetFutures(a0), Is.EquivalentTo(new List<Season> { a1 }), "Unexpected futures");
|
||||
Assert.That(timelines.GetFutures(a1), Is.EquivalentTo(new List<Season> { a2, b2, c2 }), "Unexpected futures");
|
||||
Assert.That(timelines.GetFutures(a2), Is.EquivalentTo(new List<Season> { a3, d3 }), "Unexpected futures");
|
||||
Assert.That(timelines.GetFutures(b2), Is.EquivalentTo(new List<Season> { b3 }), "Unexpected futures");
|
||||
}
|
||||
}
|
@ -10,29 +10,28 @@ 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");
|
||||
Season a0 = Season.First;
|
||||
Unit u1 = Unit.Build(Mun.Key, a0, "Austria", UnitType.Army);
|
||||
|
||||
Season s2 = s1.MakeNext();
|
||||
Unit u2 = u1.Next(Boh, s2);
|
||||
world = world.WithNewSeason(a0, out Season a1);
|
||||
Unit u2 = u1.Next(Boh.Key, a1);
|
||||
|
||||
Season s3 = s2.MakeNext();
|
||||
Unit u3 = u2.Next(Tyr, s3);
|
||||
_ = world.WithNewSeason(a1, out Season a2);
|
||||
Unit u3 = u2.Next(Tyr.Key, 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.Key), "Missing unit past");
|
||||
Assert.That(u2.Past, Is.EqualTo(u1.Key), "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.Key), "Unexpected unit location");
|
||||
Assert.That(u2.Location, Is.EqualTo(Boh.Key), "Unexpected unit location");
|
||||
Assert.That(u3.Location, Is.EqualTo(Tyr.Key), "Unexpected unit location");
|
||||
}
|
||||
}
|
26
README.md
26
README.md
@ -2,28 +2,10 @@
|
||||
|
||||
_5D Diplomacy with Multiversal Time Travel_ is a _Diplomacy_ variant that adds multiversal time travel in the style of its namesake, _5D Chess with Multiversal Time Travel_.
|
||||
|
||||
## Acknowledgements
|
||||
This project was inspired by [Oliver Lugg's proof-of-concept version](https://github.com/Oliveriver/5d-diplomacy-with-multiverse-time-travel) and based on the adjudication algorithms of Lucas B. Kruijswijk. For more information on the design, see [docs/design.md](./docs/design.md). For more information on the rules of multiversal Diplomacy, see [docs/rules.md](./docs/rules.md).
|
||||
|
||||
This project was inspired by [Oliver Lugg's proof-of-concept version](https://github.com/Oliveriver/5d-diplomacy-with-multiverse-time-travel). The implementation is based on the algorithms described by Lucas B. Kruijswijk in the chapter "The Process of Adjudication" found in the [Diplomacy Adjudicator Test Cases](http://web.inter.nl.net/users/L.B.Kruijswijk/#5) as well as ["The Math of Adjudication"](http://uk.diplom.org/pouch/Zine/S2009M/Kruijswijk/DipMath_Chp1.htm). Some of the data model is inspired by that of Martin Bruse's [godip](https://github.com/zond/godip).
|
||||
## Usage
|
||||
|
||||
## Variant rules
|
||||
This project is not ready for end users yet!
|
||||
|
||||
### Multiversal time travel and timeline forks
|
||||
|
||||
_Diplomacy_ is played on a single board, on which are placed armies and fleets. Sequential sets of orders modify the positions of these units, changing the board as time progresses. This may be described as something like an "inner" view of a single timeline. Consider instead the view from "above" the timeline, from which each successive state of the game board is comprehended in sequence. From "above", each turn from the beginning of the game to the present can be considered separately. In _5D Diplomacy with Multiversal Time Travel_, units moving to another province may also move to another turn, potentially changing the past.
|
||||
|
||||
If the outcome of a battle in the past of a timeline is changed by time travel, then the subsequent future will be different. Since the future of the original outcome is already determined, history forks, and the alternate future proceeds in an alternate timeline.
|
||||
|
||||
Just as units in _Diplomacy_ may only move to adjacent spaces, units in _5D Diplomacy with Multiversal Time Travel_ may only move to adjacent times. For the purposes of attacking, supporting, or convoying, turns within one season of each other adjacent. Branching timelines and the timelines they branched off of are adjacent, as well as timelines that branched off of the same turn in the same timeline. A unit cannot move to the province it is currently in, but it can move to the same province in another turn or another timeline.
|
||||
|
||||
When a unit changes the outcome of a battle in the past, only the timeline of the battle forks. If an army from one timeline dislodges an army in the past of a second timeline that was supporting a move in a third timeline, an alternate future is created where the army in the second timeline is dislodged. The third timeline does not fork, since the support was given in the original timeline. Similarly, if a unit moves into another timeline and causes a previously-successful move from a third timeline to become a bounce, the destination timeline forks because the outcome of the move changed, but the newly-bounced unit's origin timeline does not fork because the move succeeded in the original timeline.
|
||||
|
||||
### Sustaining timelines and time centers
|
||||
|
||||
Since there are many ways to create new timelines, the game would rapidly expand beyond all comprehension if this were not counterbalanced in some way. This happens during the _sustain phase_, which occurs after the fall movement and retreat phases and before the winter buid/disband phase.
|
||||
|
||||
(TODO)
|
||||
|
||||
### Victory conditions
|
||||
|
||||
The Great Powers of Europe can only wage multiversal wars because they are lead by extradimensional beings masquerading as human politicians. When a country is eliminated in one timeline, its extradimensional leader is executed, killing them in all timelines.
|
||||
I am working in VS Code on NixOS so currently the developer setup is optimized for that. VS Code is launched from inside a `nix develop` shell so it gets the environment. The C# debugger fails to launch on NixOS so I run Code through an Ubuntu 22.04 distrobox when I need that.
|
||||
|
4139
docs/DATC_v3_1.html
Normal file
4139
docs/DATC_v3_1.html
Normal file
File diff suppressed because it is too large
Load Diff
675
docs/GPL3.txt
Normal file
675
docs/GPL3.txt
Normal file
@ -0,0 +1,675 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
|
49
docs/design.md
Normal file
49
docs/design.md
Normal file
@ -0,0 +1,49 @@
|
||||
# Architecture
|
||||
|
||||
In lieu of a systematic overview of the architecture, here are a few scattered notes on design decisions.
|
||||
|
||||
## Provinces and Locations
|
||||
|
||||
The data model here is based on the data model of [godip](https://github.com/zond/godip). In particular, godip handles the distinction between army and fleet movement by distinguishing between Provicnces and SubProvinces, which 5dplomacy calls Locations. The graph edges that define valid paths are drawn between Locations, but occupation by a unit and being a supply center are properties of the Province as a whole. This makes it easy to represent the different paths available to armies or fleets: the land and sea graphs are unconnected and only interact at the Province level. This also provides a way to distinguish the connectivity of multiple coasts within a province.
|
||||
|
||||
As a consequence of the unconnected land and sea graphs, there is no fundamental difference between army movement and fleet movement, since the inability of armies to move into the ocean is ensured by the lack of edges between land and sea locations. Unit type still remains significant with respect to convoys, since only fleets can convoy and only armies can be convoyed. Unit type is also relevant to the interpretation of orders that do not fully specify location. And, of course, unit type matters to how clients represent the units.
|
||||
|
||||
Internally, land locations are named "land" or "l" and water locations are called "water" or "w". For example, SPA has three locations: SPA/nc, SPA/sc, and SPA/l. This provides a uniform way to handle unit location, because locations in orders without coast specifications can easily be inferred from the map and the unit type. For example, "A Mun - Tyr" can easily be inferred to mean "A Mun/l - Tyr/l" because A Mun is located in the "land" location in Mun and the "land" location in Tyr is the only connected one.
|
||||
|
||||
## Timeline notation
|
||||
|
||||
In Diplomacy, there is only one board, whose state changes atomically as a function of the previous state and the orders. Thus, there is only ever need to refer to units by the province they instantaneously occupy, e.g. "A MUN -> TYR" to order the army in Munich to move to Tyrolia. 5dplomacy needs to be able to refer to past states of the board as well as alternative timeline states of the board. The timeline of a province is specified by prefixing the timeline designation, e.g. "a-MUN" to refer to Munish in timeline a or "b-TYR" to refer to Tyrolia in timeline b. The turn of a province is specified by a suffix, e.g. "LON@3" to refer to London in turn 3.
|
||||
|
||||
## Adjudication algorithm
|
||||
|
||||
The adjuciation algorithm is based on the algorithms described by Lucas B. Kruijswijk in the [Diplomacy Adjudicator Test Cases v2.5 §5 "The Process of Adjudication"](https://web.archive.org/web/20230608074055/http://web.inter.nl.net/users/L.B.Kruijswijk/#5) as well as ["The Math of Adjudication"](http://uk.diplom.org/pouch/Zine/S2009M/Kruijswijk/DipMath_Chp1.htm). The approach taken follows the partial information algorithm later described in [DATC v3.0 §5.E](https://webdiplomacy.net/doc/DATC_v3_0.html#5.E). These algorithms are based on the recursive evaluation of binary (move succeeds, unit is dislodged, etc.) and numeric (attack strength, hold strength, etc.) decisions.
|
||||
|
||||
In order to support multiversal time travel, 5dplomacy adds an additional binary decision for each relevant timeline: whether the timeline advances. The timeline advance decision is resolved for each timeline-turn as follows:
|
||||
|
||||
- The head of a timeline always advances.
|
||||
- The target of a new (i.e. not previously adjudicated) and successful move always advances.
|
||||
- A timeline-turn adcanfes if the outcomne of a battle is changed, as follows:
|
||||
- The outcome of a dislodge decision is changed.
|
||||
- The outcome of an intra-timeline move decision is changed.
|
||||
- The outcome of an inter-timeline move into that timeline-turn is changed.
|
||||
|
||||
A timeline head advances into a new turn of the same timeline. A turn behind the head advances into a forked timeline.
|
||||
|
||||
Note that the timeline advance decision depends on the result of previously-adjudicated decisions, which informs the data model.
|
||||
|
||||
## Pure adjudication
|
||||
|
||||
The core adjudication algorithm is intended to be a pure function. That is, adjudication begins with all relevant information about the game state and orders, and it computes the result of adjudicating those orders, leaving the inputs unchanged. Data persistence is handled by a higher layer that is responsible for saving the information the adjudicator needs and constructing the input data structure. This is intended to encapsulate the adjudicator logic and decouple it from other concerns that depend on implementation details of the application.
|
||||
|
||||
## Game options
|
||||
|
||||
In order to support different decisions about how adjudication or the rules of multiversal _Diplomacy_ are implemented, the `Options` object is a grab-bag of settings that can be used to tune the adjudicator. The following options are supported:
|
||||
|
||||
- `implicitMainTimeline`: Whether orders to units with no timeline designation should be interpreted as orders for the first timeline. (This may be the default behavior to support adjudication of classical _Diplomacy_ games.)
|
||||
- `enableOpenConvoys`: Whether the open convoy order can be used.
|
||||
- `enableJumpAssists`: Whether the jump assist order can be used.
|
||||
- `victoryCondition`: The victory condition to use for the game. `"elimination"` means a player is eliminated if they are eliminated in a single timeline and the last player standing wins. `"majority"` means a player wins if they control the majority of supply centers across all timelines. `"unique"` means a player wins if they control 18 unique supply centers by name across all timelines.
|
||||
- `adjacency`: The rule to use for determining province adjacency. `"strict"` means provinces are adjacent if they are within one timeline of each other, within one turn of each other, and geographically adjacent. `"anyTimeline"` follows `"strict"` but all timelines are considered adjacent to each other.
|
||||
|
||||
> [!WARNING]
|
||||
> Options are not implemented yet.
|
65
docs/rules.md
Normal file
65
docs/rules.md
Normal file
@ -0,0 +1,65 @@
|
||||
# Multiversal Diplomacy rules
|
||||
|
||||
_Diplomacy_ is played with armies and fleets on a single board. Each set of orders results in the state of the board changing. Suppose that, instead of moving the pieces on the board, a second board were set up according to the resolution of the orders given to the first board. If this continued, the whole evolution of the game board could be observed from start to finish. One could even go back to a board state in the middle of the game and give a different set of orders, creating a new version of history and playing out a different game.
|
||||
|
||||
Now suppose that, in addition to moving to another province on the board, units could move to _another board_, changing the outcome of the turn and creating a new sequence of boards as the new history plays itself out. This is _5D Diplomacy with Multiversal Time Travel_. Units may move or support into the past, changing history and creating alternate timelines, or move or support into those alternate timelines.
|
||||
|
||||
## DATC compliance
|
||||
|
||||
When the adjudicator is in a more complete state, this section will declare the extent of the adjudicator's DATC compliance. If no unit attempts multiversal time travel, the game plays out exactly as _Diplomacy_ does normally.
|
||||
|
||||
The MDATC (Multiversal Diplomacy Adjudicator Test Cases) document defines test cases that involve multiversal time travel.
|
||||
|
||||
- 4.C.5 (missing nationality in support order), 4.C.6 (wrong nationality in support order): 5dplomacy does not support specifying the nationality of the supported unit.
|
||||
|
||||
## Variant rules
|
||||
|
||||
### Multiversal time travel and timeline forks
|
||||
|
||||
Just as units in _Diplomacy_ may only move to adjacent spaces, units in _5D Diplomacy with Multiversal Time Travel_ may only move to adjacent times. For the purposes of attacking, supporting, or convoying, turns within one season of each other adjacent. Branching timelines and the timelines they branched off of are adjacent, as well as timelines that branched off of the same turn in the same timeline. A unit cannot move to the province it is currently in, but it can move to the same province in another turn or another timeline.
|
||||
|
||||
When a unit changes the outcome of a battle in the past, only the timeline of the battle forks. If an army from one timeline dislodges an army in the past of a second timeline that was supporting a move in a third timeline, an alternate future is created where the army in the second timeline is dislodged. The third timeline does not fork, since the support was given in the original timeline. Similarly, if a unit moves into another timeline and causes a previously-successful move from a third timeline to become a bounce, the destination timeline forks because the outcome of the move changed, but the newly-bounced unit's origin timeline does not fork because the move succeeded in the original timeline.
|
||||
|
||||
### Sustaining timelines and time centers
|
||||
|
||||
Since there are many ways to create new timelines, the game would rapidly expand beyond all comprehension if this were not counterbalanced in some way. This happens during the _sustain phase_, which occurs after the fall movement and retreat phases and before the winter build/disband phase.
|
||||
|
||||
The seven capital supply centers are also considered _time centers_. During the sustain phase, each time center chooses a timeline to sustain. Each timeline with at least one time center sustaining it remains in play. All other timelines dissolve into the ether and cease to be playable. A time center's "vote" is split equally among all timelines and each fraction is owned by the owner of that time center in that timeline.
|
||||
|
||||
> [!WARNING]
|
||||
> The sustain phase is a speculative feature and has not been implemented yet.
|
||||
|
||||
### Victory conditions
|
||||
|
||||
The Great Powers of Europe can only wage multiversal wars because they are lead by extradimensional beings masquerading as human politicians. When a country is eliminated in one timeline, its extradimensional leader is executed, killing them in all timelines.
|
||||
|
||||
> [!WARNING]
|
||||
> Victory conditions have not been implemented yet.
|
||||
|
||||
### Open convoys
|
||||
|
||||
The standard _Diplomacy_ rules require that a convoy order include the convoyed unit's origin and destination. This is hard to coordinate once there are multiple turns and timelines involved. _5D Diplomacy with Multiversal Time Travel_ thus introduces the concept of an _open convoy_, a nonspecific convoy order that can become part of a convoy later. An open convoy order does not require specifying the origin or destination of the convoyed unit; the unit is simply told to expect guests. Consequently, it is possible for a unit doing an open convoy to be used by a hostile power to convoy in the opposite direction.
|
||||
|
||||
> [!WARNING]
|
||||
> Open convoys are a speculative feature and have not been implemented yet.
|
||||
|
||||
### Jump assist
|
||||
|
||||
Outside of convoys, a unit may only move one province at a time. Multiversal time travel also allows units to move back or forward by one turn and/or across by one timeline. Since time moves forward by one turn per turn, this makes it difficult to go further back into the past. The _jump assist_ order provides a way for units to intervene deeper into the past. A unit may be ordered to give a jump assist to another unit's move. For each successful jump assist given, a unit may move one more turn or timeline. Jump assists do not grant units the ability to move further geographically than they could otherwise, nor do they provide additional support to an attack.
|
||||
|
||||
> [!WARNING]
|
||||
> Jump assists are a speculative feature and have not been implemented yet.
|
||||
|
||||
## Unit designations
|
||||
|
||||
In _Diplomacy_, orders refer to provinces, such as "A Mun-Tyr". In _5D Diplomacy with Multiversal Time Travel_, this is insufficient to unambiguously identify a province, since the province exists in multiple timelines across multiple turns. The convention for identifying a multiversal location is `timeline-province@turn`, where `timeline` is the timeline's identifier and `turn` is the turn's identifier, e.g. "b-Mun@3".
|
||||
|
||||
(Why this order? Short representations for timelines and turns can be confused for each other, especially for timelines designated with `f` or `s` that might be confused for fall or spring turns. _5D Diplomacy with Multiversal Time Travel_ is already complicated enough, so the timeline and turn are put on either side of the province and delimited with different symbols.)
|
||||
|
||||
Some designation elements may be omitted for brevity. Omitted elements are interpreted according to the following rules:
|
||||
|
||||
- If the timeline is omitted from the subject of an order, the timeline is the root timeline, "a". If the turn is omitted from the subject of an order, the turn is the latest turn in the timeline.
|
||||
- If the timeline or turn are unspecified for the destination of a move or the target of a support-hold order, the timeline and turn are those of the ordered unit.
|
||||
- If the timeline or turn are unspecified for the destination of a support-move order, the timeline and turn are those of the supported unit.
|
||||
|
||||
Thus, if timeline "a" is at turn 2 and timeline "b" is at turn 1, `A Munich supports A b-Munich - Tyrolia` is equivalent to `A a-Munich@2 supports A b-Munich@1 - b-Tyrolia@1`.
|
@ -10,7 +10,7 @@
|
||||
in rec {
|
||||
devShell = pkgs.mkShell {
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT = 1;
|
||||
NIX_LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ pkgs.stdenv.cc.cc ];
|
||||
NIX_LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ pkgs.stdenv.cc.cc pkgs.icu ];
|
||||
NIX_LD = builtins.readFile "${pkgs.stdenv.cc}/nix-support/dynamic-linker";
|
||||
packages = [
|
||||
pkgs.bashInteractive
|
||||
|
Loading…
Reference in New Issue
Block a user