Compare commits
5 Commits
161e0a1ddb
...
25d903d91a
Author | SHA1 | Date |
---|---|---|
Tim Van Baak | 25d903d91a | |
Tim Van Baak | 566d29e539 | |
Tim Van Baak | 4b2712e4bc | |
Tim Van Baak | bfdf2d5636 | |
Tim Van Baak | 2e6e6c55b8 |
|
@ -60,9 +60,9 @@ public class MovementDecisions
|
|||
{
|
||||
case MoveOrder move:
|
||||
AdvanceTimeline.Ensure(
|
||||
move.Season,
|
||||
() => new(world.Seasons[move.Season], world.OrderHistory[move.Season].Orders));
|
||||
AdvanceTimeline[move.Season].Orders.Add(move);
|
||||
move.Season.Key,
|
||||
() => new(move.Season, world.OrderHistory[move.Season.Key].Orders));
|
||||
AdvanceTimeline[move.Season.Key].Orders.Add(move);
|
||||
break;
|
||||
|
||||
case SupportHoldOrder supportHold:
|
||||
|
@ -94,7 +94,7 @@ public class MovementDecisions
|
|||
(Province province, string season) UnitPoint(Unit unit)
|
||||
=> (world.Map.GetLocation(unit.Location).Province, unit.Season.Key);
|
||||
(Province province, string season) MovePoint(MoveOrder move)
|
||||
=> (move.Province, move.Season);
|
||||
=> (move.Province, move.Season.Key);
|
||||
|
||||
// Create a hold strength decision with an associated order for every province with a unit.
|
||||
foreach (UnitOrder order in relevantOrders)
|
||||
|
@ -107,12 +107,12 @@ public class MovementDecisions
|
|||
|
||||
bool IsIncoming(UnitOrder me, MoveOrder other)
|
||||
=> me != other
|
||||
&& world.Seasons[other.Season] == me.Unit.Season
|
||||
&& other.Season == me.Unit.Season
|
||||
&& other.Province == world.Map.GetLocation(me.Unit).Province;
|
||||
|
||||
bool AreOpposing(MoveOrder one, MoveOrder two)
|
||||
=> one.Season == two.Unit.Season.Key
|
||||
&& two.Season == one.Unit.Season.Key
|
||||
=> one.Season == two.Unit.Season
|
||||
&& two.Season == one.Unit.Season
|
||||
&& one.Province == world.Map.GetLocation(two.Unit).Province
|
||||
&& two.Province == world.Map.GetLocation(one.Unit).Province;
|
||||
|
||||
|
@ -153,7 +153,7 @@ public class MovementDecisions
|
|||
DoesMove[move] = new(move, opposingMove, competing);
|
||||
|
||||
// Ensure a hold strength decision exists for the destination.
|
||||
HoldStrength.Ensure(MovePoint(move), () => new(move.Province, world.Seasons[move.Season]));
|
||||
HoldStrength.Ensure(MovePoint(move), () => new(move.Province, move.Season));
|
||||
}
|
||||
else if (order is SupportOrder support)
|
||||
{
|
||||
|
|
|
@ -50,7 +50,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
|
||||
// Invalidate any order given to a unit in the past.
|
||||
AdjudicatorHelpers.InvalidateIfNotMatching(
|
||||
order => !world.GetFutures(order.Unit.Season).Any(),
|
||||
order => !world.Timelines.GetFutures(order.Unit.Season).Any(),
|
||||
ValidationReason.IneligibleForOrder,
|
||||
ref unitOrders,
|
||||
ref validationResults);
|
||||
|
@ -77,7 +77,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
|
||||
// Trivial check: a unit cannot move to where it already is.
|
||||
AdjudicatorHelpers.InvalidateIfNotMatching(
|
||||
order => !(order.Location.Key == order.Unit.Location && order.Season == order.Unit.Season.Key),
|
||||
order => !(order.Location.Key == order.Unit.Location && order.Season == order.Unit.Season),
|
||||
ValidationReason.DestinationMatchesOrigin,
|
||||
ref moveOrders,
|
||||
ref validationResults);
|
||||
|
@ -92,9 +92,9 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
// Map adjacency
|
||||
world.Map.GetLocation(order.Unit).Adjacents.Contains(order.Location)
|
||||
// Turn adjacency
|
||||
&& Math.Abs(order.Unit.Season.Turn - world.Seasons[order.Season].Turn) <= 1
|
||||
&& Math.Abs(order.Unit.Season.Turn - order.Season.Turn) <= 1
|
||||
// Timeline adjacency
|
||||
&& world.InAdjacentTimeline(order.Unit.Season, world.Seasons[order.Season]));
|
||||
&& world.Timelines.InAdjacentTimeline(order.Unit.Season, order.Season));
|
||||
List<MoveOrder> adjacentMoveOrders = moveOrdersByAdjacency[true].ToList();
|
||||
List<MoveOrder> nonAdjacentMoveOrders = moveOrdersByAdjacency[false].ToList();
|
||||
|
||||
|
@ -180,7 +180,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
// Turn adjacency
|
||||
&& Math.Abs(order.Unit.Season.Turn - order.Target.Season.Turn) <= 1
|
||||
// Timeline adjacency
|
||||
&& world.InAdjacentTimeline(order.Unit.Season, order.Target.Season),
|
||||
&& world.Timelines.InAdjacentTimeline(order.Unit.Season, order.Target.Season),
|
||||
ValidationReason.UnreachableSupport,
|
||||
ref supportHoldOrders,
|
||||
ref validationResults);
|
||||
|
@ -212,7 +212,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
// Turn adjacency
|
||||
&& Math.Abs(order.Unit.Season.Turn - order.Season.Turn) <= 1
|
||||
// Timeline adjacency
|
||||
&& world.InAdjacentTimeline(order.Unit.Season, order.Season),
|
||||
&& world.Timelines.InAdjacentTimeline(order.Unit.Season, order.Season),
|
||||
ValidationReason.UnreachableSupport,
|
||||
ref supportMoveOrders,
|
||||
ref validationResults);
|
||||
|
@ -255,7 +255,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
|
||||
// Finally, add implicit hold orders for units without legal orders.
|
||||
List<Unit> allOrderableUnits = world.Units
|
||||
.Where(unit => !world.GetFutures(unit.Season).Any())
|
||||
.Where(unit => !world.Timelines.GetFutures(unit.Season).Any())
|
||||
.ToList();
|
||||
HashSet<Unit> orderedUnits = validOrders.Select(order => order.Unit).ToHashSet();
|
||||
List<Unit> unorderedUnits = allOrderableUnits
|
||||
|
@ -315,6 +315,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
Dictionary<Season, Season> createdFutures = [];
|
||||
List<Unit> createdUnits = [];
|
||||
List<RetreatingUnit> retreats = [];
|
||||
Timelines newTimelines = world.Timelines;
|
||||
|
||||
// Populate createdFutures with the timeline fork decisions
|
||||
logger.Log(1, "Processing AdvanceTimeline decisions");
|
||||
|
@ -324,7 +325,8 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
if (advanceTimeline.Outcome == true)
|
||||
{
|
||||
// A timeline that doesn't have a future yet simply continues. Otherwise, it forks.
|
||||
createdFutures[advanceTimeline.Season] = world.ContinueOrFork(advanceTimeline.Season);
|
||||
newTimelines = newTimelines.WithNewSeason(advanceTimeline.Season, out var newFuture);
|
||||
createdFutures[advanceTimeline.Season] = newFuture;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -334,8 +336,8 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
foreach (DoesMove doesMove in moves.Values)
|
||||
{
|
||||
logger.Log(2, "{0} = {1}", doesMove, doesMove.Outcome?.ToString() ?? "?");
|
||||
Season moveSeason = world.Seasons[doesMove.Order.Season];
|
||||
if (doesMove.Outcome == true && createdFutures.TryGetValue(moveSeason, out Season? future))
|
||||
Season moveSeason = doesMove.Order.Season;
|
||||
if (doesMove.Outcome == true && createdFutures.TryGetValue(moveSeason, out Season future))
|
||||
{
|
||||
Unit next = doesMove.Order.Unit.Next(doesMove.Order.Location.Key, future);
|
||||
logger.Log(3, "Advancing unit to {0}", next);
|
||||
|
@ -416,10 +418,10 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
// TODO provide more structured information about order outcomes
|
||||
|
||||
World updated = world.Update(
|
||||
seasons: world.Seasons.Values.Concat(createdFutures.Values),
|
||||
units: world.Units.Concat(createdUnits),
|
||||
retreats: retreats,
|
||||
orders: updatedHistory);
|
||||
orders: updatedHistory,
|
||||
timelines: newTimelines);
|
||||
|
||||
logger.Log(0, "Completed update");
|
||||
|
||||
|
@ -484,7 +486,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
bool progress = false;
|
||||
|
||||
// A season at the head of a timeline always advances.
|
||||
if (!world.GetFutures(decision.Season).Any())
|
||||
if (!world.Timelines.GetFutures(decision.Season).Any())
|
||||
{
|
||||
progress |= LoggedUpdate(decision, true, depth, "A timeline head always advances");
|
||||
return progress;
|
||||
|
@ -493,8 +495,8 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
// 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.Key
|
||||
&& !world.OrderHistory[order.Season].DoesMoveOutcomes.ContainsKey(order.Unit.Key));
|
||||
.Where(order => order.Season == decision.Season
|
||||
&& !world.OrderHistory[order.Season.Key].DoesMoveOutcomes.ContainsKey(order.Unit.Key));
|
||||
foreach (MoveOrder moveOrder in newIncomingMoves)
|
||||
{
|
||||
DoesMove doesMove = decisions.DoesMove[moveOrder];
|
||||
|
@ -635,9 +637,9 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
if (// Map adjacency
|
||||
world.Map.GetLocation(decision.Order.Unit).Adjacents.Contains(decision.Order.Location)
|
||||
// Turn adjacency
|
||||
&& Math.Abs(decision.Order.Unit.Season.Turn - world.Seasons[decision.Order.Season].Turn) <= 1
|
||||
&& Math.Abs(decision.Order.Unit.Season.Turn - decision.Order.Season.Turn) <= 1
|
||||
// Timeline adjacency
|
||||
&& world.InAdjacentTimeline(decision.Order.Unit.Season, world.Seasons[decision.Order.Season]))
|
||||
&& world.Timelines.InAdjacentTimeline(decision.Order.Unit.Season, decision.Order.Season))
|
||||
{
|
||||
bool update = LoggedUpdate(decision, true, depth, "Adjacent move");
|
||||
return progress | update;
|
||||
|
@ -774,7 +776,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
// 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
|
||||
// to dislodge it.
|
||||
UnitOrder? destOrder = decisions.HoldStrength[(decision.Order.Province, decision.Order.Season)].Order;
|
||||
UnitOrder? destOrder = decisions.HoldStrength[(decision.Order.Province, decision.Order.Season.Key)].Order;
|
||||
DoesMove? destMoveAway = destOrder is MoveOrder moveAway
|
||||
? decisions.DoesMove[moveAway]
|
||||
: null;
|
||||
|
@ -793,7 +795,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
// Is failing to move away
|
||||
|| destMoveAway.Outcome == false))
|
||||
{
|
||||
Power destPower = destOrder.Unit.Power;
|
||||
string destPower = destOrder.Unit.Power;
|
||||
if (decision.Order.Unit.Power == destPower)
|
||||
{
|
||||
// Cannot dislodge own unit.
|
||||
|
@ -823,7 +825,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
// the case where it doesn't move and the attack strength is mitigated by supports not
|
||||
// helping to dislodge units of the same power as the support. The maximum tracks the
|
||||
// case where it does move and the attack strength is unmitigated.
|
||||
Power destPower = destMoveAway.Order.Unit.Power;
|
||||
string destPower = destMoveAway.Order.Unit.Power;
|
||||
int min = 1;
|
||||
int max = 1;
|
||||
foreach (SupportMoveOrder support in decision.Supports)
|
||||
|
@ -953,7 +955,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
// strength.
|
||||
NumericAdjudicationDecision defense = decision.OpposingMove != null
|
||||
? decisions.DefendStrength[decision.OpposingMove]
|
||||
: decisions.HoldStrength[(decision.Order.Province, decision.Order.Season)];
|
||||
: decisions.HoldStrength[(decision.Order.Province, decision.Order.Season.Key)];
|
||||
progress |= ResolveDecision(defense, world, decisions, depth + 1);
|
||||
|
||||
// If the attack doesn't beat the defense, resolve the move to false.
|
||||
|
|
|
@ -18,7 +18,7 @@ public static class PathFinder
|
|||
/// Determines if a convoy path exists for a move order.
|
||||
/// </summary>
|
||||
public static bool ConvoyPathExists(World world, MoveOrder order)
|
||||
=> ConvoyPathExists(world, order.Unit, order.Location, world.Seasons[order.Season]);
|
||||
=> ConvoyPathExists(world, order.Unit, order.Location, order.Season);
|
||||
|
||||
private static bool ConvoyPathExists(
|
||||
World world,
|
||||
|
@ -111,35 +111,36 @@ public static class PathFinder
|
|||
/// </summary>
|
||||
public static IEnumerable<Season> GetAdjacentSeasons(World world, Season season)
|
||||
{
|
||||
var pasts = world.Timelines.Pasts;
|
||||
List<Season> adjacents = [];
|
||||
|
||||
// The immediate past and all immediate futures are adjacent.
|
||||
if (season.Past != null) adjacents.Add(world.Seasons[season.Past]);
|
||||
adjacents.AddRange(world.GetFutures(season));
|
||||
if (pasts[season.Key] is Season immediatePast) adjacents.Add(immediatePast);
|
||||
adjacents.AddRange(world.Timelines.GetFutures(season));
|
||||
|
||||
// Find all adjacent timelines by finding all timelines that branched off of this season's
|
||||
// timeline, i.e. all futures of this season's past that have different timelines. Also
|
||||
// include any timelines that branched off of the timeline this timeline branched off from.
|
||||
List<Season> adjacentTimelineRoots = [];
|
||||
Season? current;
|
||||
for (current = season;
|
||||
current?.Past != null && world.Seasons[current.Past].Timeline == current.Timeline;
|
||||
current = world.Seasons[current.Past])
|
||||
Season? current = season;
|
||||
for (;
|
||||
pasts[current?.Key!] is Season currentPast && currentPast.Timeline == current?.Timeline;
|
||||
current = pasts[current?.Key!])
|
||||
{
|
||||
adjacentTimelineRoots.AddRange(
|
||||
world.GetFutures(current).Where(s => s.Timeline != current.Timeline));
|
||||
world.Timelines.GetFutures(current.Value).Where(s => s.Timeline != current?.Timeline));
|
||||
}
|
||||
|
||||
// At the end of the for loop, if this season is part of the first timeline, then current
|
||||
// is the root season (current.past == null); if this season is in a branched 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). Check for co-branches if this season is in a branched timeline, since
|
||||
// the first timeline by definition cannot have co-branches.
|
||||
if (current?.Past != null && world.Seasons[current.Past] is Season past)
|
||||
if (pasts[current?.Key!] is Season rootPast)
|
||||
{
|
||||
IEnumerable<Season> cobranchRoots = world
|
||||
.GetFutures(past)
|
||||
.Where(s => s.Timeline != current.Timeline && s.Timeline != past.Timeline);
|
||||
IEnumerable<Season> cobranchRoots = world.Timelines
|
||||
.GetFutures(rootPast)
|
||||
.Where(s => s.Timeline != current?.Timeline && s.Timeline != rootPast.Timeline);
|
||||
adjacentTimelineRoots.AddRange(cobranchRoots);
|
||||
}
|
||||
|
||||
|
@ -147,11 +148,13 @@ public static class PathFinder
|
|||
foreach (Season timelineRoot in adjacentTimelineRoots)
|
||||
{
|
||||
for (Season? branchSeason = timelineRoot;
|
||||
branchSeason != null && branchSeason.Turn <= season.Turn + 1;
|
||||
branchSeason = world.GetFutures(branchSeason)
|
||||
.FirstOrDefault(s => s!.Timeline == branchSeason.Timeline, null))
|
||||
branchSeason is Season branch && branch.Turn <= season.Turn + 1;
|
||||
branchSeason = world.Timelines
|
||||
.GetFutures(branchSeason!.Value)
|
||||
.Cast<Season?>()
|
||||
.FirstOrDefault(s => s?.Timeline == branchSeason?.Timeline, null))
|
||||
{
|
||||
if (branchSeason.Turn >= season.Turn - 1) adjacents.Add(branchSeason);
|
||||
if (branchSeason?.Turn >= season.Turn - 1) adjacents.Add(branchSeason.Value);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -22,11 +22,11 @@ public class Map
|
|||
/// <summary>
|
||||
/// The game powers.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<Power> Powers => _Powers.AsReadOnly();
|
||||
public IReadOnlyCollection<string> Powers => _Powers.AsReadOnly();
|
||||
|
||||
private List<Power> _Powers { get; }
|
||||
private List<string> _Powers { get; }
|
||||
|
||||
private Map(MapType type, IEnumerable<Province> provinces, IEnumerable<Power> powers)
|
||||
private Map(MapType type, IEnumerable<Province> provinces, IEnumerable<string> powers)
|
||||
{
|
||||
Type = type;
|
||||
_Provinces = provinces.ToList();
|
||||
|
@ -84,10 +84,10 @@ public class Map
|
|||
: GetLocation(provinceName, l => l.Name == coastName || l.Abbreviation == coastName);
|
||||
|
||||
/// <summary>
|
||||
/// Get a power by name. Throws if there is not exactly one such power.
|
||||
/// Get a power by full or partial name. Throws if there is not exactly one such power.
|
||||
/// </summary>
|
||||
public Power GetPower(string powerName)
|
||||
=> Powers.SingleOrDefault(p => p!.Name == powerName || p.Name.StartsWith(powerName), null)
|
||||
public string GetPower(string powerName)
|
||||
=> Powers.SingleOrDefault(p => p!.EqualsAnyCase(powerName) || p!.StartsWithAnyCase(powerName), null)
|
||||
?? throw new KeyNotFoundException($"Power {powerName} not found (powers: {string.Join(", ", Powers)})");
|
||||
|
||||
public static Map FromType(MapType type)
|
||||
|
@ -110,10 +110,7 @@ public class Map
|
|||
center.AddBorder(lef.Locations.First());
|
||||
center.AddBorder(rig.Locations.First());
|
||||
|
||||
Power a = new("Alpha");
|
||||
Power b = new("Beta");
|
||||
|
||||
return new(MapType.Test, [lef, cen, rig], [a, b]);
|
||||
return new(MapType.Test, [lef, cen, rig], ["Alpha", "Beta"]);
|
||||
});
|
||||
|
||||
public static Map Classical => _Classical.Value;
|
||||
|
@ -557,15 +554,15 @@ public class Map
|
|||
Water("WES").AddBorder(Coast("SPA", "sc"));
|
||||
#endregion
|
||||
|
||||
List<Power> powers =
|
||||
List<string> powers =
|
||||
[
|
||||
new("Austria"),
|
||||
new("England"),
|
||||
new("France"),
|
||||
new("Germany"),
|
||||
new("Italy"),
|
||||
new("Russia"),
|
||||
new("Turkey"),
|
||||
"Austria",
|
||||
"England",
|
||||
"France",
|
||||
"Germany",
|
||||
"Italy",
|
||||
"Russia",
|
||||
"Turkey",
|
||||
];
|
||||
|
||||
return new(MapType.Classical, provinces, powers);
|
||||
|
|
|
@ -21,9 +21,6 @@ public static class ModelExtensions
|
|||
return $"{coord.season.Timeline}-{coord.province.Abbreviations[0]}@{coord.season.Turn}";
|
||||
}
|
||||
|
||||
public static World ContinueOrFork(this World world, Season season, out Season future)
|
||||
{
|
||||
future = world.ContinueOrFork(season);
|
||||
return world.Update(seasons: world.Seasons.Values.Append(future));
|
||||
}
|
||||
public static World WithNewSeason(this World world, Season season, out Season future)
|
||||
=> world.Update(timelines: world.Timelines.WithNewSeason(season, out future));
|
||||
}
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
namespace MultiversalDiplomacy.Model;
|
||||
|
||||
/// <summary>
|
||||
/// One of the rival nations vying for control of the map.
|
||||
/// </summary>
|
||||
public class Power
|
||||
{
|
||||
/// <summary>
|
||||
/// The power's name.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
public Power(string name)
|
||||
{
|
||||
this.Name = name;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return this.Name;
|
||||
}
|
||||
}
|
|
@ -1,11 +1,13 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MultiversalDiplomacy.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a state of the map produced by a set of move orders on a previous season.
|
||||
/// Represents a multiversal coordinate at which a state of the map exists.
|
||||
/// </summary>
|
||||
public class Season(string? past, int turn, string timeline)
|
||||
[JsonConverter(typeof(SeasonJsonConverter))]
|
||||
public struct Season(string timeline, int turn)
|
||||
{
|
||||
/// <summary>
|
||||
/// The first turn number. This is defined to reduce confusion about whether the first turn is 0 or 1.
|
||||
|
@ -13,11 +15,9 @@ public class Season(string? past, int turn, string timeline)
|
|||
public const int FIRST_TURN = 0;
|
||||
|
||||
/// <summary>
|
||||
/// The designation of the season immediately preceding this season.
|
||||
/// If this season is an alternate timeline root, the past is from the origin timeline.
|
||||
/// The initial season does not have a past.
|
||||
/// The timeline to which this season belongs.
|
||||
/// </summary>
|
||||
public string? Past { get; } = past;
|
||||
public string Timeline { get; } = timeline;
|
||||
|
||||
/// <summary>
|
||||
/// The current turn, beginning at 0. Each season (spring and fall) is one turn.
|
||||
|
@ -26,16 +26,51 @@ public class Season(string? past, int turn, string timeline)
|
|||
/// </summary>
|
||||
public int Turn { get; } = turn;
|
||||
|
||||
/// <summary>
|
||||
/// The timeline to which this season belongs.
|
||||
/// </summary>
|
||||
public string Timeline { get; } = timeline;
|
||||
|
||||
/// <summary>
|
||||
/// The multiversal designation of this season.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public string Key => $"{this.Timeline}{this.Turn}";
|
||||
public readonly string Key => $"{this.Timeline}{this.Turn}";
|
||||
|
||||
public override string ToString() => Key;
|
||||
/// <summary>
|
||||
/// Create a new season from a tuple coordinate.
|
||||
/// </summary>
|
||||
public Season((string timeline, int turn) coord) : this(coord.timeline, coord.turn) { }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new season from a combined string designation.
|
||||
/// </summary>
|
||||
/// <param name="designation"></param>
|
||||
public Season(string designation) : this(SplitKey(designation)) { }
|
||||
|
||||
/// <summary>
|
||||
/// Extract the timeline and turn components of a season designation.
|
||||
/// </summary>
|
||||
/// <param name="seasonKey">A timeline-turn season designation.</param>
|
||||
/// <returns>The timeline and turn components.</returns>
|
||||
/// <exception cref="FormatException"></exception>
|
||||
public static (string timeline, int turn) SplitKey(string seasonKey)
|
||||
{
|
||||
int i = 1;
|
||||
for (; !char.IsAsciiDigit(seasonKey[i]) && i < seasonKey.Length; i++);
|
||||
return int.TryParse(seasonKey.AsSpan(i), out int turn)
|
||||
? (seasonKey[..i], turn)
|
||||
: throw new FormatException($"Could not parse turn from {seasonKey}");
|
||||
}
|
||||
|
||||
public override readonly string ToString() => Key;
|
||||
|
||||
/// <remarks>
|
||||
/// Seasons are essentially 2D points, so they are equal when their components are equal.
|
||||
/// </remarks>
|
||||
public override readonly bool Equals([NotNullWhen(true)] object? obj)
|
||||
=> obj is Season season
|
||||
&& Timeline == season.Timeline
|
||||
&& Turn == season.Turn;
|
||||
|
||||
public static bool operator ==(Season one, Season two) => one.Equals(two);
|
||||
|
||||
public static bool operator !=(Season one, Season two) => !(one == two);
|
||||
|
||||
public override readonly int GetHashCode() => (Timeline, Turn).GetHashCode();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MultiversalDiplomacy.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Serializes a <see cref="Season"/> as its combined designation.
|
||||
/// </summary>
|
||||
internal class SeasonJsonConverter : JsonConverter<Season>
|
||||
{
|
||||
public override Season Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
=> new(reader.GetString()!);
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, Season value, JsonSerializerOptions options)
|
||||
=> writer.WriteStringValue(value.Key);
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MultiversalDiplomacy.Model;
|
||||
|
||||
/// <summary>
|
||||
/// A shared counter for handing out new timeline designations.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(TimelineFactoryJsonConverter))]
|
||||
public class TimelineFactory(int nextTimeline)
|
||||
{
|
||||
private static readonly char[] Letters = [
|
||||
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
|
||||
'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
|
||||
'u', 'v', 'w', 'x', 'y', 'z',
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Convert a string timeline identifier to its serial number.
|
||||
/// </summary>
|
||||
/// <param name="timeline">Timeline identifier.</param>
|
||||
/// <returns>Integer.</returns>
|
||||
public static int StringToInt(string timeline)
|
||||
{
|
||||
int result = Array.IndexOf(Letters, timeline[0]);
|
||||
for (int i = 1; i < timeline.Length; i++) {
|
||||
// The result is incremented by one because timeline designations are not a true base26 system.
|
||||
// The "ones digit" maps a-z 0-25, but the "tens digit" maps a to 1, so "10" (26) is "aa" and not "a0"
|
||||
result = (result + 1) * 26;
|
||||
result += Array.IndexOf(Letters, timeline[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a timeline serial number to its string identifier.
|
||||
/// </summary>
|
||||
/// <param name="designation">Integer.</param>
|
||||
/// <returns>Timeline identifier.</returns>
|
||||
public static string IntToString(int designation) {
|
||||
static int downshift(int i ) => (i - (i % 26)) / 26;
|
||||
IEnumerable<char> result = [Letters[designation % 26]];
|
||||
for (int remainder = downshift(designation); remainder > 0; remainder = downshift(remainder) - 1) {
|
||||
// We subtract 1 after downshifting for the same reason we add 1 above after upshifting.
|
||||
//
|
||||
result = result.Prepend(Letters[(remainder % 26 + 25) % 26]);
|
||||
}
|
||||
return new string(result.ToArray());
|
||||
}
|
||||
|
||||
public TimelineFactory() : this(0) { }
|
||||
|
||||
public int nextTimeline = nextTimeline;
|
||||
|
||||
public string NextTimeline() => IntToString(nextTimeline++);
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MultiversalDiplomacy.Model;
|
||||
|
||||
internal class TimelineFactoryJsonConverter : JsonConverter<TimelineFactory>
|
||||
{
|
||||
public override TimelineFactory? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
=> new(reader.GetInt32());
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, TimelineFactory value, JsonSerializerOptions options)
|
||||
=> writer.WriteNumberValue(value.nextTimeline);
|
||||
}
|
|
@ -0,0 +1,156 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MultiversalDiplomacy.Model;
|
||||
|
||||
/// <summary>
|
||||
/// Tracks the relations between seasons.
|
||||
/// </summary>
|
||||
public class Timelines(int next, Dictionary<string, Season?> pasts)
|
||||
{
|
||||
private static readonly char[] Letters = [
|
||||
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
|
||||
'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
|
||||
'u', 'v', 'w', 'x', 'y', 'z',
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Convert a string timeline identifier to its serial number.
|
||||
/// </summary>
|
||||
/// <param name="timeline">Timeline identifier.</param>
|
||||
/// <returns>Integer.</returns>
|
||||
public static int StringToInt(string timeline)
|
||||
{
|
||||
int result = Array.IndexOf(Letters, timeline[0]);
|
||||
for (int i = 1; i < timeline.Length; i++) {
|
||||
// The result is incremented by one because timeline designations are not a true base26 system.
|
||||
// The "ones digit" maps a-z 0-25, but the "tens digit" maps a to 1, so "10" (26) is "aa" and not "a0"
|
||||
result = (result + 1) * 26;
|
||||
result += Array.IndexOf(Letters, timeline[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a timeline serial number to its string identifier.
|
||||
/// </summary>
|
||||
/// <param name="serial">Integer.</param>
|
||||
/// <returns>Timeline identifier.</returns>
|
||||
public static string IntToString(int serial) {
|
||||
static int downshift(int i ) => (i - (i % 26)) / 26;
|
||||
IEnumerable<char> result = [Letters[serial % 26]];
|
||||
for (int remainder = downshift(serial); remainder > 0; remainder = downshift(remainder) - 1) {
|
||||
// We subtract 1 after downshifting for the same reason we add 1 above after upshifting.
|
||||
result = result.Prepend(Letters[(remainder % 26 + 25) % 26]);
|
||||
}
|
||||
return new string(result.ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract the timeline and turn components of a season designation.
|
||||
/// </summary>
|
||||
/// <param name="seasonKey">A timeline-turn season designation.</param>
|
||||
/// <returns>The timeline and turn components.</returns>
|
||||
/// <exception cref="FormatException"></exception>
|
||||
public static (string timeline, int turn) SplitKey(string seasonKey)
|
||||
{
|
||||
int i = 1;
|
||||
for (; !char.IsAsciiDigit(seasonKey[i]) && i < seasonKey.Length; i++);
|
||||
return int.TryParse(seasonKey.AsSpan(i), out int turn)
|
||||
? (seasonKey[..i], turn)
|
||||
: throw new FormatException($"Could not parse turn from {seasonKey}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The next timeline to be created.
|
||||
/// </summary>
|
||||
public int Next { get; private set; } = next;
|
||||
|
||||
/// <summary>
|
||||
/// Map of season designations to their parent seasons.
|
||||
/// The set of keys here is the set of all seasons in the multiverse.
|
||||
/// </summary>
|
||||
public Dictionary<string, Season?> Pasts { get; } = pasts;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new multiverse with an initial season.
|
||||
/// </summary>
|
||||
public static Timelines Create()
|
||||
{
|
||||
Season first = new(IntToString(0), Season.FIRST_TURN);
|
||||
return new Timelines(1, new() { {first.Key, null} });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a continuation of a season if it has no futures, otherwise create a fork.
|
||||
/// </summary>
|
||||
public Timelines WithNewSeason(Season past, out Season future)
|
||||
{
|
||||
int next;
|
||||
(next, future) = GetFutureKeys(past).Any()
|
||||
? (Next + 1, new Season(IntToString(Next), past.Turn + 1))
|
||||
: (Next, new Season(past.Timeline, past.Turn + 1));
|
||||
return new Timelines(next, new(Pasts.Append(new KeyValuePair<string, Season?>(future.Key, past))));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a continuation of a season if it has no futures, otherwise create a fork.
|
||||
/// </summary>
|
||||
public Timelines WithNewSeason(string past, out Season future) => WithNewSeason(new Season(past), out future);
|
||||
|
||||
/// <summary>
|
||||
/// Get all seasons that are immediate futures of a season.
|
||||
/// </summary>
|
||||
/// <param name="season">A season.</param>
|
||||
/// <returns>The immediate futures of the season.</returns>
|
||||
public IEnumerable<string> GetFutureKeys(Season season)
|
||||
=> Pasts.Where(kvp => kvp.Value is Season future && future == season).Select(kvp => kvp.Key);
|
||||
|
||||
/// <summary>
|
||||
/// Get all seasons that are immediate futures of a season.
|
||||
/// </summary>
|
||||
/// <param name="season">A season.</param>
|
||||
/// <returns>The immediate futures of the season.</returns>
|
||||
public IEnumerable<Season> GetFutures(Season season) => GetFutureKeys(season).Select(key => new Season(key));
|
||||
|
||||
/// <summary>
|
||||
/// Returns the first season in this season's timeline. The first season is the
|
||||
/// root of the first timeline. The earliest season in each alternate timeline is
|
||||
/// the root of that timeline.
|
||||
/// </summary>
|
||||
public Season GetTimelineRoot(Season season)
|
||||
{
|
||||
Console.WriteLine($"GetTimelineRoot({season.Key})");
|
||||
return Pasts[season.Key] is Season past && season.Timeline == past.Timeline
|
||||
? GetTimelineRoot(past)
|
||||
: season;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the first season in this season's timeline. The first season is the
|
||||
/// root of the first timeline. The earliest season in each alternate timeline is
|
||||
/// the root of that timeline.
|
||||
/// </summary>
|
||||
public Season GetTimelineRoot(string season) => GetTimelineRoot(new Season(season));
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether a season is in an adjacent timeline to another season.
|
||||
/// Seasons are considered to be in adjacent timelines if they are in the same timeline,
|
||||
/// one is in a timeline that branched from the other's timeline, or both are in timelines
|
||||
/// that branched from the same point.
|
||||
/// </summary>
|
||||
public bool InAdjacentTimeline(Season one, Season two)
|
||||
{
|
||||
// Timelines are adjacent to themselves. Early out in that case.
|
||||
if (one == two) return true;
|
||||
|
||||
// If the timelines aren't identical, one of them isn't the initial trunk.
|
||||
// They can still be adjacent if one of them branched off of the other, or
|
||||
// if they both branched off of the same point.
|
||||
Season rootOne = GetTimelineRoot(one);
|
||||
Season rootTwo = GetTimelineRoot(two);
|
||||
bool oneForked = Pasts[rootOne.Key] is Season originOne && originOne.Timeline == two.Timeline;
|
||||
bool twoForked = Pasts[rootTwo.Key] is Season originTwo && originTwo.Timeline == one.Timeline;
|
||||
bool bothForked = Pasts[rootOne.Key] == Pasts[rootTwo.Key];
|
||||
return oneForked || twoForked || bothForked;
|
||||
}
|
||||
}
|
|
@ -25,7 +25,7 @@ public class Unit
|
|||
/// <summary>
|
||||
/// The allegiance of the unit.
|
||||
/// </summary>
|
||||
public Power Power { get; }
|
||||
public string Power { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of unit.
|
||||
|
@ -38,7 +38,7 @@ public class Unit
|
|||
[JsonIgnore]
|
||||
public string Key => $"{Type.ToShort()} {Season.Timeline}-{Location}@{Season.Turn}";
|
||||
|
||||
public Unit(string? past, string location, Season season, Power power, UnitType type)
|
||||
public Unit(string? past, string location, Season season, string power, UnitType type)
|
||||
{
|
||||
this.Past = past;
|
||||
this.Location = location;
|
||||
|
@ -48,13 +48,13 @@ public class Unit
|
|||
}
|
||||
|
||||
public override string ToString()
|
||||
=> $"{Power.Name[0]} {Type.ToShort()} {Season.Timeline}-{Location}@{Season.Turn}";
|
||||
=> $"{Power[0]} {Type.ToShort()} {Season.Timeline}-{Location}@{Season.Turn}";
|
||||
|
||||
/// <summary>
|
||||
/// Create a new unit. No validation is performed; the adjudicator should only call this
|
||||
/// method after accepting a build order.
|
||||
/// </summary>
|
||||
public static Unit Build(string location, Season season, Power power, UnitType type)
|
||||
public static Unit Build(string location, Season season, string power, UnitType type)
|
||||
=> new(past: null, location, season, power, type);
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -37,18 +37,7 @@ public class World
|
|||
/// The game powers.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public IReadOnlyCollection<Power> Powers => this.Map.Powers;
|
||||
|
||||
/// <summary>
|
||||
/// Lookup for seasons by designation.
|
||||
/// </summary>
|
||||
public Dictionary<string, Season> Seasons { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The first season of the game.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public Season RootSeason => Seasons["a0"];
|
||||
public IReadOnlyCollection<string> Powers => this.Map.Powers;
|
||||
|
||||
/// <summary>
|
||||
/// All units in the multiverse.
|
||||
|
@ -68,7 +57,7 @@ public class World
|
|||
/// <summary>
|
||||
/// The shared timeline number generator.
|
||||
/// </summary>
|
||||
public TimelineFactory Timelines { get; }
|
||||
public Timelines Timelines { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Immutable game options.
|
||||
|
@ -78,15 +67,13 @@ public class World
|
|||
[JsonConstructor]
|
||||
public World(
|
||||
MapType mapType,
|
||||
Dictionary<string, Season> seasons,
|
||||
List<Unit> units,
|
||||
List<RetreatingUnit> retreatingUnits,
|
||||
Dictionary<string, OrderHistory> orderHistory,
|
||||
TimelineFactory timelines,
|
||||
Timelines timelines,
|
||||
Options options)
|
||||
{
|
||||
this.Map = Map.FromType(mapType);
|
||||
this.Seasons = seasons;
|
||||
this.Units = units;
|
||||
this.RetreatingUnits = retreatingUnits;
|
||||
this.OrderHistory = orderHistory;
|
||||
|
@ -100,15 +87,13 @@ public class World
|
|||
/// </summary>
|
||||
private World(
|
||||
Map map,
|
||||
Dictionary<string, Season> seasons,
|
||||
List<Unit> units,
|
||||
List<RetreatingUnit> retreatingUnits,
|
||||
Dictionary<string, OrderHistory> orderHistory,
|
||||
TimelineFactory timelines,
|
||||
Timelines timelines,
|
||||
Options options)
|
||||
{
|
||||
this.Map = map;
|
||||
this.Seasons = seasons;
|
||||
this.Units = units;
|
||||
this.RetreatingUnits = retreatingUnits;
|
||||
this.OrderHistory = orderHistory;
|
||||
|
@ -121,18 +106,17 @@ public class World
|
|||
/// </summary>
|
||||
private World(
|
||||
World previous,
|
||||
Dictionary<string, Season>? seasons = null,
|
||||
List<Unit>? units = null,
|
||||
List<RetreatingUnit>? retreatingUnits = null,
|
||||
Dictionary<string, OrderHistory>? orderHistory = null,
|
||||
Timelines? timelines = null,
|
||||
Options? options = null)
|
||||
: this(
|
||||
previous.Map,
|
||||
seasons ?? previous.Seasons,
|
||||
units ?? previous.Units,
|
||||
retreatingUnits ?? previous.RetreatingUnits,
|
||||
orderHistory ?? previous.OrderHistory,
|
||||
previous.Timelines,
|
||||
timelines ?? previous.Timelines,
|
||||
options ?? previous.Options)
|
||||
{
|
||||
}
|
||||
|
@ -142,15 +126,12 @@ public class World
|
|||
/// </summary>
|
||||
public static World WithMap(Map map)
|
||||
{
|
||||
TimelineFactory timelines = new();
|
||||
Season a0 = new(past: null, Season.FIRST_TURN, timelines.NextTimeline());
|
||||
return new World(
|
||||
map,
|
||||
new() { {a0.Key, a0} },
|
||||
new([]),
|
||||
new([]),
|
||||
new(new Dictionary<string, OrderHistory>()),
|
||||
timelines,
|
||||
Timelines.Create(),
|
||||
new Options());
|
||||
}
|
||||
|
||||
|
@ -161,15 +142,12 @@ public class World
|
|||
=> WithMap(Map.Classical);
|
||||
|
||||
public World Update(
|
||||
IEnumerable<Season>? seasons = null,
|
||||
IEnumerable<Unit>? units = null,
|
||||
IEnumerable<RetreatingUnit>? retreats = null,
|
||||
IEnumerable<KeyValuePair<string, OrderHistory>>? orders = null)
|
||||
IEnumerable<KeyValuePair<string, OrderHistory>>? orders = null,
|
||||
Timelines? timelines = null)
|
||||
=> new(
|
||||
previous: this,
|
||||
seasons: seasons == null
|
||||
? this.Seasons
|
||||
: new(seasons.ToDictionary(season => season.Key)),
|
||||
units: units == null
|
||||
? this.Units
|
||||
: new(units.ToList()),
|
||||
|
@ -178,7 +156,8 @@ public class World
|
|||
: new(retreats.ToList()),
|
||||
orderHistory: orders == null
|
||||
? this.OrderHistory
|
||||
: new(orders.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)));
|
||||
: new(orders.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)),
|
||||
timelines: timelines ?? this.Timelines);
|
||||
|
||||
/// <summary>
|
||||
/// Create a new world with new units created from unit specs. Units specs are in the format
|
||||
|
@ -190,7 +169,7 @@ public class World
|
|||
IEnumerable<Unit> units = unitSpecs.Select(spec =>
|
||||
{
|
||||
string[] splits = spec.Split(' ', 4);
|
||||
Power power = Map.GetPower(splits[0]);
|
||||
string power = Map.GetPower(splits[0]);
|
||||
UnitType type = splits[1] switch
|
||||
{
|
||||
"A" => UnitType.Army,
|
||||
|
@ -202,7 +181,7 @@ public class World
|
|||
: splits.Length == 3
|
||||
? Map.GetWater(splits[2])
|
||||
: Map.GetWater(splits[2], splits[3]);
|
||||
Unit unit = Unit.Build(location.Key, this.RootSeason, power, type);
|
||||
Unit unit = Unit.Build(location.Key, new("a0"), power, type);
|
||||
return unit;
|
||||
});
|
||||
return this.Update(units: units);
|
||||
|
@ -239,85 +218,18 @@ public class World
|
|||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a continuation of this season if it has no futures, otherwise create a fork.
|
||||
/// </summary>
|
||||
public Season ContinueOrFork(Season season)
|
||||
=> GetFutures(season).Any()
|
||||
? new(season.Key, season.Turn + 1, Timelines.NextTimeline())
|
||||
: new(season.Key, season.Turn + 1, season.Timeline);
|
||||
|
||||
/// <summary>
|
||||
/// A standard Diplomacy game setup.
|
||||
/// </summary>
|
||||
public static World Standard => WithStandardMap().AddStandardUnits();
|
||||
|
||||
/// <summary>
|
||||
/// Get a season by coordinate. Throws if the season is not found.
|
||||
/// </summary>
|
||||
public Season GetSeason(string timeline, int turn)
|
||||
=> Seasons[$"{timeline}{turn}"];
|
||||
|
||||
/// <summary>
|
||||
/// Get all seasons that are immediate futures of a season.
|
||||
/// </summary>
|
||||
/// <param name="present">A season designation.</param>
|
||||
/// <returns>The immediate futures of the designated season.</returns>
|
||||
public IEnumerable<Season> GetFutures(string present)
|
||||
=> Seasons.Values.Where(future => future.Past == present);
|
||||
|
||||
/// <summary>
|
||||
/// Get all seasons that are immediate futures of a season.
|
||||
/// </summary>
|
||||
/// <param name="present">A season.</param>
|
||||
/// <returns>The immediate futures of the season.</returns>
|
||||
public IEnumerable<Season> GetFutures(Season present) => GetFutures(present.Key);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the first season in this season's timeline. The first season is the
|
||||
/// root of the first timeline. The earliest season in each alternate timeline is
|
||||
/// the root of that timeline.
|
||||
/// </summary>
|
||||
public Season GetTimelineRoot(Season season)
|
||||
{
|
||||
if (season.Past is null) {
|
||||
return season;
|
||||
}
|
||||
Season past = Seasons[season.Past];
|
||||
return season.Timeline == past.Timeline
|
||||
? GetTimelineRoot(past)
|
||||
: season;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether this season is in an adjacent timeline to another season.
|
||||
/// Seasons are considered to be in adjacent timelines if they are in the same timeline,
|
||||
/// one is in a timeline that branched from the other's timeline, or both are in timelines
|
||||
/// that branched from the same point.
|
||||
/// </summary>
|
||||
public bool InAdjacentTimeline(Season one, Season two)
|
||||
{
|
||||
// Timelines are adjacent to themselves. Early out in that case.
|
||||
if (one == two) return true;
|
||||
|
||||
// If the timelines aren't identical, one of them isn't the initial trunk.
|
||||
// They can still be adjacent if one of them branched off of the other, or
|
||||
// if they both branched off of the same point.
|
||||
Season rootOne = GetTimelineRoot(one);
|
||||
Season rootTwo = GetTimelineRoot(two);
|
||||
bool oneForked = rootOne.Past != null && Seasons[rootOne.Past].Timeline == two.Timeline;
|
||||
bool twoForked = rootTwo.Past != null && Seasons[rootTwo.Past].Timeline == one.Timeline;
|
||||
bool bothForked = rootOne.Past == rootTwo.Past;
|
||||
return oneForked || twoForked || bothForked;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a unit in a province. Throws if there are duplicate units.
|
||||
/// </summary>
|
||||
public Unit GetUnitAt(string provinceName, Season? season = null)
|
||||
{
|
||||
Province province = Map.GetProvince(provinceName);
|
||||
season ??= RootSeason;
|
||||
season ??= new("a0");
|
||||
Unit? foundUnit = this.Units.SingleOrDefault(
|
||||
u => Map.GetLocation(u!).Province == province && u!.Season == season,
|
||||
null)
|
||||
|
|
|
@ -17,7 +17,7 @@ public class BuildOrder : Order
|
|||
/// </summary>
|
||||
public UnitType Type { get; }
|
||||
|
||||
public BuildOrder(Power power, Location location, UnitType type)
|
||||
public BuildOrder(string power, Location location, UnitType type)
|
||||
: base (power)
|
||||
{
|
||||
this.Location = location;
|
||||
|
|
|
@ -27,7 +27,7 @@ public class ConvoyOrder : UnitOrder
|
|||
/// </summary>
|
||||
public Province Province => this.Location.Province;
|
||||
|
||||
public ConvoyOrder(Power power, Unit unit, Unit target, Season season, Location location)
|
||||
public ConvoyOrder(string power, Unit unit, Unit target, Season season, Location location)
|
||||
: base (power, unit)
|
||||
{
|
||||
this.Target = target;
|
||||
|
|
|
@ -7,6 +7,6 @@ namespace MultiversalDiplomacy.Orders;
|
|||
/// </summary>
|
||||
public class DisbandOrder : UnitOrder
|
||||
{
|
||||
public DisbandOrder(Power power, Unit unit)
|
||||
public DisbandOrder(string power, Unit unit)
|
||||
: base (power, unit) {}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ namespace MultiversalDiplomacy.Orders;
|
|||
/// </summary>
|
||||
public class HoldOrder : UnitOrder
|
||||
{
|
||||
public HoldOrder(Power power, Unit unit)
|
||||
public HoldOrder(string power, Unit unit)
|
||||
: base (power, unit) {}
|
||||
|
||||
public override string ToString()
|
||||
|
|
|
@ -9,9 +9,8 @@ public class MoveOrder : UnitOrder
|
|||
{
|
||||
/// <summary>
|
||||
/// 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>
|
||||
public string Season { get; }
|
||||
public Season Season { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The destination location to which the unit should move.
|
||||
|
@ -23,7 +22,7 @@ public class MoveOrder : UnitOrder
|
|||
/// </summary>
|
||||
public Province Province => this.Location.Province;
|
||||
|
||||
public MoveOrder(Power power, Unit unit, string season, Location location)
|
||||
public MoveOrder(string power, Unit unit, Season season, Location location)
|
||||
: base (power, unit)
|
||||
{
|
||||
this.Season = season;
|
||||
|
@ -32,7 +31,7 @@ public class MoveOrder : UnitOrder
|
|||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{this.Unit} -> {this.Season} {this.Province}";
|
||||
return $"{this.Unit} -> {Season.Timeline}-{Province}@{Season.Turn}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -22,9 +22,9 @@ public abstract class Order
|
|||
/// <summary>
|
||||
/// The power that submitted this order.
|
||||
/// </summary>
|
||||
public Power Power { get; }
|
||||
public string Power { get; }
|
||||
|
||||
public Order(Power power)
|
||||
public Order(string power)
|
||||
{
|
||||
this.Power = power;
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ public class RetreatOrder : UnitOrder
|
|||
/// </summary>
|
||||
public Location Location { get; }
|
||||
|
||||
public RetreatOrder(Power power, Unit unit, Location location)
|
||||
public RetreatOrder(string power, Unit unit, Location location)
|
||||
: base (power, unit)
|
||||
{
|
||||
this.Location = location;
|
||||
|
|
|
@ -7,7 +7,7 @@ namespace MultiversalDiplomacy.Orders;
|
|||
/// </summary>
|
||||
public class SupportHoldOrder : SupportOrder
|
||||
{
|
||||
public SupportHoldOrder(Power power, Unit unit, Unit target)
|
||||
public SupportHoldOrder(string power, Unit unit, Unit target)
|
||||
: base (power, unit, target)
|
||||
{
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ public class SupportMoveOrder : SupportOrder
|
|||
/// </summary>
|
||||
public (Province province, Season season) Point => (this.Province, this.Season);
|
||||
|
||||
public SupportMoveOrder(Power power, Unit unit, Unit target, Season season, Location location)
|
||||
public SupportMoveOrder(string power, Unit unit, Unit target, Season season, Location location)
|
||||
: base(power, unit, target)
|
||||
{
|
||||
this.Season = season;
|
||||
|
@ -41,6 +41,6 @@ public class SupportMoveOrder : SupportOrder
|
|||
|
||||
public bool IsSupportFor(MoveOrder move)
|
||||
=> this.Target == move.Unit
|
||||
&& this.Season.Key == move.Season
|
||||
&& this.Season == move.Season
|
||||
&& this.Location == move.Location;
|
||||
}
|
|
@ -12,7 +12,7 @@ public abstract class SupportOrder : UnitOrder
|
|||
/// </summary>
|
||||
public Unit Target { get; }
|
||||
|
||||
public SupportOrder(Power power, Unit unit, Unit target)
|
||||
public SupportOrder(string power, Unit unit, Unit target)
|
||||
: base (power, unit)
|
||||
{
|
||||
this.Target = target;
|
||||
|
|
|
@ -17,7 +17,7 @@ public class SustainOrder : Order
|
|||
/// </summary>
|
||||
public int Timeline { get; }
|
||||
|
||||
public SustainOrder(Power power, Location timeCenter, int timeline)
|
||||
public SustainOrder(string power, Location timeCenter, int timeline)
|
||||
: base (power)
|
||||
{
|
||||
this.TimeCenter = timeCenter;
|
||||
|
|
|
@ -22,7 +22,7 @@ public abstract class UnitOrder : Order
|
|||
/// </summary>
|
||||
public Unit Unit { get; }
|
||||
|
||||
public UnitOrder(Power power, Unit unit) : base(power)
|
||||
public UnitOrder(string power, Unit unit) : base(power)
|
||||
{
|
||||
this.Unit = unit;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
namespace System;
|
||||
|
||||
public static class StringExtensions
|
||||
{
|
||||
public static bool EqualsAnyCase(this string str, string? other)
|
||||
=> str.Equals(other, StringComparison.InvariantCultureIgnoreCase);
|
||||
|
||||
public static bool StartsWithAnyCase(this string str, string other)
|
||||
=> str.StartsWith(other, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
|
@ -33,16 +33,16 @@ public class TimeTravelTest
|
|||
|
||||
// Confirm that there are now four seasons: three in the main timeline and one in a fork.
|
||||
Assert.That(
|
||||
world.Seasons.Values.Where(s => s.Timeline == s0.Timeline).Count(),
|
||||
world.Timelines.Pasts.Keys.Select(key => new Season(key)).Where(s => s.Timeline == s0.Timeline).Count(),
|
||||
Is.EqualTo(3),
|
||||
"Failed to advance main timeline after last unit left");
|
||||
Assert.That(
|
||||
world.Seasons.Values.Where(s => s.Timeline != s0.Timeline).Count(),
|
||||
world.Timelines.Pasts.Keys.Select(key => new Season(key)).Where(s => s.Timeline != s0.Timeline).Count(),
|
||||
Is.EqualTo(1),
|
||||
"Failed to fork timeline when unit moved in");
|
||||
|
||||
// Confirm that there is a unit in Tyr b1 originating from Mun a1
|
||||
Season fork = world.Seasons["b1"];
|
||||
Season fork = new("b1");
|
||||
Unit originalUnit = world.GetUnitAt("Mun", s0);
|
||||
Unit aMun0 = world.GetUnitAt("Mun", s1);
|
||||
Unit aTyr = world.GetUnitAt("Tyr", fork);
|
||||
|
@ -91,7 +91,7 @@ public class TimeTravelTest
|
|||
|
||||
// Confirm that an alternate future is created.
|
||||
World world = setup.UpdateWorld();
|
||||
Season fork = world.Seasons["b1"];
|
||||
Season fork = new("b1");
|
||||
Unit tyr1 = world.GetUnitAt("Tyr", fork);
|
||||
Assert.That(
|
||||
tyr1.Past,
|
||||
|
@ -136,12 +136,12 @@ public class TimeTravelTest
|
|||
// change the past and therefore did not create a new timeline.
|
||||
World world = setup.UpdateWorld();
|
||||
Assert.That(
|
||||
world.GetFutures(s0).Count(),
|
||||
world.Timelines.GetFutures(s0).Count(),
|
||||
Is.EqualTo(1),
|
||||
"A failed move incorrectly forked the timeline");
|
||||
Assert.That(world.GetFutures(s1).Count(), Is.EqualTo(1));
|
||||
Season s2 = world.GetSeason(s1.Timeline, s1.Turn + 1);
|
||||
Assert.That(world.GetFutures(s2).Count(), Is.EqualTo(0));
|
||||
Assert.That(world.Timelines.GetFutures(s1).Count(), Is.EqualTo(1));
|
||||
Season s2 = new(s1.Timeline, s1.Turn + 1);
|
||||
Assert.That(world.Timelines.GetFutures(s2).Count(), Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -178,12 +178,12 @@ public class TimeTravelTest
|
|||
// ...since it succeeded anyway, no fork is created.
|
||||
World world = setup.UpdateWorld();
|
||||
Assert.That(
|
||||
world.GetFutures(s0).Count(),
|
||||
world.Timelines.GetFutures(s0).Count(),
|
||||
Is.EqualTo(1),
|
||||
"A superfluous support incorrectly forked the timeline");
|
||||
Assert.That(world.GetFutures(s1).Count(), Is.EqualTo(1));
|
||||
Season s2 = world.GetSeason(s1.Timeline, s1.Turn + 1);
|
||||
Assert.That(world.GetFutures(s2).Count(), Is.EqualTo(0));
|
||||
Assert.That(world.Timelines.GetFutures(s1).Count(), Is.EqualTo(1));
|
||||
Season s2 = new(s1.Timeline, s1.Turn + 1);
|
||||
Assert.That(world.Timelines.GetFutures(s2).Count(), Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -226,17 +226,17 @@ public class TimeTravelTest
|
|||
// Since both seasons were at the head of their timelines, there should be no forking.
|
||||
World world = setup.UpdateWorld();
|
||||
Assert.That(
|
||||
world.GetFutures(a2).Count(),
|
||||
world.Timelines.GetFutures(a2).Count(),
|
||||
Is.EqualTo(1),
|
||||
"A cross-timeline support incorrectly forked the head of the timeline");
|
||||
Assert.That(
|
||||
world.GetFutures(b1).Count(),
|
||||
world.Timelines.GetFutures(b1).Count(),
|
||||
Is.EqualTo(1),
|
||||
"A cross-timeline support incorrectly forked the head of the timeline");
|
||||
Season a3 = world.GetSeason(a2.Timeline, a2.Turn + 1);
|
||||
Assert.That(world.GetFutures(a3).Count(), Is.EqualTo(0));
|
||||
Season b2 = world.GetSeason(b1.Timeline, b1.Turn + 1);
|
||||
Assert.That(world.GetFutures(b2).Count(), Is.EqualTo(0));
|
||||
Season a3 = new(a2.Timeline, a2.Turn + 1);
|
||||
Assert.That(world.Timelines.GetFutures(a3).Count(), Is.EqualTo(0));
|
||||
Season b2 = new(b1.Timeline, b1.Turn + 1);
|
||||
Assert.That(world.Timelines.GetFutures(b2).Count(), Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -297,11 +297,11 @@ public class TimeTravelTest
|
|||
// wasn't changed in this timeline.
|
||||
World world = setup.UpdateWorld();
|
||||
Assert.That(
|
||||
world.GetFutures(a3).Count(),
|
||||
world.Timelines.GetFutures(a3).Count(),
|
||||
Is.EqualTo(1),
|
||||
"A cross-timeline support cut incorrectly forked the timeline");
|
||||
Assert.That(
|
||||
world.GetFutures(b2).Count(),
|
||||
world.Timelines.GetFutures(b2).Count(),
|
||||
Is.EqualTo(1),
|
||||
"A cross-timeline support cut incorrectly forked the timeline");
|
||||
}
|
||||
|
|
|
@ -18,21 +18,86 @@ public class TimelineFactoryTest
|
|||
[TestCase(78, "ca")]
|
||||
public void RoundTripTimelineKeys(int number, string designation)
|
||||
{
|
||||
Assert.That(TimelineFactory.IntToString(number), Is.EqualTo(designation), "Incorrect string");
|
||||
Assert.That(TimelineFactory.StringToInt(designation), Is.EqualTo(number), "Incorrect number");
|
||||
Assert.That(Timelines.IntToString(number), Is.EqualTo(designation), "Incorrect string");
|
||||
Assert.That(Timelines.StringToInt(designation), Is.EqualTo(number), "Incorrect number");
|
||||
}
|
||||
|
||||
[TestCase("a0", "a", 0)]
|
||||
[TestCase("a1", "a", 1)]
|
||||
[TestCase("a10", "a", 10)]
|
||||
[TestCase("aa2", "aa", 2)]
|
||||
[TestCase("aa22", "aa", 22)]
|
||||
public void SeasonKeySplit(string key, string timeline, int turn)
|
||||
{
|
||||
Assert.That(Timelines.SplitKey(key), Is.EqualTo((timeline, turn)), "Failed to split key");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void NoSharedFactoryState()
|
||||
{
|
||||
TimelineFactory one = new();
|
||||
TimelineFactory two = new();
|
||||
Timelines one = Timelines.Create()
|
||||
.WithNewSeason(new Season("a0"), out var s1)
|
||||
.WithNewSeason(new Season("a0"), out var s2)
|
||||
.WithNewSeason(new Season("a0"), out var s3);
|
||||
Timelines two = Timelines.Create()
|
||||
.WithNewSeason(new Season("a0"), out var s4)
|
||||
.WithNewSeason(new Season("a0"), out var s5);
|
||||
|
||||
Assert.That(one.NextTimeline(), Is.EqualTo("a"));
|
||||
Assert.That(one.NextTimeline(), Is.EqualTo("b"));
|
||||
Assert.That(one.NextTimeline(), Is.EqualTo("c"));
|
||||
Assert.That(s1.Timeline, Is.EqualTo("a"));
|
||||
Assert.That(s2.Timeline, Is.EqualTo("b"));
|
||||
Assert.That(s3.Timeline, Is.EqualTo("c"));
|
||||
Assert.That(s4.Timeline, Is.EqualTo("a"), "Unexpected first timeline");
|
||||
Assert.That(s5.Timeline, Is.EqualTo("b"), "Unexpected second timeline");
|
||||
}
|
||||
|
||||
Assert.That(two.NextTimeline(), Is.EqualTo("a"));
|
||||
Assert.That(two.NextTimeline(), Is.EqualTo("b"));
|
||||
[Test]
|
||||
public void TimelineForking()
|
||||
{
|
||||
Timelines timelines = Timelines.Create()
|
||||
.WithNewSeason("a0", out var a1)
|
||||
.WithNewSeason(a1, out var a2)
|
||||
.WithNewSeason(a2, out var a3)
|
||||
.WithNewSeason(a1, out var b2)
|
||||
.WithNewSeason(b2, out var b3)
|
||||
.WithNewSeason(a1, out var c2)
|
||||
.WithNewSeason(a2, out var d3);
|
||||
Season a0 = new("a0");
|
||||
|
||||
Assert.That(
|
||||
timelines.Pasts.Keys,
|
||||
Is.EquivalentTo(new List<string> { "a0", "a1", "a2", "a3", "b2", "b3", "c2", "d3" }),
|
||||
"Unexpected seasons");
|
||||
|
||||
Assert.That(a1.Timeline, Is.EqualTo("a"), "Unexpected trunk timeline");
|
||||
Assert.That(a2.Timeline, Is.EqualTo("a"), "Unexpected trunk timeline");
|
||||
Assert.That(a3.Timeline, Is.EqualTo("a"), "Unexpected trunk timeline");
|
||||
Assert.That(b2.Timeline, Is.EqualTo("b"), "Unexpected first alt");
|
||||
Assert.That(b3.Timeline, Is.EqualTo("b"), "Unexpected first alt");
|
||||
Assert.That(c2.Timeline, Is.EqualTo("c"), "Unexpected second alt");
|
||||
Assert.That(d3.Timeline, Is.EqualTo("d"), "Unexpected third alt");
|
||||
|
||||
Assert.That(a1.Turn, Is.EqualTo(Season.FIRST_TURN + 1), "Unexpected a1 turn number");
|
||||
Assert.That(a2.Turn, Is.EqualTo(Season.FIRST_TURN + 2), "Unexpected a2 turn number");
|
||||
Assert.That(a3.Turn, Is.EqualTo(Season.FIRST_TURN + 3), "Unexpected a3 turn number");
|
||||
Assert.That(b2.Turn, Is.EqualTo(Season.FIRST_TURN + 2), "Unexpected b2 turn number");
|
||||
Assert.That(b3.Turn, Is.EqualTo(Season.FIRST_TURN + 3), "Unexpected b3 turn number");
|
||||
Assert.That(c2.Turn, Is.EqualTo(Season.FIRST_TURN + 2), "Unexpected c2 turn number");
|
||||
Assert.That(d3.Turn, Is.EqualTo(Season.FIRST_TURN + 3), "Unexpected d3 turn number");
|
||||
|
||||
Assert.That(timelines.GetTimelineRoot(a0), Is.EqualTo(a0), "Expected timeline root to be reflexive");
|
||||
Assert.That(timelines.GetTimelineRoot(a3), Is.EqualTo(a0), "Expected trunk timeline to have root");
|
||||
Assert.That(timelines.GetTimelineRoot(b2), Is.EqualTo(b2), "Expected alt timeline root to be reflexive");
|
||||
Assert.That(timelines.GetTimelineRoot(b3), Is.EqualTo(b2), "Expected alt timeline to root at first fork");
|
||||
Assert.That(timelines.GetTimelineRoot(c2), Is.EqualTo(c2), "Expected alt timeline root to be reflexive");
|
||||
Assert.That(timelines.GetTimelineRoot(d3), Is.EqualTo(d3), "Expected alt timeline root to be reflexive");
|
||||
|
||||
Assert.That(timelines.InAdjacentTimeline(b3, a3), Is.True, "Expected alts to be adjacent to origin");
|
||||
Assert.That(timelines.InAdjacentTimeline(b3, c2), Is.True, "Expected alts with common origin to be adjacent");
|
||||
Assert.That(timelines.InAdjacentTimeline(b3, d3), Is.False, "Expected alts from different origins not to be adjacent");
|
||||
|
||||
Assert.That(timelines.GetFutures(a0), Is.EquivalentTo(new List<Season> { a1 }), "Unexpected futures");
|
||||
Assert.That(timelines.GetFutures(a1), Is.EquivalentTo(new List<Season> { a2, b2, c2 }), "Unexpected futures");
|
||||
Assert.That(timelines.GetFutures(a2), Is.EquivalentTo(new List<Season> { a3, d3 }), "Unexpected futures");
|
||||
Assert.That(timelines.GetFutures(b2), Is.EquivalentTo(new List<Season> { b3 }), "Unexpected futures");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -168,18 +168,16 @@ public class MovementAdjudicatorTest
|
|||
|
||||
World updated = setup.UpdateWorld();
|
||||
|
||||
// Confirm the future was created
|
||||
Assert.That(updated.Seasons.Values.Count, Is.EqualTo(2));
|
||||
Season future = updated.Seasons.Values.Single(s => s != updated.RootSeason);
|
||||
Assert.That(future.Past, Is.EqualTo(updated.RootSeason.ToString()));
|
||||
Assert.That(updated.GetFutures(future), Is.Empty);
|
||||
Assert.That(future.Timeline, Is.EqualTo(updated.RootSeason.Timeline));
|
||||
Assert.That(future.Turn, Is.EqualTo(Season.FIRST_TURN + 1));
|
||||
|
||||
// Confirm the unit was created
|
||||
Assert.That(updated.Units.Count, Is.EqualTo(2));
|
||||
Unit second = updated.Units.Single(u => u.Past != null);
|
||||
Assert.That(second.Location, Is.EqualTo(mun.Order.Unit.Location));
|
||||
Assert.That(second.Season.Timeline, Is.EqualTo(mun.Order.Unit.Season.Timeline));
|
||||
|
||||
// Confirm that the unit's season exists
|
||||
CollectionAssert.Contains(updated.Timelines.Pasts.Keys, second.Season.Key, "Season was not added");
|
||||
CollectionAssert.DoesNotContain(updated.Timelines.Pasts.Values, second.Season.Key, "Season should not have a future");
|
||||
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -199,9 +197,9 @@ public class MovementAdjudicatorTest
|
|||
World updated = setup.UpdateWorld();
|
||||
|
||||
// Confirm the future was created
|
||||
Season s2 = updated.Seasons["a1"];
|
||||
Assert.That(s2.Past, Is.EqualTo(s1.ToString()));
|
||||
Assert.That(updated.GetFutures(s2), Is.Empty);
|
||||
Season s2 = new(s1.Timeline, s1.Turn + 1);
|
||||
Assert.That(updated.Timelines.Pasts[s2.Key], Is.EqualTo(s1));
|
||||
Assert.That(updated.Timelines.GetFutures(s2), Is.Empty);
|
||||
Assert.That(s2.Timeline, Is.EqualTo(s1.Timeline));
|
||||
Assert.That(s2.Turn, Is.EqualTo(s1.Turn + 1));
|
||||
|
||||
|
@ -227,7 +225,7 @@ public class MovementAdjudicatorTest
|
|||
|
||||
// Update the world again
|
||||
updated = setup.UpdateWorld();
|
||||
Season s3 = updated.GetSeason(s2.Timeline, s2.Turn + 1);
|
||||
Season s3 = new(s2.Timeline, s2.Turn + 1);
|
||||
Unit u3 = updated.GetUnitAt("Mun", s3);
|
||||
Assert.That(u3.Past, Is.EqualTo(mun2.Order.Unit.Key));
|
||||
}
|
||||
|
@ -249,9 +247,9 @@ public class MovementAdjudicatorTest
|
|||
World updated = setup.UpdateWorld();
|
||||
|
||||
// Confirm the future was created
|
||||
Season s2 = updated.GetSeason(s1.Timeline, s1.Turn + 1);
|
||||
Assert.That(s2.Past, Is.EqualTo(s1.ToString()));
|
||||
Assert.That(updated.GetFutures(s2), Is.Empty);
|
||||
Season s2 = new(s1.Timeline, s1.Turn + 1);
|
||||
Assert.That(updated.Timelines.Pasts[s2.Key], Is.EqualTo(s1));
|
||||
Assert.That(updated.Timelines.GetFutures(s2), Is.Empty);
|
||||
Assert.That(s2.Timeline, Is.EqualTo(s1.Timeline));
|
||||
Assert.That(s2.Turn, Is.EqualTo(s1.Turn + 1));
|
||||
|
||||
|
@ -277,7 +275,7 @@ public class MovementAdjudicatorTest
|
|||
|
||||
// Update the world again
|
||||
updated = setup.UpdateWorld();
|
||||
Season s3 = updated.GetSeason(s2.Timeline, s2.Turn + 1);
|
||||
Season s3 = new(s2.Timeline, s2.Turn + 1);
|
||||
Unit u3 = updated.GetUnitAt("Mun", s3);
|
||||
Assert.That(u3.Past, Is.EqualTo(u2.Key));
|
||||
}
|
||||
|
|
|
@ -1,62 +0,0 @@
|
|||
using MultiversalDiplomacy.Model;
|
||||
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace MultiversalDiplomacyTests;
|
||||
|
||||
public class SeasonTests
|
||||
{
|
||||
[Test]
|
||||
public void TimelineForking()
|
||||
{
|
||||
World world = World.WithMap(Map.Test);
|
||||
Season a0 = world.Seasons["a0"];
|
||||
world = world
|
||||
.ContinueOrFork(a0, out Season a1)
|
||||
.ContinueOrFork(a1, out Season a2)
|
||||
.ContinueOrFork(a2, out Season a3)
|
||||
.ContinueOrFork(a1, out Season b2)
|
||||
.ContinueOrFork(b2, out Season b3)
|
||||
.ContinueOrFork(a1, out Season c2)
|
||||
.ContinueOrFork(a2, out Season d3);
|
||||
|
||||
Assert.That(
|
||||
world.Seasons.Values.Select(season => season.ToString()),
|
||||
Is.EquivalentTo(new List<string> { "a0", "a1", "a2", "a3", "b2", "b3", "c2", "d3" }),
|
||||
"Unexpected seasons");
|
||||
|
||||
Assert.That(a0.Timeline, Is.EqualTo("a"), "Unexpected trunk timeline");
|
||||
Assert.That(a1.Timeline, Is.EqualTo("a"), "Unexpected trunk timeline");
|
||||
Assert.That(a2.Timeline, Is.EqualTo("a"), "Unexpected trunk timeline");
|
||||
Assert.That(a3.Timeline, Is.EqualTo("a"), "Unexpected trunk timeline");
|
||||
Assert.That(b2.Timeline, Is.EqualTo("b"), "Unexpected first alt");
|
||||
Assert.That(b3.Timeline, Is.EqualTo("b"), "Unexpected first alt");
|
||||
Assert.That(c2.Timeline, Is.EqualTo("c"), "Unexpected second alt");
|
||||
Assert.That(d3.Timeline, Is.EqualTo("d"), "Unexpected third alt");
|
||||
|
||||
Assert.That(a0.Turn, Is.EqualTo(Season.FIRST_TURN + 0), "Unexpected first turn number");
|
||||
Assert.That(a1.Turn, Is.EqualTo(Season.FIRST_TURN + 1), "Unexpected next turn number");
|
||||
Assert.That(a2.Turn, Is.EqualTo(Season.FIRST_TURN + 2), "Unexpected next turn number");
|
||||
Assert.That(a3.Turn, Is.EqualTo(Season.FIRST_TURN + 3), "Unexpected next turn number");
|
||||
Assert.That(b2.Turn, Is.EqualTo(Season.FIRST_TURN + 2), "Unexpected fork turn number");
|
||||
Assert.That(b3.Turn, Is.EqualTo(Season.FIRST_TURN + 3), "Unexpected fork turn number");
|
||||
Assert.That(c2.Turn, Is.EqualTo(Season.FIRST_TURN + 2), "Unexpected fork turn number");
|
||||
Assert.That(d3.Turn, Is.EqualTo(Season.FIRST_TURN + 3), "Unexpected fork turn number");
|
||||
|
||||
Assert.That(world.GetTimelineRoot(a0), Is.EqualTo(a0), "Expected timeline root to be reflexive");
|
||||
Assert.That(world.GetTimelineRoot(a3), Is.EqualTo(a0), "Expected trunk timeline to have root");
|
||||
Assert.That(world.GetTimelineRoot(b2), Is.EqualTo(b2), "Expected alt timeline root to be reflexive");
|
||||
Assert.That(world.GetTimelineRoot(b3), Is.EqualTo(b2), "Expected alt timeline to root at first fork");
|
||||
Assert.That(world.GetTimelineRoot(c2), Is.EqualTo(c2), "Expected alt timeline root to be reflexive");
|
||||
Assert.That(world.GetTimelineRoot(d3), Is.EqualTo(d3), "Expected alt timeline root to be reflexive");
|
||||
|
||||
Assert.That(world.InAdjacentTimeline(b3, a3), Is.True, "Expected alts to be adjacent to origin");
|
||||
Assert.That(world.InAdjacentTimeline(b3, c2), Is.True, "Expected alts with common origin to be adjacent");
|
||||
Assert.That(world.InAdjacentTimeline(b3, d3), Is.False, "Expected alts from different origins not to be adjacent");
|
||||
|
||||
Assert.That(world.GetFutures(a0), Is.EquivalentTo(new List<Season> { a1 }), "Unexpected futures");
|
||||
Assert.That(world.GetFutures(a1), Is.EquivalentTo(new List<Season> { a2, b2, c2 }), "Unexpected futures");
|
||||
Assert.That(world.GetFutures(a2), Is.EquivalentTo(new List<Season> { a3, d3 }), "Unexpected futures");
|
||||
Assert.That(world.GetFutures(b2), Is.EquivalentTo(new List<Season> { b3 }), "Unexpected futures");
|
||||
}
|
||||
}
|
|
@ -18,31 +18,31 @@ public class SerializationTest
|
|||
public void SerializeRoundTrip_NewGame()
|
||||
{
|
||||
World world = World.WithStandardMap();
|
||||
string serialized = JsonSerializer.Serialize(world, Options);
|
||||
World? deserialized = JsonSerializer.Deserialize<World>(serialized, Options);
|
||||
|
||||
Assert.That(deserialized, Is.Not.Null, "Failed to deserialize");
|
||||
Assert.That(deserialized!.Map, Is.Not.Null, "Failed to deserialize map");
|
||||
Assert.That(deserialized!.Seasons, Is.Not.Null, "Failed to deserialize seasons");
|
||||
Assert.That(deserialized!.Units, Is.Not.Null, "Failed to deserialize units");
|
||||
Assert.That(deserialized!.RetreatingUnits, Is.Not.Null, "Failed to deserialize retreats");
|
||||
Assert.That(deserialized!.OrderHistory, Is.Not.Null, "Failed to deserialize history");
|
||||
Assert.That(deserialized!.Timelines, Is.Not.Null, "Failed to deserialize timelines");
|
||||
Assert.That(deserialized!.Options, Is.Not.Null, "Failed to deserialize options");
|
||||
Assert.That(deserialized.Timelines.nextTimeline, Is.EqualTo(world.Timelines.nextTimeline));
|
||||
|
||||
JsonElement document = JsonSerializer.SerializeToDocument(world, Options).RootElement;
|
||||
Assert.That(
|
||||
document.EnumerateObject().Select(prop => prop.Name),
|
||||
Is.EquivalentTo(new List<string> {
|
||||
"mapType",
|
||||
"seasons",
|
||||
"units",
|
||||
"retreatingUnits",
|
||||
"orderHistory",
|
||||
"options",
|
||||
"timelines",
|
||||
}));
|
||||
|
||||
string serialized = JsonSerializer.Serialize(world, Options);
|
||||
World? deserialized = JsonSerializer.Deserialize<World>(serialized, Options);
|
||||
|
||||
Assert.That(deserialized, Is.Not.Null, "Failed to deserialize");
|
||||
Assert.That(deserialized!.Map, Is.Not.Null, "Failed to deserialize map");
|
||||
Assert.That(deserialized!.Units, Is.Not.Null, "Failed to deserialize units");
|
||||
Assert.That(deserialized!.RetreatingUnits, Is.Not.Null, "Failed to deserialize retreats");
|
||||
Assert.That(deserialized!.OrderHistory, Is.Not.Null, "Failed to deserialize history");
|
||||
Assert.That(deserialized!.Timelines, Is.Not.Null, "Failed to deserialize timelines");
|
||||
Assert.That(deserialized!.Timelines.Pasts, Is.Not.Null, "Failed to deserialize timeline pasts");
|
||||
Assert.That(deserialized!.Timelines.Next, Is.EqualTo(world.Timelines.Next));
|
||||
Assert.That(deserialized!.Options, Is.Not.Null, "Failed to deserialize options");
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -80,13 +80,13 @@ public class SerializationTest
|
|||
Assert.That(reserialized.OrderHistory["a0"].IsDislodgedOutcomes.Count, Is.GreaterThan(0), "Missing dislodges");
|
||||
});
|
||||
|
||||
Assert.Ignore();
|
||||
Assert.Ignore("Serialization doesn't fully work yet");
|
||||
|
||||
// Resume the test case
|
||||
setup = new(reserialized, MovementPhaseAdjudicator.Instance);
|
||||
setup[("a", 1)]
|
||||
["Germany"]
|
||||
.Army("Mun").Supports.Army("Mun", season: reserialized.Seasons["a0"]).MoveTo("Tyr").GetReference(out var mun1)
|
||||
.Army("Mun").Supports.Army("Mun", season: new("a0")).MoveTo("Tyr").GetReference(out var mun1)
|
||||
["Austria"]
|
||||
.Army("Tyr").Holds();
|
||||
|
||||
|
@ -102,7 +102,7 @@ public class SerializationTest
|
|||
|
||||
// Confirm that an alternate future is created.
|
||||
World world = setup.UpdateWorld();
|
||||
Season fork = world.Seasons["b1"];
|
||||
Season fork = new("b1");
|
||||
Unit tyr1 = world.GetUnitAt("Tyr", fork);
|
||||
Assert.That(
|
||||
tyr1.Past,
|
||||
|
|
|
@ -3,6 +3,7 @@ using System.Collections.ObjectModel;
|
|||
using MultiversalDiplomacy.Adjudicate;
|
||||
using MultiversalDiplomacy.Model;
|
||||
using MultiversalDiplomacy.Orders;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace MultiversalDiplomacyTests;
|
||||
|
||||
|
@ -240,7 +241,7 @@ public class TestCaseBuilder
|
|||
/// Get the context for defining the orders for a season.
|
||||
/// </summary>
|
||||
public ISeasonContext this[(string timeline, int turn) seasonCoord]
|
||||
=> new SeasonContext(this, this.World.GetSeason(seasonCoord.timeline, seasonCoord.turn));
|
||||
=> new SeasonContext(this, new(seasonCoord));
|
||||
|
||||
/// <summary>
|
||||
/// Get a unit matching a description. If no such unit exists, one is created and added to the
|
||||
|
@ -254,7 +255,7 @@ public class TestCaseBuilder
|
|||
/// of this type.
|
||||
/// </param>
|
||||
private Unit GetOrBuildUnit(
|
||||
Power power,
|
||||
string power,
|
||||
Location location,
|
||||
Season season,
|
||||
UnitType type)
|
||||
|
@ -344,13 +345,15 @@ public class TestCaseBuilder
|
|||
{
|
||||
public TestCaseBuilder Builder;
|
||||
public SeasonContext SeasonContext;
|
||||
public Power Power;
|
||||
public string Power;
|
||||
|
||||
public PowerContext(SeasonContext seasonContext, Power Power)
|
||||
public PowerContext(SeasonContext seasonContext, string power)
|
||||
{
|
||||
Assert.That(power, Is.AnyOf([.. seasonContext.Builder.World.Map.Powers]), "Invalid power");
|
||||
|
||||
this.Builder = seasonContext.Builder;
|
||||
this.SeasonContext = seasonContext;
|
||||
this.Power = Power;
|
||||
this.Power = power;
|
||||
}
|
||||
|
||||
public ISeasonContext this[(string timeline, int turn) seasonCoord]
|
||||
|
@ -361,7 +364,7 @@ public class TestCaseBuilder
|
|||
|
||||
public IUnitContext Army(string provinceName, string? powerName = null)
|
||||
{
|
||||
Power power = powerName == null
|
||||
string power = powerName == null
|
||||
? this.Power
|
||||
: this.Builder.World.Map.GetPower(powerName);
|
||||
Location location = this.Builder.World.Map.GetLand(provinceName);
|
||||
|
@ -372,7 +375,7 @@ public class TestCaseBuilder
|
|||
|
||||
public IUnitContext Fleet(string provinceName, string? coast = null, string? powerName = null)
|
||||
{
|
||||
Power power = powerName == null
|
||||
string power = powerName == null
|
||||
? this.Power
|
||||
: this.Builder.World.Map.GetPower(powerName);
|
||||
Location location = this.Builder.World.Map.GetWater(provinceName, coast);
|
||||
|
@ -419,7 +422,7 @@ public class TestCaseBuilder
|
|||
MoveOrder moveOrder = new MoveOrder(
|
||||
this.PowerContext.Power,
|
||||
this.Unit,
|
||||
destSeason.Key,
|
||||
destSeason,
|
||||
destination);
|
||||
this.Builder.OrderList.Add(moveOrder);
|
||||
return new OrderDefinedContext<MoveOrder>(this, moveOrder);
|
||||
|
@ -449,7 +452,7 @@ public class TestCaseBuilder
|
|||
|
||||
public IConvoyDestinationContext Army(string provinceName, string? powerName = null)
|
||||
{
|
||||
Power power = powerName == null
|
||||
string power = powerName == null
|
||||
? this.PowerContext.Power
|
||||
: this.Builder.World.Map.GetPower(powerName);
|
||||
Location location = this.Builder.World.Map.GetLand(provinceName);
|
||||
|
@ -463,7 +466,7 @@ public class TestCaseBuilder
|
|||
string? coast = null,
|
||||
string? powerName = null)
|
||||
{
|
||||
Power power = powerName == null
|
||||
string power = powerName == null
|
||||
? this.PowerContext.Power
|
||||
: this.Builder.World.Map.GetPower(powerName);
|
||||
Location location = this.Builder.World.Map.GetWater(provinceName, coast);
|
||||
|
@ -524,7 +527,7 @@ public class TestCaseBuilder
|
|||
Season? season = null,
|
||||
string? powerName = null)
|
||||
{
|
||||
Power power = powerName == null
|
||||
string power = powerName == null
|
||||
? this.PowerContext.Power
|
||||
: this.Builder.World.Map.GetPower(powerName);
|
||||
Location location = this.Builder.World.Map.GetLand(provinceName);
|
||||
|
@ -539,7 +542,7 @@ public class TestCaseBuilder
|
|||
string? coast = null,
|
||||
string? powerName = null)
|
||||
{
|
||||
Power power = powerName == null
|
||||
string power = powerName == null
|
||||
? this.PowerContext.Power
|
||||
: this.Builder.World.Map.GetPower(powerName);
|
||||
Location location = this.Builder.World.Map.GetWater(provinceName, coast);
|
||||
|
|
|
@ -28,15 +28,15 @@ class TestCaseBuilderTest
|
|||
Assert.That(setup.World.Units, Is.Not.Empty, "Expected units to be created");
|
||||
|
||||
Unit armyLON = setup.World.GetUnitAt("London");
|
||||
Assert.That(armyLON.Power.Name, Is.EqualTo("England"), "Unit created with wrong power");
|
||||
Assert.That(armyLON.Power, Is.EqualTo("England"), "Unit created with wrong power");
|
||||
Assert.That(armyLON.Type, Is.EqualTo(UnitType.Army), "Unit created with wrong type");
|
||||
|
||||
Unit fleetIRI = setup.World.GetUnitAt("Irish Sea");
|
||||
Assert.That(fleetIRI.Power.Name, Is.EqualTo("England"), "Unit created with wrong power");
|
||||
Assert.That(fleetIRI.Power, Is.EqualTo("England"), "Unit created with wrong power");
|
||||
Assert.That(fleetIRI.Type, Is.EqualTo(UnitType.Fleet), "Unit created with wrong type");
|
||||
|
||||
Unit fleetSTP = setup.World.GetUnitAt("Saint Petersburg");
|
||||
Assert.That(fleetSTP.Power.Name, Is.EqualTo("Russia"), "Unit created with wrong power");
|
||||
Assert.That(fleetSTP.Power, Is.EqualTo("Russia"), "Unit created with wrong power");
|
||||
Assert.That(fleetSTP.Type, Is.EqualTo(UnitType.Fleet), "Unit created with wrong type");
|
||||
Assert.That(
|
||||
fleetSTP.Location,
|
||||
|
@ -124,7 +124,7 @@ class TestCaseBuilderTest
|
|||
Assert.That(orderMun, Is.Not.Null, "Expected order reference");
|
||||
Assert.That(
|
||||
orderMun.Order.Power,
|
||||
Is.EqualTo(setup.World.Map.GetPower("Germany")),
|
||||
Is.EqualTo("Germany"),
|
||||
"Wrong power");
|
||||
Assert.That(
|
||||
orderMun.Order.Unit.Location,
|
||||
|
|
|
@ -13,14 +13,13 @@ public class UnitTests
|
|||
Location Mun = world.Map.GetLand("Mun"),
|
||||
Boh = world.Map.GetLand("Boh"),
|
||||
Tyr = world.Map.GetLand("Tyr");
|
||||
Power pw1 = world.Map.GetPower("Austria");
|
||||
Season a0 = world.RootSeason;
|
||||
Unit u1 = Unit.Build(Mun.Key, a0, pw1, UnitType.Army);
|
||||
Season a0 = new("a0");
|
||||
Unit u1 = Unit.Build(Mun.Key, a0, "Austria", UnitType.Army);
|
||||
|
||||
world = world.ContinueOrFork(a0, out Season a1);
|
||||
world = world.WithNewSeason(a0, out Season a1);
|
||||
Unit u2 = u1.Next(Boh.Key, a1);
|
||||
|
||||
_ = world.ContinueOrFork(a1, out Season a2);
|
||||
_ = world.WithNewSeason(a1, out Season a2);
|
||||
Unit u3 = u2.Next(Tyr.Key, a2);
|
||||
|
||||
Assert.That(u3.Past, Is.EqualTo(u2.Key), "Missing unit past");
|
||||
|
|
Loading…
Reference in New Issue