diff --git a/MultiversalDiplomacy/Model/Season.cs b/MultiversalDiplomacy/Model/Season.cs index cbbc7d8..3f9a1ce 100644 --- a/MultiversalDiplomacy/Model/Season.cs +++ b/MultiversalDiplomacy/Model/Season.cs @@ -5,16 +5,6 @@ namespace MultiversalDiplomacy.Model; /// 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. /// @@ -83,7 +73,7 @@ public class Season return new Season( past: null, turn: FIRST_TURN, - timeline: factory.NextTimeline(), + timeline: TimelineFactory.StringToInt(factory.NextTimeline()), factory: factory); } @@ -97,7 +87,7 @@ public class Season /// 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); + => new Season(this, this.Turn + 1, TimelineFactory.StringToInt(Timelines.NextTimeline()), this.Timelines); /// /// Returns the first season in this season's timeline. The first season is the diff --git a/MultiversalDiplomacy/Model/TimelineFactory.cs b/MultiversalDiplomacy/Model/TimelineFactory.cs new file mode 100644 index 0000000..1921842 --- /dev/null +++ b/MultiversalDiplomacy/Model/TimelineFactory.cs @@ -0,0 +1,50 @@ +namespace MultiversalDiplomacy.Model; + +/// +/// A shared counter for handing out new timeline designations. +/// +internal class TimelineFactory +{ + 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 designation) { + static int downshift(int i ) => (i - (i % 26)) / 26; + IEnumerable result = [Letters[designation % 26]]; + for (int remainder = downshift(designation); 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()); + } + + private int nextTimeline = 0; + + public string NextTimeline() => IntToString(nextTimeline++); +} diff --git a/MultiversalDiplomacy/MultiversalDiplomacy.csproj b/MultiversalDiplomacy/MultiversalDiplomacy.csproj index 91b464a..76512ea 100644 --- a/MultiversalDiplomacy/MultiversalDiplomacy.csproj +++ b/MultiversalDiplomacy/MultiversalDiplomacy.csproj @@ -7,4 +7,10 @@ enable + + + <_Parameter1>MultiversalDiplomacyTests + + + diff --git a/MultiversalDiplomacyTests/Model/TimelineFactoryTest.cs b/MultiversalDiplomacyTests/Model/TimelineFactoryTest.cs new file mode 100644 index 0000000..a804653 --- /dev/null +++ b/MultiversalDiplomacyTests/Model/TimelineFactoryTest.cs @@ -0,0 +1,38 @@ +using MultiversalDiplomacy.Model; + +using NUnit.Framework; + +namespace MultiversalDiplomacyTests.Model; + +public class TimelineFactoryTest +{ + [TestCase(0, "a")] + [TestCase(1, "b")] + [TestCase(25, "z")] + [TestCase(26, "aa")] + [TestCase(27, "ab")] + [TestCase(51, "az")] + [TestCase(52, "ba")] + [TestCase(53, "bb")] + [TestCase(77, "bz")] + [TestCase(78, "ca")] + public void RoundTripTimelineDesignations(int number, string designation) + { + Assert.That(TimelineFactory.IntToString(number), Is.EqualTo(designation), "Incorrect string"); + Assert.That(TimelineFactory.StringToInt(designation), Is.EqualTo(number), "Incorrect number"); + } + + [Test] + public void NoSharedFactoryState() + { + TimelineFactory one = new(); + TimelineFactory two = new(); + + Assert.That(one.NextTimeline(), Is.EqualTo("a")); + Assert.That(one.NextTimeline(), Is.EqualTo("b")); + Assert.That(one.NextTimeline(), Is.EqualTo("c")); + + Assert.That(two.NextTimeline(), Is.EqualTo("a")); + Assert.That(two.NextTimeline(), Is.EqualTo("b")); + } +}