Compare commits

..

No commits in common. "161e0a1ddbad36e195417d26f63fb482041ab139" and "601ce2d297168e6bbc7706443c8ab0f1ead558ca" have entirely different histories.

18 changed files with 70 additions and 82 deletions

View File

@ -47,7 +47,7 @@ public class MovementDecisions
var submittedOrdersBySeason = orders.Cast<UnitOrder>().ToLookup(order => order.Unit.Season);
foreach (var group in submittedOrdersBySeason)
{
AdvanceTimeline[group.Key.Key] = new(group.Key, group);
AdvanceTimeline[group.Key.Designation] = new(group.Key, group);
}
// Create timeline decisions for each season potentially affected by the submitted orders.
@ -67,20 +67,20 @@ public class MovementDecisions
case SupportHoldOrder supportHold:
AdvanceTimeline.Ensure(
supportHold.Target.Season.Key,
() => new(supportHold.Target.Season, world.OrderHistory[supportHold.Target.Season.Key].Orders));
AdvanceTimeline[supportHold.Target.Season.Key].Orders.Add(supportHold);
supportHold.Target.Season.Designation,
() => new(supportHold.Target.Season, world.OrderHistory[supportHold.Target.Season.Designation].Orders));
AdvanceTimeline[supportHold.Target.Season.Designation].Orders.Add(supportHold);
break;
case SupportMoveOrder supportMove:
AdvanceTimeline.Ensure(
supportMove.Target.Season.Key,
() => new(supportMove.Target.Season, world.OrderHistory[supportMove.Target.Season.Key].Orders));
AdvanceTimeline[supportMove.Target.Season.Key].Orders.Add(supportMove);
supportMove.Target.Season.Designation,
() => new(supportMove.Target.Season, world.OrderHistory[supportMove.Target.Season.Designation].Orders));
AdvanceTimeline[supportMove.Target.Season.Designation].Orders.Add(supportMove);
AdvanceTimeline.Ensure(
supportMove.Season.Key,
() => new(supportMove.Season, world.OrderHistory[supportMove.Season.Key].Orders));
AdvanceTimeline[supportMove.Season.Key].Orders.Add(supportMove);
supportMove.Season.Designation,
() => new(supportMove.Season, world.OrderHistory[supportMove.Season.Designation].Orders));
AdvanceTimeline[supportMove.Season.Designation].Orders.Add(supportMove);
break;
}
}
@ -92,7 +92,7 @@ public class MovementDecisions
.ToList();
(Province province, string season) UnitPoint(Unit unit)
=> (world.Map.GetLocation(unit.Location).Province, unit.Season.Key);
=> (world.Map.GetLocation(unit.Location).Province, unit.Season.Designation);
(Province province, string season) MovePoint(MoveOrder move)
=> (move.Province, move.Season);
@ -111,8 +111,8 @@ public class MovementDecisions
&& other.Province == world.Map.GetLocation(me.Unit).Province;
bool AreOpposing(MoveOrder one, MoveOrder two)
=> one.Season == two.Unit.Season.Key
&& two.Season == one.Unit.Season.Key
=> one.Season == two.Unit.Season.Designation
&& two.Season == one.Unit.Season.Designation
&& one.Province == world.Map.GetLocation(two.Unit).Province
&& two.Province == world.Map.GetLocation(one.Unit).Province;
@ -173,7 +173,7 @@ public class MovementDecisions
{
// Ensure a hold strength decision exists for the target's destination.
HoldStrength.Ensure(
(supportMove.Province, supportMove.Season.Key),
(supportMove.Province, supportMove.Season.Designation),
() => new(supportMove.Province, supportMove.Season));
}
}

View File

@ -77,7 +77,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
// Trivial check: a unit cannot move to where it already is.
AdjudicatorHelpers.InvalidateIfNotMatching(
order => !(order.Location.Key == order.Unit.Location && order.Season == order.Unit.Season.Key),
order => !(order.Location.Designation == order.Unit.Location && order.Season == order.Unit.Season.Designation),
ValidationReason.DestinationMatchesOrigin,
ref moveOrders,
ref validationResults);
@ -138,7 +138,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
// Trivial check: cannot convoy a unit to its own location
AdjudicatorHelpers.InvalidateIfNotMatching(
order => !(
order.Location.Key == order.Target.Location
order.Location.Designation == order.Target.Location
&& order.Season == order.Target.Season),
ValidationReason.DestinationMatchesOrigin,
ref convoyOrders,
@ -337,7 +337,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
Season moveSeason = world.Seasons[doesMove.Order.Season];
if (doesMove.Outcome == true && createdFutures.TryGetValue(moveSeason, out Season? future))
{
Unit next = doesMove.Order.Unit.Next(doesMove.Order.Location.Key, future);
Unit next = doesMove.Order.Unit.Next(doesMove.Order.Location.Designation, future);
logger.Log(3, "Advancing unit to {0}", next);
createdUnits.Add(next);
}
@ -366,7 +366,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
if (isDislodged.Outcome == false)
{
// Non-dislodged units continue into the future.
Unit next = order.Unit.Next(world.Map.GetLocation(order.Unit).Key, future);
Unit next = order.Unit.Next(world.Map.GetLocation(order.Unit).Designation, future);
logger.Log(3, "Advancing unit to {0}", next);
createdUnits.Add(next);
}
@ -387,14 +387,14 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
Dictionary<string, OrderHistory> newHistory = [];
foreach (UnitOrder unitOrder in decisions.OfType<IsDislodged>().Select(d => d.Order))
{
newHistory.Ensure(unitOrder.Unit.Season.Key, () => new([], [], []));
OrderHistory history = newHistory[unitOrder.Unit.Season.Key];
newHistory.Ensure(unitOrder.Unit.Season.Designation, () => new([], [], []));
OrderHistory history = newHistory[unitOrder.Unit.Season.Designation];
// TODO does this add every order to every season??
history.Orders.Add(unitOrder);
history.IsDislodgedOutcomes[unitOrder.Unit.Key] = dislodges[unitOrder.Unit].Outcome == true;
history.IsDislodgedOutcomes[unitOrder.Unit.Designation] = dislodges[unitOrder.Unit].Outcome == true;
if (unitOrder is MoveOrder moveOrder)
{
history.DoesMoveOutcomes[moveOrder.Unit.Key] = moves[moveOrder].Outcome == true;
history.DoesMoveOutcomes[moveOrder.Unit.Designation] = moves[moveOrder].Outcome == true;
}
}
@ -493,8 +493,8 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
// The season target of a new (i.e. not previously adjudicated) and successful move always advances.
IEnumerable<MoveOrder> newIncomingMoves = decision.Orders
.OfType<MoveOrder>()
.Where(order => order.Season == decision.Season.Key
&& !world.OrderHistory[order.Season].DoesMoveOutcomes.ContainsKey(order.Unit.Key));
.Where(order => order.Season == decision.Season.Designation
&& !world.OrderHistory[order.Season].DoesMoveOutcomes.ContainsKey(order.Unit.Designation));
foreach (MoveOrder moveOrder in newIncomingMoves)
{
DoesMove doesMove = decisions.DoesMove[moveOrder];
@ -511,14 +511,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.Key];
OrderHistory history = world.OrderHistory[decision.Season.Designation];
bool anyUnresolved = false;
foreach (UnitOrder order in decision.Orders)
{
// TODO these aren't timeline-specific
IsDislodged dislodged = decisions.IsDislodged[order.Unit];
progress |= ResolveDecision(dislodged, world, decisions, depth + 1);
if (history.IsDislodgedOutcomes.TryGetValue(order.Unit.Key, out bool previous)
if (history.IsDislodgedOutcomes.TryGetValue(order.Unit.Designation, out bool previous)
&& dislodged.Resolved
&& dislodged.Outcome != previous)
{
@ -535,7 +535,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
{
DoesMove moves = decisions.DoesMove[moveOrder];
progress |= ResolveDecision(moves, world, decisions, depth + 1);
if (history.DoesMoveOutcomes.TryGetValue(moveOrder.Unit.Key, out bool previousMove)
if (history.DoesMoveOutcomes.TryGetValue(moveOrder.Unit.Designation, out bool previousMove)
&& moves.Resolved
&& moves.Outcome != previousMove)
if (moves.Resolved && moves.Outcome != previousMove)

View File

@ -69,7 +69,7 @@ public static class PathFinder
// If not, add this location to the to-visit set if it isn't a coast, has a fleet,
// and hasn't already been visited.
if (!adjLocation.Province.Locations.Any(l => l.Type == LocationType.Land)
&& fleets.ContainsKey((adjLocation.Key, adjSeason))
&& fleets.ContainsKey((adjLocation.Designation, adjSeason))
&& !visited.Contains((adjLocation, adjSeason)))
{
toVisit.Enqueue((adjLocation, adjSeason));

View File

@ -42,7 +42,7 @@ public class Location
/// <summary>
/// The unique name of this location in the map.
/// </summary>
public string Key => $"{this.ProvinceName}/{this.Abbreviation}";
public string Designation => $"{this.ProvinceName}/{this.Abbreviation}";
public Location(Province province, string name, string abbreviation, LocationType type)
{

View File

@ -34,7 +34,7 @@ public class Map
LocationLookup = Provinces
.SelectMany(province => province.Locations)
.ToDictionary(location => location.Key);
.ToDictionary(location => location.Designation);
}
/// <summary>

View File

@ -35,7 +35,7 @@ public class Season(string? past, int turn, string timeline)
/// The multiversal designation of this season.
/// </summary>
[JsonIgnore]
public string Key => $"{this.Timeline}{this.Turn}";
public string Designation => $"{this.Timeline}{this.Turn}";
public override string ToString() => Key;
public override string ToString() => Designation;
}

View File

@ -36,7 +36,7 @@ public class Unit
/// A unique designation for this unit.
/// </summary>
[JsonIgnore]
public string Key => $"{Type.ToShort()} {Season.Timeline}-{Location}@{Season.Turn}";
public string Designation => $"{Type.ToShort()} {Season.Timeline}-{Location}@{Season.Turn}";
public Unit(string? past, string location, Season season, Power power, UnitType type)
{
@ -61,5 +61,5 @@ public class Unit
/// Advance this unit's timeline to a new location and season.
/// </summary>
public Unit Next(string location, Season season)
=> new(past: this.Key, location, season, this.Power, this.Type);
=> new(past: this.Designation, location, season, this.Power, this.Type);
}

View File

@ -146,7 +146,7 @@ public class World
Season a0 = new(past: null, Season.FIRST_TURN, timelines.NextTimeline());
return new World(
map,
new() { {a0.Key, a0} },
new() { {a0.Designation, a0} },
new([]),
new([]),
new(new Dictionary<string, OrderHistory>()),
@ -169,7 +169,7 @@ public class World
previous: this,
seasons: seasons == null
? this.Seasons
: new(seasons.ToDictionary(season => season.Key)),
: new(seasons.ToDictionary(season => season.Designation)),
units: units == null
? this.Units
: new(units.ToList()),
@ -202,7 +202,7 @@ public class World
: splits.Length == 3
? Map.GetWater(splits[2])
: Map.GetWater(splits[2], splits[3]);
Unit unit = Unit.Build(location.Key, this.RootSeason, power, type);
Unit unit = Unit.Build(location.Designation, this.RootSeason, power, type);
return unit;
});
return this.Update(units: units);
@ -244,8 +244,8 @@ public class World
/// </summary>
public Season ContinueOrFork(Season season)
=> GetFutures(season).Any()
? new(season.Key, season.Turn + 1, Timelines.NextTimeline())
: new(season.Key, season.Turn + 1, season.Timeline);
? new(season.Designation, season.Turn + 1, Timelines.NextTimeline())
: new(season.Designation, season.Turn + 1, season.Timeline);
/// <summary>
/// A standard Diplomacy game setup.
@ -271,7 +271,7 @@ public class World
/// </summary>
/// <param name="present">A season.</param>
/// <returns>The immediate futures of the season.</returns>
public IEnumerable<Season> GetFutures(Season present) => GetFutures(present.Key);
public IEnumerable<Season> GetFutures(Season present) => GetFutures(present.Designation);
/// <summary>
/// Returns the first season in this season's timeline. The first season is the
@ -325,7 +325,7 @@ public class World
return foundUnit;
}
public Unit GetUnitByKey(string designation)
=> Units.SingleOrDefault(u => u!.Key == designation, null)
public Unit GetUnitByDesignation(string designation)
=> Units.SingleOrDefault(u => u!.Designation == designation, null)
?? throw new KeyNotFoundException($"Unit {designation} not found");
}

View File

@ -41,6 +41,6 @@ public class SupportMoveOrder : SupportOrder
public bool IsSupportFor(MoveOrder move)
=> this.Target == move.Unit
&& this.Season.Key == move.Season
&& this.Season.Designation == move.Season
&& this.Location == move.Location;
}

View File

@ -46,12 +46,12 @@ public class TimeTravelTest
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));
Assert.That(aTyr.Past, Is.EqualTo(mun1.Order.Unit.Designation));
Assert.That(world.GetUnitByDesignation(aTyr.Past!).Past, Is.EqualTo(mun0.Order.Unit.Designation));
// Confirm that there is a unit in Mun b1 originating from Mun a0
Unit aMun1 = world.GetUnitAt("Mun", fork);
Assert.That(aMun1.Past, Is.EqualTo(originalUnit.Key));
Assert.That(aMun1.Past, Is.EqualTo(originalUnit.Designation));
}
[Test]
@ -95,7 +95,7 @@ public class TimeTravelTest
Unit tyr1 = world.GetUnitAt("Tyr", fork);
Assert.That(
tyr1.Past,
Is.EqualTo(mun0.Order.Unit.Key),
Is.EqualTo(mun0.Order.Unit.Designation),
"Expected A Mun a0 to advance to Tyr b1");
Assert.That(
world.RetreatingUnits.Count,

View File

@ -16,7 +16,7 @@ public class TimelineFactoryTest
[TestCase(53, "bb")]
[TestCase(77, "bz")]
[TestCase(78, "ca")]
public void RoundTripTimelineKeys(int number, string designation)
public void RoundTripTimelineDesignations(int number, string designation)
{
Assert.That(TimelineFactory.IntToString(number), Is.EqualTo(designation), "Incorrect string");
Assert.That(TimelineFactory.StringToInt(designation), Is.EqualTo(number), "Incorrect number");

View File

@ -209,7 +209,7 @@ public class MovementAdjudicatorTest
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.Key));
Assert.That(u2.Past, Is.EqualTo(mun1.Order.Unit.Designation));
Assert.That(u2.Season, Is.EqualTo(s2));
setup[("a", 1)]
@ -229,7 +229,7 @@ public class MovementAdjudicatorTest
updated = setup.UpdateWorld();
Season s3 = updated.GetSeason(s2.Timeline, s2.Turn + 1);
Unit u3 = updated.GetUnitAt("Mun", s3);
Assert.That(u3.Past, Is.EqualTo(mun2.Order.Unit.Key));
Assert.That(u3.Past, Is.EqualTo(mun2.Order.Unit.Designation));
}
[Test]
@ -259,7 +259,7 @@ public class MovementAdjudicatorTest
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.Key));
Assert.That(u2.Past, Is.EqualTo(mun1.Order.Unit.Designation));
Assert.That(u2.Season, Is.EqualTo(s2));
setup[("a", 1)]
@ -279,6 +279,6 @@ public class MovementAdjudicatorTest
updated = setup.UpdateWorld();
Season s3 = updated.GetSeason(s2.Timeline, s2.Turn + 1);
Unit u3 = updated.GetUnitAt("Mun", s3);
Assert.That(u3.Past, Is.EqualTo(u2.Key));
Assert.That(u3.Past, Is.EqualTo(u2.Designation));
}
}

