2024-08-15 20:51:41 +00:00
|
|
|
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>
|
2024-08-25 03:36:31 +00:00
|
|
|
/// Map of season designations to their parent seasons. Every season has an entry, so
|
|
|
|
/// the set of keys is the set of existing seasons.
|
2024-08-15 20:51:41 +00:00
|
|
|
/// </summary>
|
|
|
|
public Dictionary<string, Season?> Pasts { get; } = pasts;
|
|
|
|
|
2024-08-25 03:36:31 +00:00
|
|
|
/// <summary>
|
|
|
|
/// All seasons in the multiverse.
|
|
|
|
/// </summary>
|
|
|
|
[JsonIgnore]
|
|
|
|
public IEnumerable<Season> Seasons => Pasts.Keys.Select(key => new Season(key));
|
|
|
|
|
2024-08-15 20:51:41 +00:00
|
|
|
/// <summary>
|
|
|
|
/// Create a new multiverse with an initial season.
|
|
|
|
/// </summary>
|
|
|
|
public static Timelines Create()
|
2024-08-15 23:36:50 +00:00
|
|
|
=> new(StringToInt(Season.First.Timeline) + 1, new() { {Season.First.Key, null} });
|
2024-08-15 20:51:41 +00:00
|
|
|
|
|
|
|
/// <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;
|
|
|
|
}
|
|
|
|
}
|