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.DoesMove.Values);
|
||||
|
||||
public MovementDecisions(List<Order> orders)
|
||||
public MovementDecisions(World world, List<Order> orders)
|
||||
{
|
||||
this.IsDislodged = new();
|
||||
this.HasPath = new();
|
||||
|
@ -35,6 +35,50 @@ public class MovementDecisions
|
|||
this.PreventStrength = 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>())
|
||||
{
|
||||
// Create a dislodge decision for this unit.
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
using System.Collections.ObjectModel;
|
||||
|
||||
using MultiversalDiplomacy.Adjudicate.Decision;
|
||||
using MultiversalDiplomacy.Model;
|
||||
using MultiversalDiplomacy.Orders;
|
||||
|
@ -266,23 +268,21 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
public List<AdjudicationDecision> AdjudicateOrders(World world, List<Order> orders)
|
||||
{
|
||||
// Define all adjudication decisions to be made.
|
||||
MovementDecisions decisions = new(orders);
|
||||
|
||||
List<AdjudicationDecision> unresolvedDecisions = decisions.Values.ToList();
|
||||
MovementDecisions decisions = new(world, orders);
|
||||
|
||||
// Adjudicate all decisions.
|
||||
bool progress = false;
|
||||
do
|
||||
{
|
||||
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);
|
||||
if (decision.Resolved) unresolvedDecisions.Remove(decision);
|
||||
}
|
||||
} while (progress);
|
||||
|
||||
if (unresolvedDecisions.Any())
|
||||
if (decisions.Values.Any(d => !d.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
|
||||
|
||||
World updated = world.Update(
|
||||
seasons: world.Seasons.Concat(createdFutures.Values),
|
||||
units: world.Units.Concat(createdUnits),
|
||||
retreats: retreats);
|
||||
retreats: retreats,
|
||||
orders: world.GivenOrders.Concat(newOrders));
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
using System.Collections.ObjectModel;
|
||||
|
||||
using MultiversalDiplomacy.Orders;
|
||||
|
||||
namespace MultiversalDiplomacy.Model;
|
||||
|
||||
/// <summary>
|
||||
|
@ -37,6 +39,11 @@ public class World
|
|||
/// </summary>
|
||||
public ReadOnlyCollection<RetreatingUnit> RetreatingUnits { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Orders given to units in each season.
|
||||
/// </summary>
|
||||
public ReadOnlyDictionary<Season, ReadOnlyCollection<Order>> GivenOrders { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Immutable game options.
|
||||
/// </summary>
|
||||
|
@ -52,6 +59,7 @@ public class World
|
|||
Season rootSeason,
|
||||
ReadOnlyCollection<Unit> units,
|
||||
ReadOnlyCollection<RetreatingUnit> retreatingUnits,
|
||||
ReadOnlyDictionary<Season, ReadOnlyCollection<Order>> givenOrders,
|
||||
Options options)
|
||||
{
|
||||
this.Provinces = provinces;
|
||||
|
@ -60,6 +68,7 @@ public class World
|
|||
this.RootSeason = rootSeason;
|
||||
this.Units = units;
|
||||
this.RetreatingUnits = retreatingUnits;
|
||||
this.GivenOrders = givenOrders;
|
||||
this.Options = options;
|
||||
}
|
||||
|
||||
|
@ -73,6 +82,7 @@ public class World
|
|||
ReadOnlyCollection<Season>? seasons = null,
|
||||
ReadOnlyCollection<Unit>? units = null,
|
||||
ReadOnlyCollection<RetreatingUnit>? retreatingUnits = null,
|
||||
ReadOnlyDictionary<Season, ReadOnlyCollection<Order>>? givenOrders = null,
|
||||
Options? options = null)
|
||||
: this(
|
||||
provinces ?? previous.Provinces,
|
||||
|
@ -81,6 +91,7 @@ public class World
|
|||
previous.RootSeason, // Can't change the root season
|
||||
units ?? previous.Units,
|
||||
retreatingUnits ?? previous.RetreatingUnits,
|
||||
givenOrders ?? previous.GivenOrders,
|
||||
options ?? previous.Options)
|
||||
{
|
||||
}
|
||||
|
@ -98,6 +109,7 @@ public class World
|
|||
root,
|
||||
new(new List<Unit>()),
|
||||
new(new List<RetreatingUnit>()),
|
||||
new(new Dictionary<Season, ReadOnlyCollection<Order>>()),
|
||||
new Options());
|
||||
}
|
||||
|
||||
|
@ -110,12 +122,22 @@ public class World
|
|||
public World Update(
|
||||
IEnumerable<Season>? seasons = null,
|
||||
IEnumerable<Unit>? units = null,
|
||||
IEnumerable<RetreatingUnit>? retreats = null)
|
||||
IEnumerable<RetreatingUnit>? retreats = null,
|
||||
IEnumerable<KeyValuePair<Season, ReadOnlyCollection<Order>>>? orders = null)
|
||||
=> new World(
|
||||
previous: this,
|
||||
seasons: seasons == null ? this.Seasons : new(seasons.ToList()),
|
||||
units: units == null ? this.Units : new(units.ToList()),
|
||||
retreatingUnits: retreats == null ? this.RetreatingUnits : new(retreats.ToList()));
|
||||
seasons: seasons == null
|
||||
? this.Seasons
|
||||
: 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>
|
||||
/// 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>
|
||||
/// Give the unit a move order.
|
||||
/// </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>
|
||||
/// Give the unit a convoy order.
|
||||
|
@ -372,15 +378,19 @@ public class TestCaseBuilder
|
|||
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
|
||||
? this.Builder.World.GetLand(provinceName)
|
||||
: this.Builder.World.GetWater(provinceName, coast);
|
||||
Season destSeason = season ?? this.SeasonContext.Season;
|
||||
MoveOrder moveOrder = new MoveOrder(
|
||||
this.PowerContext.Power,
|
||||
this.Unit,
|
||||
this.SeasonContext.Season,
|
||||
destSeason,
|
||||
destination);
|
||||
this.Builder.OrderList.Add(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