Save previous orders so time travel can re-adjudicate them
This commit is contained in:
parent
6a6810ef07
commit
6948db29df
|
@ -24,7 +24,7 @@ public class MovementDecisions
|
||||||
.Concat(this.PreventStrength.Values)
|
.Concat(this.PreventStrength.Values)
|
||||||
.Concat(this.DoesMove.Values);
|
.Concat(this.DoesMove.Values);
|
||||||
|
|
||||||
public MovementDecisions(List<Order> orders)
|
public MovementDecisions(World world, List<Order> orders)
|
||||||
{
|
{
|
||||||
this.IsDislodged = new();
|
this.IsDislodged = new();
|
||||||
this.HasPath = new();
|
this.HasPath = new();
|
||||||
|
@ -35,6 +35,50 @@ public class MovementDecisions
|
||||||
this.PreventStrength = new();
|
this.PreventStrength = new();
|
||||||
this.DoesMove = new();
|
this.DoesMove = new();
|
||||||
|
|
||||||
|
// Record which seasons are referenced by the order set.
|
||||||
|
HashSet<Season> orderedSeasons = new();
|
||||||
|
foreach (UnitOrder order in orders.Cast<UnitOrder>())
|
||||||
|
{
|
||||||
|
_ = orderedSeasons.Add(order.Unit.Season);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expand the order list to include any other seasons that are potentially affected.
|
||||||
|
// In the event that those seasons don't end up affected (all moves to it fail, all
|
||||||
|
// supports to it are cut), it is still safe to re-adjudicate everything because
|
||||||
|
// adjudication is deterministic and doesn't produce side effects.
|
||||||
|
HashSet<Season> affectedSeasons = new();
|
||||||
|
foreach (Order order in orders)
|
||||||
|
{
|
||||||
|
switch (order)
|
||||||
|
{
|
||||||
|
case MoveOrder move:
|
||||||
|
if (!orderedSeasons.Contains(move.Season))
|
||||||
|
{
|
||||||
|
affectedSeasons.Add(move.Season);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SupportHoldOrder supportHold:
|
||||||
|
if (!orderedSeasons.Contains(supportHold.Target.Season))
|
||||||
|
{
|
||||||
|
affectedSeasons.Add(supportHold.Target.Season);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SupportMoveOrder supportMove:
|
||||||
|
if (!orderedSeasons.Contains(supportMove.Target.Season))
|
||||||
|
{
|
||||||
|
affectedSeasons.Add(supportMove.Target.Season);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach (Season season in affectedSeasons)
|
||||||
|
{
|
||||||
|
orders.AddRange(world.GivenOrders[season]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the relevant decisions for each order.
|
||||||
foreach (UnitOrder order in orders.Cast<UnitOrder>())
|
foreach (UnitOrder order in orders.Cast<UnitOrder>())
|
||||||
{
|
{
|
||||||
// Create a dislodge decision for this unit.
|
// Create a dislodge decision for this unit.
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
using MultiversalDiplomacy.Adjudicate.Decision;
|
using MultiversalDiplomacy.Adjudicate.Decision;
|
||||||
using MultiversalDiplomacy.Model;
|
using MultiversalDiplomacy.Model;
|
||||||
using MultiversalDiplomacy.Orders;
|
using MultiversalDiplomacy.Orders;
|
||||||
|
@ -266,23 +268,21 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||||
public List<AdjudicationDecision> AdjudicateOrders(World world, List<Order> orders)
|
public List<AdjudicationDecision> AdjudicateOrders(World world, List<Order> orders)
|
||||||
{
|
{
|
||||||
// Define all adjudication decisions to be made.
|
// Define all adjudication decisions to be made.
|
||||||
MovementDecisions decisions = new(orders);
|
MovementDecisions decisions = new(world, orders);
|
||||||
|
|
||||||
List<AdjudicationDecision> unresolvedDecisions = decisions.Values.ToList();
|
|
||||||
|
|
||||||
// Adjudicate all decisions.
|
// Adjudicate all decisions.
|
||||||
bool progress = false;
|
bool progress = false;
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
progress = false;
|
progress = false;
|
||||||
foreach (AdjudicationDecision decision in unresolvedDecisions.ToList())
|
foreach (AdjudicationDecision decision in decisions.Values)
|
||||||
{
|
{
|
||||||
|
// This will noop without progress if the decision is already resolved
|
||||||
progress |= ResolveDecision(decision, world, decisions);
|
progress |= ResolveDecision(decision, world, decisions);
|
||||||
if (decision.Resolved) unresolvedDecisions.Remove(decision);
|
|
||||||
}
|
}
|
||||||
} while (progress);
|
} while (progress);
|
||||||
|
|
||||||
if (unresolvedDecisions.Any())
|
if (decisions.Values.Any(d => !d.Resolved))
|
||||||
{
|
{
|
||||||
throw new ApplicationException("Some orders not resolved!");
|
throw new ApplicationException("Some orders not resolved!");
|
||||||
}
|
}
|
||||||
|
@ -355,12 +355,23 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sort the new orders by season to save for posterity in case of attacks from the future.
|
||||||
|
IEnumerable<KeyValuePair<Season, ReadOnlyCollection<Order>>> newOrders = decisions
|
||||||
|
.OfType<IsDislodged>()
|
||||||
|
.GroupBy(
|
||||||
|
keySelector: d => d.Order.Unit.Season,
|
||||||
|
elementSelector: d => d.Order as Order)
|
||||||
|
.Where(group => !world.GivenOrders.ContainsKey(group.Key))
|
||||||
|
.Select(group => new KeyValuePair<Season, ReadOnlyCollection<Order>>(
|
||||||
|
group.Key, new(group.ToList())));
|
||||||
|
|
||||||
// TODO provide more structured information about order outcomes
|
// TODO provide more structured information about order outcomes
|
||||||
|
|
||||||
World updated = world.Update(
|
World updated = world.Update(
|
||||||
seasons: world.Seasons.Concat(createdFutures.Values),
|
seasons: world.Seasons.Concat(createdFutures.Values),
|
||||||
units: world.Units.Concat(createdUnits),
|
units: world.Units.Concat(createdUnits),
|
||||||
retreats: retreats);
|
retreats: retreats,
|
||||||
|
orders: world.GivenOrders.Concat(newOrders));
|
||||||
|
|
||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
|
using MultiversalDiplomacy.Orders;
|
||||||
|
|
||||||
namespace MultiversalDiplomacy.Model;
|
namespace MultiversalDiplomacy.Model;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -37,6 +39,11 @@ public class World
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ReadOnlyCollection<RetreatingUnit> RetreatingUnits { get; }
|
public ReadOnlyCollection<RetreatingUnit> RetreatingUnits { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Orders given to units in each season.
|
||||||
|
/// </summary>
|
||||||
|
public ReadOnlyDictionary<Season, ReadOnlyCollection<Order>> GivenOrders { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Immutable game options.
|
/// Immutable game options.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -52,6 +59,7 @@ public class World
|
||||||
Season rootSeason,
|
Season rootSeason,
|
||||||
ReadOnlyCollection<Unit> units,
|
ReadOnlyCollection<Unit> units,
|
||||||
ReadOnlyCollection<RetreatingUnit> retreatingUnits,
|
ReadOnlyCollection<RetreatingUnit> retreatingUnits,
|
||||||
|
ReadOnlyDictionary<Season, ReadOnlyCollection<Order>> givenOrders,
|
||||||
Options options)
|
Options options)
|
||||||
{
|
{
|
||||||
this.Provinces = provinces;
|
this.Provinces = provinces;
|
||||||
|
@ -60,6 +68,7 @@ public class World
|
||||||
this.RootSeason = rootSeason;
|
this.RootSeason = rootSeason;
|
||||||
this.Units = units;
|
this.Units = units;
|
||||||
this.RetreatingUnits = retreatingUnits;
|
this.RetreatingUnits = retreatingUnits;
|
||||||
|
this.GivenOrders = givenOrders;
|
||||||
this.Options = options;
|
this.Options = options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,6 +82,7 @@ public class World
|
||||||
ReadOnlyCollection<Season>? seasons = null,
|
ReadOnlyCollection<Season>? seasons = null,
|
||||||
ReadOnlyCollection<Unit>? units = null,
|
ReadOnlyCollection<Unit>? units = null,
|
||||||
ReadOnlyCollection<RetreatingUnit>? retreatingUnits = null,
|
ReadOnlyCollection<RetreatingUnit>? retreatingUnits = null,
|
||||||
|
ReadOnlyDictionary<Season, ReadOnlyCollection<Order>>? givenOrders = null,
|
||||||
Options? options = null)
|
Options? options = null)
|
||||||
: this(
|
: this(
|
||||||
provinces ?? previous.Provinces,
|
provinces ?? previous.Provinces,
|
||||||
|
@ -81,6 +91,7 @@ public class World
|
||||||
previous.RootSeason, // Can't change the root season
|
previous.RootSeason, // Can't change the root season
|
||||||
units ?? previous.Units,
|
units ?? previous.Units,
|
||||||
retreatingUnits ?? previous.RetreatingUnits,
|
retreatingUnits ?? previous.RetreatingUnits,
|
||||||
|
givenOrders ?? previous.GivenOrders,
|
||||||
options ?? previous.Options)
|
options ?? previous.Options)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
@ -98,6 +109,7 @@ public class World
|
||||||
root,
|
root,
|
||||||
new(new List<Unit>()),
|
new(new List<Unit>()),
|
||||||
new(new List<RetreatingUnit>()),
|
new(new List<RetreatingUnit>()),
|
||||||
|
new(new Dictionary<Season, ReadOnlyCollection<Order>>()),
|
||||||
new Options());
|
new Options());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,12 +122,22 @@ public class World
|
||||||
public World Update(
|
public World Update(
|
||||||
IEnumerable<Season>? seasons = null,
|
IEnumerable<Season>? seasons = null,
|
||||||
IEnumerable<Unit>? units = null,
|
IEnumerable<Unit>? units = null,
|
||||||
IEnumerable<RetreatingUnit>? retreats = null)
|
IEnumerable<RetreatingUnit>? retreats = null,
|
||||||
|
IEnumerable<KeyValuePair<Season, ReadOnlyCollection<Order>>>? orders = null)
|
||||||
=> new World(
|
=> new World(
|
||||||
previous: this,
|
previous: this,
|
||||||
seasons: seasons == null ? this.Seasons : new(seasons.ToList()),
|
seasons: seasons == null
|
||||||
units: units == null ? this.Units : new(units.ToList()),
|
? this.Seasons
|
||||||
retreatingUnits: retreats == null ? this.RetreatingUnits : new(retreats.ToList()));
|
: new(seasons.ToList()),
|
||||||
|
units: units == null
|
||||||
|
? this.Units
|
||||||
|
: new(units.ToList()),
|
||||||
|
retreatingUnits: retreats == null
|
||||||
|
? this.RetreatingUnits
|
||||||
|
: new(retreats.ToList()),
|
||||||
|
givenOrders: orders == null
|
||||||
|
? this.GivenOrders
|
||||||
|
: new(orders.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new world with new units created from unit specs. Units specs are in the format
|
/// Create a new world with new units created from unit specs. Units specs are in the format
|
||||||
|
|
|
@ -76,7 +76,13 @@ public class TestCaseBuilder
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Give the unit a move order.
|
/// Give the unit a move order.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IOrderDefinedContext<MoveOrder> MovesTo(string provinceName, string? coast = null);
|
/// <param name="season">
|
||||||
|
/// The destination season. If not specified, defaults to the same season as the unit.
|
||||||
|
/// </param>
|
||||||
|
public IOrderDefinedContext<MoveOrder> MovesTo(
|
||||||
|
string provinceName,
|
||||||
|
Season? season = null,
|
||||||
|
string? coast = null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Give the unit a convoy order.
|
/// Give the unit a convoy order.
|
||||||
|
@ -372,15 +378,19 @@ public class TestCaseBuilder
|
||||||
return new OrderDefinedContext<HoldOrder>(this, order);
|
return new OrderDefinedContext<HoldOrder>(this, order);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IOrderDefinedContext<MoveOrder> MovesTo(string provinceName, string? coast = null)
|
public IOrderDefinedContext<MoveOrder> MovesTo(
|
||||||
|
string provinceName,
|
||||||
|
Season? season = null,
|
||||||
|
string? coast = null)
|
||||||
{
|
{
|
||||||
Location destination = this.Unit.Type == UnitType.Army
|
Location destination = this.Unit.Type == UnitType.Army
|
||||||
? this.Builder.World.GetLand(provinceName)
|
? this.Builder.World.GetLand(provinceName)
|
||||||
: this.Builder.World.GetWater(provinceName, coast);
|
: this.Builder.World.GetWater(provinceName, coast);
|
||||||
|
Season destSeason = season ?? this.SeasonContext.Season;
|
||||||
MoveOrder moveOrder = new MoveOrder(
|
MoveOrder moveOrder = new MoveOrder(
|
||||||
this.PowerContext.Power,
|
this.PowerContext.Power,
|
||||||
this.Unit,
|
this.Unit,
|
||||||
this.SeasonContext.Season,
|
destSeason,
|
||||||
destination);
|
destination);
|
||||||
this.Builder.OrderList.Add(moveOrder);
|
this.Builder.OrderList.Add(moveOrder);
|
||||||
return new OrderDefinedContext<MoveOrder>(this, moveOrder);
|
return new OrderDefinedContext<MoveOrder>(this, moveOrder);
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
using MultiversalDiplomacy.Adjudicate;
|
||||||
|
using MultiversalDiplomacy.Adjudicate.Decision;
|
||||||
|
using MultiversalDiplomacy.Model;
|
||||||
|
|
||||||
|
using NUnit.Framework;
|
||||||
|
|
||||||
|
namespace MultiversalDiplomacyTests;
|
||||||
|
|
||||||
|
public class TimeTravelTest
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void MoveIntoOwnPast()
|
||||||
|
{
|
||||||
|
TestCaseBuilder setup = new(World.WithStandardMap());
|
||||||
|
|
||||||
|
// Hold once so the timeline has a past.
|
||||||
|
setup[(0, 0)]
|
||||||
|
.GetReference(out Season s0)
|
||||||
|
["Germany"]
|
||||||
|
.Army("Mun").Holds().GetReference(out var mun0);
|
||||||
|
|
||||||
|
setup.ValidateOrders(MovementPhaseAdjudicator.Instance);
|
||||||
|
setup.AdjudicateOrders(MovementPhaseAdjudicator.Instance);
|
||||||
|
setup = new(setup.UpdateWorld(MovementPhaseAdjudicator.Instance));
|
||||||
|
|
||||||
|
// Move into the past of the same timeline.
|
||||||
|
setup[(1, 0)]
|
||||||
|
.GetReference(out Season s1)
|
||||||
|
["Germany"]
|
||||||
|
.Army("Mun").MovesTo("Tyr", season: s0).GetReference(out var mun1);
|
||||||
|
|
||||||
|
setup.ValidateOrders(MovementPhaseAdjudicator.Instance);
|
||||||
|
Assert.That(mun1, Is.Valid);
|
||||||
|
setup.AdjudicateOrders(MovementPhaseAdjudicator.Instance);
|
||||||
|
Assert.That(mun1, Is.Victorious);
|
||||||
|
|
||||||
|
World world = setup.UpdateWorld(MovementPhaseAdjudicator.Instance);
|
||||||
|
|
||||||
|
// Confirm that there are now three seasons: the root, a future off the root, and a fork.
|
||||||
|
Assert.That(world.Seasons.Count, Is.EqualTo(3));
|
||||||
|
Assert.That(world.Seasons.Where(s => s.Timeline != s0.Timeline).Count(), Is.EqualTo(1));
|
||||||
|
Season fork = world.Seasons.Where(s => s.Timeline != s0.Timeline).Single();
|
||||||
|
Assert.That(s0.Futures, Contains.Item(s1));
|
||||||
|
Assert.That(s0.Futures, Contains.Item(fork));
|
||||||
|
Assert.That(fork.Past, Is.EqualTo(s0));
|
||||||
|
|
||||||
|
// Confirm that there is a unit in Tyr 1:1 originating from Mun 1:0
|
||||||
|
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 Mun 1:1 originating from Mun 0:0
|
||||||
|
Unit aMun1 = world.GetUnitAt("Mun", fork.Coord);
|
||||||
|
Assert.That(aMun1.Past, Is.EqualTo(originalUnit));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue