5dplomacy/MultiversalDiplomacy/Model/Timelines.cs

153 lines
6.4 KiB
C#

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()
=> new(StringToInt(Season.First.Timeline) + 1, new() { {Season.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)
{
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;
}
}