View File

@ -95,9 +95,9 @@ public class SerializationTest
var adjudications = setup.AdjudicateOrders();
Assert.That(mun1, Is.NotCut);
Console.WriteLine(string.Join(", ", adjudications.Select(a => a.ToString())));
DoesMove mun0move = adjudications.OfType<DoesMove>().Single(move => move.Order.Unit.Key == mun0.Order.Unit.Key);
DoesMove mun0move = adjudications.OfType<DoesMove>().Single(move => move.Order.Unit.Designation == mun0.Order.Unit.Designation);
Assert.That(mun0move.Outcome, Is.True);
IsDislodged tyr0dislodge = adjudications.OfType<IsDislodged>().Single(dis => dis.Order.Unit.Key == tyr0.Order.Unit.Key);
IsDislodged tyr0dislodge = adjudications.OfType<IsDislodged>().Single(dis => dis.Order.Unit.Designation == tyr0.Order.Unit.Designation);
Assert.That(tyr0dislodge, Is.True);
// Confirm that an alternate future is created.
@ -106,7 +106,7 @@ public class SerializationTest
Unit tyr1 = world.GetUnitAt("Tyr", fork);
Assert.That(
tyr1.Past,
Is.EqualTo(mun0.Order.Unit.Key),
Is.EqualTo(mun0.Order.Unit.Designation),
"Expected A Mun a0 to advance to Tyr b1");
Assert.That(
world.RetreatingUnits.Count,

View File

@ -270,7 +270,7 @@ public class TestCaseBuilder
}
// Not found
Unit newUnit = Unit.Build(location.Key, season, power, type);
Unit newUnit = Unit.Build(location.Designation, season, power, type);
this.World = this.World.Update(units: this.World.Units.Append(newUnit));
return newUnit;
}
@ -419,7 +419,7 @@ public class TestCaseBuilder
MoveOrder moveOrder = new MoveOrder(
this.PowerContext.Power,
this.Unit,
destSeason.Key,
destSeason.Designation,
destination);
this.Builder.OrderList.Add(moveOrder);
return new OrderDefinedContext<MoveOrder>(this, moveOrder);

