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