diff --git a/MultiversalDiplomacy/Adjudicate/Decision/MovementDecisions.cs b/MultiversalDiplomacy/Adjudicate/Decision/MovementDecisions.cs index a85201c..3e4db9b 100644 --- a/MultiversalDiplomacy/Adjudicate/Decision/MovementDecisions.cs +++ b/MultiversalDiplomacy/Adjudicate/Decision/MovementDecisions.cs @@ -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) { diff --git a/MultiversalDiplomacy/Adjudicate/MovementPhaseAdjudicator.cs b/MultiversalDiplomacy/Adjudicate/MovementPhaseAdjudicator.cs index 9b338f9..c221c22 100644 --- a/MultiversalDiplomacy/Adjudicate/MovementPhaseAdjudicator.cs +++ b/MultiversalDiplomacy/Adjudicate/MovementPhaseAdjudicator.cs @@ -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 adjacentMoveOrders = moveOrdersByAdjacency[true].ToList(); List 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 allOrderableUnits = world.Units - .Where(unit => !world.GetFutures(unit.Season).Any()) + .Where(unit => !world.Timelines.GetFutures(unit.Season).Any()) .ToList(); HashSet orderedUnits = validOrders.Select(order => order.Unit).ToHashSet(); List unorderedUnits = allOrderableUnits @@ -315,6 +315,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator Dictionary createdFutures = []; List createdUnits = []; List 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 newIncomingMoves = decision.Orders .OfType() - .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; @@ -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. diff --git a/MultiversalDiplomacy/Adjudicate/PathFinder.cs b/MultiversalDiplomacy/Adjudicate/PathFinder.cs index 8d8d28d..e4e7512 100644 --- a/MultiversalDiplomacy/Adjudicate/PathFinder.cs +++ b/MultiversalDiplomacy/Adjudicate/PathFinder.cs @@ -18,7 +18,7 @@ public static class PathFinder /// Determines if a convoy path exists for a move order. /// 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 /// public static IEnumerable GetAdjacentSeasons(World world, Season season) { + var pasts = world.Timelines.Pasts; List 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 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 cobranchRoots = world - .GetFutures(past) - .Where(s => s.Timeline != current.Timeline && s.Timeline != past.Timeline); + IEnumerable 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() + .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); } } diff --git a/MultiversalDiplomacy/Model/ModelExtensions.cs b/MultiversalDiplomacy/Model/ModelExtensions.cs index 29e4a68..3412756 100644 --- a/MultiversalDiplomacy/Model/ModelExtensions.cs +++ b/MultiversalDiplomacy/Model/ModelExtensions.cs @@ -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)); } diff --git a/MultiversalDiplomacy/Model/Season.cs b/MultiversalDiplomacy/Model/Season.cs index 91a4e10..573252d 100644 --- a/MultiversalDiplomacy/Model/Season.cs +++ b/MultiversalDiplomacy/Model/Season.cs @@ -1,11 +1,13 @@ +using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; namespace MultiversalDiplomacy.Model; /// -/// 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. /// -public class Season(string? past, int turn, string timeline) +[JsonConverter(typeof(SeasonJsonConverter))] +public struct Season(string timeline, int turn) { /// /// 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; /// - /// 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. /// - public string? Past { get; } = past; + public string Timeline { get; } = timeline; /// /// 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) /// public int Turn { get; } = turn; - /// - /// The timeline to which this season belongs. - /// - public string Timeline { get; } = timeline; - /// /// The multiversal designation of this season. /// [JsonIgnore] - public string Key => $"{this.Timeline}{this.Turn}"; + public readonly string Key => $"{this.Timeline}{this.Turn}"; - public override string ToString() => Key; + /// + /// Create a new season from a tuple coordinate. + /// + public Season((string timeline, int turn) coord) : this(coord.timeline, coord.turn) { } + + /// + /// Create a new season from a combined string designation. + /// + /// + public Season(string designation) : this(SplitKey(designation)) { } + + /// + /// Extract the timeline and turn components of a season designation. + /// + /// A timeline-turn season designation. + /// The timeline and turn components. + /// + 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; + + /// + /// Seasons are essentially 2D points, so they are equal when their components are equal. + /// + 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(); } diff --git a/MultiversalDiplomacy/Model/SeasonJsonConverter.cs b/MultiversalDiplomacy/Model/SeasonJsonConverter.cs new file mode 100644 index 0000000..59934cc --- /dev/null +++ b/MultiversalDiplomacy/Model/SeasonJsonConverter.cs @@ -0,0 +1,16 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MultiversalDiplomacy.Model; + +/// +/// Serializes a as its combined designation. +/// +internal class SeasonJsonConverter : JsonConverter +{ + 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); +} diff --git a/MultiversalDiplomacy/Model/TimelineFactory.cs b/MultiversalDiplomacy/Model/TimelineFactory.cs deleted file mode 100644 index 4cee83e..0000000 --- a/MultiversalDiplomacy/Model/TimelineFactory.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System.Text.Json.Serialization; - -namespace MultiversalDiplomacy.Model; - -/// -/// A shared counter for handing out new timeline designations. -/// -[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', - ]; - - /// - /// Convert a string timeline identifier to its serial number. - /// - /// Timeline identifier. - /// Integer. - 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; - } - - /// - /// Convert a timeline serial number to its string identifier. - /// - /// Integer. - /// Timeline identifier. - public static string IntToString(int serial) { - static int downshift(int i ) => (i - (i % 26)) / 26; - IEnumerable 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()); - } - - /// - /// Extract the timeline and turn components of a season designation. - /// - /// A timeline-turn season designation. - /// The timeline and turn components. - /// - 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 TimelineFactory() : this(0) { } - - public int nextTimeline = nextTimeline; - - public string NextTimeline() => IntToString(nextTimeline++); -} diff --git a/MultiversalDiplomacy/Model/TimelineFactoryJsonConverter.cs b/MultiversalDiplomacy/Model/TimelineFactoryJsonConverter.cs deleted file mode 100644 index 684097b..0000000 --- a/MultiversalDiplomacy/Model/TimelineFactoryJsonConverter.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace MultiversalDiplomacy.Model; - -internal class TimelineFactoryJsonConverter : JsonConverter -{ - 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); -} diff --git a/MultiversalDiplomacy/Model/Timelines.cs b/MultiversalDiplomacy/Model/Timelines.cs new file mode 100644 index 0000000..0751da9 --- /dev/null +++ b/MultiversalDiplomacy/Model/Timelines.cs @@ -0,0 +1,156 @@ +using System.Text.Json.Serialization; + +namespace MultiversalDiplomacy.Model; + +/// +/// Tracks the relations between seasons. +/// +public class Timelines(int next, Dictionary 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', + ]; + + /// + /// Convert a string timeline identifier to its serial number. + /// + /// Timeline identifier. + /// Integer. + 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; + } + + /// + /// Convert a timeline serial number to its string identifier. + /// + /// Integer. + /// Timeline identifier. + public static string IntToString(int serial) { + static int downshift(int i ) => (i - (i % 26)) / 26; + IEnumerable 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()); + } + + /// + /// Extract the timeline and turn components of a season designation. + /// + /// A timeline-turn season designation. + /// The timeline and turn components. + /// + 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}"); + } + + /// + /// The next timeline to be created. + /// + public int Next { get; private set; } = next; + + /// + /// Map of season designations to their parent seasons. + /// The set of keys here is the set of all seasons in the multiverse. + /// + public Dictionary Pasts { get; } = pasts; + + /// + /// Create a new multiverse with an initial season. + /// + public static Timelines Create() + { + Season first = new(IntToString(0), Season.FIRST_TURN); + return new Timelines(1, new() { {first.Key, null} }); + } + + /// + /// Create a continuation of a season if it has no futures, otherwise create a fork. + /// + 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(future.Key, past)))); + } + + /// + /// Create a continuation of a season if it has no futures, otherwise create a fork. + /// + public Timelines WithNewSeason(string past, out Season future) => WithNewSeason(new Season(past), out future); + + /// + /// Get all seasons that are immediate futures of a season. + /// + /// A season. + /// The immediate futures of the season. + public IEnumerable GetFutureKeys(Season season) + => Pasts.Where(kvp => kvp.Value is Season future && future == season).Select(kvp => kvp.Key); + + /// + /// Get all seasons that are immediate futures of a season. + /// + /// A season. + /// The immediate futures of the season. + public IEnumerable GetFutures(Season season) => GetFutureKeys(season).Select(key => new Season(key)); + + /// + /// 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. + /// + public Season GetTimelineRoot(Season season) + { + Console.WriteLine($"GetTimelineRoot({season.Key})"); + return Pasts[season.Key] is Season past && season.Timeline == past.Timeline + ? GetTimelineRoot(past) + : season; + } + + /// + /// 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. + /// + public Season GetTimelineRoot(string season) => GetTimelineRoot(new Season(season)); + + /// + /// 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. + /// + 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; + } +} diff --git a/MultiversalDiplomacy/Model/World.cs b/MultiversalDiplomacy/Model/World.cs index 227aca6..ea0c65b 100644 --- a/MultiversalDiplomacy/Model/World.cs +++ b/MultiversalDiplomacy/Model/World.cs @@ -39,17 +39,6 @@ public class World [JsonIgnore] public IReadOnlyCollection Powers => this.Map.Powers; - /// - /// Lookup for seasons by designation. - /// - public Dictionary Seasons { get; } - - /// - /// The first season of the game. - /// - [JsonIgnore] - public Season RootSeason => Seasons["a0"]; - /// /// All units in the multiverse. /// @@ -68,7 +57,7 @@ public class World /// /// The shared timeline number generator. /// - public TimelineFactory Timelines { get; } + public Timelines Timelines { get; } /// /// Immutable game options. @@ -78,15 +67,13 @@ public class World [JsonConstructor] public World( MapType mapType, - Dictionary seasons, List units, List retreatingUnits, Dictionary 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 /// private World( Map map, - Dictionary seasons, List units, List retreatingUnits, Dictionary 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 /// private World( World previous, - Dictionary? seasons = null, List? units = null, List? retreatingUnits = null, Dictionary? 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 /// 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()), - timelines, + Timelines.Create(), new Options()); } @@ -161,15 +142,12 @@ public class World => WithMap(Map.Classical); public World Update( - IEnumerable? seasons = null, IEnumerable? units = null, IEnumerable? retreats = null, - IEnumerable>? orders = null) + IEnumerable>? 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); /// /// Create a new world with new units created from unit specs. Units specs are in the format @@ -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 ); } - /// - /// Create a continuation of this season if it has no futures, otherwise create a fork. - /// - public Season ContinueOrFork(Season season) - => GetFutures(season).Any() - ? new(season.Key, season.Turn + 1, Timelines.NextTimeline()) - : new(season.Key, season.Turn + 1, season.Timeline); - /// /// A standard Diplomacy game setup. /// public static World Standard => WithStandardMap().AddStandardUnits(); - /// - /// Get a season by coordinate. Throws if the season is not found. - /// - public Season GetSeason(string timeline, int turn) - => Seasons[$"{timeline}{turn}"]; - - /// - /// Get all seasons that are immediate futures of a season. - /// - /// A season designation. - /// The immediate futures of the designated season. - public IEnumerable GetFutures(string present) - => Seasons.Values.Where(future => future.Past == present); - - /// - /// Get all seasons that are immediate futures of a season. - /// - /// A season. - /// The immediate futures of the season. - public IEnumerable GetFutures(Season present) => GetFutures(present.Key); - - /// - /// 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. - /// - 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; - } - - /// - /// 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. - /// - 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; - } - /// /// Returns a unit in a province. Throws if there are duplicate units. /// 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) diff --git a/MultiversalDiplomacy/Orders/MoveOrder.cs b/MultiversalDiplomacy/Orders/MoveOrder.cs index 5140c67..de5cec3 100644 --- a/MultiversalDiplomacy/Orders/MoveOrder.cs +++ b/MultiversalDiplomacy/Orders/MoveOrder.cs @@ -9,9 +9,8 @@ public class MoveOrder : UnitOrder { /// /// The destination season to which the unit should move. - /// TODO replace this with timeline and turn individually so ToString can do the proper format /// - public string Season { get; } + public Season Season { get; } /// /// The destination location to which the unit should move. @@ -23,7 +22,7 @@ public class MoveOrder : UnitOrder /// public Province Province => this.Location.Province; - public MoveOrder(string 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}"; } /// diff --git a/MultiversalDiplomacy/Orders/SupportMoveOrder.cs b/MultiversalDiplomacy/Orders/SupportMoveOrder.cs index b9c83c2..e66148a 100644 --- a/MultiversalDiplomacy/Orders/SupportMoveOrder.cs +++ b/MultiversalDiplomacy/Orders/SupportMoveOrder.cs @@ -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; } \ No newline at end of file diff --git a/MultiversalDiplomacyTests/MDATC_A.cs b/MultiversalDiplomacyTests/MDATC_A.cs index cfa4c44..ee3fac9 100644 --- a/MultiversalDiplomacyTests/MDATC_A.cs +++ b/MultiversalDiplomacyTests/MDATC_A.cs @@ -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"); } diff --git a/MultiversalDiplomacyTests/Model/TimelineFactoryTest.cs b/MultiversalDiplomacyTests/Model/TimelineFactoryTest.cs index 56a0c75..8b83a98 100644 --- a/MultiversalDiplomacyTests/Model/TimelineFactoryTest.cs +++ b/MultiversalDiplomacyTests/Model/TimelineFactoryTest.cs @@ -18,8 +18,8 @@ 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)] @@ -29,20 +29,75 @@ public class TimelineFactoryTest [TestCase("aa22", "aa", 22)] public void SeasonKeySplit(string key, string timeline, int turn) { - Assert.That(TimelineFactory.SplitKey(key), Is.EqualTo((timeline, turn)), "Failed to split key"); + 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 { "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 { a1 }), "Unexpected futures"); + Assert.That(timelines.GetFutures(a1), Is.EquivalentTo(new List { a2, b2, c2 }), "Unexpected futures"); + Assert.That(timelines.GetFutures(a2), Is.EquivalentTo(new List { a3, d3 }), "Unexpected futures"); + Assert.That(timelines.GetFutures(b2), Is.EquivalentTo(new List { b3 }), "Unexpected futures"); } } diff --git a/MultiversalDiplomacyTests/MovementAdjudicatorTest.cs b/MultiversalDiplomacyTests/MovementAdjudicatorTest.cs index 240f684..b7d66ff 100644 --- a/MultiversalDiplomacyTests/MovementAdjudicatorTest.cs +++ b/MultiversalDiplomacyTests/MovementAdjudicatorTest.cs @@ -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)); } diff --git a/MultiversalDiplomacyTests/SeasonTests.cs b/MultiversalDiplomacyTests/SeasonTests.cs deleted file mode 100644 index 17a44c9..0000000 --- a/MultiversalDiplomacyTests/SeasonTests.cs +++ /dev/null @@ -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 { "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 { a1 }), "Unexpected futures"); - Assert.That(world.GetFutures(a1), Is.EquivalentTo(new List { a2, b2, c2 }), "Unexpected futures"); - Assert.That(world.GetFutures(a2), Is.EquivalentTo(new List { a3, d3 }), "Unexpected futures"); - Assert.That(world.GetFutures(b2), Is.EquivalentTo(new List { b3 }), "Unexpected futures"); - } -} diff --git a/MultiversalDiplomacyTests/SerializationTest.cs b/MultiversalDiplomacyTests/SerializationTest.cs index ba9da29..aa52449 100644 --- a/MultiversalDiplomacyTests/SerializationTest.cs +++ b/MultiversalDiplomacyTests/SerializationTest.cs @@ -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(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 { "mapType", - "seasons", "units", "retreatingUnits", "orderHistory", "options", "timelines", })); + + string serialized = JsonSerializer.Serialize(world, Options); + World? deserialized = JsonSerializer.Deserialize(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, diff --git a/MultiversalDiplomacyTests/TestCaseBuilder.cs b/MultiversalDiplomacyTests/TestCaseBuilder.cs index 32aa991..3e047fb 100644 --- a/MultiversalDiplomacyTests/TestCaseBuilder.cs +++ b/MultiversalDiplomacyTests/TestCaseBuilder.cs @@ -241,7 +241,7 @@ public class TestCaseBuilder /// Get the context for defining the orders for a season. /// public ISeasonContext this[(string timeline, int turn) seasonCoord] - => new SeasonContext(this, this.World.GetSeason(seasonCoord.timeline, seasonCoord.turn)); + => new SeasonContext(this, new(seasonCoord)); /// /// Get a unit matching a description. If no such unit exists, one is created and added to the @@ -422,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(this, moveOrder); diff --git a/MultiversalDiplomacyTests/UnitTests.cs b/MultiversalDiplomacyTests/UnitTests.cs index 41eefa0..d9fa565 100644 --- a/MultiversalDiplomacyTests/UnitTests.cs +++ b/MultiversalDiplomacyTests/UnitTests.cs @@ -13,13 +13,13 @@ public class UnitTests Location Mun = world.Map.GetLand("Mun"), Boh = world.Map.GetLand("Boh"), Tyr = world.Map.GetLand("Tyr"); - Season a0 = world.RootSeason; + 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");