View File

@ -40,7 +40,7 @@ class TestCaseBuilderTest
Assert.That(fleetSTP.Type, Is.EqualTo(UnitType.Fleet), "Unit created with wrong type");
Assert.That(
fleetSTP.Location,
Is.EqualTo(setup.World.Map.GetWater("STP", "wc").Key),
Is.EqualTo(setup.World.Map.GetWater("STP", "wc").Designation),
"Unit created on wrong coast");
}
@ -128,7 +128,7 @@ class TestCaseBuilderTest
"Wrong power");
Assert.That(
orderMun.Order.Unit.Location,
Is.EqualTo(setup.World.Map.GetLand("Mun").Key),
Is.EqualTo(setup.World.Map.GetLand("Mun").Designation),
"Wrong unit");
Assert.That(

View File

@ -15,24 +15,24 @@ public class UnitTests
Tyr = world.Map.GetLand("Tyr");
Power pw1 = world.Map.GetPower("Austria");
Season a0 = world.RootSeason;
Unit u1 = Unit.Build(Mun.Key, a0, pw1, UnitType.Army);
Unit u1 = Unit.Build(Mun.Designation, a0, pw1, UnitType.Army);
world = world.ContinueOrFork(a0, out Season a1);
Unit u2 = u1.Next(Boh.Key, a1);
Unit u2 = u1.Next(Boh.Designation, a1);
_ = world.ContinueOrFork(a1, out Season a2);
Unit u3 = u2.Next(Tyr.Key, a2);
Unit u3 = u2.Next(Tyr.Designation, a2);
Assert.That(u3.Past, Is.EqualTo(u2.Key), "Missing unit past");
Assert.That(u2.Past, Is.EqualTo(u1.Key), "Missing unit past");
Assert.That(u3.Past, Is.EqualTo(u2.Designation), "Missing unit past");
Assert.That(u2.Past, Is.EqualTo(u1.Designation), "Missing unit past");
Assert.That(u1.Past, Is.Null, "Unexpected unit past");
Assert.That(u1.Season, Is.EqualTo(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.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");
Assert.That(u1.Location, Is.EqualTo(Mun.Designation), "Unexpected unit location");
Assert.That(u2.Location, Is.EqualTo(Boh.Designation), "Unexpected unit location");
Assert.That(u3.Location, Is.EqualTo(Tyr.Designation), "Unexpected unit location");
}
}

View File

@ -4,11 +4,9 @@ In lieu of a systematic overview of the architecture, here are a few scattered n
## 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.
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 a Province's supply center or occupation by a unit are at the Province level. This makes it easy to represent the different paths available to armies or fleets, since each is essentially moving through a distinct graph from the other, while still interacting at the Province level. It 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 special significance to unit type in movement, since the inability of fleets to move to land locations is ensured by the lack of edges from land locations to sea locations. The primary difference between unit types becomes "can convoy" and "can move by convoy", as well as how the units are represented by clients.
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.
As a consequence of the unconnected land and sea graphs, there is no special significance to unit type in movement, since the inability of fleets to move to land locations is ensured by the lack of edges from land locations to sea locations. Unit type is still relevant to convoy orders and how clients represent the units.
## Timeline notation
@ -40,13 +38,3 @@ The core adjudication algorithm is intended to be a pure function. That is, adju
> [!WARNING]
> This is not complete and the adjudicator is still stateful.
## 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.

View File

@ -43,7 +43,7 @@ The standard _Diplomacy_ rules require that a convoy order include the convoyed
### 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.
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.
> [!WARNING]
> Jump assists are a speculative feature and have not been implemented yet.