Compare commits
1 Commits
5d311bc08c
...
c4a14f1d7e
Author | SHA1 | Date |
---|---|---|
Jaculabilis | c4a14f1d7e |
|
@ -2,15 +2,7 @@
|
|||
"terminal.integrated.profiles.linux": {
|
||||
"nix develop": {
|
||||
"path": "nix",
|
||||
"args": ["develop", "--impure"]
|
||||
}
|
||||
},
|
||||
"terminal.integrated.profiles.windows": {
|
||||
"nix develop": {
|
||||
"path": [
|
||||
"${env:windir}\\System32\\wsl.exe"
|
||||
],
|
||||
"args": ["-d", "NixOS", "--", "nix", "develop"]
|
||||
"args": ["develop"]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,9 +2,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
|||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.30114.105
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MultiversalDiplomacy", "MultiversalDiplomacy/MultiversalDiplomacy.csproj", "{DD4C458A-EB75-4DFA-B06D-7F2BF8470460}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MultiversalDiplomacy", "MultiversalDiplomacy\MultiversalDiplomacy.csproj", "{DD4C458A-EB75-4DFA-B06D-7F2BF8470460}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MultiversalDiplomacyTests", "MultiversalDiplomacyTests/MultiversalDiplomacyTests.csproj", "{6DD39198-428C-4385-B870-A1BDED1E5F8A}"
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MultiversalDiplomacyTests", "MultiversalDiplomacyTests\MultiversalDiplomacyTests.csproj", "{6DD39198-428C-4385-B870-A1BDED1E5F8A}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
using MultiversalDiplomacy.Model;
|
||||
using MultiversalDiplomacy.Orders;
|
||||
|
||||
namespace MultiversalDiplomacy.Adjudicate.Decision;
|
||||
|
||||
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;
|
||||
this.Orders = orders.ToList();
|
||||
}
|
||||
}
|
|
@ -8,9 +8,6 @@ public class AttackStrength : NumericAdjudicationDecision
|
|||
public List<SupportMoveOrder> Supports { get; }
|
||||
public MoveOrder? OpposingMove { get; }
|
||||
|
||||
public override string ToString()
|
||||
=> $"AttackStrength({Order})";
|
||||
|
||||
public AttackStrength(MoveOrder order, IEnumerable<SupportMoveOrder> supports, MoveOrder? opposingMove = null)
|
||||
{
|
||||
this.Order = order;
|
||||
|
|
|
@ -6,6 +6,11 @@ public abstract class BinaryAdjudicationDecision : AdjudicationDecision
|
|||
|
||||
public override bool Resolved => this.Outcome != null;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{this.GetType().Name}={this.Outcome}";
|
||||
}
|
||||
|
||||
public bool Update(bool outcome)
|
||||
{
|
||||
if (this.Outcome == null)
|
||||
|
|
|
@ -7,9 +7,6 @@ public class DefendStrength : NumericAdjudicationDecision
|
|||
public MoveOrder Order { get; }
|
||||
public List<SupportMoveOrder> Supports { get; }
|
||||
|
||||
public override string ToString()
|
||||
=> $"DefendStrength({Order})";
|
||||
|
||||
public DefendStrength(MoveOrder order, IEnumerable<SupportMoveOrder> supports)
|
||||
{
|
||||
this.Order = order;
|
||||
|
|
|
@ -8,9 +8,6 @@ public class DoesMove : BinaryAdjudicationDecision
|
|||
public MoveOrder? OpposingMove { get; }
|
||||
public List<MoveOrder> Competing { get; }
|
||||
|
||||
public override string ToString()
|
||||
=> $"DoesMove({Order})";
|
||||
|
||||
public DoesMove(MoveOrder order, MoveOrder? opposingMove, IEnumerable<MoveOrder> competing)
|
||||
{
|
||||
this.Order = order;
|
||||
|
|
|
@ -7,9 +7,6 @@ public class GivesSupport : BinaryAdjudicationDecision
|
|||
public SupportOrder Order { get; }
|
||||
public List<MoveOrder> Cuts { get; }
|
||||
|
||||
public override string ToString()
|
||||
=> $"GivesSupport({Order})";
|
||||
|
||||
public GivesSupport(SupportOrder order, IEnumerable<MoveOrder> cuts)
|
||||
{
|
||||
this.Order = order;
|
||||
|
|
|
@ -6,9 +6,6 @@ public class HasPath : BinaryAdjudicationDecision
|
|||
{
|
||||
public MoveOrder Order { get; }
|
||||
|
||||
public override string ToString()
|
||||
=> $"HasPath({Order})";
|
||||
|
||||
public HasPath(MoveOrder order)
|
||||
{
|
||||
this.Order = order;
|
||||
|
|
|
@ -10,11 +10,6 @@ public class HoldStrength : NumericAdjudicationDecision
|
|||
public UnitOrder? Order { get; }
|
||||
public List<SupportHoldOrder> Supports { get; }
|
||||
|
||||
public override string ToString()
|
||||
=> Order is null
|
||||
? $"HoldStrength({Province.Abbreviations[0]})"
|
||||
: $"HoldStrength({Order.Unit})";
|
||||
|
||||
public HoldStrength(Province province, Season season, UnitOrder? order = null)
|
||||
{
|
||||
this.Province = province;
|
||||
|
|
|
@ -7,9 +7,6 @@ public class IsDislodged : BinaryAdjudicationDecision
|
|||
public UnitOrder Order { get; }
|
||||
public List<MoveOrder> Incoming { get; }
|
||||
|
||||
public override string ToString()
|
||||
=> $"IsDislodged({Order.Unit})";
|
||||
|
||||
public IsDislodged(UnitOrder order, IEnumerable<MoveOrder> incoming)
|
||||
{
|
||||
this.Order = order;
|
||||
|
|
|
@ -13,145 +13,139 @@ public class MovementDecisions
|
|||
public Dictionary<MoveOrder, DefendStrength> DefendStrength { get; }
|
||||
public Dictionary<MoveOrder, PreventStrength> PreventStrength { get; }
|
||||
public Dictionary<MoveOrder, DoesMove> DoesMove { get; }
|
||||
public Dictionary<Season, AdvanceTimeline> AdvanceTimeline { get; }
|
||||
|
||||
public IEnumerable<AdjudicationDecision> 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);
|
||||
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);
|
||||
|
||||
public MovementDecisions(World world, List<Order> orders)
|
||||
{
|
||||
IsDislodged = new();
|
||||
HasPath = new();
|
||||
GivesSupport = new();
|
||||
HoldStrength = new();
|
||||
AttackStrength = new();
|
||||
DefendStrength = new();
|
||||
PreventStrength = new();
|
||||
DoesMove = new();
|
||||
AdvanceTimeline = new();
|
||||
this.IsDislodged = new();
|
||||
this.HasPath = new();
|
||||
this.GivesSupport = new();
|
||||
this.HoldStrength = new();
|
||||
this.AttackStrength = new();
|
||||
this.DefendStrength = new();
|
||||
this.PreventStrength = new();
|
||||
this.DoesMove = new();
|
||||
|
||||
// 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)
|
||||
// Record which seasons are referenced by the order set.
|
||||
HashSet<Season> orderedSeasons = new();
|
||||
foreach (UnitOrder order in orders.Cast<UnitOrder>())
|
||||
{
|
||||
AdvanceTimeline[group.Key] = new(group.Key, group);
|
||||
_ = orderedSeasons.Add(order.Unit.Season);
|
||||
}
|
||||
|
||||
// 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.
|
||||
// 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:
|
||||
AdvanceTimeline.Ensure(
|
||||
move.Season,
|
||||
() => new(move.Season, world.OrderHistory[move.Season].Orders));
|
||||
AdvanceTimeline[move.Season].Orders.Add(move);
|
||||
if (!orderedSeasons.Contains(move.Season))
|
||||
{
|
||||
affectedSeasons.Add(move.Season);
|
||||
}
|
||||
break;
|
||||
|
||||
case SupportHoldOrder supportHold:
|
||||
AdvanceTimeline.Ensure(
|
||||
supportHold.Target.Season,
|
||||
() => new(supportHold.Target.Season, world.OrderHistory[supportHold.Target.Season].Orders));
|
||||
AdvanceTimeline[supportHold.Target.Season].Orders.Add(supportHold);
|
||||
if (!orderedSeasons.Contains(supportHold.Target.Season))
|
||||
{
|
||||
affectedSeasons.Add(supportHold.Target.Season);
|
||||
}
|
||||
break;
|
||||
|
||||
case SupportMoveOrder supportMove:
|
||||
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);
|
||||
if (!orderedSeasons.Contains(supportMove.Target.Season))
|
||||
{
|
||||
affectedSeasons.Add(supportMove.Target.Season);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Get the orders in the affected timelines.
|
||||
List<UnitOrder> relevantOrders = AdvanceTimeline.Values
|
||||
.SelectMany(at => at.Orders)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
// Create a hold strength decision with an associated order for every province with a unit.
|
||||
foreach (UnitOrder order in relevantOrders)
|
||||
foreach (Season season in affectedSeasons)
|
||||
{
|
||||
HoldStrength[order.Unit.Point] = new(order.Unit.Point, order);
|
||||
orders.AddRange(world.GivenOrders[season]);
|
||||
}
|
||||
|
||||
// Create all other relevant decisions for each order in the affected timelines.
|
||||
foreach (UnitOrder order in relevantOrders)
|
||||
// Create the relevant decisions for each order.
|
||||
foreach (UnitOrder order in orders.Cast<UnitOrder>())
|
||||
{
|
||||
// Create a dislodge decision for this unit.
|
||||
List<MoveOrder> incoming = relevantOrders
|
||||
List<MoveOrder> incoming = orders
|
||||
.OfType<MoveOrder>()
|
||||
.Where(order.IsIncoming)
|
||||
.ToList();
|
||||
IsDislodged[order.Unit] = new(order, incoming);
|
||||
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);
|
||||
|
||||
if (order is MoveOrder move)
|
||||
{
|
||||
// Find supports corresponding to this move.
|
||||
List<SupportMoveOrder> supports = relevantOrders
|
||||
List<SupportMoveOrder> supports = orders
|
||||
.OfType<SupportMoveOrder>()
|
||||
.Where(support => support.IsSupportFor(move))
|
||||
.ToList();
|
||||
|
||||
// Determine if this move is a head-to-head battle.
|
||||
MoveOrder? opposingMove = relevantOrders
|
||||
MoveOrder? opposingMove = orders
|
||||
.OfType<MoveOrder>()
|
||||
.FirstOrDefault(other => other!.IsOpposing(move), null);
|
||||
.FirstOrDefault(other => other != null && other.IsOpposing(move), null);
|
||||
|
||||
// Find competing moves.
|
||||
List<MoveOrder> competing = relevantOrders
|
||||
List<MoveOrder> competing = orders
|
||||
.OfType<MoveOrder>()
|
||||
.Where(move.IsCompeting)
|
||||
.ToList();
|
||||
|
||||
// Create the move-related decisions.
|
||||
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);
|
||||
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);
|
||||
|
||||
// Ensure a hold strength decision exists for the destination.
|
||||
HoldStrength.Ensure(move.Point, () => new(move.Point));
|
||||
if (!this.HoldStrength.ContainsKey(move.Point))
|
||||
{
|
||||
this.HoldStrength[move.Point] = new(move.Point);
|
||||
}
|
||||
}
|
||||
else if (order is SupportOrder support)
|
||||
{
|
||||
// Create the support decision.
|
||||
GivesSupport[support] = new(support, incoming);
|
||||
this.GivesSupport[support] = new(support, incoming);
|
||||
|
||||
// Ensure a hold strength decision exists for the target's province.
|
||||
HoldStrength.Ensure(support.Target.Point, () => new(support.Target.Point));
|
||||
if (!this.HoldStrength.ContainsKey(support.Target.Point))
|
||||
{
|
||||
this.HoldStrength[support.Target.Point] = new(support.Target.Point);
|
||||
}
|
||||
|
||||
if (support is SupportHoldOrder supportHold)
|
||||
{
|
||||
HoldStrength[support.Target.Point].Supports.Add(supportHold);
|
||||
this.HoldStrength[support.Target.Point].Supports.Add(supportHold);
|
||||
}
|
||||
else if (support is SupportMoveOrder supportMove)
|
||||
{
|
||||
// Ensure a hold strength decision exists for the target's destination.
|
||||
HoldStrength.Ensure(supportMove.Point, () => new(supportMove.Point));
|
||||
if (!this.HoldStrength.ContainsKey(supportMove.Point))
|
||||
{
|
||||
this.HoldStrength[supportMove.Point] = new(supportMove.Point);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,9 +8,6 @@ public class PreventStrength : NumericAdjudicationDecision
|
|||
public List<SupportMoveOrder> Supports { get; }
|
||||
public MoveOrder? OpposingMove { get; }
|
||||
|
||||
public override string ToString()
|
||||
=> $"PreventStrength({Order})";
|
||||
|
||||
public PreventStrength(MoveOrder order, IEnumerable<SupportMoveOrder> supports, MoveOrder? opposingMove = null)
|
||||
{
|
||||
this.Order = order;
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
namespace MultiversalDiplomacy.Adjudicate.Logging;
|
||||
|
||||
public class ConsoleLogger : IAdjudicatorLogger
|
||||
{
|
||||
public static ConsoleLogger Instance { get; } = new();
|
||||
|
||||
public void Log(int contextLevel, string message, params object[] args)
|
||||
{
|
||||
string spacing = string.Format($"{{0,{2 * contextLevel}}}", string.Empty);
|
||||
string formattedMessage = string.Format(message, args);
|
||||
Console.WriteLine(spacing + formattedMessage);
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
namespace MultiversalDiplomacy.Adjudicate.Logging;
|
||||
|
||||
public interface IAdjudicatorLogger
|
||||
{
|
||||
public void Log(int contextLevel, string message, params object[] args);
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
using System.Collections.ObjectModel;
|
||||
|
||||
using MultiversalDiplomacy.Adjudicate.Decision;
|
||||
using MultiversalDiplomacy.Adjudicate.Logging;
|
||||
using MultiversalDiplomacy.Model;
|
||||
using MultiversalDiplomacy.Orders;
|
||||
|
||||
|
@ -10,14 +11,7 @@ namespace MultiversalDiplomacy.Adjudicate;
|
|||
/// </summary>
|
||||
public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||
{
|
||||
public static IPhaseAdjudicator Instance { get; } = new MovementPhaseAdjudicator(ConsoleLogger.Instance);
|
||||
|
||||
private IAdjudicatorLogger logger { get; }
|
||||
|
||||
public MovementPhaseAdjudicator(IAdjudicatorLogger logger)
|
||||
{
|
||||
this.logger = logger;
|
||||
}
|
||||
public static IPhaseAdjudicator Instance { get; } = new MovementPhaseAdjudicator();
|
||||
|
||||
public List<OrderValidation> ValidateOrders(World world, List<Order> orders)
|
||||
{
|
||||
|
@ -273,42 +267,34 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
|
||||
public List<AdjudicationDecision> AdjudicateOrders(World world, List<Order> orders)
|
||||
{
|
||||
logger.Log(0, "Beginning adjudication");
|
||||
// Define all adjudication decisions to be made.
|
||||
MovementDecisions decisions = new(world, orders);
|
||||
|
||||
// Adjudicate all decisions.
|
||||
bool progress = false;
|
||||
int loopNum = 1;
|
||||
do
|
||||
{
|
||||
logger.Log(1, "Beginning loop {0}", loopNum++);
|
||||
progress = false;
|
||||
foreach (AdjudicationDecision decision in decisions.Values)
|
||||
{
|
||||
// This will noop without progress if the decision is already resolved
|
||||
progress |= ResolveDecision(decision, world, decisions, depth: 2);
|
||||
progress |= ResolveDecision(decision, world, decisions);
|
||||
}
|
||||
} while (progress && decisions.Values.Any(decision => !decision.Resolved));
|
||||
} while (progress);
|
||||
|
||||
if (decisions.Values.Any(d => !d.Resolved))
|
||||
{
|
||||
throw new ApplicationException("Some orders not resolved!");
|
||||
}
|
||||
logger.Log(0, "Completed adjudication");
|
||||
|
||||
return decisions.Values.ToList();
|
||||
}
|
||||
|
||||
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.
|
||||
|
@ -316,37 +302,25 @@ 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)
|
||||
{
|
||||
logger.Log(2, "{0} = {1}", doesMove, doesMove.Outcome?.ToString() ?? "?");
|
||||
Season moveSeason = doesMove.Order.Season;
|
||||
if (doesMove.Outcome == true && createdFutures.ContainsKey(moveSeason))
|
||||
if (doesMove.Outcome == true)
|
||||
{
|
||||
Unit next = doesMove.Order.Unit.Next(doesMove.Order.Location, createdFutures[moveSeason]);
|
||||
logger.Log(3, "Advancing unit to {0}", next);
|
||||
createdUnits.Add(next);
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
@ -357,26 +331,22 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
continue;
|
||||
}
|
||||
|
||||
logger.Log(2, "{0} = {1}", isDislodged, isDislodged.Outcome?.ToString() ?? "?");
|
||||
if (!createdFutures.ContainsKey(order.Unit.Season))
|
||||
if (!createdFutures.TryGetValue(order.Unit.Season, out Season? future))
|
||||
{
|
||||
logger.Log(3, "Skipping order because no future was created");
|
||||
continue;
|
||||
// Any unit given an order is, by definition, at the front of a timeline.
|
||||
future = order.Unit.Season.MakeNext();
|
||||
createdFutures[order.Unit.Season] = future;
|
||||
}
|
||||
|
||||
Season future = createdFutures[order.Unit.Season];
|
||||
// For each stationary unit that wasn't dislodged, continue it into the future.
|
||||
if (isDislodged.Outcome == false)
|
||||
{
|
||||
// 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);
|
||||
createdUnits.Add(order.Unit.Next(order.Unit.Location, future));
|
||||
}
|
||||
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();
|
||||
|
@ -385,35 +355,15 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
// 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
|
||||
|
||||
|
@ -421,153 +371,33 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
seasons: world.Seasons.Concat(createdFutures.Values),
|
||||
units: world.Units.Concat(createdUnits),
|
||||
retreats: retreats,
|
||||
orders: updatedHistory);
|
||||
|
||||
logger.Log(0, "Completed update");
|
||||
orders: world.GivenOrders.Concat(newOrders));
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
private bool LoggedUpdate(BinaryAdjudicationDecision decision, bool outcome, int depth, string message)
|
||||
{
|
||||
bool updated = decision.Update(outcome);
|
||||
if (updated)
|
||||
{
|
||||
logger.Log(depth, "{0}: {1}", outcome, message);
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
private bool LoggedUpdate(NumericAdjudicationDecision decision, int min, int max, int depth, string message)
|
||||
{
|
||||
bool updated = decision.Update(min, max);
|
||||
if (updated)
|
||||
{
|
||||
logger.Log(depth, "{0}, {1}: {2}", min, max, message);
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to resolve an adjudication decision.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// If any adjudication was further determined, returns true. If nothing was further determined, returns false.
|
||||
/// </returns>
|
||||
private bool ResolveDecision(
|
||||
AdjudicationDecision decision,
|
||||
World world,
|
||||
MovementDecisions decisions,
|
||||
int depth)
|
||||
{
|
||||
logger.Log(depth, "ResolveDecision({0})", decision);
|
||||
return decision.Resolved ? false : decision switch
|
||||
MovementDecisions decisions)
|
||||
=> 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),
|
||||
HoldStrength d => ResolveHoldStrength(d, world, decisions, depth + 1),
|
||||
AttackStrength d => ResolveAttackStrength(d, world, decisions, depth + 1),
|
||||
DefendStrength d => ResolveDefendStrength(d, world, decisions, depth + 1),
|
||||
PreventStrength d => ResolvePreventStrength(d, world, decisions, depth + 1),
|
||||
DoesMove d => ResolveDoesUnitMove(d, world, decisions, depth + 1),
|
||||
IsDislodged d => ResolveIsUnitDislodged(d, world, decisions),
|
||||
HasPath d => ResolveDoesMoveHavePath(d, world, decisions),
|
||||
GivesSupport d => ResolveIsSupportGiven(d, world, decisions),
|
||||
HoldStrength d => ResolveHoldStrength(d, world, decisions),
|
||||
AttackStrength d => ResolveAttackStrength(d, world, decisions),
|
||||
DefendStrength d => ResolveDefendStrength(d, world, decisions),
|
||||
PreventStrength d => ResolvePreventStrength(d, world, decisions),
|
||||
DoesMove d => ResolveDoesUnitMove(d, world, decisions),
|
||||
_ => throw new NotSupportedException($"Unknown decision type: {decision.GetType()}")
|
||||
};
|
||||
}
|
||||
|
||||
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 new (i.e. not previously adjudicated) and successful move always advances.
|
||||
IEnumerable<MoveOrder> newIncomingMoves = decision.Orders
|
||||
.OfType<MoveOrder>()
|
||||
.Where(order => order.Season == decision.Season
|
||||
&& !world.OrderHistory[order.Season].DoesMoveOutcomes.ContainsKey(order));
|
||||
foreach (MoveOrder moveOrder in newIncomingMoves)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
if (history.IsDislodgedOutcomes.TryGetValue(order.Unit, out bool previous)
|
||||
&& 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);
|
||||
if (history.DoesMoveOutcomes.TryGetValue(moveOrder, out bool previousMove)
|
||||
&& moves.Resolved
|
||||
&& moves.Outcome != previousMove)
|
||||
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,
|
||||
MovementDecisions decisions,
|
||||
int depth)
|
||||
MovementDecisions decisions)
|
||||
{
|
||||
logger.Log(depth, "IsUnitDislodged({0})", decision.Order.Unit);
|
||||
bool progress = false;
|
||||
|
||||
// If this unit was ordered to move and is doing so successfully, it cannot be dislodged
|
||||
|
@ -575,13 +405,13 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
if (decision.Order is MoveOrder moveOrder)
|
||||
{
|
||||
DoesMove move = decisions.DoesMove[moveOrder];
|
||||
progress |= ResolveDecision(move, world, decisions, depth + 1);
|
||||
progress |= ResolveDecision(move, world, decisions);
|
||||
|
||||
// If this unit received a move order and the move is successful, it cannot be
|
||||
// dislodged.
|
||||
if (move.Outcome == true)
|
||||
{
|
||||
progress |= LoggedUpdate(decision, false, depth, "Unit successfully moves");
|
||||
progress |= decision.Update(false);
|
||||
return progress;
|
||||
}
|
||||
|
||||
|
@ -599,12 +429,12 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
foreach (MoveOrder dislodger in decision.Incoming)
|
||||
{
|
||||
DoesMove move = decisions.DoesMove[dislodger];
|
||||
progress |= ResolveDecision(move, world, decisions, depth + 1);
|
||||
progress |= ResolveDecision(move, world, decisions);
|
||||
|
||||
// If at least one invader will move, this unit is dislodged.
|
||||
if (move.Outcome == true)
|
||||
{
|
||||
progress |= LoggedUpdate(decision, true, depth, "Invading unit successfully moves");
|
||||
progress |= decision.Update(true);
|
||||
return progress;
|
||||
}
|
||||
|
||||
|
@ -618,7 +448,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
|
||||
if (!potentialDislodger)
|
||||
{
|
||||
progress |= LoggedUpdate(decision, false, depth, "No invader can move");
|
||||
progress |= decision.Update(false);
|
||||
}
|
||||
|
||||
return progress;
|
||||
|
@ -627,10 +457,8 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
private bool ResolveDoesMoveHavePath(
|
||||
HasPath decision,
|
||||
World world,
|
||||
MovementDecisions decisions,
|
||||
int depth)
|
||||
MovementDecisions decisions)
|
||||
{
|
||||
logger.Log(depth, "DoesMoveHavePath({0})", decision.Order);
|
||||
bool progress= false;
|
||||
|
||||
// If the origin and destination are adjacent, then there is a path.
|
||||
|
@ -641,8 +469,8 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
// Timeline adjacency
|
||||
&& decision.Order.Unit.Season.InAdjacentTimeline(decision.Order.Season))
|
||||
{
|
||||
bool update = LoggedUpdate(decision, true, depth, "Adjacent move");
|
||||
return progress | update;
|
||||
progress |= decision.Update(true);
|
||||
return progress;
|
||||
}
|
||||
|
||||
// If the origin and destination are not adjacent, then the decision resolves to whether
|
||||
|
@ -658,10 +486,8 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
private bool ResolveIsSupportGiven(
|
||||
GivesSupport decision,
|
||||
World world,
|
||||
MovementDecisions decisions,
|
||||
int depth)
|
||||
MovementDecisions decisions)
|
||||
{
|
||||
logger.Log(depth, "IsSupportGiven({0})", decision.Order);
|
||||
bool progress = false;
|
||||
|
||||
// Support is cut when a unit moves into the supporting unit's province with nonzero
|
||||
|
@ -670,13 +496,13 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
foreach (MoveOrder cut in decision.Cuts)
|
||||
{
|
||||
AttackStrength attack = decisions.AttackStrength[cut];
|
||||
progress |= ResolveDecision(attack, world, decisions, depth + 1);
|
||||
progress |= ResolveDecision(attack, world, decisions);
|
||||
|
||||
// If at least one attack has a nonzero minimum, the support decision can be resolved
|
||||
// to false.
|
||||
if (attack.MinValue > 0)
|
||||
{
|
||||
progress |= LoggedUpdate(decision, false, depth, "An attacker has nonzero attack strength");
|
||||
progress |= decision.Update(false);
|
||||
return progress;
|
||||
}
|
||||
|
||||
|
@ -690,10 +516,10 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
|
||||
// Support is also cut if the unit is dislodged.
|
||||
IsDislodged dislodge = decisions.IsDislodged[decision.Order.Unit];
|
||||
progress |= ResolveDecision(dislodge, world, decisions, depth + 1);
|
||||
progress |= ResolveDecision(dislodge, world, decisions);
|
||||
if (dislodge.Outcome == true)
|
||||
{
|
||||
progress |= LoggedUpdate(decision, false, depth, "Unit dislodged");
|
||||
progress |= decision.Update(false);
|
||||
return progress;
|
||||
}
|
||||
|
||||
|
@ -701,7 +527,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
// resolved to false, then the support is given.
|
||||
if (!potentialNonzeroAttack && dislodge.Outcome == false)
|
||||
{
|
||||
progress |= LoggedUpdate(decision, true, depth, "No successful attack or dislodge");
|
||||
progress |= decision.Update(true);
|
||||
return progress;
|
||||
}
|
||||
|
||||
|
@ -712,16 +538,14 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
private bool ResolveHoldStrength(
|
||||
HoldStrength decision,
|
||||
World world,
|
||||
MovementDecisions decisions,
|
||||
int depth)
|
||||
MovementDecisions decisions)
|
||||
{
|
||||
logger.Log(depth, "HoldStrength({0})", decision.Province);
|
||||
bool progress = false;
|
||||
|
||||
// If no unit is in the province, the hold strength is zero.
|
||||
if (decision.Order == null)
|
||||
{
|
||||
progress |= LoggedUpdate(decision, 0, 0, depth, "No unit in the province");
|
||||
progress |= decision.Update(0, 0);
|
||||
return progress;
|
||||
}
|
||||
|
||||
|
@ -729,13 +553,10 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
if (decision.Order is MoveOrder move)
|
||||
{
|
||||
DoesMove moves = decisions.DoesMove[move];
|
||||
progress |= ResolveDecision(moves, world, decisions, depth + 1);
|
||||
progress |= LoggedUpdate(
|
||||
decision,
|
||||
min: moves.Outcome != false ? 0 : 1,
|
||||
max: moves.Outcome == true ? 0 : 1,
|
||||
depth,
|
||||
"Updated based on unit's move success");
|
||||
progress |= ResolveDecision(moves, world, decisions);
|
||||
progress |= decision.Update(
|
||||
moves.Outcome != false ? 0 : 1,
|
||||
moves.Outcome == true ? 0 : 1);
|
||||
return progress;
|
||||
}
|
||||
// If a unit without a move order is in the province, add up the supports.
|
||||
|
@ -746,11 +567,11 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
foreach (SupportHoldOrder support in decision.Supports)
|
||||
{
|
||||
GivesSupport givesSupport = decisions.GivesSupport[support];
|
||||
progress |= ResolveDecision(givesSupport, world, decisions, depth + 1);
|
||||
progress |= ResolveDecision(givesSupport, world, decisions);
|
||||
if (givesSupport.Outcome == true) min += 1;
|
||||
if (givesSupport.Outcome != false) max += 1;
|
||||
}
|
||||
progress |= LoggedUpdate(decision, min, max, depth, "Updated based on unit's supports");
|
||||
progress |= decision.Update(min, max);
|
||||
return progress;
|
||||
}
|
||||
}
|
||||
|
@ -758,18 +579,16 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
private bool ResolveAttackStrength(
|
||||
AttackStrength decision,
|
||||
World world,
|
||||
MovementDecisions decisions,
|
||||
int depth)
|
||||
MovementDecisions decisions)
|
||||
{
|
||||
logger.Log(depth, "AttackStrength({0})", decision.Order);
|
||||
bool progress = false;
|
||||
|
||||
// If there is no path, the attack strength is zero.
|
||||
var hasPath = decisions.HasPath[decision.Order];
|
||||
progress |= ResolveDecision(hasPath, world, decisions, depth + 1);
|
||||
progress |= ResolveDecision(hasPath, world, decisions);
|
||||
if (hasPath.Outcome == false)
|
||||
{
|
||||
progress |= LoggedUpdate(decision, 0, 0, depth, "No path");
|
||||
progress |= decision.Update(0, 0);
|
||||
return progress;
|
||||
}
|
||||
|
||||
|
@ -782,7 +601,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
: null;
|
||||
if (destMoveAway != null)
|
||||
{
|
||||
progress |= ResolveDecision(destMoveAway, world, decisions, depth + 1);
|
||||
progress |= ResolveDecision(destMoveAway, world, decisions);
|
||||
}
|
||||
if (// In any case here, there will have to be a unit at the destination with an order,
|
||||
// which means that destOrder will have to be populated. Including this in the if
|
||||
|
@ -799,7 +618,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
if (decision.Order.Unit.Power == destPower)
|
||||
{
|
||||
// Cannot dislodge own unit.
|
||||
progress |= LoggedUpdate(decision, 0, 0, depth, "Cannot dislodge own unit");
|
||||
progress |= decision.Update(0, 0);
|
||||
return progress;
|
||||
}
|
||||
else
|
||||
|
@ -811,11 +630,11 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
{
|
||||
if (support.Unit.Power == destPower) continue;
|
||||
GivesSupport givesSupport = decisions.GivesSupport[support];
|
||||
progress |= ResolveDecision(givesSupport, world, decisions, depth + 1);
|
||||
progress |= ResolveDecision(givesSupport, world, decisions);
|
||||
if (givesSupport.Outcome == true) min += 1;
|
||||
if (givesSupport.Outcome != false) max += 1;
|
||||
}
|
||||
progress |= LoggedUpdate(decision, min, max, depth, "Updated with supports from other powers");
|
||||
progress |= decision.Update(min, max);
|
||||
return progress;
|
||||
}
|
||||
}
|
||||
|
@ -831,13 +650,13 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
foreach (SupportMoveOrder support in decision.Supports)
|
||||
{
|
||||
GivesSupport givesSupport = decisions.GivesSupport[support];
|
||||
progress |= ResolveDecision(givesSupport, world, decisions, depth + 1);
|
||||
progress |= ResolveDecision(givesSupport, world, decisions);
|
||||
if (support.Unit.Power != destPower && givesSupport.Outcome == true) min += 1;
|
||||
if (givesSupport.Outcome != false) max += 1;
|
||||
}
|
||||
// Force min to zero in case of an attempt to disloge a unit of the same power.
|
||||
if (decision.Order.Unit.Power == destPower) min = 0;
|
||||
progress |= LoggedUpdate(decision, min, max, depth, "Updated with supports");
|
||||
progress |= decision.Update(min, max);
|
||||
return progress;
|
||||
}
|
||||
else
|
||||
|
@ -849,11 +668,11 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
foreach (SupportMoveOrder support in decision.Supports)
|
||||
{
|
||||
GivesSupport givesSupport = decisions.GivesSupport[support];
|
||||
progress |= ResolveDecision(givesSupport, world, decisions, depth + 1);
|
||||
progress |= ResolveDecision(givesSupport, world, decisions);
|
||||
if (givesSupport.Outcome == true) min += 1;
|
||||
if (givesSupport.Outcome != false) max += 1;
|
||||
}
|
||||
progress |= LoggedUpdate(decision, min, max, depth, "Updated with supports from all powers");
|
||||
progress |= decision.Update(min, max);
|
||||
return progress;
|
||||
}
|
||||
}
|
||||
|
@ -861,10 +680,8 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
private bool ResolveDefendStrength(
|
||||
DefendStrength decision,
|
||||
World world,
|
||||
MovementDecisions decisions,
|
||||
int depth)
|
||||
MovementDecisions decisions)
|
||||
{
|
||||
logger.Log(depth, "DefendStrength({0})", decision.Order);
|
||||
bool progress = false;
|
||||
|
||||
// The defend strength is equal to one plus, at least, the number of known successful
|
||||
|
@ -874,11 +691,11 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
foreach (SupportMoveOrder support in decision.Supports)
|
||||
{
|
||||
GivesSupport givesSupport = decisions.GivesSupport[support];
|
||||
progress |= ResolveDecision(givesSupport, world, decisions, depth + 1);
|
||||
progress |= ResolveDecision(givesSupport, world, decisions);
|
||||
if (givesSupport.Outcome == true) min += 1;
|
||||
if (givesSupport.Outcome != false) max += 1;
|
||||
}
|
||||
progress |= LoggedUpdate(decision, min, max, depth, "Updated based on unit's supports");
|
||||
progress |= decision.Update(min, max);
|
||||
|
||||
return progress;
|
||||
}
|
||||
|
@ -886,18 +703,16 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
private bool ResolvePreventStrength(
|
||||
PreventStrength decision,
|
||||
World world,
|
||||
MovementDecisions decisions,
|
||||
int depth)
|
||||
MovementDecisions decisions)
|
||||
{
|
||||
logger.Log(depth, "PreventStrength({0})", decision.Order);
|
||||
bool progress = false;
|
||||
|
||||
// If there is no path, the prevent strength is zero.
|
||||
var hasPath = decisions.HasPath[decision.Order];
|
||||
progress |= ResolveDecision(hasPath, world, decisions, depth + 1);
|
||||
progress |= ResolveDecision(hasPath, world, decisions);
|
||||
if (hasPath.Outcome == false)
|
||||
{
|
||||
progress |= LoggedUpdate(decision, 0, 0, depth, "No path to prevent");
|
||||
progress |= decision.Update(0, 0);
|
||||
return progress;
|
||||
}
|
||||
|
||||
|
@ -906,7 +721,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
if (decision.OpposingMove != null
|
||||
&& decisions.DoesMove[decision.OpposingMove].Outcome == true)
|
||||
{
|
||||
progress |= LoggedUpdate(decision, 0, 0, depth, "Cannot prevent in lost head-to-head");
|
||||
progress |= decision.Update(0, 0);
|
||||
return progress;
|
||||
}
|
||||
|
||||
|
@ -918,7 +733,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
foreach (SupportMoveOrder support in decision.Supports)
|
||||
{
|
||||
GivesSupport givesSupport = decisions.GivesSupport[support];
|
||||
progress |= ResolveDecision(givesSupport, world, decisions, depth + 1);
|
||||
progress |= ResolveDecision(givesSupport, world, decisions);
|
||||
if (givesSupport.Outcome == true) min += 1;
|
||||
if (givesSupport.Outcome != false) max += 1;
|
||||
}
|
||||
|
@ -932,7 +747,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
min = 0;
|
||||
}
|
||||
|
||||
progress |= LoggedUpdate(decision, min, max, depth, "Updated based on unit's supports");
|
||||
progress |= decision.Update(min, max);
|
||||
|
||||
return progress;
|
||||
}
|
||||
|
@ -940,15 +755,13 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
private bool ResolveDoesUnitMove(
|
||||
DoesMove decision,
|
||||
World world,
|
||||
MovementDecisions decisions,
|
||||
int depth)
|
||||
MovementDecisions decisions)
|
||||
{
|
||||
logger.Log(depth, "DoesUnitMove({0})", decision.Order);
|
||||
bool progress = false;
|
||||
|
||||
// Resolve the move's attack strength.
|
||||
AttackStrength attack = decisions.AttackStrength[decision.Order];
|
||||
progress |= ResolveDecision(attack, world, decisions, depth + 1);
|
||||
progress |= ResolveDecision(attack, world, decisions);
|
||||
|
||||
// In a head to head battle, the threshold for the attack strength to beat is the opposing
|
||||
// defend strength. Outside a head to head battle, the threshold is the destination's hold
|
||||
|
@ -956,12 +769,12 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
NumericAdjudicationDecision defense = decision.OpposingMove != null
|
||||
? decisions.DefendStrength[decision.OpposingMove]
|
||||
: decisions.HoldStrength[decision.Order.Point];
|
||||
progress |= ResolveDecision(defense, world, decisions, depth + 1);
|
||||
progress |= ResolveDecision(defense, world, decisions);
|
||||
|
||||
// If the attack doesn't beat the defense, resolve the move to false.
|
||||
if (attack.MaxValue <= defense.MinValue)
|
||||
{
|
||||
progress |= LoggedUpdate(decision, false, depth, "Attack can't beat defense");
|
||||
progress |= decision.Update(false);
|
||||
return progress;
|
||||
}
|
||||
|
||||
|
@ -970,11 +783,11 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
foreach (MoveOrder order in decision.Competing)
|
||||
{
|
||||
PreventStrength prevent = decisions.PreventStrength[order];
|
||||
progress |= ResolveDecision(prevent, world, decisions, depth + 1);
|
||||
progress |= ResolveDecision(prevent, world, decisions);
|
||||
// If attack doesn't beat the prevent, resolve the move to false.
|
||||
if (attack.MaxValue <= prevent.MinValue)
|
||||
{
|
||||
progress |= LoggedUpdate(decision, false, depth, "Attack can't beat prevent");
|
||||
progress |= decision.Update(false);
|
||||
return progress;
|
||||
}
|
||||
// If the attack doesn't beat the prevent, it can't resolve to true.
|
||||
|
@ -986,11 +799,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
|
||||
// If the attack didn't resolve to false because the defense or a prevent beat it, then
|
||||
// attempt to resolve it to true based on whether it beat the defense and all prevents.
|
||||
progress |= LoggedUpdate(
|
||||
decision,
|
||||
attack.MinValue > defense.MaxValue && beatsAllCompetingMoves,
|
||||
depth,
|
||||
"Updated based on competing moves");
|
||||
progress |= decision.Update(attack.MinValue > defense.MaxValue && beatsAllCompetingMoves);
|
||||
return progress;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
namespace MultiversalDiplomacy.Model;
|
||||
|
||||
public static class ModelExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Short representation of a <see cref="UnitType"/>.
|
||||
/// </summary>
|
||||
public static string ToShort(this UnitType unitType)
|
||||
=> unitType switch
|
||||
{
|
||||
UnitType.Army => "A",
|
||||
UnitType.Fleet => "F",
|
||||
_ => throw new NotSupportedException($"Unknown unit type {unitType}"),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Short representation of a multiversal location.
|
||||
/// </summary>
|
||||
public static string ToShort(this (Province province, Season season) coord)
|
||||
{
|
||||
return $"{coord.season.Timeline}-{coord.province.Abbreviations[0]}@{coord.season.Turn}";
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
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()
|
||||
{
|
||||
return $"{this.Timeline}@{this.Turn}";
|
||||
return $"{this.Turn}:{this.Timeline}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -51,7 +51,7 @@ public class Unit
|
|||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{this.Power.Name[0]} {this.Type.ToShort()} {(this.Province, this.Season).ToShort()}";
|
||||
return $"{this.Power} {this.Type} {this.Province} {this.Season}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -42,7 +42,7 @@ public class World
|
|||
/// <summary>
|
||||
/// Orders given to units in each season.
|
||||
/// </summary>
|
||||
public ReadOnlyDictionary<Season, OrderHistory> OrderHistory { get; }
|
||||
public ReadOnlyDictionary<Season, ReadOnlyCollection<Order>> GivenOrders { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Immutable game options.
|
||||
|
@ -59,7 +59,7 @@ public class World
|
|||
Season rootSeason,
|
||||
ReadOnlyCollection<Unit> units,
|
||||
ReadOnlyCollection<RetreatingUnit> retreatingUnits,
|
||||
ReadOnlyDictionary<Season, OrderHistory> orderHistory,
|
||||
ReadOnlyDictionary<Season, ReadOnlyCollection<Order>> givenOrders,
|
||||
Options options)
|
||||
{
|
||||
this.Provinces = provinces;
|
||||
|
@ -68,7 +68,7 @@ public class World
|
|||
this.RootSeason = rootSeason;
|
||||
this.Units = units;
|
||||
this.RetreatingUnits = retreatingUnits;
|
||||
this.OrderHistory = orderHistory;
|
||||
this.GivenOrders = givenOrders;
|
||||
this.Options = options;
|
||||
}
|
||||
|
||||
|
@ -82,7 +82,7 @@ public class World
|
|||
ReadOnlyCollection<Season>? seasons = null,
|
||||
ReadOnlyCollection<Unit>? units = null,
|
||||
ReadOnlyCollection<RetreatingUnit>? retreatingUnits = null,
|
||||
ReadOnlyDictionary<Season, OrderHistory>? orderHistory = null,
|
||||
ReadOnlyDictionary<Season, ReadOnlyCollection<Order>>? givenOrders = 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,
|
||||
orderHistory ?? previous.OrderHistory,
|
||||
givenOrders ?? previous.GivenOrders,
|
||||
options ?? previous.Options)
|
||||
{
|
||||
}
|
||||
|
@ -109,7 +109,7 @@ public class World
|
|||
root,
|
||||
new(new List<Unit>()),
|
||||
new(new List<RetreatingUnit>()),
|
||||
new(new Dictionary<Season, OrderHistory>()),
|
||||
new(new Dictionary<Season, ReadOnlyCollection<Order>>()),
|
||||
new Options());
|
||||
}
|
||||
|
||||
|
@ -123,7 +123,7 @@ public class World
|
|||
IEnumerable<Season>? seasons = null,
|
||||
IEnumerable<Unit>? units = null,
|
||||
IEnumerable<RetreatingUnit>? retreats = null,
|
||||
IEnumerable<KeyValuePair<Season, OrderHistory>>? orders = null)
|
||||
IEnumerable<KeyValuePair<Season, ReadOnlyCollection<Order>>>? orders = null)
|
||||
=> new World(
|
||||
previous: this,
|
||||
seasons: seasons == null
|
||||
|
@ -135,8 +135,8 @@ public class World
|
|||
retreatingUnits: retreats == null
|
||||
? this.RetreatingUnits
|
||||
: new(retreats.ToList()),
|
||||
orderHistory: orders == null
|
||||
? this.OrderHistory
|
||||
givenOrders: orders == null
|
||||
? this.GivenOrders
|
||||
: new(orders.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)));
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
|
|
@ -37,6 +37,6 @@ public class ConvoyOrder : UnitOrder
|
|||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{this.Unit} C {this.Target} -> {(this.Province, this.Season).ToShort()}";
|
||||
return $"{this.Unit} convoys {this.Target} -> {this.Province} {this.Season}";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ public class MoveOrder : UnitOrder
|
|||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{this.Unit} -> {(this.Province, this.Season).ToShort()}";
|
||||
return $"{this.Unit} -> {this.Province} {this.Season}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -14,6 +14,6 @@ public class SupportHoldOrder : SupportOrder
|
|||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{this.Unit} S {this.Target}";
|
||||
return $"{this.Unit} supports {this.Target}";
|
||||
}
|
||||
}
|
|
@ -36,7 +36,7 @@ public class SupportMoveOrder : SupportOrder
|
|||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{this.Unit} S {this.Target} -> {(this.Province, this.Season).ToShort()}";
|
||||
return $"{this.Unit} supports {this.Target} -> {this.Province} {this.Season}";
|
||||
}
|
||||
|
||||
public bool IsSupportFor(MoveOrder move)
|
||||
|
|
|
@ -195,7 +195,7 @@ public class TimeTravelTest
|
|||
setup[(0, 0)]
|
||||
.GetReference(out var s0_0)
|
||||
["England"].Army("Lon").Holds()
|
||||
["Austria"].Army("Tyr").Holds()
|
||||
["Austria"].Army("Boh").Holds()
|
||||
["Germany"].Army("Mun").Holds()
|
||||
.Execute()
|
||||
[(1, 0)]
|
||||
|
@ -288,6 +288,7 @@ public class TimeTravelTest
|
|||
// The attack on Mun 2:1 is repelled, but the support is cut.
|
||||
setup.ValidateOrders();
|
||||
Assert.That(tyr2_2, Is.Valid);
|
||||
Assert.That(mun2_1, Is.Valid);
|
||||
setup.AdjudicateOrders();
|
||||
Assert.That(tyr2_2, Is.Repelled);
|
||||
Assert.That(mun2_1, Is.NotDislodged);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
|
|
150
README.md
150
README.md
|
@ -1,29 +1,151 @@
|
|||
# 5D Diplomacy With Multiversal Time Travel
|
||||
|
||||
_5D Diplomacy with Multiversal Time Travel_ is a _Diplomacy_ variant that adds multiversal time travel in the style of its namesake, _5D Chess with Multiversal Time Travel_.
|
||||
## So you want to conquer Europe with a declarative build system
|
||||
|
||||
## Acknowledgements
|
||||
Let's start out by initializing the project. I always hate this part of projects; it's much easier to pick up something with an established codebase and ecosystem and figure out how to modify it to be slightly different than it is to strain genius from the empty space of possibility _de novo_. The ultimate goal of this project is summoning military aid from beyond space and time, though, so we're going to have to get used to it.
|
||||
|
||||
This project was inspired by [Oliver Lugg's proof-of-concept version](https://github.com/Oliveriver/5d-diplomacy-with-multiverse-time-travel). The implementation is based on the algorithms described by Lucas B. Kruijswijk in the chapter "The Process of Adjudication" found in the [Diplomacy Adjudicator Test Cases](http://web.inter.nl.net/users/L.B.Kruijswijk/#5) as well as ["The Math of Adjudication"](http://uk.diplom.org/pouch/Zine/S2009M/Kruijswijk/DipMath_Chp1.htm). Some of the data model is inspired by that of Martin Bruse's [godip](https://github.com/zond/godip).
|
||||
A `nix flake init` gives us a fairly useless flake template:
|
||||
|
||||
## Variant rules
|
||||
```
|
||||
{
|
||||
description = "A very basic flake";
|
||||
|
||||
### Multiversal time travel and timeline forks
|
||||
outputs = { self, nixpkgs }: {
|
||||
|
||||
_Diplomacy_ is played on a single board, on which are placed armies and fleets. Sequential sets of orders modify the positions of these units, changing the board as time progresses. This may be described as something like an "inner" view of a single timeline. Consider instead the view from "above" the timeline, from which each successive state of the game board is comprehended in sequence. From "above", each turn from the beginning of the game to the present can be considered separately. In _5D Diplomacy with Multiversal Time Travel_, units moving to another province may also move to another turn, potentially changing the past.
|
||||
packages.x86_64-linux.hello = nixpkgs.legacyPackages.x86_64-linux.hello;
|
||||
|
||||
If the outcome of a battle in the past of a timeline is changed by time travel, then the subsequent future will be different. Since the future of the original outcome is already determined, history forks, and the alternate future proceeds in an alternate timeline.
|
||||
defaultPackage.x86_64-linux = self.packages.x86_64-linux.hello;
|
||||
|
||||
Just as units in _Diplomacy_ may only move to adjacent spaces, units in _5D Diplomacy with Multiversal Time Travel_ may only move to adjacent times. For the purposes of attacking, supporting, or convoying, turns within one season of each other adjacent. Branching timelines and the timelines they branched off of are adjacent, as well as timelines that branched off of the same turn in the same timeline. A unit cannot move to the province it is currently in, but it can move to the same province in another turn or another timeline.
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
When a unit changes the outcome of a battle in the past, only the timeline of the battle forks. If an army from one timeline dislodges an army in the past of a second timeline that was supporting a move in a third timeline, an alternate future is created where the army in the second timeline is dislodged. The third timeline does not fork, since the support was given in the original timeline. Similarly, if a unit moves into another timeline and causes a previously-successful move from a third timeline to become a bounce, the destination timeline forks because the outcome of the move changed, but the newly-bounced unit's origin timeline does not fork because the move succeeded in the original timeline.
|
||||
We're going to replace every line in this file, but at least we got a start. Let's also `git init` and set that part up.
|
||||
|
||||
### Sustaining timelines and time centers
|
||||
```
|
||||
$ git init
|
||||
$ git config --add user.name Jaculabilis
|
||||
$ git config --add user.email jaculabilis@git.alogoulogoi.com
|
||||
$ git add flake.nix README.md
|
||||
$ git commit -m "Initial commit"
|
||||
$ git remote add origin gitea@git.alogoulogoi.com:Jaculabilis/5dplomacy.git
|
||||
$ git push -u origin master
|
||||
```
|
||||
|
||||
Since there are many ways to create new timelines, the game would rapidly expand beyond all comprehension if this were not counterbalanced in some way. This happens during the _sustain phase_, which occurs after the fall movement and retreat phases and before the winter buid/disband phase.
|
||||
We're doing this in .NET, so we need the .NET SDK. To do that, we're going to delcare a development environment in the flake config.
|
||||
|
||||
(TODO)
|
||||
```
|
||||
inputs.flake-utils.url = "github:numtide/flake-utils";
|
||||
|
||||
### Victory conditions
|
||||
outputs = { self, nixpkgs, flake-utils }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let pkgs = nixpkgs.legacyPackages.${system};
|
||||
in rec {
|
||||
devShell = pkgs.mkShell {
|
||||
packages = [ pkgs.dotnet-sdk ];
|
||||
};
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
The Great Powers of Europe can only wage multiversal wars because they are lead by extradimensional beings masquerading as human politicians. When a country is eliminated in one timeline, its extradimensional leader is executed, killing them in all timelines.
|
||||
Declaring `inputs.flake-utils` adds the `flake-utils` package as a dependency, which just gives us Nix helper functions. What's important here is that the `packages.x86_64-linux.hello` above has been abstracted away behind the `eachDefaultSystem` function: now we define our outputs with the `system` input as context, and flake-utils will define our outputs for each default system.
|
||||
|
||||
Basically, stripping the boilerplate, we're just doing this:
|
||||
|
||||
```
|
||||
rec {
|
||||
devShell = pkgs.mkShell {
|
||||
packages = [ pkgs.dotnet-sdk ];
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
`pkgs.mkShell` is the derivation builder that creates shell environments. It takes `packages` as a list of input packages that will be made available in the shell environment it creates. We add `dotnet-sdk` to this, commit the changes to git, and enter our new shell with `nix develop`:
|
||||
|
||||
```
|
||||
$ which dotnet
|
||||
/nix/store/87s452c8wj2zmy21q8q394f6rzf5y1br-dotnet-sdk-6.0.100/bin/dotnet
|
||||
```
|
||||
|
||||
So now we have our development tools (well, tool). The `dotnet --help` text tells us a few things about telemetry, so let's define a prompt so we know when we're in the nix shell and then set the telemetry opt-out.
|
||||
|
||||
```
|
||||
shellHook = ''
|
||||
PS1="5dplomacy:\W$ "
|
||||
'';
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT = 1;
|
||||
```
|
||||
|
||||
Now let's start creating the project. dotnet has a lot of template options. We'll eventually want to have a web client and server, for true multiplayer, but first we want to build the core infrastructure that we can slap a server on top of. So, we're just going to make a console app for now.
|
||||
|
||||
```
|
||||
5dplomacy$ dotnet new console -n MultiversalDiplomacy -o MultiversalDiplomacy
|
||||
```
|
||||
|
||||
.NET 6 makes the Main() method implicit, but this program is going to become more complicated than a single Main(), so let's put the whole boilerplate back.
|
||||
|
||||
```
|
||||
using System;
|
||||
|
||||
namespace MultiversalDiplomacy
|
||||
{
|
||||
internal class Program
|
||||
{
|
||||
static void Main(string[] args)
|
||||
{
|
||||
Console.WriteLine("stab");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
And when we run it through `dotnet`:
|
||||
|
||||
```
|
||||
5dplomacy$ dotnet run --project MultiversalDiplomacy/
|
||||
stab
|
||||
```
|
||||
|
||||
Neat. VS Code doesn't seamlessly work with nix, so to get the extensions working we'll need to restart it from an existing `nix develop` shell so `dotnet` is on the path. For the integrated terminal, we can creating a profile for `nix develop` and set it as the default profile.
|
||||
|
||||
```
|
||||
"terminal.integrated.profiles.linux": {
|
||||
"nix develop": {
|
||||
"path": "nix",
|
||||
"args": ["develop"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Comprehending all of time and space
|
||||
|
||||
Now for the data model. The state of the world can be described in three layers, so to speak.
|
||||
|
||||
1. The board is the same across time and multiverse. Since it consists of (i) provinces and (ii) borders connecting two provinces, we're going to model it like a graph. However, a simple graph won't work because provinces are differently accessible to armies and fleets, and coasts are part of the same province while having distinct connectivity. Following the data model described in [godip](https://github.com/zond/godip), we will model this by subdividing provinces and making connections between those subdivisions. This will effectively create multiple distinct graphs for fleets and armies within the map, with some nodes grouped for control purposes into provinces. Since the map itself does not change across time, we can make this "layer" completely immutable and shared between turns and timelines.
|
||||
2. Since we need to preserve the state of the past in order to time travel effectively, we won't be mutating a board state. Instead, we'll use orders submitted for each turn to append a new copy of the board state. This can't be represented by a simple list, since we can have more than one timeline branch off of a particular turn, so instead we'll use something like a directed graph and create turns pointing to their immediate past.
|
||||
3. Given the map of the board and the state of all timelines, all units have a spatial location on the board and a temporal location in one of the turns. As the game progresses and timelines are extended, we'll create copies of the unit in each turn it appears in.
|
||||
|
||||
This is going to get complicated, and we're looking forward to implementing the [Diplomacy Adjudicator Test Cases](http://web.inter.nl.net/users/L.B.Kruijswijk/), so let's also create a test project:
|
||||
|
||||
```
|
||||
5dplomacy:5dplomacy$ dotnet new nunit --name MultiversalDiplomacyTests --output MultiversalDiplomacyTests
|
||||
```
|
||||
|
||||
I think dotnet will fetch NUnit when it needs it, but to get it into our environment so VS Code recognizes it, we add it to the nix shell:
|
||||
|
||||
```
|
||||
packages = [ pkgs.dotnet-sdk pkgs.dotnetPackages.NUnit3 ];
|
||||
```
|
||||
|
||||
After writing some basic tests, we can run them with:
|
||||
|
||||
```
|
||||
5dplomacy:5dplomacy$ dotnet test MultiversalDiplomacyTests/
|
||||
[...]
|
||||
Starting test execution, please wait...
|
||||
A total of 1 test files matched the specified pattern.
|
||||
|
||||
Passed! - Failed: 0, Passed: 2, Skipped: 0, Total: 2, Duration: 29 ms - 5dplomacy/MultiversalDiplomacyTests/bin/Debug/net6.0/MultiversalDiplomacyTests.dll (net6.0)
|
||||
```
|
||||
|
||||
Neat.
|
||||
|
|
18
flake.lock
18
flake.lock
|
@ -2,11 +2,11 @@
|
|||
"nodes": {
|
||||
"flake-utils": {
|
||||
"locked": {
|
||||
"lastModified": 1667395993,
|
||||
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
|
||||
"lastModified": 1644229661,
|
||||
"narHash": "sha256-1YdnJAsNy69bpcjuoKdOYQX0YxZBiCYZo4Twxerqv7k=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
|
||||
"rev": "3cecb5b042f7f209c56ffd8371b2711a290ec797",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -17,18 +17,16 @@
|
|||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1717179513,
|
||||
"narHash": "sha256-vboIEwIQojofItm2xGCdZCzW96U85l9nDW3ifMuAIdM=",
|
||||
"lastModified": 1644486793,
|
||||
"narHash": "sha256-EeijR4guVHgVv+JpOX3cQO+1XdrkJfGmiJ9XVsVU530=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "63dacb46bf939521bdc93981b4cbb7ecb58427a0",
|
||||
"rev": "1882c6b7368fd284ad01b0a5b5601ef136321292",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "24.05",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
"id": "nixpkgs",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
|
|
12
flake.nix
12
flake.nix
|
@ -1,7 +1,6 @@
|
|||
{
|
||||
description = "5D Diplomacy With Multiversal Time Travel";
|
||||
|
||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/24.05";
|
||||
inputs.flake-utils.url = "github:numtide/flake-utils";
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils }:
|
||||
|
@ -9,14 +8,11 @@
|
|||
let pkgs = nixpkgs.legacyPackages.${system};
|
||||
in rec {
|
||||
devShell = pkgs.mkShell {
|
||||
shellHook = ''
|
||||
PS1="5dplomacy:\W$ "
|
||||
'';
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT = 1;
|
||||
NIX_LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ pkgs.stdenv.cc.cc ];
|
||||
NIX_LD = builtins.readFile "${pkgs.stdenv.cc}/nix-support/dynamic-linker";
|
||||
packages = [
|
||||
pkgs.bashInteractive
|
||||
pkgs.dotnet-sdk_8
|
||||
pkgs.dotnetPackages.NUnit3
|
||||
];
|
||||
packages = [ pkgs.dotnet-sdk pkgs.dotnetPackages.NUnit3 ];
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue