diff --git a/MultiversalDiplomacy/Model/Season.cs b/MultiversalDiplomacy/Model/Season.cs new file mode 100644 index 0000000..50721bc --- /dev/null +++ b/MultiversalDiplomacy/Model/Season.cs @@ -0,0 +1,109 @@ +namespace MultiversalDiplomacy.Model; + +/// +/// Represents a state of the map produced by a set of move orders on a previous season. +/// +public class Season +{ + /// + /// A shared counter for handing out new timeline numbers. + /// + private class TimelineFactory + { + private int nextTimeline = 0; + + public int NextTimeline() => nextTimeline++; + } + + /// + /// The first turn number. + /// + public const int FIRST_TURN = 0; + + /// + /// 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. + /// + public Season? Past { get; } + + /// + /// The current turn, beginning at 0. Each season (spring and fall) is one turn. + /// Phases that only occur after the fall phase occur when Turn % 2 == 1. + /// The current year is (Turn / 2) + 1901. + /// + public int Turn { get; } + + /// + /// The timeline to which this season belongs. + /// + public int Timeline { get; } + + /// + /// The shared timeline number generator. + /// + private TimelineFactory Timelines { get; } + + private Season(Season? past, int turn, int timeline, TimelineFactory factory) + { + this.Past = past; + this.Turn = turn; + this.Timeline = timeline; + this.Timelines = factory; + } + + /// + /// Create a root season at the beginning of time. + /// + public static Season MakeRoot() + { + TimelineFactory factory = new TimelineFactory(); + return new Season( + past: null, + turn: FIRST_TURN, + timeline: factory.NextTimeline(), + factory: factory); + } + + /// + /// Create a season immediately after this one in the same timeline. + /// + public Season MakeNext() + => new Season(this, this.Turn + 1, this.Timeline, this.Timelines); + + /// + /// Create a season immediately after this one in a new timeline. + /// + public Season MakeFork() + => new Season(this, this.Turn + 1, this.Timelines.NextTimeline(), this.Timelines); + + /// + /// 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 TimelineRoot() + => this.Past != null && this.Timeline == this.Past.Timeline + ? this.Past.TimelineRoot() + : this; + + /// + /// Returns whether this season is in an adjacent timeline to another season. + /// + public bool InAdjacentTimeline(Season other) + { + // Timelines are adjacent to themselves. Early out in that case. + if (this.Timeline == other.Timeline) 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 thisRoot = this.TimelineRoot(); + Season otherRoot = other.TimelineRoot(); + return // One branched off the other + thisRoot.Past?.Timeline == other.Timeline + || otherRoot.Past?.Timeline == this.Timeline + // Both branched off of the same point + || thisRoot.Past == otherRoot.Past; + } +} diff --git a/MultiversalDiplomacyTests/SeasonTests.cs b/MultiversalDiplomacyTests/SeasonTests.cs new file mode 100644 index 0000000..0bb2556 --- /dev/null +++ b/MultiversalDiplomacyTests/SeasonTests.cs @@ -0,0 +1,50 @@ +using MultiversalDiplomacy.Model; + +using NUnit.Framework; + +namespace MultiversalDiplomacyTests; + +public class SeasonTests +{ + [Test] + public void TimelineForking() + { + Season a0 = Season.MakeRoot(); + Season a1 = a0.MakeNext(); + Season a2 = a1.MakeNext(); + Season a3 = a2.MakeNext(); + Season b1 = a1.MakeFork(); + Season b2 = b1.MakeNext(); + Season c1 = a1.MakeFork(); + Season d1 = a2.MakeFork(); + + Assert.That(a0.Timeline, Is.EqualTo(0), "Unexpected trunk timeline number"); + Assert.That(a1.Timeline, Is.EqualTo(0), "Unexpected trunk timeline number"); + Assert.That(a2.Timeline, Is.EqualTo(0), "Unexpected trunk timeline number"); + Assert.That(a3.Timeline, Is.EqualTo(0), "Unexpected trunk timeline number"); + Assert.That(b1.Timeline, Is.EqualTo(1), "Unexpected first alt number"); + Assert.That(b2.Timeline, Is.EqualTo(1), "Unexpected first alt number"); + Assert.That(c1.Timeline, Is.EqualTo(2), "Unexpected second alt number"); + Assert.That(d1.Timeline, Is.EqualTo(3), "Unexpected third alt number"); + + 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(b1.Turn, Is.EqualTo(Season.FIRST_TURN + 2), "Unexpected fork turn number"); + Assert.That(b2.Turn, Is.EqualTo(Season.FIRST_TURN + 3), "Unexpected fork turn number"); + Assert.That(c1.Turn, Is.EqualTo(Season.FIRST_TURN + 2), "Unexpected fork turn number"); + Assert.That(d1.Turn, Is.EqualTo(Season.FIRST_TURN + 3), "Unexpected fork turn number"); + + Assert.That(a0.TimelineRoot(), Is.EqualTo(a0), "Expected timeline root to be reflexive"); + Assert.That(a3.TimelineRoot(), Is.EqualTo(a0), "Expected trunk timeline to have root"); + Assert.That(b1.TimelineRoot(), Is.EqualTo(b1), "Expected alt timeline root to be reflexive"); + Assert.That(b2.TimelineRoot(), Is.EqualTo(b1), "Expected alt timeline to root at first fork"); + Assert.That(c1.TimelineRoot(), Is.EqualTo(c1), "Expected alt timeline root to be reflexive"); + Assert.That(d1.TimelineRoot(), Is.EqualTo(d1), "Expected alt timeline root to be reflexive"); + + Assert.That(b2.InAdjacentTimeline(a3), Is.True, "Expected alts to be adjacent to origin"); + Assert.That(b2.InAdjacentTimeline(c1), Is.True, "Expected alts with common origin to be adjacent"); + Assert.That(b2.InAdjacentTimeline(d1), Is.False, "Expected alts from different origins not to be adjacent"); + } +} \ No newline at end of file