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. Every season has an entry, so /// the set of keys is the set of existing seasons. /// public Dictionary Pasts { get; } = pasts; /// /// All seasons in the multiverse. /// [JsonIgnore] public IEnumerable Seasons => Pasts.Keys.Select(key => new Season(key)); /// /// Create a new multiverse with an initial season. /// public static Timelines Create() => new(StringToInt(Season.First.Timeline) + 1, new() { {Season.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) { 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; } }