Implement AdvanceTimeline resolution

This commit is contained in:
Jaculabilis 2022-11-08 16:25:47 -08:00
parent 7471a035f0
commit 39c3aabe45
7 changed files with 290 additions and 109 deletions

View File

@ -0,0 +1,21 @@
namespace System.Collections.Generic;
public static class AdjudicationDictionaryExtensions
{
/// <summary>
/// Create and add a value to a dictionary only if the key is not already present.
/// </summary>
/// <param name="dictionary">The dictionary to check for the key.</param>
/// <param name="key">The key to check and use if it isn't already present.</param>
/// <param name="valueFunc">A function that returns the value to insert if the key is not present.</param>
public static void Ensure<TKey, TValue>(
this IDictionary<TKey, TValue> dictionary,
TKey key,
Func<TValue> valueFunc)
{
if (!dictionary.ContainsKey(key))
{
dictionary[key] = valueFunc();
}
}
}

View File

@ -8,6 +8,9 @@ public class AdvanceTimeline : BinaryAdjudicationDecision
public Season Season { get; }
public List<UnitOrder> Orders { get; }
public override string ToString()
=> $"AdvanceTimeline({Season})";
public AdvanceTimeline(Season season, IEnumerable<UnitOrder> orders)
{
this.Season = season;

View File

@ -16,139 +16,139 @@ public class MovementDecisions
public Dictionary<Season, AdvanceTimeline> AdvanceTimeline { get; }
public IEnumerable<AdjudicationDecision> Values =>
this.IsDislodged.Values.Cast<AdjudicationDecision>()
.Concat(this.HasPath.Values)
.Concat(this.GivesSupport.Values)
.Concat(this.HoldStrength.Values)
.Concat(this.AttackStrength.Values)
.Concat(this.DefendStrength.Values)
.Concat(this.PreventStrength.Values)
.Concat(this.DoesMove.Values)
.Concat(this.AdvanceTimeline.Values);
IsDislodged.Values.Cast<AdjudicationDecision>()
.Concat(HasPath.Values)
.Concat(GivesSupport.Values)
.Concat(HoldStrength.Values)
.Concat(AttackStrength.Values)
.Concat(DefendStrength.Values)
.Concat(PreventStrength.Values)
.Concat(DoesMove.Values)
.Concat(AdvanceTimeline.Values);
public MovementDecisions(World world, List<Order> orders)
{
this.IsDislodged = new();
this.HasPath = new();
this.GivesSupport = new();
this.HoldStrength = new();
this.AttackStrength = new();
this.DefendStrength = new();
this.PreventStrength = new();
this.DoesMove = new();
this.AdvanceTimeline = new();
IsDislodged = new();
HasPath = new();
GivesSupport = new();
HoldStrength = new();
AttackStrength = new();
DefendStrength = new();
PreventStrength = new();
DoesMove = new();
AdvanceTimeline = new();
// Record which seasons are referenced by the order set.
HashSet<Season> orderedSeasons = new();
foreach (UnitOrder order in orders.Cast<UnitOrder>())
// The orders argument only contains the submitted orders. The adjudicator will need to adjudicate not only
// presently submitted orders, but also previously submitted orders if present orders affect the past. This
// necessitates doing some lookups to find all affected seasons.
// At a minimum, the submitted orders imply a dislodge decision for each unit, which affects every season those
// orders were given to.
var submittedOrdersBySeason = orders.Cast<UnitOrder>().ToLookup(order => order.Unit.Season);
foreach (var group in submittedOrdersBySeason)
{
_ = orderedSeasons.Add(order.Unit.Season);
AdvanceTimeline[group.Key] = new(group.Key, group);
}
// 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();
// Create timeline decisions for each season potentially affected by the submitted orders.
// Since adjudication is deterministic and pure, if none of the affecting orders succeed,
// the adjudication decisions for the extra seasons will resolve the same way and the
// advance decision for the timeline will resolve false.
foreach (Order order in orders)
{
switch (order)
{
case MoveOrder move:
if (!orderedSeasons.Contains(move.Season))
{
affectedSeasons.Add(move.Season);
}
AdvanceTimeline.Ensure(
move.Season,
() => new(move.Season, world.OrderHistory[move.Season].Orders));
AdvanceTimeline[move.Season].Orders.Add(move);
break;
case SupportHoldOrder supportHold:
if (!orderedSeasons.Contains(supportHold.Target.Season))
{
affectedSeasons.Add(supportHold.Target.Season);
}
AdvanceTimeline.Ensure(
supportHold.Target.Season,
() => new(supportHold.Target.Season, world.OrderHistory[supportHold.Target.Season].Orders));
AdvanceTimeline[supportHold.Target.Season].Orders.Add(supportHold);
break;
case SupportMoveOrder supportMove:
if (!orderedSeasons.Contains(supportMove.Target.Season))
{
affectedSeasons.Add(supportMove.Target.Season);
}
AdvanceTimeline.Ensure(
supportMove.Target.Season,
() => new(supportMove.Target.Season, world.OrderHistory[supportMove.Target.Season].Orders));
AdvanceTimeline[supportMove.Target.Season].Orders.Add(supportMove);
AdvanceTimeline.Ensure(
supportMove.Season,
() => new(supportMove.Season, world.OrderHistory[supportMove.Season].Orders));
AdvanceTimeline[supportMove.Season].Orders.Add(supportMove);
break;
}
}
foreach (Season season in affectedSeasons)
// Get the orders in the affected timelines.
List<UnitOrder> relevantOrders = AdvanceTimeline.Values.SelectMany(at => at.Orders).ToList();
// Create a hold strength decision with an associated order for every province with a unit.
foreach (UnitOrder order in relevantOrders)
{
orders.AddRange(world.GivenOrders[season]);
HoldStrength[order.Unit.Point] = new(order.Unit.Point, order);
}
// Create the relevant decisions for each order.
foreach (UnitOrder order in orders.Cast<UnitOrder>())
// 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 = orders
List<MoveOrder> incoming = relevantOrders
.OfType<MoveOrder>()
.Where(order.IsIncoming)
.ToList();
this.IsDislodged[order.Unit] = new(order, incoming);
// Ensure a hold strength decision exists. Overwrite any previous once, since it may
// have been created without an order by a previous move or support.
this.HoldStrength[order.Unit.Point] = new(order.Unit.Point, order);
IsDislodged[order.Unit] = new(order, incoming);
if (order is MoveOrder move)
{
// Find supports corresponding to this move.
List<SupportMoveOrder> supports = orders
List<SupportMoveOrder> supports = relevantOrders
.OfType<SupportMoveOrder>()
.Where(support => support.IsSupportFor(move))
.ToList();
// Determine if this move is a head-to-head battle.
MoveOrder? opposingMove = orders
MoveOrder? opposingMove = relevantOrders
.OfType<MoveOrder>()
.FirstOrDefault(other => other != null && other.IsOpposing(move), null);
.FirstOrDefault(other => other!.IsOpposing(move), null);
// Find competing moves.
List<MoveOrder> competing = orders
List<MoveOrder> competing = relevantOrders
.OfType<MoveOrder>()
.Where(move.IsCompeting)
.ToList();
// Create the move-related decisions.
this.HasPath[move] = new(move);
this.AttackStrength[move] = new(move, supports, opposingMove);
this.DefendStrength[move] = new(move, supports);
this.PreventStrength[move] = new(move, supports, opposingMove);
this.DoesMove[move] = new(move, opposingMove, competing);
HasPath[move] = new(move);
AttackStrength[move] = new(move, supports, opposingMove);
DefendStrength[move] = new(move, supports);
PreventStrength[move] = new(move, supports, opposingMove);
DoesMove[move] = new(move, opposingMove, competing);
// Ensure a hold strength decision exists for the destination.
if (!this.HoldStrength.ContainsKey(move.Point))
{
this.HoldStrength[move.Point] = new(move.Point);
}
HoldStrength.Ensure(move.Point, () => new(move.Point));
}
else if (order is SupportOrder support)
{
// Create the support decision.
this.GivesSupport[support] = new(support, incoming);
GivesSupport[support] = new(support, incoming);
// Ensure a hold strength decision exists for the target's province.
if (!this.HoldStrength.ContainsKey(support.Target.Point))
{
this.HoldStrength[support.Target.Point] = new(support.Target.Point);
}
HoldStrength.Ensure(support.Target.Point, () => new(support.Target.Point));
if (support is SupportHoldOrder supportHold)
{
this.HoldStrength[support.Target.Point].Supports.Add(supportHold);
HoldStrength[support.Target.Point].Supports.Add(supportHold);
}
else if (support is SupportMoveOrder supportMove)
{
// Ensure a hold strength decision exists for the target's destination.
if (!this.HoldStrength.ContainsKey(supportMove.Point))
{
this.HoldStrength[supportMove.Point] = new(supportMove.Point);
}
HoldStrength.Ensure(supportMove.Point, () => new(supportMove.Point));
}
}
}

View File

@ -291,7 +291,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
// This will noop without progress if the decision is already resolved
progress |= ResolveDecision(decision, world, decisions, depth: 2);
}
} while (progress);
} while (progress && decisions.Values.Any(decision => !decision.Resolved));
if (decisions.Values.Any(d => !d.Resolved))
{
@ -304,9 +304,13 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
public World UpdateWorld(World world, List<AdjudicationDecision> decisions)
{
logger.Log(0, "Updating world");
Dictionary<MoveOrder, DoesMove> moves = decisions
.OfType<DoesMove>()
.ToDictionary(dm => dm.Order);
Dictionary<Unit, IsDislodged> dislodges = decisions
.OfType<IsDislodged>()
.ToDictionary(dm => dm.Order.Unit);
// 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.
@ -314,25 +318,37 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
List<Unit> createdUnits = new();
List<RetreatingUnit> retreats = new();
// Populate createdFutures with the timeline fork decisions
logger.Log(1, "Processing AdvanceTimeline decisions");
foreach (AdvanceTimeline advanceTimeline in decisions.OfType<AdvanceTimeline>())
{
logger.Log(2, "{0} = {1}", advanceTimeline, advanceTimeline.Outcome?.ToString() ?? "?");
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();
}
}
// Successful move orders result in the unit moving to the destination and creating a new
// future, while unsuccessful move orders are processed the same way as non-move orders.
logger.Log(1, "Processing successful moves");
foreach (DoesMove doesMove in moves.Values)
{
if (doesMove.Outcome == true)
logger.Log(2, "{0} = {1}", doesMove, doesMove.Outcome?.ToString() ?? "?");
Season moveSeason = doesMove.Order.Season;
if (doesMove.Outcome == true && createdFutures.ContainsKey(moveSeason))
{
if (!createdFutures.TryGetValue(doesMove.Order.Season, out Season? future))
{
// A timeline that doesn't have a future yet simply continues. Otherwise, it forks.
future = !doesMove.Order.Season.Futures.Any()
? doesMove.Order.Season.MakeNext()
: doesMove.Order.Season.MakeFork();
createdFutures[doesMove.Order.Season] = future;
}
createdUnits.Add(doesMove.Order.Unit.Next(doesMove.Order.Location, future));
Unit next = doesMove.Order.Unit.Next(doesMove.Order.Location, createdFutures[moveSeason]);
logger.Log(3, "Advancing unit to {0}", next);
createdUnits.Add(next);
}
}
// Process unsuccessful moves, all holds, and all supports.
logger.Log(1, "Processing stationary orders");
foreach (IsDislodged isDislodged in decisions.OfType<IsDislodged>())
{
UnitOrder order = isDislodged.Order;
@ -343,22 +359,26 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
continue;
}
if (!createdFutures.TryGetValue(order.Unit.Season, out Season? future))
logger.Log(2, "{0} = {1}", isDislodged, isDislodged.Outcome?.ToString() ?? "?");
if (!createdFutures.ContainsKey(order.Unit.Season))
{
// Any unit given an order is, by definition, at the front of a timeline.
future = order.Unit.Season.MakeNext();
createdFutures[order.Unit.Season] = future;
logger.Log(3, "Skipping order because no future was created");
continue;
}
// For each stationary unit that wasn't dislodged, continue it into the future.
Season future = createdFutures[order.Unit.Season];
if (isDislodged.Outcome == false)
{
createdUnits.Add(order.Unit.Next(order.Unit.Location, future));
// Non-dislodged units continue into the future.
Unit next = order.Unit.Next(order.Unit.Location, future);
logger.Log(3, "Advancing unit to {0}", next);
createdUnits.Add(next);
}
else
{
// 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
.Select(loc => (future, loc))
.ToList();
@ -367,15 +387,35 @@ 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())));
// Record the adjudication results to the season's order history
Dictionary<Season, OrderHistory> newHistory = new();
foreach (UnitOrder unitOrder in decisions.OfType<IsDislodged>().Select(d => d.Order))
{
newHistory.Ensure(unitOrder.Unit.Season, () => new());
OrderHistory history = newHistory[unitOrder.Unit.Season];
// TODO does this add every order to every season??
history.Orders.Add(unitOrder);
history.IsDislodgedOutcomes[unitOrder.Unit] = dislodges[unitOrder.Unit].Outcome == true;
if (unitOrder is MoveOrder moveOrder)
{
history.DoesMoveOutcomes[moveOrder] = moves[moveOrder].Outcome == true;
}
}
// Log the new order history
foreach ((Season season, OrderHistory history) in newHistory)
{
string verb = world.OrderHistory.ContainsKey(season) ? "Updating" : "Adding";
logger.Log(1, "{0} history for {1}", verb, season);
foreach (UnitOrder order in history.Orders)
{
logger.Log(2, "{0}", order);
}
}
IEnumerable<KeyValuePair<Season, OrderHistory>> updatedHistory = world.OrderHistory
.Where(kvp => !newHistory.ContainsKey(kvp.Key))
.Concat(newHistory);
// TODO provide more structured information about order outcomes
@ -383,7 +423,9 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
seasons: world.Seasons.Concat(createdFutures.Values),
units: world.Units.Concat(createdUnits),
retreats: retreats,
orders: world.GivenOrders.Concat(newOrders));
orders: updatedHistory);
logger.Log(0, "Completed update");
return updated;
}
@ -423,6 +465,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
logger.Log(depth, "ResolveDecision({0})", decision);
return decision.Resolved ? false : decision switch
{
AdvanceTimeline d => ResolveAdvanceTimeline(d, world, decisions, depth + 1),
IsDislodged d => ResolveIsUnitDislodged(d, world, decisions, depth + 1),
HasPath d => ResolveDoesMoveHavePath(d, world, decisions, depth + 1),
GivesSupport d => ResolveIsSupportGiven(d, world, decisions, depth + 1),
@ -435,6 +478,92 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
};
}
private bool ResolveAdvanceTimeline(
AdvanceTimeline decision,
World world,
MovementDecisions decisions,
int depth)
{
logger.Log(depth, "AdvanceTimeline({0})", decision.Season);
bool progress = false;
// A season at the head of a timeline always advances.
if (!decision.Season.Futures.Any())
{
progress |= LoggedUpdate(decision, true, depth, "A timeline head always advances");
return progress;
}
// The season target of a successful move always advances.
IEnumerable<MoveOrder> incomingMoveOrders = decision.Orders
.OfType<MoveOrder>()
.Where(order => order.Season == decision.Season);
logger.Log(depth, "decision.Orders = {0}", string.Join(", ", decision.Orders));
logger.Log(depth, "incomingMoveOrders = {0}", string.Join(", ", incomingMoveOrders));
foreach (MoveOrder moveOrder in incomingMoveOrders)
{
DoesMove doesMove = decisions.DoesMove[moveOrder];
progress |= ResolveDecision(doesMove, world, decisions, depth + 1);
if (doesMove.Outcome == true)
{
progress |= LoggedUpdate(decision, true, depth, $"Advanced by {doesMove.Order}");
return progress;
}
}
// TODO needs to also resolve true if anyone moves into this timeline
// Seasons not at the head of a timeline advance if the outcome of a battle is changed.
// The outcome of a battle is changed if:
// 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];
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);
bool previous = history.IsDislodgedOutcomes[order.Unit];
if (dislodged.Resolved && dislodged.Outcome != previous)
{
progress |= LoggedUpdate(
decision,
true,
depth,
$"History changed for {order.Unit}: dislodge {previous} => {dislodged.Outcome}");
return progress;
}
anyUnresolved |= !dislodged.Resolved;
if (order is MoveOrder moveOrder)
{
DoesMove moves = decisions.DoesMove[moveOrder];
progress |= ResolveDecision(moves, world, decisions, depth + 1);
bool previousMove = history.DoesMoveOutcomes[moveOrder];
if (moves.Resolved && moves.Outcome != previousMove)
{
progress |= LoggedUpdate(
decision,
true,
depth,
$"History changed for {order}: moves {previousMove} => {moves.Outcome}");
return progress;
}
anyUnresolved |= !moves.Resolved;
}
}
if (!anyUnresolved)
{
progress |= LoggedUpdate(decision, false, depth, "No resolved changes to history");
}
return progress;
}
private bool ResolveIsUnitDislodged(
IsDislodged decision,
World world,

View File

@ -0,0 +1,28 @@
using System.Collections.ObjectModel;
using MultiversalDiplomacy.Orders;
namespace MultiversalDiplomacy.Model;
public class OrderHistory
{
public List<UnitOrder> Orders;
public Dictionary<Unit, bool> IsDislodgedOutcomes;
public Dictionary<MoveOrder, bool> DoesMoveOutcomes;
public OrderHistory()
: this(new(), new(), new())
{}
public OrderHistory(
List<UnitOrder> orders,
Dictionary<Unit, bool> isDislodgedOutcomes,
Dictionary<MoveOrder, bool> doesMoveOutcomes)
{
this.Orders = new(orders);
this.IsDislodgedOutcomes = new(isDislodgedOutcomes);
this.DoesMoveOutcomes = new(doesMoveOutcomes);
}
}

View File

@ -71,7 +71,7 @@ public class Season
public override string ToString()
{
return $"{this.Turn}:{this.Timeline}";
return $"{this.Timeline}@{this.Turn}";
}
/// <summary>

View File

@ -42,7 +42,7 @@ public class World
/// <summary>
/// Orders given to units in each season.
/// </summary>
public ReadOnlyDictionary<Season, ReadOnlyCollection<Order>> GivenOrders { get; }
public ReadOnlyDictionary<Season, OrderHistory> OrderHistory { get; }
/// <summary>
/// Immutable game options.
@ -59,7 +59,7 @@ public class World
Season rootSeason,
ReadOnlyCollection<Unit> units,
ReadOnlyCollection<RetreatingUnit> retreatingUnits,
ReadOnlyDictionary<Season, ReadOnlyCollection<Order>> givenOrders,
ReadOnlyDictionary<Season, OrderHistory> orderHistory,
Options options)
{
this.Provinces = provinces;
@ -68,7 +68,7 @@ public class World
this.RootSeason = rootSeason;
this.Units = units;
this.RetreatingUnits = retreatingUnits;
this.GivenOrders = givenOrders;
this.OrderHistory = orderHistory;
this.Options = options;
}
@ -82,7 +82,7 @@ public class World
ReadOnlyCollection<Season>? seasons = null,
ReadOnlyCollection<Unit>? units = null,
ReadOnlyCollection<RetreatingUnit>? retreatingUnits = null,
ReadOnlyDictionary<Season, ReadOnlyCollection<Order>>? givenOrders = null,
ReadOnlyDictionary<Season, OrderHistory>? orderHistory = null,
Options? options = null)
: this(
provinces ?? previous.Provinces,
@ -91,7 +91,7 @@ public class World
previous.RootSeason, // Can't change the root season
units ?? previous.Units,
retreatingUnits ?? previous.RetreatingUnits,
givenOrders ?? previous.GivenOrders,
orderHistory ?? previous.OrderHistory,
options ?? previous.Options)
{
}
@ -109,7 +109,7 @@ public class World
root,
new(new List<Unit>()),
new(new List<RetreatingUnit>()),
new(new Dictionary<Season, ReadOnlyCollection<Order>>()),
new(new Dictionary<Season, OrderHistory>()),
new Options());
}
@ -123,7 +123,7 @@ public class World
IEnumerable<Season>? seasons = null,
IEnumerable<Unit>? units = null,
IEnumerable<RetreatingUnit>? retreats = null,
IEnumerable<KeyValuePair<Season, ReadOnlyCollection<Order>>>? orders = null)
IEnumerable<KeyValuePair<Season, OrderHistory>>? orders = null)
=> new World(
previous: this,
seasons: seasons == null
@ -135,8 +135,8 @@ public class World
retreatingUnits: retreats == null
? this.RetreatingUnits
: new(retreats.ToList()),
givenOrders: orders == null
? this.GivenOrders
orderHistory: orders == null
? this.OrderHistory
: new(orders.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)));
/// <summary>