Compare commits

..

6 Commits

16 changed files with 124 additions and 97 deletions

View File

@ -60,9 +60,9 @@ public class MovementDecisions
{ {
case MoveOrder move: case MoveOrder move:
AdvanceTimeline.Ensure( AdvanceTimeline.Ensure(
move.Season, world.Seasons[move.Season],
() => new(move.Season, world.OrderHistory[move.Season.Designation].Orders)); () => new(world.Seasons[move.Season], world.OrderHistory[move.Season].Orders));
AdvanceTimeline[move.Season].Orders.Add(move); AdvanceTimeline[world.Seasons[move.Season]].Orders.Add(move);
break; break;
case SupportHoldOrder supportHold: case SupportHoldOrder supportHold:
@ -91,23 +91,25 @@ public class MovementDecisions
.Distinct() .Distinct()
.ToList(); .ToList();
(Province province, Season season) Point(Unit unit) (Province province, Season season) UnitPoint(Unit unit)
=> (world.Map.GetLocation(unit.Location).Province, unit.Season); => (world.Map.GetLocation(unit.Location).Province, unit.Season);
(Province province, Season season) MovePoint(MoveOrder move)
=> (move.Province, world.Seasons[move.Season]);
// Create a hold strength decision with an associated order for every province with a unit. // Create a hold strength decision with an associated order for every province with a unit.
foreach (UnitOrder order in relevantOrders) foreach (UnitOrder order in relevantOrders)
{ {
HoldStrength[Point(order.Unit)] = new(Point(order.Unit), order); HoldStrength[UnitPoint(order.Unit)] = new(UnitPoint(order.Unit), order);
} }
bool IsIncoming(UnitOrder me, MoveOrder other) bool IsIncoming(UnitOrder me, MoveOrder other)
=> me != other => me != other
&& other.Season == me.Unit.Season && world.Seasons[other.Season] == me.Unit.Season
&& other.Province == world.Map.GetLocation(me.Unit).Province; && other.Province == world.Map.GetLocation(me.Unit).Province;
bool AreOpposing(MoveOrder one, MoveOrder two) bool AreOpposing(MoveOrder one, MoveOrder two)
=> one.Season == two.Unit.Season => one.Season == two.Unit.Season.Designation
&& two.Season == one.Unit.Season && two.Season == one.Unit.Season.Designation
&& one.Province == world.Map.GetLocation(two.Unit).Province && one.Province == world.Map.GetLocation(two.Unit).Province
&& two.Province == world.Map.GetLocation(one.Unit).Province; && two.Province == world.Map.GetLocation(one.Unit).Province;
@ -148,7 +150,7 @@ public class MovementDecisions
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.
HoldStrength.Ensure(move.Point, () => new(move.Point)); HoldStrength.Ensure(MovePoint(move), () => new(MovePoint(move)));
} }
else if (order is SupportOrder support) else if (order is SupportOrder support)
{ {
@ -156,11 +158,11 @@ public class MovementDecisions
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.
HoldStrength.Ensure(Point(support.Target), () => new(Point(support.Target))); HoldStrength.Ensure(UnitPoint(support.Target), () => new(UnitPoint(support.Target)));
if (support is SupportHoldOrder supportHold) if (support is SupportHoldOrder supportHold)
{ {
HoldStrength[Point(support.Target)].Supports.Add(supportHold); HoldStrength[UnitPoint(support.Target)].Supports.Add(supportHold);
} }
else if (support is SupportMoveOrder supportMove) else if (support is SupportMoveOrder supportMove)
{ {

View File

@ -77,7 +77,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
// Trivial check: a unit cannot move to where it already is. // Trivial check: a unit cannot move to where it already is.
AdjudicatorHelpers.InvalidateIfNotMatching( AdjudicatorHelpers.InvalidateIfNotMatching(
order => !(order.Location.Designation == order.Unit.Location && order.Season == order.Unit.Season), order => !(order.Location.Designation == order.Unit.Location && order.Season == order.Unit.Season.Designation),
ValidationReason.DestinationMatchesOrigin, ValidationReason.DestinationMatchesOrigin,
ref moveOrders, ref moveOrders,
ref validationResults); ref validationResults);
@ -92,9 +92,9 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
// Map adjacency // Map adjacency
world.Map.GetLocation(order.Unit).Adjacents.Contains(order.Location) world.Map.GetLocation(order.Unit).Adjacents.Contains(order.Location)
// Turn adjacency // Turn adjacency
&& Math.Abs(order.Unit.Season.Turn - order.Season.Turn) <= 1 && Math.Abs(order.Unit.Season.Turn - world.Seasons[order.Season].Turn) <= 1
// Timeline adjacency // Timeline adjacency
&& world.InAdjacentTimeline(order.Unit.Season, order.Season)); && world.InAdjacentTimeline(order.Unit.Season, world.Seasons[order.Season]));
List<MoveOrder> adjacentMoveOrders = moveOrdersByAdjacency[true].ToList(); List<MoveOrder> adjacentMoveOrders = moveOrdersByAdjacency[true].ToList();
List<MoveOrder> nonAdjacentMoveOrders = moveOrdersByAdjacency[false].ToList(); List<MoveOrder> nonAdjacentMoveOrders = moveOrdersByAdjacency[false].ToList();
@ -334,10 +334,10 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
foreach (DoesMove doesMove in moves.Values) foreach (DoesMove doesMove in moves.Values)
{ {
logger.Log(2, "{0} = {1}", doesMove, doesMove.Outcome?.ToString() ?? "?"); logger.Log(2, "{0} = {1}", doesMove, doesMove.Outcome?.ToString() ?? "?");
Season moveSeason = doesMove.Order.Season; Season moveSeason = world.Seasons[doesMove.Order.Season];
if (doesMove.Outcome == true && createdFutures.ContainsKey(moveSeason)) if (doesMove.Outcome == true && createdFutures.TryGetValue(moveSeason, out Season? future))
{ {
Unit next = doesMove.Order.Unit.Next(doesMove.Order.Location.Designation, createdFutures[moveSeason]); Unit next = doesMove.Order.Unit.Next(doesMove.Order.Location.Designation, future);
logger.Log(3, "Advancing unit to {0}", next); logger.Log(3, "Advancing unit to {0}", next);
createdUnits.Add(next); createdUnits.Add(next);
} }
@ -387,14 +387,14 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
Dictionary<string, OrderHistory> newHistory = []; Dictionary<string, OrderHistory> newHistory = [];
foreach (UnitOrder unitOrder in decisions.OfType<IsDislodged>().Select(d => d.Order)) foreach (UnitOrder unitOrder in decisions.OfType<IsDislodged>().Select(d => d.Order))
{ {
newHistory.Ensure(unitOrder.Unit.Season.Designation, () => new()); newHistory.Ensure(unitOrder.Unit.Season.Designation, () => new([], [], []));
OrderHistory history = newHistory[unitOrder.Unit.Season.Designation]; OrderHistory history = newHistory[unitOrder.Unit.Season.Designation];
// TODO does this add every order to every season?? // TODO does this add every order to every season??
history.Orders.Add(unitOrder); history.Orders.Add(unitOrder);
history.IsDislodgedOutcomes[unitOrder.Unit] = dislodges[unitOrder.Unit].Outcome == true; history.IsDislodgedOutcomes[unitOrder.Unit.Designation] = dislodges[unitOrder.Unit].Outcome == true;
if (unitOrder is MoveOrder moveOrder) if (unitOrder is MoveOrder moveOrder)
{ {
history.DoesMoveOutcomes[moveOrder] = moves[moveOrder].Outcome == true; history.DoesMoveOutcomes[moveOrder.Unit.Designation] = moves[moveOrder].Outcome == true;
} }
} }
@ -416,7 +416,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
// TODO provide more structured information about order outcomes // TODO provide more structured information about order outcomes
World updated = world.Update( World updated = world.Update(
seasons: world.Seasons.Concat(createdFutures.Values), seasons: world.Seasons.Values.Concat(createdFutures.Values),
units: world.Units.Concat(createdUnits), units: world.Units.Concat(createdUnits),
retreats: retreats, retreats: retreats,
orders: updatedHistory); orders: updatedHistory);
@ -493,8 +493,8 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
// The season target of a new (i.e. not previously adjudicated) and successful move always advances. // The season target of a new (i.e. not previously adjudicated) and successful move always advances.
IEnumerable<MoveOrder> newIncomingMoves = decision.Orders IEnumerable<MoveOrder> newIncomingMoves = decision.Orders
.OfType<MoveOrder>() .OfType<MoveOrder>()
.Where(order => order.Season == decision.Season .Where(order => order.Season == decision.Season.Designation
&& !world.OrderHistory[order.Season.Designation].DoesMoveOutcomes.ContainsKey(order)); && !world.OrderHistory[order.Season].DoesMoveOutcomes.ContainsKey(order.Unit.Designation));
foreach (MoveOrder moveOrder in newIncomingMoves) foreach (MoveOrder moveOrder in newIncomingMoves)
{ {
DoesMove doesMove = decisions.DoesMove[moveOrder]; DoesMove doesMove = decisions.DoesMove[moveOrder];
@ -518,7 +518,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
// TODO these aren't timeline-specific // TODO these aren't timeline-specific
IsDislodged dislodged = decisions.IsDislodged[order.Unit]; IsDislodged dislodged = decisions.IsDislodged[order.Unit];
progress |= ResolveDecision(dislodged, world, decisions, depth + 1); progress |= ResolveDecision(dislodged, world, decisions, depth + 1);
if (history.IsDislodgedOutcomes.TryGetValue(order.Unit, out bool previous) if (history.IsDislodgedOutcomes.TryGetValue(order.Unit.Designation, out bool previous)
&& dislodged.Resolved && dislodged.Resolved
&& dislodged.Outcome != previous) && dislodged.Outcome != previous)
{ {
@ -535,7 +535,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
{ {
DoesMove moves = decisions.DoesMove[moveOrder]; DoesMove moves = decisions.DoesMove[moveOrder];
progress |= ResolveDecision(moves, world, decisions, depth + 1); progress |= ResolveDecision(moves, world, decisions, depth + 1);
if (history.DoesMoveOutcomes.TryGetValue(moveOrder, out bool previousMove) if (history.DoesMoveOutcomes.TryGetValue(moveOrder.Unit.Designation, out bool previousMove)
&& moves.Resolved && moves.Resolved
&& moves.Outcome != previousMove) && moves.Outcome != previousMove)
if (moves.Resolved && moves.Outcome != previousMove) if (moves.Resolved && moves.Outcome != previousMove)
@ -635,9 +635,9 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
if (// Map adjacency if (// Map adjacency
world.Map.GetLocation(decision.Order.Unit).Adjacents.Contains(decision.Order.Location) world.Map.GetLocation(decision.Order.Unit).Adjacents.Contains(decision.Order.Location)
// Turn adjacency // Turn adjacency
&& Math.Abs(decision.Order.Unit.Season.Turn - decision.Order.Season.Turn) <= 1 && Math.Abs(decision.Order.Unit.Season.Turn - world.Seasons[decision.Order.Season].Turn) <= 1
// Timeline adjacency // Timeline adjacency
&& world.InAdjacentTimeline(decision.Order.Unit.Season, decision.Order.Season)) && world.InAdjacentTimeline(decision.Order.Unit.Season, world.Seasons[decision.Order.Season]))
{ {
bool update = LoggedUpdate(decision, true, depth, "Adjacent move"); bool update = LoggedUpdate(decision, true, depth, "Adjacent move");
return progress | update; return progress | update;
@ -774,7 +774,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
// If there is a head to head battle, a unit at the destination that isn't moving away, or // If there is a head to head battle, a unit at the destination that isn't moving away, or
// a unit at the destination that will fail to move away, then the attacking unit will have // a unit at the destination that will fail to move away, then the attacking unit will have
// to dislodge it. // to dislodge it.
UnitOrder? destOrder = decisions.HoldStrength[decision.Order.Point].Order; UnitOrder? destOrder = decisions.HoldStrength[(decision.Order.Province, world.Seasons[decision.Order.Season])].Order;
DoesMove? destMoveAway = destOrder is MoveOrder moveAway DoesMove? destMoveAway = destOrder is MoveOrder moveAway
? decisions.DoesMove[moveAway] ? decisions.DoesMove[moveAway]
: null; : null;
@ -953,7 +953,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
// strength. // strength.
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.Province, world.Seasons[decision.Order.Season])];
progress |= ResolveDecision(defense, world, decisions, depth + 1); 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.

View File

@ -18,7 +18,7 @@ public static class PathFinder
/// Determines if a convoy path exists for a move order. /// Determines if a convoy path exists for a move order.
/// </summary> /// </summary>
public static bool ConvoyPathExists(World world, MoveOrder order) public static bool ConvoyPathExists(World world, MoveOrder order)
=> ConvoyPathExists(world, order.Unit, order.Location, order.Season); => ConvoyPathExists(world, order.Unit, order.Location, world.Seasons[order.Season]);
private static bool ConvoyPathExists( private static bool ConvoyPathExists(
World world, World world,
@ -114,7 +114,7 @@ public static class PathFinder
List<Season> adjacents = []; List<Season> adjacents = [];
// The immediate past and all immediate futures are adjacent. // The immediate past and all immediate futures are adjacent.
if (season.Past != null) adjacents.Add(world.GetSeason(season.Past)); if (season.Past != null) adjacents.Add(world.Seasons[season.Past]);
adjacents.AddRange(world.GetFutures(season)); adjacents.AddRange(world.GetFutures(season));
// Find all adjacent timelines by finding all timelines that branched off of this season's // Find all adjacent timelines by finding all timelines that branched off of this season's
@ -123,8 +123,8 @@ public static class PathFinder
List<Season> adjacentTimelineRoots = []; List<Season> adjacentTimelineRoots = [];
Season? current; Season? current;
for (current = season; for (current = season;
current?.Past != null && world.GetSeason(current.Past).Timeline == current.Timeline; current?.Past != null && world.Seasons[current.Past].Timeline == current.Timeline;
current = world.GetSeason(current.Past)) current = world.Seasons[current.Past])
{ {
adjacentTimelineRoots.AddRange( adjacentTimelineRoots.AddRange(
world.GetFutures(current).Where(s => s.Timeline != current.Timeline)); world.GetFutures(current).Where(s => s.Timeline != current.Timeline));
@ -135,7 +135,7 @@ public static class PathFinder
// then current is the branch timeline's root season (current.past.timeline != // then current is the branch timeline's root season (current.past.timeline !=
// current.timeline). There are co-branches if this season is in a branched timeline, since // current.timeline). There are co-branches if this season is in a branched timeline, since
// the first timeline by definition cannot have co-branches. // the first timeline by definition cannot have co-branches.
if (current?.Past != null && world.GetSeason(current.Past) is Season past) if (current?.Past != null && world.Seasons[current.Past] is Season past)
{ {
IEnumerable<Season> cobranchRoots = world IEnumerable<Season> cobranchRoots = world
.GetFutures(past) .GetFutures(past)

View File

@ -24,6 +24,6 @@ public static class ModelExtensions
public static World ContinueOrFork(this World world, Season season, out Season future) public static World ContinueOrFork(this World world, Season season, out Season future)
{ {
future = world.ContinueOrFork(season); future = world.ContinueOrFork(season);
return world.Update(seasons: world.Seasons.Append(future)); return world.Update(seasons: world.Seasons.Values.Append(future));
} }
} }

View File

@ -1,4 +1,4 @@
using System.Collections.ObjectModel; using System.Text.Json.Serialization;
using MultiversalDiplomacy.Orders; using MultiversalDiplomacy.Orders;
@ -6,20 +6,23 @@ namespace MultiversalDiplomacy.Model;
public class OrderHistory public class OrderHistory
{ {
public List<UnitOrder> Orders; public List<UnitOrder> Orders { get; }
public Dictionary<Unit, bool> IsDislodgedOutcomes; /// <summary>
/// Map from unit designation to dislodge outcome.
/// </summary>
public Dictionary<string, bool> IsDislodgedOutcomes { get; }
public Dictionary<MoveOrder, bool> DoesMoveOutcomes; /// <summary>
/// Map from designation of the ordered unit to move outcome.
public OrderHistory() /// </summary>
: this([], [], []) public Dictionary<string, bool> DoesMoveOutcomes { get; }
{}
[JsonConstructor]
public OrderHistory( public OrderHistory(
List<UnitOrder> orders, List<UnitOrder> orders,
Dictionary<Unit, bool> isDislodgedOutcomes, Dictionary<string, bool> isDislodgedOutcomes,
Dictionary<MoveOrder, bool> doesMoveOutcomes) Dictionary<string, bool> doesMoveOutcomes)
{ {
this.Orders = new(orders); this.Orders = new(orders);
this.IsDislodgedOutcomes = new(isDislodgedOutcomes); this.IsDislodgedOutcomes = new(isDislodgedOutcomes);

View File

@ -1,8 +1,11 @@
using System.Text.Json.Serialization;
namespace MultiversalDiplomacy.Model; namespace MultiversalDiplomacy.Model;
/// <summary> /// <summary>
/// The type of a unit. /// The type of a unit.
/// </summary> /// </summary>
[JsonConverter(typeof(JsonStringEnumConverter<UnitType>))]
public enum UnitType public enum UnitType
{ {
/// <summary> /// <summary>

View File

@ -39,22 +39,16 @@ public class World
[JsonIgnore] [JsonIgnore]
public IReadOnlyCollection<Power> Powers => this.Map.Powers; public IReadOnlyCollection<Power> Powers => this.Map.Powers;
/// <summary>
/// The state of the multiverse.
/// </summary>
public List<Season> Seasons { get; }
/// <summary> /// <summary>
/// Lookup for seasons by designation. /// Lookup for seasons by designation.
/// </summary> /// </summary>
[JsonIgnore] public Dictionary<string, Season> Seasons { get; }
public Dictionary<string, Season> SeasonLookup { get; }
/// <summary> /// <summary>
/// The first season of the game. /// The first season of the game.
/// </summary> /// </summary>
[JsonIgnore] [JsonIgnore]
public Season RootSeason => GetSeason("a0"); public Season RootSeason => Seasons["a0"];
/// <summary> /// <summary>
/// All units in the multiverse. /// All units in the multiverse.
@ -84,7 +78,7 @@ public class World
[JsonConstructor] [JsonConstructor]
public World( public World(
MapType mapType, MapType mapType,
List<Season> seasons, Dictionary<string, Season> seasons,
List<Unit> units, List<Unit> units,
List<RetreatingUnit> retreatingUnits, List<RetreatingUnit> retreatingUnits,
Dictionary<string, OrderHistory> orderHistory, Dictionary<string, OrderHistory> orderHistory,
@ -99,7 +93,6 @@ public class World
this.Timelines = timelines; this.Timelines = timelines;
this.Options = options; this.Options = options;
this.SeasonLookup = new(Seasons.ToDictionary(season => $"{season.Timeline}{season.Turn}"));
} }
/// <summary> /// <summary>
@ -107,7 +100,7 @@ public class World
/// </summary> /// </summary>
private World( private World(
Map map, Map map,
List<Season> seasons, Dictionary<string, Season> seasons,
List<Unit> units, List<Unit> units,
List<RetreatingUnit> retreatingUnits, List<RetreatingUnit> retreatingUnits,
Dictionary<string, OrderHistory> orderHistory, Dictionary<string, OrderHistory> orderHistory,
@ -121,8 +114,6 @@ public class World
this.OrderHistory = orderHistory; this.OrderHistory = orderHistory;
this.Timelines = timelines; this.Timelines = timelines;
this.Options = options; this.Options = options;
this.SeasonLookup = new(Seasons.ToDictionary(season => $"{season.Timeline}{season.Turn}"));
} }
/// <summary> /// <summary>
@ -130,7 +121,7 @@ public class World
/// </summary> /// </summary>
private World( private World(
World previous, World previous,
List<Season>? seasons = null, Dictionary<string, Season>? seasons = null,
List<Unit>? units = null, List<Unit>? units = null,
List<RetreatingUnit>? retreatingUnits = null, List<RetreatingUnit>? retreatingUnits = null,
Dictionary<string, OrderHistory>? orderHistory = null, Dictionary<string, OrderHistory>? orderHistory = null,
@ -152,9 +143,10 @@ public class World
public static World WithMap(Map map) public static World WithMap(Map map)
{ {
TimelineFactory timelines = new(); TimelineFactory timelines = new();
Season a0 = new(past: null, Season.FIRST_TURN, timelines.NextTimeline());
return new World( return new World(
map, map,
new([new(past: null, Season.FIRST_TURN, timelines.NextTimeline())]), new() { {a0.Designation, a0} },
new([]), new([]),
new([]), new([]),
new(new Dictionary<string, OrderHistory>()), new(new Dictionary<string, OrderHistory>()),
@ -177,7 +169,7 @@ public class World
previous: this, previous: this,
seasons: seasons == null seasons: seasons == null
? this.Seasons ? this.Seasons
: new(seasons.ToList()), : new(seasons.ToDictionary(season => season.Designation)),
units: units == null units: units == null
? this.Units ? this.Units
: new(units.ToList()), : new(units.ToList()),
@ -264,13 +256,7 @@ public class World
/// Get a season by coordinate. Throws if the season is not found. /// Get a season by coordinate. Throws if the season is not found.
/// </summary> /// </summary>
public Season GetSeason(string timeline, int turn) public Season GetSeason(string timeline, int turn)
=> GetSeason($"{timeline}{turn}"); => Seasons[$"{timeline}{turn}"];
/// <summary>
/// Get a season by designation.
/// </summary>
public Season GetSeason(string designation)
=> SeasonLookup[designation];
/// <summary> /// <summary>
/// Get all seasons that are immediate futures of a season. /// Get all seasons that are immediate futures of a season.
@ -278,7 +264,7 @@ public class World
/// <param name="present">A season designation.</param> /// <param name="present">A season designation.</param>
/// <returns>The immediate futures of the designated season.</returns> /// <returns>The immediate futures of the designated season.</returns>
public IEnumerable<Season> GetFutures(string present) public IEnumerable<Season> GetFutures(string present)
=> Seasons.Where(future => future.Past == present); => Seasons.Values.Where(future => future.Past == present);
/// <summary> /// <summary>
/// Get all seasons that are immediate futures of a season. /// Get all seasons that are immediate futures of a season.
@ -297,7 +283,7 @@ public class World
if (season.Past is null) { if (season.Past is null) {
return season; return season;
} }
Season past = SeasonLookup[season.Past]; Season past = Seasons[season.Past];
return season.Timeline == past.Timeline return season.Timeline == past.Timeline
? GetTimelineRoot(past) ? GetTimelineRoot(past)
: season; : season;
@ -319,8 +305,8 @@ public class World
// if they both branched off of the same point. // if they both branched off of the same point.
Season rootOne = GetTimelineRoot(one); Season rootOne = GetTimelineRoot(one);
Season rootTwo = GetTimelineRoot(two); Season rootTwo = GetTimelineRoot(two);
bool oneForked = rootOne.Past != null && GetSeason(rootOne.Past).Timeline == two.Timeline; bool oneForked = rootOne.Past != null && Seasons[rootOne.Past].Timeline == two.Timeline;
bool twoForked = rootTwo.Past != null && GetSeason(rootTwo.Past).Timeline == one.Timeline; bool twoForked = rootTwo.Past != null && Seasons[rootTwo.Past].Timeline == one.Timeline;
bool bothForked = rootOne.Past == rootTwo.Past; bool bothForked = rootOne.Past == rootTwo.Past;
return oneForked || twoForked || bothForked; return oneForked || twoForked || bothForked;
} }

View File

@ -9,8 +9,9 @@ public class MoveOrder : UnitOrder
{ {
/// <summary> /// <summary>
/// The destination season to which the unit should move. /// The destination season to which the unit should move.
/// TODO replace this with timeline and turn individually so ToString can do the proper format
/// </summary> /// </summary>
public Season Season { get; } public string Season { get; }
/// <summary> /// <summary>
/// The destination location to which the unit should move. /// The destination location to which the unit should move.
@ -22,12 +23,7 @@ public class MoveOrder : UnitOrder
/// </summary> /// </summary>
public Province Province => this.Location.Province; public Province Province => this.Location.Province;
/// <summary> public MoveOrder(Power power, Unit unit, string season, Location location)
/// The destination's spatiotemporal location as a province-season tuple.
/// </summary>
public (Province province, Season season) Point => (this.Province, this.Season);
public MoveOrder(Power power, Unit unit, Season season, Location location)
: base (power, unit) : base (power, unit)
{ {
this.Season = season; this.Season = season;
@ -36,7 +32,7 @@ public class MoveOrder : UnitOrder
public override string ToString() public override string ToString()
{ {
return $"{this.Unit} -> {(this.Province, this.Season).ToShort()}"; return $"{this.Unit} -> {this.Season} {this.Province}";
} }
/// <summary> /// <summary>

View File

@ -1,3 +1,5 @@
using System.Text.Json.Serialization;
using MultiversalDiplomacy.Model; using MultiversalDiplomacy.Model;
namespace MultiversalDiplomacy.Orders; namespace MultiversalDiplomacy.Orders;
@ -5,6 +7,16 @@ namespace MultiversalDiplomacy.Orders;
/// <summary> /// <summary>
/// A submitted action by a power. /// A submitted action by a power.
/// </summary> /// </summary>
[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")]
[JsonDerivedType(typeof(BuildOrder), typeDiscriminator: "move")]
[JsonDerivedType(typeof(ConvoyOrder), typeDiscriminator: "convoy")]
[JsonDerivedType(typeof(DisbandOrder), typeDiscriminator: "disband")]
[JsonDerivedType(typeof(HoldOrder), typeDiscriminator: "hold")]
[JsonDerivedType(typeof(MoveOrder), typeDiscriminator: "move")]
[JsonDerivedType(typeof(RetreatOrder), typeDiscriminator: "retreat")]
[JsonDerivedType(typeof(SupportHoldOrder), typeDiscriminator: "supportHold")]
[JsonDerivedType(typeof(SupportMoveOrder), typeDiscriminator: "supportMove")]
[JsonDerivedType(typeof(SustainOrder), typeDiscriminator: "sustain")]
public abstract class Order public abstract class Order
{ {
/// <summary> /// <summary>

View File

@ -41,6 +41,6 @@ public class SupportMoveOrder : SupportOrder
public bool IsSupportFor(MoveOrder move) public bool IsSupportFor(MoveOrder move)
=> this.Target == move.Unit => this.Target == move.Unit
&& this.Season == move.Season && this.Season.Designation == move.Season
&& this.Location == move.Location; && this.Location == move.Location;
} }

View File

@ -1,3 +1,5 @@
using System.Text.Json.Serialization;
using MultiversalDiplomacy.Model; using MultiversalDiplomacy.Model;
namespace MultiversalDiplomacy.Orders; namespace MultiversalDiplomacy.Orders;
@ -5,6 +7,14 @@ namespace MultiversalDiplomacy.Orders;
/// <summary> /// <summary>
/// An order given to a specific unit. /// An order given to a specific unit.
/// </summary> /// </summary>
[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")]
[JsonDerivedType(typeof(ConvoyOrder), typeDiscriminator: "convoy")]
[JsonDerivedType(typeof(DisbandOrder), typeDiscriminator: "disband")]
[JsonDerivedType(typeof(HoldOrder), typeDiscriminator: "hold")]
[JsonDerivedType(typeof(MoveOrder), typeDiscriminator: "move")]
[JsonDerivedType(typeof(RetreatOrder), typeDiscriminator: "retreat")]
[JsonDerivedType(typeof(SupportHoldOrder), typeDiscriminator: "supportHold")]
[JsonDerivedType(typeof(SupportMoveOrder), typeDiscriminator: "supportMove")]
public abstract class UnitOrder : Order public abstract class UnitOrder : Order
{ {
/// <summary> /// <summary>

View File

@ -33,16 +33,16 @@ public class TimeTravelTest
// Confirm that there are now four seasons: three in the main timeline and one in a fork. // Confirm that there are now four seasons: three in the main timeline and one in a fork.
Assert.That( Assert.That(
world.Seasons.Where(s => s.Timeline == s0.Timeline).Count(), world.Seasons.Values.Where(s => s.Timeline == s0.Timeline).Count(),
Is.EqualTo(3), Is.EqualTo(3),
"Failed to advance main timeline after last unit left"); "Failed to advance main timeline after last unit left");
Assert.That( Assert.That(
world.Seasons.Where(s => s.Timeline != s0.Timeline).Count(), world.Seasons.Values.Where(s => s.Timeline != s0.Timeline).Count(),
Is.EqualTo(1), Is.EqualTo(1),
"Failed to fork timeline when unit moved in"); "Failed to fork timeline when unit moved in");
// Confirm that there is a unit in Tyr b1 originating from Mun a1 // Confirm that there is a unit in Tyr b1 originating from Mun a1
Season fork = world.GetSeason("b1"); Season fork = world.Seasons["b1"];
Unit originalUnit = world.GetUnitAt("Mun", s0); Unit originalUnit = world.GetUnitAt("Mun", s0);
Unit aMun0 = world.GetUnitAt("Mun", s1); Unit aMun0 = world.GetUnitAt("Mun", s1);
Unit aTyr = world.GetUnitAt("Tyr", fork); Unit aTyr = world.GetUnitAt("Tyr", fork);
@ -91,7 +91,7 @@ public class TimeTravelTest
// Confirm that an alternate future is created. // Confirm that an alternate future is created.
World world = setup.UpdateWorld(); World world = setup.UpdateWorld();
Season fork = world.GetSeason("b1"); Season fork = world.Seasons["b1"];
Unit tyr1 = world.GetUnitAt("Tyr", fork); Unit tyr1 = world.GetUnitAt("Tyr", fork);
Assert.That( Assert.That(
tyr1.Past, tyr1.Past,

View File

@ -169,8 +169,8 @@ public class MovementAdjudicatorTest
World updated = setup.UpdateWorld(); World updated = setup.UpdateWorld();
// Confirm the future was created // Confirm the future was created
Assert.That(updated.Seasons.Count, Is.EqualTo(2)); Assert.That(updated.Seasons.Values.Count, Is.EqualTo(2));
Season future = updated.Seasons.Single(s => s != updated.RootSeason); Season future = updated.Seasons.Values.Single(s => s != updated.RootSeason);
Assert.That(future.Past, Is.EqualTo(updated.RootSeason.ToString())); Assert.That(future.Past, Is.EqualTo(updated.RootSeason.ToString()));
Assert.That(updated.GetFutures(future), Is.Empty); Assert.That(updated.GetFutures(future), Is.Empty);
Assert.That(future.Timeline, Is.EqualTo(updated.RootSeason.Timeline)); Assert.That(future.Timeline, Is.EqualTo(updated.RootSeason.Timeline));
@ -199,7 +199,7 @@ public class MovementAdjudicatorTest
World updated = setup.UpdateWorld(); World updated = setup.UpdateWorld();
// Confirm the future was created // Confirm the future was created
Season s2 = updated.GetSeason("a1"); Season s2 = updated.Seasons["a1"];
Assert.That(s2.Past, Is.EqualTo(s1.ToString())); Assert.That(s2.Past, Is.EqualTo(s1.ToString()));
Assert.That(updated.GetFutures(s2), Is.Empty); Assert.That(updated.GetFutures(s2), Is.Empty);
Assert.That(s2.Timeline, Is.EqualTo(s1.Timeline)); Assert.That(s2.Timeline, Is.EqualTo(s1.Timeline));

View File

@ -10,7 +10,7 @@ public class SeasonTests
public void TimelineForking() public void TimelineForking()
{ {
World world = World.WithMap(Map.Test); World world = World.WithMap(Map.Test);
Season a0 = world.GetSeason("a0"); Season a0 = world.Seasons["a0"];
world = world world = world
.ContinueOrFork(a0, out Season a1) .ContinueOrFork(a0, out Season a1)
.ContinueOrFork(a1, out Season a2) .ContinueOrFork(a1, out Season a2)
@ -21,7 +21,7 @@ public class SeasonTests
.ContinueOrFork(a2, out Season d3); .ContinueOrFork(a2, out Season d3);
Assert.That( Assert.That(
world.Seasons.Select(season => season.ToString()), world.Seasons.Values.Select(season => season.ToString()),
Is.EquivalentTo(new List<string> { "a0", "a1", "a2", "a3", "b2", "b3", "c2", "d3" }), Is.EquivalentTo(new List<string> { "a0", "a1", "a2", "a3", "b2", "b3", "c2", "d3" }),
"Unexpected seasons"); "Unexpected seasons");

View File

@ -1,6 +1,7 @@
using System.Text.Json; using System.Text.Json;
using MultiversalDiplomacy.Adjudicate; using MultiversalDiplomacy.Adjudicate;
using MultiversalDiplomacy.Adjudicate.Decision;
using MultiversalDiplomacy.Model; using MultiversalDiplomacy.Model;
using NUnit.Framework; using NUnit.Framework;
@ -64,30 +65,44 @@ public class SerializationTest
Assert.That(tyr0, Is.NotDislodged); Assert.That(tyr0, Is.NotDislodged);
setup.UpdateWorld(); setup.UpdateWorld();
Assert.That(setup.World.OrderHistory["a0"].Orders.Count, Is.GreaterThan(0), "Missing orders");
Assert.That(setup.World.OrderHistory["a0"].DoesMoveOutcomes.Count, Is.GreaterThan(0), "Missing moves");
Assert.That(setup.World.OrderHistory["a0"].IsDislodgedOutcomes.Count, Is.GreaterThan(0), "Missing dislodges");
// Serialize and deserialize the world // Serialize and deserialize the world
string serialized = JsonSerializer.Serialize(setup.World, Options); string serialized = JsonSerializer.Serialize(setup.World, Options);
// Console.WriteLine(serialized);
World reserialized = JsonSerializer.Deserialize<World>(serialized, Options) World reserialized = JsonSerializer.Deserialize<World>(serialized, Options)
?? throw new AssertionException("Failed to reserialize world"); ?? throw new AssertionException("Failed to reserialize world");
Assert.Multiple(() => {
Assert.That(reserialized.OrderHistory["a0"].Orders.Count, Is.GreaterThan(0), "Missing orders");
Assert.That(reserialized.OrderHistory["a0"].DoesMoveOutcomes.Count, Is.GreaterThan(0), "Missing moves");
Assert.That(reserialized.OrderHistory["a0"].IsDislodgedOutcomes.Count, Is.GreaterThan(0), "Missing dislodges");
});
Assert.Ignore();
// Resume the test case // Resume the test case
setup = new(reserialized, MovementPhaseAdjudicator.Instance); setup = new(reserialized, MovementPhaseAdjudicator.Instance);
setup[("a", 1)] setup[("a", 1)]
["Germany"] ["Germany"]
.Army("Mun").Supports.Army("Mun", season: reserialized.GetSeason("a0")).MoveTo("Tyr").GetReference(out var mun1) .Army("Mun").Supports.Army("Mun", season: reserialized.Seasons["a0"]).MoveTo("Tyr").GetReference(out var mun1)
["Austria"] ["Austria"]
.Army("Tyr").Holds(); .Army("Tyr").Holds();
setup.ValidateOrders(); setup.ValidateOrders();
Assert.That(mun1, Is.Valid); Assert.That(mun1, Is.Valid);
setup.AdjudicateOrders(); var adjudications = setup.AdjudicateOrders();
Assert.That(mun1, Is.NotCut); Assert.That(mun1, Is.NotCut);
Assert.That(mun0, Is.Victorious); Console.WriteLine(string.Join(", ", adjudications.Select(a => a.ToString())));
Assert.That(tyr0, Is.Dislodged); DoesMove mun0move = adjudications.OfType<DoesMove>().Single(move => move.Order.Unit.Designation == mun0.Order.Unit.Designation);
Assert.That(mun0move.Outcome, Is.True);
IsDislodged tyr0dislodge = adjudications.OfType<IsDislodged>().Single(dis => dis.Order.Unit.Designation == tyr0.Order.Unit.Designation);
Assert.That(tyr0dislodge, Is.True);
// Confirm that an alternate future is created. // Confirm that an alternate future is created.
World world = setup.UpdateWorld(); World world = setup.UpdateWorld();
Season fork = world.GetSeason("b1"); Season fork = world.Seasons["b1"];
Unit tyr1 = world.GetUnitAt("Tyr", fork); Unit tyr1 = world.GetUnitAt("Tyr", fork);
Assert.That( Assert.That(
tyr1.Past, tyr1.Past,

View File

@ -419,7 +419,7 @@ public class TestCaseBuilder
MoveOrder moveOrder = new MoveOrder( MoveOrder moveOrder = new MoveOrder(
this.PowerContext.Power, this.PowerContext.Power,
this.Unit, this.Unit,
destSeason, destSeason.Designation,
destination); destination);
this.Builder.OrderList.Add(moveOrder); this.Builder.OrderList.Add(moveOrder);
return new OrderDefinedContext<MoveOrder>(this, moveOrder); return new OrderDefinedContext<MoveOrder>(this, moveOrder);