Compare commits
25 Commits
c4a14f1d7e
...
5d311bc08c
Author | SHA1 | Date |
---|---|---|
Jaculabilis | 5d311bc08c | |
Tim Van Baak | 3db01c0ffd | |
Tim Van Baak | 0fd9c93a70 | |
Tim Van Baak | 5b4758a4ed | |
Tim Van Baak | 7b2176b1d2 | |
Tim Van Baak | 4bbd29ac93 | |
Tim Van Baak | 26e268c3a0 | |
Jaculabilis | 0f201610d2 | |
Jaculabilis | a63ff3992b | |
Jaculabilis | b241d206f4 | |
Jaculabilis | 069cb4c548 | |
Jaculabilis | 95ed8c7682 | |
Jaculabilis | 39c3aabe45 | |
Jaculabilis | 7471a035f0 | |
Jaculabilis | a565ee1b05 | |
Jaculabilis | 25d707b3b8 | |
Jaculabilis | c0a9330d2e | |
Jaculabilis | 23a826c815 | |
Jaculabilis | 46c28a087c | |
Jaculabilis | 6347b52d4a | |
Jaculabilis | 63289bce54 | |
Jaculabilis | 2e1d72d0f4 | |
Jaculabilis | 94037959e1 | |
Jaculabilis | 15fde7340c | |
Jaculabilis | 18c11c7ffd |
|
@ -2,7 +2,15 @@
|
||||||
"terminal.integrated.profiles.linux": {
|
"terminal.integrated.profiles.linux": {
|
||||||
"nix develop": {
|
"nix develop": {
|
||||||
"path": "nix",
|
"path": "nix",
|
||||||
"args": ["develop"]
|
"args": ["develop", "--impure"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"terminal.integrated.profiles.windows": {
|
||||||
|
"nix develop": {
|
||||||
|
"path": [
|
||||||
|
"${env:windir}\\System32\\wsl.exe"
|
||||||
|
],
|
||||||
|
"args": ["-d", "NixOS", "--", "nix", "develop"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -2,9 +2,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Version 16
|
# Visual Studio Version 16
|
||||||
VisualStudioVersion = 16.0.30114.105
|
VisualStudioVersion = 16.0.30114.105
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
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
|
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
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
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,6 +8,9 @@ public class AttackStrength : NumericAdjudicationDecision
|
||||||
public List<SupportMoveOrder> Supports { get; }
|
public List<SupportMoveOrder> Supports { get; }
|
||||||
public MoveOrder? OpposingMove { get; }
|
public MoveOrder? OpposingMove { get; }
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
=> $"AttackStrength({Order})";
|
||||||
|
|
||||||
public AttackStrength(MoveOrder order, IEnumerable<SupportMoveOrder> supports, MoveOrder? opposingMove = null)
|
public AttackStrength(MoveOrder order, IEnumerable<SupportMoveOrder> supports, MoveOrder? opposingMove = null)
|
||||||
{
|
{
|
||||||
this.Order = order;
|
this.Order = order;
|
||||||
|
|
|
@ -6,11 +6,6 @@ public abstract class BinaryAdjudicationDecision : AdjudicationDecision
|
||||||
|
|
||||||
public override bool Resolved => this.Outcome != null;
|
public override bool Resolved => this.Outcome != null;
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
{
|
|
||||||
return $"{this.GetType().Name}={this.Outcome}";
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Update(bool outcome)
|
public bool Update(bool outcome)
|
||||||
{
|
{
|
||||||
if (this.Outcome == null)
|
if (this.Outcome == null)
|
||||||
|
|
|
@ -7,6 +7,9 @@ public class DefendStrength : NumericAdjudicationDecision
|
||||||
public MoveOrder Order { get; }
|
public MoveOrder Order { get; }
|
||||||
public List<SupportMoveOrder> Supports { get; }
|
public List<SupportMoveOrder> Supports { get; }
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
=> $"DefendStrength({Order})";
|
||||||
|
|
||||||
public DefendStrength(MoveOrder order, IEnumerable<SupportMoveOrder> supports)
|
public DefendStrength(MoveOrder order, IEnumerable<SupportMoveOrder> supports)
|
||||||
{
|
{
|
||||||
this.Order = order;
|
this.Order = order;
|
||||||
|
|
|
@ -8,6 +8,9 @@ public class DoesMove : BinaryAdjudicationDecision
|
||||||
public MoveOrder? OpposingMove { get; }
|
public MoveOrder? OpposingMove { get; }
|
||||||
public List<MoveOrder> Competing { get; }
|
public List<MoveOrder> Competing { get; }
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
=> $"DoesMove({Order})";
|
||||||
|
|
||||||
public DoesMove(MoveOrder order, MoveOrder? opposingMove, IEnumerable<MoveOrder> competing)
|
public DoesMove(MoveOrder order, MoveOrder? opposingMove, IEnumerable<MoveOrder> competing)
|
||||||
{
|
{
|
||||||
this.Order = order;
|
this.Order = order;
|
||||||
|
|
|
@ -7,6 +7,9 @@ public class GivesSupport : BinaryAdjudicationDecision
|
||||||
public SupportOrder Order { get; }
|
public SupportOrder Order { get; }
|
||||||
public List<MoveOrder> Cuts { get; }
|
public List<MoveOrder> Cuts { get; }
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
=> $"GivesSupport({Order})";
|
||||||
|
|
||||||
public GivesSupport(SupportOrder order, IEnumerable<MoveOrder> cuts)
|
public GivesSupport(SupportOrder order, IEnumerable<MoveOrder> cuts)
|
||||||
{
|
{
|
||||||
this.Order = order;
|
this.Order = order;
|
||||||
|
|
|
@ -6,6 +6,9 @@ public class HasPath : BinaryAdjudicationDecision
|
||||||
{
|
{
|
||||||
public MoveOrder Order { get; }
|
public MoveOrder Order { get; }
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
=> $"HasPath({Order})";
|
||||||
|
|
||||||
public HasPath(MoveOrder order)
|
public HasPath(MoveOrder order)
|
||||||
{
|
{
|
||||||
this.Order = order;
|
this.Order = order;
|
||||||
|
|
|
@ -10,6 +10,11 @@ public class HoldStrength : NumericAdjudicationDecision
|
||||||
public UnitOrder? Order { get; }
|
public UnitOrder? Order { get; }
|
||||||
public List<SupportHoldOrder> Supports { 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)
|
public HoldStrength(Province province, Season season, UnitOrder? order = null)
|
||||||
{
|
{
|
||||||
this.Province = province;
|
this.Province = province;
|
||||||
|
|
|
@ -7,6 +7,9 @@ public class IsDislodged : BinaryAdjudicationDecision
|
||||||
public UnitOrder Order { get; }
|
public UnitOrder Order { get; }
|
||||||
public List<MoveOrder> Incoming { get; }
|
public List<MoveOrder> Incoming { get; }
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
=> $"IsDislodged({Order.Unit})";
|
||||||
|
|
||||||
public IsDislodged(UnitOrder order, IEnumerable<MoveOrder> incoming)
|
public IsDislodged(UnitOrder order, IEnumerable<MoveOrder> incoming)
|
||||||
{
|
{
|
||||||
this.Order = order;
|
this.Order = order;
|
||||||
|
|
|
@ -13,139 +13,145 @@ public class MovementDecisions
|
||||||
public Dictionary<MoveOrder, DefendStrength> DefendStrength { get; }
|
public Dictionary<MoveOrder, DefendStrength> DefendStrength { get; }
|
||||||
public Dictionary<MoveOrder, PreventStrength> PreventStrength { get; }
|
public Dictionary<MoveOrder, PreventStrength> PreventStrength { get; }
|
||||||
public Dictionary<MoveOrder, DoesMove> DoesMove { get; }
|
public Dictionary<MoveOrder, DoesMove> DoesMove { 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(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();
|
||||||
|
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)
|
||||||
|
.Distinct()
|
||||||
|
.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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,9 @@ public class PreventStrength : NumericAdjudicationDecision
|
||||||
public List<SupportMoveOrder> Supports { get; }
|
public List<SupportMoveOrder> Supports { get; }
|
||||||
public MoveOrder? OpposingMove { get; }
|
public MoveOrder? OpposingMove { get; }
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
=> $"PreventStrength({Order})";
|
||||||
|
|
||||||
public PreventStrength(MoveOrder order, IEnumerable<SupportMoveOrder> supports, MoveOrder? opposingMove = null)
|
public PreventStrength(MoveOrder order, IEnumerable<SupportMoveOrder> supports, MoveOrder? opposingMove = null)
|
||||||
{
|
{
|
||||||
this.Order = order;
|
this.Order = order;
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
namespace MultiversalDiplomacy.Adjudicate.Logging;
|
||||||
|
|
||||||
|
public interface IAdjudicatorLogger
|
||||||
|
{
|
||||||
|
public void Log(int contextLevel, string message, params object[] args);
|
||||||
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
|
|
||||||
using MultiversalDiplomacy.Adjudicate.Decision;
|
using MultiversalDiplomacy.Adjudicate.Decision;
|
||||||
|
using MultiversalDiplomacy.Adjudicate.Logging;
|
||||||
using MultiversalDiplomacy.Model;
|
using MultiversalDiplomacy.Model;
|
||||||
using MultiversalDiplomacy.Orders;
|
using MultiversalDiplomacy.Orders;
|
||||||
|
|
||||||
|
@ -11,7 +10,14 @@ namespace MultiversalDiplomacy.Adjudicate;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||||
{
|
{
|
||||||
public static IPhaseAdjudicator Instance { get; } = new MovementPhaseAdjudicator();
|
public static IPhaseAdjudicator Instance { get; } = new MovementPhaseAdjudicator(ConsoleLogger.Instance);
|
||||||
|
|
||||||
|
private IAdjudicatorLogger logger { get; }
|
||||||
|
|
||||||
|
public MovementPhaseAdjudicator(IAdjudicatorLogger logger)
|
||||||
|
{
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
public List<OrderValidation> ValidateOrders(World world, List<Order> orders)
|
public List<OrderValidation> ValidateOrders(World world, List<Order> orders)
|
||||||
{
|
{
|
||||||
|
@ -267,34 +273,42 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||||
|
|
||||||
public List<AdjudicationDecision> AdjudicateOrders(World world, List<Order> orders)
|
public List<AdjudicationDecision> AdjudicateOrders(World world, List<Order> orders)
|
||||||
{
|
{
|
||||||
|
logger.Log(0, "Beginning adjudication");
|
||||||
// Define all adjudication decisions to be made.
|
// Define all adjudication decisions to be made.
|
||||||
MovementDecisions decisions = new(world, orders);
|
MovementDecisions decisions = new(world, orders);
|
||||||
|
|
||||||
// Adjudicate all decisions.
|
// Adjudicate all decisions.
|
||||||
bool progress = false;
|
bool progress = false;
|
||||||
|
int loopNum = 1;
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
|
logger.Log(1, "Beginning loop {0}", loopNum++);
|
||||||
progress = false;
|
progress = false;
|
||||||
foreach (AdjudicationDecision decision in decisions.Values)
|
foreach (AdjudicationDecision decision in decisions.Values)
|
||||||
{
|
{
|
||||||
// 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);
|
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))
|
||||||
{
|
{
|
||||||
throw new ApplicationException("Some orders not resolved!");
|
throw new ApplicationException("Some orders not resolved!");
|
||||||
}
|
}
|
||||||
|
logger.Log(0, "Completed adjudication");
|
||||||
|
|
||||||
return decisions.Values.ToList();
|
return decisions.Values.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
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.
|
||||||
|
@ -302,25 +316,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;
|
||||||
|
@ -331,22 +357,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();
|
||||||
|
@ -355,15 +385,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
|
||||||
|
|
||||||
|
@ -371,33 +421,153 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(
|
private bool ResolveDecision(
|
||||||
AdjudicationDecision decision,
|
AdjudicationDecision decision,
|
||||||
World world,
|
World world,
|
||||||
MovementDecisions decisions)
|
MovementDecisions decisions,
|
||||||
=> decision.Resolved ? false : decision switch
|
int depth)
|
||||||
{
|
{
|
||||||
IsDislodged d => ResolveIsUnitDislodged(d, world, decisions),
|
logger.Log(depth, "ResolveDecision({0})", decision);
|
||||||
HasPath d => ResolveDoesMoveHavePath(d, world, decisions),
|
return decision.Resolved ? false : decision switch
|
||||||
GivesSupport d => ResolveIsSupportGiven(d, world, decisions),
|
{
|
||||||
HoldStrength d => ResolveHoldStrength(d, world, decisions),
|
AdvanceTimeline d => ResolveAdvanceTimeline(d, world, decisions, depth + 1),
|
||||||
AttackStrength d => ResolveAttackStrength(d, world, decisions),
|
IsDislodged d => ResolveIsUnitDislodged(d, world, decisions, depth + 1),
|
||||||
DefendStrength d => ResolveDefendStrength(d, world, decisions),
|
HasPath d => ResolveDoesMoveHavePath(d, world, decisions, depth + 1),
|
||||||
PreventStrength d => ResolvePreventStrength(d, world, decisions),
|
GivesSupport d => ResolveIsSupportGiven(d, world, decisions, depth + 1),
|
||||||
DoesMove d => ResolveDoesUnitMove(d, world, decisions),
|
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),
|
||||||
_ => throw new NotSupportedException($"Unknown decision type: {decision.GetType()}")
|
_ => 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(
|
private bool ResolveIsUnitDislodged(
|
||||||
IsDislodged decision,
|
IsDislodged decision,
|
||||||
World world,
|
World world,
|
||||||
MovementDecisions decisions)
|
MovementDecisions decisions,
|
||||||
|
int depth)
|
||||||
{
|
{
|
||||||
|
logger.Log(depth, "IsUnitDislodged({0})", decision.Order.Unit);
|
||||||
bool progress = false;
|
bool progress = false;
|
||||||
|
|
||||||
// If this unit was ordered to move and is doing so successfully, it cannot be dislodged
|
// If this unit was ordered to move and is doing so successfully, it cannot be dislodged
|
||||||
|
@ -405,13 +575,13 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||||
if (decision.Order is MoveOrder moveOrder)
|
if (decision.Order is MoveOrder moveOrder)
|
||||||
{
|
{
|
||||||
DoesMove move = decisions.DoesMove[moveOrder];
|
DoesMove move = decisions.DoesMove[moveOrder];
|
||||||
progress |= ResolveDecision(move, world, decisions);
|
progress |= ResolveDecision(move, world, decisions, depth + 1);
|
||||||
|
|
||||||
// If this unit received a move order and the move is successful, it cannot be
|
// If this unit received a move order and the move is successful, it cannot be
|
||||||
// dislodged.
|
// dislodged.
|
||||||
if (move.Outcome == true)
|
if (move.Outcome == true)
|
||||||
{
|
{
|
||||||
progress |= decision.Update(false);
|
progress |= LoggedUpdate(decision, false, depth, "Unit successfully moves");
|
||||||
return progress;
|
return progress;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -429,12 +599,12 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||||
foreach (MoveOrder dislodger in decision.Incoming)
|
foreach (MoveOrder dislodger in decision.Incoming)
|
||||||
{
|
{
|
||||||
DoesMove move = decisions.DoesMove[dislodger];
|
DoesMove move = decisions.DoesMove[dislodger];
|
||||||
progress |= ResolveDecision(move, world, decisions);
|
progress |= ResolveDecision(move, world, decisions, depth + 1);
|
||||||
|
|
||||||
// If at least one invader will move, this unit is dislodged.
|
// If at least one invader will move, this unit is dislodged.
|
||||||
if (move.Outcome == true)
|
if (move.Outcome == true)
|
||||||
{
|
{
|
||||||
progress |= decision.Update(true);
|
progress |= LoggedUpdate(decision, true, depth, "Invading unit successfully moves");
|
||||||
return progress;
|
return progress;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -448,7 +618,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||||
|
|
||||||
if (!potentialDislodger)
|
if (!potentialDislodger)
|
||||||
{
|
{
|
||||||
progress |= decision.Update(false);
|
progress |= LoggedUpdate(decision, false, depth, "No invader can move");
|
||||||
}
|
}
|
||||||
|
|
||||||
return progress;
|
return progress;
|
||||||
|
@ -457,8 +627,10 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||||
private bool ResolveDoesMoveHavePath(
|
private bool ResolveDoesMoveHavePath(
|
||||||
HasPath decision,
|
HasPath decision,
|
||||||
World world,
|
World world,
|
||||||
MovementDecisions decisions)
|
MovementDecisions decisions,
|
||||||
|
int depth)
|
||||||
{
|
{
|
||||||
|
logger.Log(depth, "DoesMoveHavePath({0})", decision.Order);
|
||||||
bool progress= false;
|
bool progress= false;
|
||||||
|
|
||||||
// If the origin and destination are adjacent, then there is a path.
|
// If the origin and destination are adjacent, then there is a path.
|
||||||
|
@ -469,8 +641,8 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||||
// Timeline adjacency
|
// Timeline adjacency
|
||||||
&& decision.Order.Unit.Season.InAdjacentTimeline(decision.Order.Season))
|
&& decision.Order.Unit.Season.InAdjacentTimeline(decision.Order.Season))
|
||||||
{
|
{
|
||||||
progress |= decision.Update(true);
|
bool update = LoggedUpdate(decision, true, depth, "Adjacent move");
|
||||||
return progress;
|
return progress | update;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the origin and destination are not adjacent, then the decision resolves to whether
|
// If the origin and destination are not adjacent, then the decision resolves to whether
|
||||||
|
@ -486,8 +658,10 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||||
private bool ResolveIsSupportGiven(
|
private bool ResolveIsSupportGiven(
|
||||||
GivesSupport decision,
|
GivesSupport decision,
|
||||||
World world,
|
World world,
|
||||||
MovementDecisions decisions)
|
MovementDecisions decisions,
|
||||||
|
int depth)
|
||||||
{
|
{
|
||||||
|
logger.Log(depth, "IsSupportGiven({0})", decision.Order);
|
||||||
bool progress = false;
|
bool progress = false;
|
||||||
|
|
||||||
// Support is cut when a unit moves into the supporting unit's province with nonzero
|
// Support is cut when a unit moves into the supporting unit's province with nonzero
|
||||||
|
@ -496,13 +670,13 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||||
foreach (MoveOrder cut in decision.Cuts)
|
foreach (MoveOrder cut in decision.Cuts)
|
||||||
{
|
{
|
||||||
AttackStrength attack = decisions.AttackStrength[cut];
|
AttackStrength attack = decisions.AttackStrength[cut];
|
||||||
progress |= ResolveDecision(attack, world, decisions);
|
progress |= ResolveDecision(attack, world, decisions, depth + 1);
|
||||||
|
|
||||||
// If at least one attack has a nonzero minimum, the support decision can be resolved
|
// If at least one attack has a nonzero minimum, the support decision can be resolved
|
||||||
// to false.
|
// to false.
|
||||||
if (attack.MinValue > 0)
|
if (attack.MinValue > 0)
|
||||||
{
|
{
|
||||||
progress |= decision.Update(false);
|
progress |= LoggedUpdate(decision, false, depth, "An attacker has nonzero attack strength");
|
||||||
return progress;
|
return progress;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -516,10 +690,10 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||||
|
|
||||||
// Support is also cut if the unit is dislodged.
|
// Support is also cut if the unit is dislodged.
|
||||||
IsDislodged dislodge = decisions.IsDislodged[decision.Order.Unit];
|
IsDislodged dislodge = decisions.IsDislodged[decision.Order.Unit];
|
||||||
progress |= ResolveDecision(dislodge, world, decisions);
|
progress |= ResolveDecision(dislodge, world, decisions, depth + 1);
|
||||||
if (dislodge.Outcome == true)
|
if (dislodge.Outcome == true)
|
||||||
{
|
{
|
||||||
progress |= decision.Update(false);
|
progress |= LoggedUpdate(decision, false, depth, "Unit dislodged");
|
||||||
return progress;
|
return progress;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -527,7 +701,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||||
// resolved to false, then the support is given.
|
// resolved to false, then the support is given.
|
||||||
if (!potentialNonzeroAttack && dislodge.Outcome == false)
|
if (!potentialNonzeroAttack && dislodge.Outcome == false)
|
||||||
{
|
{
|
||||||
progress |= decision.Update(true);
|
progress |= LoggedUpdate(decision, true, depth, "No successful attack or dislodge");
|
||||||
return progress;
|
return progress;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -538,14 +712,16 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||||
private bool ResolveHoldStrength(
|
private bool ResolveHoldStrength(
|
||||||
HoldStrength decision,
|
HoldStrength decision,
|
||||||
World world,
|
World world,
|
||||||
MovementDecisions decisions)
|
MovementDecisions decisions,
|
||||||
|
int depth)
|
||||||
{
|
{
|
||||||
|
logger.Log(depth, "HoldStrength({0})", decision.Province);
|
||||||
bool progress = false;
|
bool progress = false;
|
||||||
|
|
||||||
// If no unit is in the province, the hold strength is zero.
|
// If no unit is in the province, the hold strength is zero.
|
||||||
if (decision.Order == null)
|
if (decision.Order == null)
|
||||||
{
|
{
|
||||||
progress |= decision.Update(0, 0);
|
progress |= LoggedUpdate(decision, 0, 0, depth, "No unit in the province");
|
||||||
return progress;
|
return progress;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -553,10 +729,13 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||||
if (decision.Order is MoveOrder move)
|
if (decision.Order is MoveOrder move)
|
||||||
{
|
{
|
||||||
DoesMove moves = decisions.DoesMove[move];
|
DoesMove moves = decisions.DoesMove[move];
|
||||||
progress |= ResolveDecision(moves, world, decisions);
|
progress |= ResolveDecision(moves, world, decisions, depth + 1);
|
||||||
progress |= decision.Update(
|
progress |= LoggedUpdate(
|
||||||
moves.Outcome != false ? 0 : 1,
|
decision,
|
||||||
moves.Outcome == true ? 0 : 1);
|
min: moves.Outcome != false ? 0 : 1,
|
||||||
|
max: moves.Outcome == true ? 0 : 1,
|
||||||
|
depth,
|
||||||
|
"Updated based on unit's move success");
|
||||||
return progress;
|
return progress;
|
||||||
}
|
}
|
||||||
// If a unit without a move order is in the province, add up the supports.
|
// If a unit without a move order is in the province, add up the supports.
|
||||||
|
@ -567,11 +746,11 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||||
foreach (SupportHoldOrder support in decision.Supports)
|
foreach (SupportHoldOrder support in decision.Supports)
|
||||||
{
|
{
|
||||||
GivesSupport givesSupport = decisions.GivesSupport[support];
|
GivesSupport givesSupport = decisions.GivesSupport[support];
|
||||||
progress |= ResolveDecision(givesSupport, world, decisions);
|
progress |= ResolveDecision(givesSupport, world, decisions, depth + 1);
|
||||||
if (givesSupport.Outcome == true) min += 1;
|
if (givesSupport.Outcome == true) min += 1;
|
||||||
if (givesSupport.Outcome != false) max += 1;
|
if (givesSupport.Outcome != false) max += 1;
|
||||||
}
|
}
|
||||||
progress |= decision.Update(min, max);
|
progress |= LoggedUpdate(decision, min, max, depth, "Updated based on unit's supports");
|
||||||
return progress;
|
return progress;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -579,16 +758,18 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||||
private bool ResolveAttackStrength(
|
private bool ResolveAttackStrength(
|
||||||
AttackStrength decision,
|
AttackStrength decision,
|
||||||
World world,
|
World world,
|
||||||
MovementDecisions decisions)
|
MovementDecisions decisions,
|
||||||
|
int depth)
|
||||||
{
|
{
|
||||||
|
logger.Log(depth, "AttackStrength({0})", decision.Order);
|
||||||
bool progress = false;
|
bool progress = false;
|
||||||
|
|
||||||
// If there is no path, the attack strength is zero.
|
// If there is no path, the attack strength is zero.
|
||||||
var hasPath = decisions.HasPath[decision.Order];
|
var hasPath = decisions.HasPath[decision.Order];
|
||||||
progress |= ResolveDecision(hasPath, world, decisions);
|
progress |= ResolveDecision(hasPath, world, decisions, depth + 1);
|
||||||
if (hasPath.Outcome == false)
|
if (hasPath.Outcome == false)
|
||||||
{
|
{
|
||||||
progress |= decision.Update(0, 0);
|
progress |= LoggedUpdate(decision, 0, 0, depth, "No path");
|
||||||
return progress;
|
return progress;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -601,7 +782,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||||
: null;
|
: null;
|
||||||
if (destMoveAway != null)
|
if (destMoveAway != null)
|
||||||
{
|
{
|
||||||
progress |= ResolveDecision(destMoveAway, world, decisions);
|
progress |= ResolveDecision(destMoveAway, world, decisions, depth + 1);
|
||||||
}
|
}
|
||||||
if (// In any case here, there will have to be a unit at the destination with an order,
|
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
|
// which means that destOrder will have to be populated. Including this in the if
|
||||||
|
@ -618,7 +799,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||||
if (decision.Order.Unit.Power == destPower)
|
if (decision.Order.Unit.Power == destPower)
|
||||||
{
|
{
|
||||||
// Cannot dislodge own unit.
|
// Cannot dislodge own unit.
|
||||||
progress |= decision.Update(0, 0);
|
progress |= LoggedUpdate(decision, 0, 0, depth, "Cannot dislodge own unit");
|
||||||
return progress;
|
return progress;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -630,11 +811,11 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||||
{
|
{
|
||||||
if (support.Unit.Power == destPower) continue;
|
if (support.Unit.Power == destPower) continue;
|
||||||
GivesSupport givesSupport = decisions.GivesSupport[support];
|
GivesSupport givesSupport = decisions.GivesSupport[support];
|
||||||
progress |= ResolveDecision(givesSupport, world, decisions);
|
progress |= ResolveDecision(givesSupport, world, decisions, depth + 1);
|
||||||
if (givesSupport.Outcome == true) min += 1;
|
if (givesSupport.Outcome == true) min += 1;
|
||||||
if (givesSupport.Outcome != false) max += 1;
|
if (givesSupport.Outcome != false) max += 1;
|
||||||
}
|
}
|
||||||
progress |= decision.Update(min, max);
|
progress |= LoggedUpdate(decision, min, max, depth, "Updated with supports from other powers");
|
||||||
return progress;
|
return progress;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -650,13 +831,13 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||||
foreach (SupportMoveOrder support in decision.Supports)
|
foreach (SupportMoveOrder support in decision.Supports)
|
||||||
{
|
{
|
||||||
GivesSupport givesSupport = decisions.GivesSupport[support];
|
GivesSupport givesSupport = decisions.GivesSupport[support];
|
||||||
progress |= ResolveDecision(givesSupport, world, decisions);
|
progress |= ResolveDecision(givesSupport, world, decisions, depth + 1);
|
||||||
if (support.Unit.Power != destPower && givesSupport.Outcome == true) min += 1;
|
if (support.Unit.Power != destPower && givesSupport.Outcome == true) min += 1;
|
||||||
if (givesSupport.Outcome != false) max += 1;
|
if (givesSupport.Outcome != false) max += 1;
|
||||||
}
|
}
|
||||||
// Force min to zero in case of an attempt to disloge a unit of the same power.
|
// 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;
|
if (decision.Order.Unit.Power == destPower) min = 0;
|
||||||
progress |= decision.Update(min, max);
|
progress |= LoggedUpdate(decision, min, max, depth, "Updated with supports");
|
||||||
return progress;
|
return progress;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -668,11 +849,11 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||||
foreach (SupportMoveOrder support in decision.Supports)
|
foreach (SupportMoveOrder support in decision.Supports)
|
||||||
{
|
{
|
||||||
GivesSupport givesSupport = decisions.GivesSupport[support];
|
GivesSupport givesSupport = decisions.GivesSupport[support];
|
||||||
progress |= ResolveDecision(givesSupport, world, decisions);
|
progress |= ResolveDecision(givesSupport, world, decisions, depth + 1);
|
||||||
if (givesSupport.Outcome == true) min += 1;
|
if (givesSupport.Outcome == true) min += 1;
|
||||||
if (givesSupport.Outcome != false) max += 1;
|
if (givesSupport.Outcome != false) max += 1;
|
||||||
}
|
}
|
||||||
progress |= decision.Update(min, max);
|
progress |= LoggedUpdate(decision, min, max, depth, "Updated with supports from all powers");
|
||||||
return progress;
|
return progress;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -680,8 +861,10 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||||
private bool ResolveDefendStrength(
|
private bool ResolveDefendStrength(
|
||||||
DefendStrength decision,
|
DefendStrength decision,
|
||||||
World world,
|
World world,
|
||||||
MovementDecisions decisions)
|
MovementDecisions decisions,
|
||||||
|
int depth)
|
||||||
{
|
{
|
||||||
|
logger.Log(depth, "DefendStrength({0})", decision.Order);
|
||||||
bool progress = false;
|
bool progress = false;
|
||||||
|
|
||||||
// The defend strength is equal to one plus, at least, the number of known successful
|
// The defend strength is equal to one plus, at least, the number of known successful
|
||||||
|
@ -691,11 +874,11 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||||
foreach (SupportMoveOrder support in decision.Supports)
|
foreach (SupportMoveOrder support in decision.Supports)
|
||||||
{
|
{
|
||||||
GivesSupport givesSupport = decisions.GivesSupport[support];
|
GivesSupport givesSupport = decisions.GivesSupport[support];
|
||||||
progress |= ResolveDecision(givesSupport, world, decisions);
|
progress |= ResolveDecision(givesSupport, world, decisions, depth + 1);
|
||||||
if (givesSupport.Outcome == true) min += 1;
|
if (givesSupport.Outcome == true) min += 1;
|
||||||
if (givesSupport.Outcome != false) max += 1;
|
if (givesSupport.Outcome != false) max += 1;
|
||||||
}
|
}
|
||||||
progress |= decision.Update(min, max);
|
progress |= LoggedUpdate(decision, min, max, depth, "Updated based on unit's supports");
|
||||||
|
|
||||||
return progress;
|
return progress;
|
||||||
}
|
}
|
||||||
|
@ -703,16 +886,18 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||||
private bool ResolvePreventStrength(
|
private bool ResolvePreventStrength(
|
||||||
PreventStrength decision,
|
PreventStrength decision,
|
||||||
World world,
|
World world,
|
||||||
MovementDecisions decisions)
|
MovementDecisions decisions,
|
||||||
|
int depth)
|
||||||
{
|
{
|
||||||
|
logger.Log(depth, "PreventStrength({0})", decision.Order);
|
||||||
bool progress = false;
|
bool progress = false;
|
||||||
|
|
||||||
// If there is no path, the prevent strength is zero.
|
// If there is no path, the prevent strength is zero.
|
||||||
var hasPath = decisions.HasPath[decision.Order];
|
var hasPath = decisions.HasPath[decision.Order];
|
||||||
progress |= ResolveDecision(hasPath, world, decisions);
|
progress |= ResolveDecision(hasPath, world, decisions, depth + 1);
|
||||||
if (hasPath.Outcome == false)
|
if (hasPath.Outcome == false)
|
||||||
{
|
{
|
||||||
progress |= decision.Update(0, 0);
|
progress |= LoggedUpdate(decision, 0, 0, depth, "No path to prevent");
|
||||||
return progress;
|
return progress;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -721,7 +906,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||||
if (decision.OpposingMove != null
|
if (decision.OpposingMove != null
|
||||||
&& decisions.DoesMove[decision.OpposingMove].Outcome == true)
|
&& decisions.DoesMove[decision.OpposingMove].Outcome == true)
|
||||||
{
|
{
|
||||||
progress |= decision.Update(0, 0);
|
progress |= LoggedUpdate(decision, 0, 0, depth, "Cannot prevent in lost head-to-head");
|
||||||
return progress;
|
return progress;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -733,7 +918,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||||
foreach (SupportMoveOrder support in decision.Supports)
|
foreach (SupportMoveOrder support in decision.Supports)
|
||||||
{
|
{
|
||||||
GivesSupport givesSupport = decisions.GivesSupport[support];
|
GivesSupport givesSupport = decisions.GivesSupport[support];
|
||||||
progress |= ResolveDecision(givesSupport, world, decisions);
|
progress |= ResolveDecision(givesSupport, world, decisions, depth + 1);
|
||||||
if (givesSupport.Outcome == true) min += 1;
|
if (givesSupport.Outcome == true) min += 1;
|
||||||
if (givesSupport.Outcome != false) max += 1;
|
if (givesSupport.Outcome != false) max += 1;
|
||||||
}
|
}
|
||||||
|
@ -747,7 +932,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||||
min = 0;
|
min = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
progress |= decision.Update(min, max);
|
progress |= LoggedUpdate(decision, min, max, depth, "Updated based on unit's supports");
|
||||||
|
|
||||||
return progress;
|
return progress;
|
||||||
}
|
}
|
||||||
|
@ -755,13 +940,15 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||||
private bool ResolveDoesUnitMove(
|
private bool ResolveDoesUnitMove(
|
||||||
DoesMove decision,
|
DoesMove decision,
|
||||||
World world,
|
World world,
|
||||||
MovementDecisions decisions)
|
MovementDecisions decisions,
|
||||||
|
int depth)
|
||||||
{
|
{
|
||||||
|
logger.Log(depth, "DoesUnitMove({0})", decision.Order);
|
||||||
bool progress = false;
|
bool progress = false;
|
||||||
|
|
||||||
// Resolve the move's attack strength.
|
// Resolve the move's attack strength.
|
||||||
AttackStrength attack = decisions.AttackStrength[decision.Order];
|
AttackStrength attack = decisions.AttackStrength[decision.Order];
|
||||||
progress |= ResolveDecision(attack, world, decisions);
|
progress |= ResolveDecision(attack, world, decisions, depth + 1);
|
||||||
|
|
||||||
// In a head to head battle, the threshold for the attack strength to beat is the opposing
|
// 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
|
// defend strength. Outside a head to head battle, the threshold is the destination's hold
|
||||||
|
@ -769,12 +956,12 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||||
NumericAdjudicationDecision defense = decision.OpposingMove != null
|
NumericAdjudicationDecision defense = decision.OpposingMove != null
|
||||||
? decisions.DefendStrength[decision.OpposingMove]
|
? decisions.DefendStrength[decision.OpposingMove]
|
||||||
: decisions.HoldStrength[decision.Order.Point];
|
: decisions.HoldStrength[decision.Order.Point];
|
||||||
progress |= ResolveDecision(defense, world, decisions);
|
progress |= ResolveDecision(defense, world, decisions, depth + 1);
|
||||||
|
|
||||||
// If the attack doesn't beat the defense, resolve the move to false.
|
// If the attack doesn't beat the defense, resolve the move to false.
|
||||||
if (attack.MaxValue <= defense.MinValue)
|
if (attack.MaxValue <= defense.MinValue)
|
||||||
{
|
{
|
||||||
progress |= decision.Update(false);
|
progress |= LoggedUpdate(decision, false, depth, "Attack can't beat defense");
|
||||||
return progress;
|
return progress;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -783,11 +970,11 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||||
foreach (MoveOrder order in decision.Competing)
|
foreach (MoveOrder order in decision.Competing)
|
||||||
{
|
{
|
||||||
PreventStrength prevent = decisions.PreventStrength[order];
|
PreventStrength prevent = decisions.PreventStrength[order];
|
||||||
progress |= ResolveDecision(prevent, world, decisions);
|
progress |= ResolveDecision(prevent, world, decisions, depth + 1);
|
||||||
// If attack doesn't beat the prevent, resolve the move to false.
|
// If attack doesn't beat the prevent, resolve the move to false.
|
||||||
if (attack.MaxValue <= prevent.MinValue)
|
if (attack.MaxValue <= prevent.MinValue)
|
||||||
{
|
{
|
||||||
progress |= decision.Update(false);
|
progress |= LoggedUpdate(decision, false, depth, "Attack can't beat prevent");
|
||||||
return progress;
|
return progress;
|
||||||
}
|
}
|
||||||
// If the attack doesn't beat the prevent, it can't resolve to true.
|
// If the attack doesn't beat the prevent, it can't resolve to true.
|
||||||
|
@ -799,7 +986,11 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
||||||
|
|
||||||
// If the attack didn't resolve to false because the defense or a prevent beat it, then
|
// 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.
|
// attempt to resolve it to true based on whether it beat the defense and all prevents.
|
||||||
progress |= decision.Update(attack.MinValue > defense.MaxValue && beatsAllCompetingMoves);
|
progress |= LoggedUpdate(
|
||||||
|
decision,
|
||||||
|
attack.MinValue > defense.MaxValue && beatsAllCompetingMoves,
|
||||||
|
depth,
|
||||||
|
"Updated based on competing moves");
|
||||||
return progress;
|
return progress;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
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}";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -51,7 +51,7 @@ public class Unit
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return $"{this.Power} {this.Type} {this.Province} {this.Season}";
|
return $"{this.Power.Name[0]} {this.Type.ToShort()} {(this.Province, this.Season).ToShort()}";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <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>
|
||||||
|
|
|
@ -2,9 +2,13 @@
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -37,6 +37,6 @@ public class ConvoyOrder : UnitOrder
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return $"{this.Unit} convoys {this.Target} -> {this.Province} {this.Season}";
|
return $"{this.Unit} C {this.Target} -> {(this.Province, this.Season).ToShort()}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,7 @@ public class MoveOrder : UnitOrder
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return $"{this.Unit} -> {this.Province} {this.Season}";
|
return $"{this.Unit} -> {(this.Province, this.Season).ToShort()}";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
@ -14,6 +14,6 @@ public class SupportHoldOrder : SupportOrder
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return $"{this.Unit} supports {this.Target}";
|
return $"{this.Unit} S {this.Target}";
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -36,7 +36,7 @@ public class SupportMoveOrder : SupportOrder
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return $"{this.Unit} supports {this.Target} -> {this.Province} {this.Season}";
|
return $"{this.Unit} S {this.Target} -> {(this.Province, this.Season).ToShort()}";
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsSupportFor(MoveOrder move)
|
public bool IsSupportFor(MoveOrder move)
|
||||||
|
|
|
@ -1,12 +1,41 @@
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
|
using CommandLine;
|
||||||
|
using CommandLine.Text;
|
||||||
|
|
||||||
namespace MultiversalDiplomacy
|
namespace MultiversalDiplomacy
|
||||||
{
|
{
|
||||||
|
[Verb("adjudicate", HelpText = "")]
|
||||||
|
internal class AdjudicateOptions
|
||||||
|
{
|
||||||
|
[Option('i', "input")]
|
||||||
|
public string? InputFile { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
internal class Program
|
internal class Program
|
||||||
{
|
{
|
||||||
static void Main(string[] args)
|
static void Main(string[] args)
|
||||||
{
|
{
|
||||||
Console.WriteLine("stab");
|
var parser = new Parser(options =>
|
||||||
|
{
|
||||||
|
options.AutoVersion = false;
|
||||||
|
options.HelpWriter = null;
|
||||||
|
});
|
||||||
|
var parseResult = parser.ParseArguments(args, typeof(AdjudicateOptions));
|
||||||
|
var helpText = HelpText.AutoBuild(parseResult, options =>
|
||||||
|
{
|
||||||
|
options.AdditionalNewLineAfterOption = false;
|
||||||
|
return HelpText.DefaultParsingErrorsHandler(parseResult, options);
|
||||||
|
});
|
||||||
|
|
||||||
|
parseResult
|
||||||
|
.WithParsed<AdjudicateOptions>(Adjudicate)
|
||||||
|
.WithNotParsed(errs => Console.WriteLine(helpText));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Adjudicate(AdjudicateOptions args)
|
||||||
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -195,7 +195,7 @@ public class TimeTravelTest
|
||||||
setup[(0, 0)]
|
setup[(0, 0)]
|
||||||
.GetReference(out var s0_0)
|
.GetReference(out var s0_0)
|
||||||
["England"].Army("Lon").Holds()
|
["England"].Army("Lon").Holds()
|
||||||
["Austria"].Army("Boh").Holds()
|
["Austria"].Army("Tyr").Holds()
|
||||||
["Germany"].Army("Mun").Holds()
|
["Germany"].Army("Mun").Holds()
|
||||||
.Execute()
|
.Execute()
|
||||||
[(1, 0)]
|
[(1, 0)]
|
||||||
|
@ -288,7 +288,6 @@ public class TimeTravelTest
|
||||||
// The attack on Mun 2:1 is repelled, but the support is cut.
|
// The attack on Mun 2:1 is repelled, but the support is cut.
|
||||||
setup.ValidateOrders();
|
setup.ValidateOrders();
|
||||||
Assert.That(tyr2_2, Is.Valid);
|
Assert.That(tyr2_2, Is.Valid);
|
||||||
Assert.That(mun2_1, Is.Valid);
|
|
||||||
setup.AdjudicateOrders();
|
setup.AdjudicateOrders();
|
||||||
Assert.That(tyr2_2, Is.Repelled);
|
Assert.That(tyr2_2, Is.Repelled);
|
||||||
Assert.That(mun2_1, Is.NotDislodged);
|
Assert.That(mun2_1, Is.NotDislodged);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
|
|
150
README.md
150
README.md
|
@ -1,151 +1,29 @@
|
||||||
# 5D Diplomacy With Multiversal Time Travel
|
# 5D Diplomacy With Multiversal Time Travel
|
||||||
|
|
||||||
## So you want to conquer Europe with a declarative build system
|
_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_.
|
||||||
|
|
||||||
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.
|
## Acknowledgements
|
||||||
|
|
||||||
A `nix flake init` gives us a fairly useless flake template:
|
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).
|
||||||
|
|
||||||
```
|
## Variant rules
|
||||||
{
|
|
||||||
description = "A very basic flake";
|
|
||||||
|
|
||||||
outputs = { self, nixpkgs }: {
|
### Multiversal time travel and timeline forks
|
||||||
|
|
||||||
packages.x86_64-linux.hello = nixpkgs.legacyPackages.x86_64-linux.hello;
|
_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.
|
||||||
|
|
||||||
defaultPackage.x86_64-linux = self.packages.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.
|
||||||
|
|
||||||
};
|
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.
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
```
|
### 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
|
|
||||||
```
|
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
```
|
(TODO)
|
||||||
inputs.flake-utils.url = "github:numtide/flake-utils";
|
|
||||||
|
|
||||||
outputs = { self, nixpkgs, flake-utils }:
|
### Victory conditions
|
||||||
flake-utils.lib.eachDefaultSystem (system:
|
|
||||||
let pkgs = nixpkgs.legacyPackages.${system};
|
|
||||||
in rec {
|
|
||||||
devShell = pkgs.mkShell {
|
|
||||||
packages = [ pkgs.dotnet-sdk ];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
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": {
|
"nodes": {
|
||||||
"flake-utils": {
|
"flake-utils": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1644229661,
|
"lastModified": 1667395993,
|
||||||
"narHash": "sha256-1YdnJAsNy69bpcjuoKdOYQX0YxZBiCYZo4Twxerqv7k=",
|
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "flake-utils",
|
"repo": "flake-utils",
|
||||||
"rev": "3cecb5b042f7f209c56ffd8371b2711a290ec797",
|
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -17,16 +17,18 @@
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1644486793,
|
"lastModified": 1717179513,
|
||||||
"narHash": "sha256-EeijR4guVHgVv+JpOX3cQO+1XdrkJfGmiJ9XVsVU530=",
|
"narHash": "sha256-vboIEwIQojofItm2xGCdZCzW96U85l9nDW3ifMuAIdM=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "1882c6b7368fd284ad01b0a5b5601ef136321292",
|
"rev": "63dacb46bf939521bdc93981b4cbb7ecb58427a0",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"id": "nixpkgs",
|
"owner": "NixOS",
|
||||||
"type": "indirect"
|
"ref": "24.05",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
|
|
12
flake.nix
12
flake.nix
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
description = "5D Diplomacy With Multiversal Time Travel";
|
description = "5D Diplomacy With Multiversal Time Travel";
|
||||||
|
|
||||||
|
inputs.nixpkgs.url = "github:NixOS/nixpkgs/24.05";
|
||||||
inputs.flake-utils.url = "github:numtide/flake-utils";
|
inputs.flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
|
||||||
outputs = { self, nixpkgs, flake-utils }:
|
outputs = { self, nixpkgs, flake-utils }:
|
||||||
|
@ -8,11 +9,14 @@
|
||||||
let pkgs = nixpkgs.legacyPackages.${system};
|
let pkgs = nixpkgs.legacyPackages.${system};
|
||||||
in rec {
|
in rec {
|
||||||
devShell = pkgs.mkShell {
|
devShell = pkgs.mkShell {
|
||||||
shellHook = ''
|
|
||||||
PS1="5dplomacy:\W$ "
|
|
||||||
'';
|
|
||||||
DOTNET_CLI_TELEMETRY_OPTOUT = 1;
|
DOTNET_CLI_TELEMETRY_OPTOUT = 1;
|
||||||
packages = [ pkgs.dotnet-sdk pkgs.dotnetPackages.NUnit3 ];
|
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
|
||||||
|
];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in New Issue