Implement AdvanceTimeline resolution
This commit is contained in:
parent
7471a035f0
commit
39c3aabe45
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,9 @@ public class AdvanceTimeline : BinaryAdjudicationDecision
|
||||||
public Season Season { get; }
|
public Season Season { get; }
|
||||||
public List<UnitOrder> Orders { get; }
|
public List<UnitOrder> Orders { get; }
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
=> $"AdvanceTimeline({Season})";
|
||||||
|
|
||||||
public AdvanceTimeline(Season season, IEnumerable<UnitOrder> orders)
|
public AdvanceTimeline(Season season, IEnumerable<UnitOrder> orders)
|
||||||
{
|
{
|
||||||
this.Season = season;
|
this.Season = season;
|
||||||
|
|
|
@ -16,139 +16,139 @@ public class MovementDecisions
|
||||||
public Dictionary<Season, AdvanceTimeline> AdvanceTimeline { get; }
|
public Dictionary<Season, AdvanceTimeline> AdvanceTimeline { get; }
|
||||||
|
|
||||||
public IEnumerable<AdjudicationDecision> Values =>
|
public IEnumerable<AdjudicationDecision> Values =>
|
||||||
this.IsDislodged.Values.Cast<AdjudicationDecision>()
|
IsDislodged.Values.Cast<AdjudicationDecision>()
|
||||||
.Concat(this.HasPath.Values)
|
.Concat(HasPath.Values)
|
||||||
.Concat(this.GivesSupport.Values)
|
.Concat(GivesSupport.Values)
|
||||||
.Concat(this.HoldStrength.Values)
|
.Concat(HoldStrength.Values)
|
||||||
.Concat(this.AttackStrength.Values)
|
.Concat(AttackStrength.Values)
|
||||||
.Concat(this.DefendStrength.Values)
|
.Concat(DefendStrength.Values)
|
||||||
.Concat(this.PreventStrength.Values)
|
.Concat(PreventStrength.Values)
|
||||||
.Concat(this.DoesMove.Values)
|
.Concat(DoesMove.Values)
|
||||||
.Concat(this.AdvanceTimeline.Values);
|
.Concat(AdvanceTimeline.Values);
|
||||||
|
|
||||||
public MovementDecisions(World world, List<Order> orders)
|
public MovementDecisions(World world, List<Order> orders)
|
||||||
{
|
{
|
||||||
this.IsDislodged = new();
|
IsDislodged = new();
|
||||||
this.HasPath = new();
|
HasPath = new();
|
||||||
this.GivesSupport = new();
|
GivesSupport = new();
|
||||||
this.HoldStrength = new();
|
HoldStrength = new();
|
||||||
this.AttackStrength = new();
|
AttackStrength = new();
|
||||||
this.DefendStrength = new();
|
DefendStrength = new();
|
||||||
this.PreventStrength = new();
|
PreventStrength = new();
|
||||||
this.DoesMove = new();
|
DoesMove = new();
|
||||||
this.AdvanceTimeline = new();
|
AdvanceTimeline = new();
|
||||||
|
|
||||||
// Record which seasons are referenced by the order set.
|
// The orders argument only contains the submitted orders. The adjudicator will need to adjudicate not only
|
||||||
HashSet<Season> orderedSeasons = new();
|
// presently submitted orders, but also previously submitted orders if present orders affect the past. This
|
||||||
foreach (UnitOrder order in orders.Cast<UnitOrder>())
|
// 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.
|
// Create timeline decisions for each season potentially affected by the submitted orders.
|
||||||
// In the event that those seasons don't end up affected (all moves to it fail, all
|
// Since adjudication is deterministic and pure, if none of the affecting orders succeed,
|
||||||
// supports to it are cut), it is still safe to re-adjudicate everything because
|
// the adjudication decisions for the extra seasons will resolve the same way and the
|
||||||
// adjudication is deterministic and doesn't produce side effects.
|
// advance decision for the timeline will resolve false.
|
||||||
HashSet<Season> affectedSeasons = new();
|
|
||||||
foreach (Order order in orders)
|
foreach (Order order in orders)
|
||||||
{
|
{
|
||||||
switch (order)
|
switch (order)
|
||||||
{
|
{
|
||||||
case MoveOrder move:
|
case MoveOrder move:
|
||||||
if (!orderedSeasons.Contains(move.Season))
|
AdvanceTimeline.Ensure(
|
||||||
{
|
move.Season,
|
||||||
affectedSeasons.Add(move.Season);
|
() => new(move.Season, world.OrderHistory[move.Season].Orders));
|
||||||
}
|
AdvanceTimeline[move.Season].Orders.Add(move);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SupportHoldOrder supportHold:
|
case SupportHoldOrder supportHold:
|
||||||
if (!orderedSeasons.Contains(supportHold.Target.Season))
|
AdvanceTimeline.Ensure(
|
||||||
{
|
supportHold.Target.Season,
|
||||||
affectedSeasons.Add(supportHold.Target.Season);
|
() => new(supportHold.Target.Season, world.OrderHistory[supportHold.Target.Season].Orders));
|
||||||
}
|
AdvanceTimeline[supportHold.Target.Season].Orders.Add(supportHold);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SupportMoveOrder supportMove:
|
case SupportMoveOrder supportMove:
|
||||||
if (!orderedSeasons.Contains(supportMove.Target.Season))
|
AdvanceTimeline.Ensure(
|
||||||
{
|
supportMove.Target.Season,
|
||||||
affectedSeasons.Add(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;
|
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.
|
// Create all other relevant decisions for each order in the affected timelines.
|
||||||
foreach (UnitOrder order in orders.Cast<UnitOrder>())
|
foreach (UnitOrder order in relevantOrders)
|
||||||
{
|
{
|
||||||
// Create a dislodge decision for this unit.
|
// Create a dislodge decision for this unit.
|
||||||
List<MoveOrder> incoming = orders
|
List<MoveOrder> incoming = relevantOrders
|
||||||
.OfType<MoveOrder>()
|
.OfType<MoveOrder>()
|
||||||
.Where(order.IsIncoming)
|
.Where(order.IsIncoming)
|
||||||
.ToList();
|
.ToList();
|
||||||
this.IsDislodged[order.Unit] = new(order, incoming);
|
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);
|
|
||||||
|
|
||||||
if (order is MoveOrder move)
|
if (order is MoveOrder move)
|
||||||
{
|
{
|
||||||
// Find supports corresponding to this move.
|
// Find supports corresponding to this move.
|
||||||
List<SupportMoveOrder> supports = orders
|
List<SupportMoveOrder> supports = relevantOrders
|
||||||
.OfType<SupportMoveOrder>()
|
.OfType<SupportMoveOrder>()
|
||||||
.Where(support => support.IsSupportFor(move))
|
.Where(support => support.IsSupportFor(move))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
// Determine if this move is a head-to-head battle.
|
// Determine if this move is a head-to-head battle.
|
||||||
MoveOrder? opposingMove = orders
|
MoveOrder? opposingMove = relevantOrders
|
||||||
.OfType<MoveOrder>()
|
.OfType<MoveOrder>()
|
||||||
.FirstOrDefault(other => other != null && other.IsOpposing(move), null);
|
.FirstOrDefault(other => other!.IsOpposing(move), null);
|
||||||
|
|
||||||
// Find competing moves.
|
// Find competing moves.
|
||||||
List<MoveOrder> competing = orders
|
List<MoveOrder> competing = relevantOrders
|
||||||
.OfType<MoveOrder>()
|
.OfType<MoveOrder>()
|
||||||
.Where(move.IsCompeting)
|
.Where(move.IsCompeting)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
// Create the move-related decisions.
|
// Create the move-related decisions.
|
||||||
this.HasPath[move] = new(move);
|
HasPath[move] = new(move);
|
||||||
this.AttackStrength[move] = new(move, supports, opposingMove);
|
AttackStrength[move] = new(move, supports, opposingMove);
|
||||||
this.DefendStrength[move] = new(move, supports);
|
DefendStrength[move] = new(move, supports);
|
||||||
this.PreventStrength[move] = new(move, supports, opposingMove);
|
PreventStrength[move] = new(move, supports, opposingMove);
|
||||||
this.DoesMove[move] = new(move, opposingMove, competing);
|
DoesMove[move] = new(move, opposingMove, competing);
|
||||||
|
|
||||||
// Ensure a hold strength decision exists for the destination.
|
// Ensure a hold strength decision exists for the destination.
|
||||||
if (!this.HoldStrength.ContainsKey(move.Point))
|
HoldStrength.Ensure(move.Point, () => new(move.Point));
|
||||||
{
|
|
||||||
this.HoldStrength[move.Point] = new(move.Point);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (order is SupportOrder support)
|
else if (order is SupportOrder support)
|
||||||
{
|
{
|
||||||
// Create the support decision.
|
// 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.
|
// Ensure a hold strength decision exists for the target's province.
|
||||||
if (!this.HoldStrength.ContainsKey(support.Target.Point))
|
HoldStrength.Ensure(support.Target.Point, () => new(support.Target.Point));
|
||||||
{
|
|
||||||
this.HoldStrength[support.Target.Point] = new(support.Target.Point);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (support is SupportHoldOrder supportHold)
|
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)
|
else if (support is SupportMoveOrder supportMove)
|
||||||
{
|
{
|
||||||
// Ensure a hold strength decision exists for the target's destination.
|
// Ensure a hold strength decision exists for the target's destination.
|
||||||
if (!this.HoldStrength.ContainsKey(supportMove.Point))
|
HoldStrength.Ensure(supportMove.Point, () => new(supportMove.Point));
|
||||||
{
|
|
||||||
this.HoldStrength[supportMove.Point] = new(supportMove.Point);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -291,7 +291,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||||
// This will noop without progress if the decision is already resolved
|
// This will noop without progress if the decision is already resolved
|
||||||
progress |= ResolveDecision(decision, world, decisions, depth: 2);
|
progress |= ResolveDecision(decision, world, decisions, depth: 2);
|
||||||
}
|
}
|
||||||
} while (progress);
|
} while (progress && decisions.Values.Any(decision => !decision.Resolved));
|
||||||
|
|
||||||
if (decisions.Values.Any(d => !d.Resolved))
|
if (decisions.Values.Any(d => !d.Resolved))
|
||||||
{
|
{
|
||||||
|
@ -304,9 +304,13 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||||
|
|
||||||
public World UpdateWorld(World world, List<AdjudicationDecision> decisions)
|
public World UpdateWorld(World world, List<AdjudicationDecision> decisions)
|
||||||
{
|
{
|
||||||
|
logger.Log(0, "Updating world");
|
||||||
Dictionary<MoveOrder, DoesMove> moves = decisions
|
Dictionary<MoveOrder, DoesMove> moves = decisions
|
||||||
.OfType<DoesMove>()
|
.OfType<DoesMove>()
|
||||||
.ToDictionary(dm => dm.Order);
|
.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
|
// 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.
|
// record of when a future season has been created.
|
||||||
|
@ -314,25 +318,37 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||||
List<Unit> createdUnits = new();
|
List<Unit> createdUnits = new();
|
||||||
List<RetreatingUnit> retreats = new();
|
List<RetreatingUnit> retreats = new();
|
||||||
|
|
||||||
// Successful move orders result in the unit moving to the destination and creating a new
|
// Populate createdFutures with the timeline fork decisions
|
||||||
// future, while unsuccessful move orders are processed the same way as non-move orders.
|
logger.Log(1, "Processing AdvanceTimeline decisions");
|
||||||
foreach (DoesMove doesMove in moves.Values)
|
foreach (AdvanceTimeline advanceTimeline in decisions.OfType<AdvanceTimeline>())
|
||||||
{
|
{
|
||||||
if (doesMove.Outcome == true)
|
logger.Log(2, "{0} = {1}", advanceTimeline, advanceTimeline.Outcome?.ToString() ?? "?");
|
||||||
{
|
if (advanceTimeline.Outcome == true)
|
||||||
if (!createdFutures.TryGetValue(doesMove.Order.Season, out Season? future))
|
|
||||||
{
|
{
|
||||||
// A timeline that doesn't have a future yet simply continues. Otherwise, it forks.
|
// A timeline that doesn't have a future yet simply continues. Otherwise, it forks.
|
||||||
future = !doesMove.Order.Season.Futures.Any()
|
createdFutures[advanceTimeline.Season] = !advanceTimeline.Season.Futures.Any()
|
||||||
? doesMove.Order.Season.MakeNext()
|
? advanceTimeline.Season.MakeNext()
|
||||||
: doesMove.Order.Season.MakeFork();
|
: advanceTimeline.Season.MakeFork();
|
||||||
createdFutures[doesMove.Order.Season] = future;
|
|
||||||
}
|
}
|
||||||
createdUnits.Add(doesMove.Order.Unit.Next(doesMove.Order.Location, future));
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
{
|
||||||
|
logger.Log(2, "{0} = {1}", doesMove, doesMove.Outcome?.ToString() ?? "?");
|
||||||
|
Season moveSeason = doesMove.Order.Season;
|
||||||
|
if (doesMove.Outcome == true && createdFutures.ContainsKey(moveSeason))
|
||||||
|
{
|
||||||
|
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.
|
// Process unsuccessful moves, all holds, and all supports.
|
||||||
|
logger.Log(1, "Processing stationary orders");
|
||||||
foreach (IsDislodged isDislodged in decisions.OfType<IsDislodged>())
|
foreach (IsDislodged isDislodged in decisions.OfType<IsDislodged>())
|
||||||
{
|
{
|
||||||
UnitOrder order = isDislodged.Order;
|
UnitOrder order = isDislodged.Order;
|
||||||
|
@ -343,22 +359,26 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||||
continue;
|
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.
|
logger.Log(3, "Skipping order because no future was created");
|
||||||
future = order.Unit.Season.MakeNext();
|
continue;
|
||||||
createdFutures[order.Unit.Season] = future;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// For each stationary unit that wasn't dislodged, continue it into the future.
|
Season future = createdFutures[order.Unit.Season];
|
||||||
if (isDislodged.Outcome == false)
|
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
|
else
|
||||||
{
|
{
|
||||||
// Create a retreat for each dislodged unit.
|
// Create a retreat for each dislodged unit.
|
||||||
// TODO check valid retreats and disbands
|
// TODO check valid retreats and disbands
|
||||||
|
logger.Log(3, "Creating retreat for {0}", order.Unit);
|
||||||
var validRetreats = order.Unit.Location.Adjacents
|
var validRetreats = order.Unit.Location.Adjacents
|
||||||
.Select(loc => (future, loc))
|
.Select(loc => (future, loc))
|
||||||
.ToList();
|
.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.
|
// Record the adjudication results to the season's order history
|
||||||
IEnumerable<KeyValuePair<Season, ReadOnlyCollection<Order>>> newOrders = decisions
|
Dictionary<Season, OrderHistory> newHistory = new();
|
||||||
.OfType<IsDislodged>()
|
foreach (UnitOrder unitOrder in decisions.OfType<IsDislodged>().Select(d => d.Order))
|
||||||
.GroupBy(
|
{
|
||||||
keySelector: d => d.Order.Unit.Season,
|
newHistory.Ensure(unitOrder.Unit.Season, () => new());
|
||||||
elementSelector: d => d.Order as Order)
|
OrderHistory history = newHistory[unitOrder.Unit.Season];
|
||||||
.Where(group => !world.GivenOrders.ContainsKey(group.Key))
|
// TODO does this add every order to every season??
|
||||||
.Select(group => new KeyValuePair<Season, ReadOnlyCollection<Order>>(
|
history.Orders.Add(unitOrder);
|
||||||
group.Key, new(group.ToList())));
|
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
|
// TODO provide more structured information about order outcomes
|
||||||
|
|
||||||
|
@ -383,7 +423,9 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||||
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));
|
orders: updatedHistory);
|
||||||
|
|
||||||
|
logger.Log(0, "Completed update");
|
||||||
|
|
||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
|
@ -423,6 +465,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||||
logger.Log(depth, "ResolveDecision({0})", decision);
|
logger.Log(depth, "ResolveDecision({0})", decision);
|
||||||
return decision.Resolved ? false : decision switch
|
return decision.Resolved ? false : decision switch
|
||||||
{
|
{
|
||||||
|
AdvanceTimeline d => ResolveAdvanceTimeline(d, world, decisions, depth + 1),
|
||||||
IsDislodged d => ResolveIsUnitDislodged(d, world, decisions, depth + 1),
|
IsDislodged d => ResolveIsUnitDislodged(d, world, decisions, depth + 1),
|
||||||
HasPath d => ResolveDoesMoveHavePath(d, world, decisions, depth + 1),
|
HasPath d => ResolveDoesMoveHavePath(d, world, decisions, depth + 1),
|
||||||
GivesSupport d => ResolveIsSupportGiven(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(
|
private bool ResolveIsUnitDislodged(
|
||||||
IsDislodged decision,
|
IsDislodged decision,
|
||||||
World world,
|
World world,
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -71,7 +71,7 @@ public class Season
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return $"{this.Turn}:{this.Timeline}";
|
return $"{this.Timeline}@{this.Turn}";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
@ -42,7 +42,7 @@ public class World
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Orders given to units in each season.
|
/// Orders given to units in each season.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ReadOnlyDictionary<Season, ReadOnlyCollection<Order>> GivenOrders { get; }
|
public ReadOnlyDictionary<Season, OrderHistory> OrderHistory { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Immutable game options.
|
/// Immutable game options.
|
||||||
|
@ -59,7 +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,
|
ReadOnlyDictionary<Season, OrderHistory> orderHistory,
|
||||||
Options options)
|
Options options)
|
||||||
{
|
{
|
||||||
this.Provinces = provinces;
|
this.Provinces = provinces;
|
||||||
|
@ -68,7 +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.OrderHistory = orderHistory;
|
||||||
this.Options = options;
|
this.Options = options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,7 +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,
|
ReadOnlyDictionary<Season, OrderHistory>? orderHistory = null,
|
||||||
Options? options = null)
|
Options? options = null)
|
||||||
: this(
|
: this(
|
||||||
provinces ?? previous.Provinces,
|
provinces ?? previous.Provinces,
|
||||||
|
@ -91,7 +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,
|
orderHistory ?? previous.OrderHistory,
|
||||||
options ?? previous.Options)
|
options ?? previous.Options)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
@ -109,7 +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(new Dictionary<Season, OrderHistory>()),
|
||||||
new Options());
|
new Options());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,7 +123,7 @@ public class World
|
||||||
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)
|
IEnumerable<KeyValuePair<Season, OrderHistory>>? orders = null)
|
||||||
=> new World(
|
=> new World(
|
||||||
previous: this,
|
previous: this,
|
||||||
seasons: seasons == null
|
seasons: seasons == null
|
||||||
|
@ -135,8 +135,8 @@ public class World
|
||||||
retreatingUnits: retreats == null
|
retreatingUnits: retreats == null
|
||||||
? this.RetreatingUnits
|
? this.RetreatingUnits
|
||||||
: new(retreats.ToList()),
|
: new(retreats.ToList()),
|
||||||
givenOrders: orders == null
|
orderHistory: orders == null
|
||||||
? this.GivenOrders
|
? this.OrderHistory
|
||||||
: new(orders.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)));
|
: new(orders.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
Loading…
Reference in New Issue