diff --git a/MultiversalDiplomacy/Adjudicate/MovementPhaseAdjudicator.cs b/MultiversalDiplomacy/Adjudicate/MovementPhaseAdjudicator.cs index bc675c7..e249ce5 100644 --- a/MultiversalDiplomacy/Adjudicate/MovementPhaseAdjudicator.cs +++ b/MultiversalDiplomacy/Adjudicate/MovementPhaseAdjudicator.cs @@ -92,7 +92,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator // Map adjacency order.Unit.Location.Adjacents.Contains(order.Location) // Turn adjacency - && Math.Abs(order.Unit.Season.Turn - order.Season.Turn) <= 1 + && order.Unit.Season.Turn - order.Season.Turn <= 1 // Timeline adjacency && order.Unit.Season.InAdjacentTimeline(order.Season)); List adjacentMoveOrders = moveOrdersByAdjacency[true].ToList(); @@ -178,7 +178,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator order.Unit.Location.Adjacents.Any( adjLocation => adjLocation.Province == order.Target.Province) // Turn adjacency - && Math.Abs(order.Unit.Season.Turn - order.Target.Season.Turn) <= 1 + && order.Unit.Season.Turn - order.Target.Season.Turn <= 1 // Timeline adjacency && order.Unit.Season.InAdjacentTimeline(order.Target.Season), ValidationReason.UnreachableSupport, @@ -210,7 +210,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator order.Unit.Location.Adjacents.Any( adjLocation => adjLocation.Province == order.Province) // Turn adjacency - && Math.Abs(order.Unit.Season.Turn - order.Season.Turn) <= 1 + && order.Unit.Season.Turn - order.Season.Turn <= 1 // Timeline adjacency && order.Unit.Season.InAdjacentTimeline(order.Season), ValidationReason.UnreachableSupport, @@ -531,6 +531,20 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator $"History changed for {order.Unit}: dislodge {previous} => {dislodged.Outcome}"); return progress; } + // logger.Log(0, "world.OrderHistory"); + // foreach ((Season s, OrderHistory oh) in world.OrderHistory) + // { + // logger.Log(1, $"{s}"); + // logger.Log(2, "Orders:"); + // foreach (UnitOrder o in oh.Orders) + // logger.Log(3, $"{o}"); + // logger.Log(2, "Dislodges:"); + // foreach ((Unit u, bool outcome) in oh.IsDislodgedOutcomes) + // logger.Log(3, $"{u} = {outcome}"); + // logger.Log(2, "Moves:"); + // foreach ((MoveOrder m, bool outcome) in oh.DoesMoveOutcomes) + // logger.Log(3, $"{m} = {outcome}"); + // } anyUnresolved |= !dislodged.Resolved; if (order is MoveOrder moveOrder) @@ -637,7 +651,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator if (// Map adjacency decision.Order.Unit.Location.Adjacents.Contains(decision.Order.Location) // Turn adjacency - && Math.Abs(decision.Order.Unit.Season.Turn - decision.Order.Season.Turn) <= 1 + && decision.Order.Unit.Season.Turn - decision.Order.Season.Turn <= 1 // Timeline adjacency && decision.Order.Unit.Season.InAdjacentTimeline(decision.Order.Season)) { diff --git a/MultiversalDiplomacy/Model/Season.cs b/MultiversalDiplomacy/Model/Season.cs index cbbc7d8..5a5ae82 100644 --- a/MultiversalDiplomacy/Model/Season.cs +++ b/MultiversalDiplomacy/Model/Season.cs @@ -5,21 +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. - /// - 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. @@ -32,7 +17,7 @@ public class Season /// Phases that only occur after the fall phase occur when Turn % 2 == 1. /// The current year is (Turn / 2) + 1901. /// - public int Turn { get; } + public Turn Turn { get; } /// /// The timeline to which this season belongs. @@ -42,10 +27,10 @@ public class Season /// /// The season's spatial location as a turn-timeline tuple. /// - public (int Turn, int Timeline) Coord => (this.Turn, this.Timeline); + public (Turn Turn, int Timeline) Coord => (this.Turn, this.Timeline); /// - /// The shared timeline number generator. + /// The shared timeline number generator created by the root season. /// private TimelineFactory Timelines { get; } @@ -55,13 +40,13 @@ public class Season public IEnumerable Futures => this.FutureList; private List FutureList { get; } - private Season(Season? past, int turn, int timeline, TimelineFactory factory) + private Season(Season? past, Turn turn, int timeline, TimelineFactory factory) { - this.Past = past; - this.Turn = turn; - this.Timeline = timeline; - this.Timelines = factory; - this.FutureList = new(); + Past = past; + Turn = turn; + Timeline = timeline; + Timelines = factory; + FutureList = new(); if (past != null) { @@ -71,9 +56,12 @@ public class Season public override string ToString() { - return $"{this.Timeline}@{this.Turn}"; + return $"{this.Timeline}@{Turn}"; } + public string ToShort() + => $"{Timeline}@{Turn.ToShort()}"; + /// /// Create a root season at the beginning of time. /// @@ -82,8 +70,8 @@ public class Season TimelineFactory factory = new TimelineFactory(); return new Season( past: null, - turn: FIRST_TURN, - timeline: factory.NextTimeline(), + turn: new Turn(Turn.FIRST_TURN), + timeline: factory.Next().number, factory: factory); } @@ -91,13 +79,13 @@ public class Season /// Create a season immediately after this one in the same timeline. /// public Season MakeNext() - => new Season(this, this.Turn + 1, this.Timeline, this.Timelines); + => new Season(this, Turn.Next, Timeline, 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); + => new Season(this, Turn.Next, Timelines.Next().number, Timelines); /// /// Returns the first season in this season's timeline. The first season is the @@ -105,8 +93,8 @@ public class Season /// the root of that timeline. /// public Season TimelineRoot() - => this.Past != null && this.Timeline == this.Past.Timeline - ? this.Past.TimelineRoot() + => Past != null && Timeline == Past.Timeline + ? Past.TimelineRoot() : this; /// @@ -172,7 +160,7 @@ public class Season foreach (Season timelineRoot in adjacentTimelineRoots) { for (Season? branchSeason = timelineRoot; - branchSeason != null && branchSeason.Turn <= this.Turn + 1; + branchSeason != null && branchSeason.Turn <= this.Turn.Next; branchSeason = branchSeason.FutureList .FirstOrDefault(s => s!.Timeline == branchSeason.Timeline, null)) { diff --git a/MultiversalDiplomacy/Model/Timeline.cs b/MultiversalDiplomacy/Model/Timeline.cs new file mode 100644 index 0000000..348f321 --- /dev/null +++ b/MultiversalDiplomacy/Model/Timeline.cs @@ -0,0 +1,139 @@ +using System.Diagnostics.CodeAnalysis; + +namespace MultiversalDiplomacy.Model; + +/// +/// A timeline of a game. Essentially a wrapper around for tracking the multiversal dimension. +/// +public struct Timeline +{ + public readonly int number; + + /// + /// Timeline designations for differentiating timelines. Shortenable to the first character. + /// + private static readonly string[] primaryDesignations = new string[] + { + "alfa", + "bravo", + "charlie", + "delta", + "echo", + "foxtrot", + "golf", + "hotel", + "india", + "juliett", + "kilo", + "lima", + "mike", + "november", + "oscar", + "papa", + "quebec", + "romeo", + "sierra", + "tango", + "uniform", + "victor", + "whiskey", + "xray", + "yankee", + "zulu" + }; + + /// + /// Disambiguators used once all primary timeline designations have been used. + /// + private static readonly string[] secondaryDesignations = new string[] + { + "", + "-prime", + "-second", + "-third", + "-fourth", + "-fifth", + "-sixth", + "-seventh", + "-eighth", + "-ninth", + }; + + /// + /// Short versions of the secondary timeline disambiguators. + /// + private static readonly string[] secondaryDesignationsShort = new string[] + { + "", + "'", + "\"", + "\x2073", + "\x2074", + "\x2075", + "\x2076", + "\x2077", + "\x2078", + "\x2079", + }; + + public Timeline(int number) + { + if (number < FIRST_TIMELINE) + { + throw new ArgumentException($"Invalid timeline number: {number}", nameof(number)); + } + if (number >= primaryDesignations.Length * secondaryDesignations.Length) + { + throw new ArgumentException($"Timeline number too high: {number}", nameof(number)); + } + this.number = number; + } + + /// + /// The first timeline number. + /// + public const int FIRST_TIMELINE = 0; + + /// + /// Which primary designation the timeline has. + /// + private int primaryIndex => number % primaryDesignations.Length; + + /// + /// Which secondary designation the timeline has. + /// + private int secondaryIndex => number / primaryDesignations.Length; + + /// + /// Returns the timeline's full string representation. + /// + /// The timeline designation as a string, such as "bravo" or "delta-prime". + public override string ToString() + => primaryDesignations[primaryIndex] + secondaryDesignations[secondaryIndex]; + + /// + /// Returns a shorter string representation of the timeline. + /// + /// The timeline's designation as a string, such as "b" or "d'". + public string ToShort() + => primaryDesignations[primaryIndex][0] + secondaryDesignationsShort[secondaryIndex]; + + /// + /// Returns a value indicating whether this instance is equal to a specified value. + /// + /// true if obj has the same value as this instance; otherwise, false. + public override bool Equals([NotNullWhen(true)] object? obj) + => obj is Timeline other + ? number.Equals(other.number) + : false; + + /// + /// Returns the hash code for this instance. + /// + /// A 32-bit signed integer hash code. + public override int GetHashCode() + => number.GetHashCode(); + + public static int operator -(Timeline first, Timeline second) + => Math.Abs(first.number - second.number); +} \ No newline at end of file diff --git a/MultiversalDiplomacy/Model/TimelineFactory.cs b/MultiversalDiplomacy/Model/TimelineFactory.cs new file mode 100644 index 0000000..5a7241b --- /dev/null +++ b/MultiversalDiplomacy/Model/TimelineFactory.cs @@ -0,0 +1,16 @@ +namespace MultiversalDiplomacy.Model; + +/// +/// A ratchet counter for creating timelines. +/// +public class TimelineFactory +{ + private int nextTimeline; + + public TimelineFactory() + { + nextTimeline = 0; + } + + public Timeline Next() => new(nextTimeline++); +} diff --git a/MultiversalDiplomacy/Model/Turn.cs b/MultiversalDiplomacy/Model/Turn.cs new file mode 100644 index 0000000..689f388 --- /dev/null +++ b/MultiversalDiplomacy/Model/Turn.cs @@ -0,0 +1,74 @@ +using System.Diagnostics.CodeAnalysis; + +namespace MultiversalDiplomacy.Model; + +/// +/// A turn of a game. Essentially a wrapper around for tracking the temporal dimension. +/// +public struct Turn +{ + /// + /// The first turn number. + /// + public const int FIRST_TURN = 0; + + /// + /// The first turn. + /// + public static readonly Turn First = new(FIRST_TURN); + + /// + /// This turn's number. + /// + private readonly int number; + + public Turn(int number) + { + if (number < FIRST_TURN) + { + throw new ArgumentException($"Invalid turn number: {number}", nameof(number)); + } + this.number = number; + } + + /// + /// Returns the turn's string representation as a Diplomacy season. + /// + /// The season and year as a string, e.g. "Spring 1901". + public override string ToString() + => $"{(number % 2 == 0 ? "Spring" : "Fall")} {1901 + (number / 2)}"; + + /// + /// Returns a shorter string representation of the turn. + /// + /// The season and year as a string, e.g. "S'01". + public string ToShort() + => $"{(number % 2 == 0 ? "S" : "F")}'{number / 2:D2}"; + + /// + /// Returns a value indicating whether this instance is equal to a specified value. + /// + /// true if obj has the same value as this instance; otherwise, false. + public override bool Equals([NotNullWhen(true)] object? obj) + => obj is Turn other + ? number.Equals(other.number) + : false; + + /// + /// Returns the hash code for this instance. + /// + /// A 32-bit signed integer hash code. + public override int GetHashCode() + => number.GetHashCode(); + + public static int operator -(Turn first, Turn second) + => Math.Abs(first.number - second.number); + + public static bool operator ==(Turn first, Turn second) + => first.number == second.number; + + public static bool operator !=(Turn first, Turn second) + => first.number != second.number; + + public Turn Next => new Turn(number + 1); +} \ No newline at end of file diff --git a/MultiversalDiplomacy/Model/World.cs b/MultiversalDiplomacy/Model/World.cs index 799933e..b9b8a66 100644 --- a/MultiversalDiplomacy/Model/World.cs +++ b/MultiversalDiplomacy/Model/World.cs @@ -256,7 +256,7 @@ public class World /// /// Get a season by coordinate. Throws if the season is not found. /// - public Season GetSeason(int turn, int timeline) + public Season GetSeason(Turn turn, int timeline) { Season? foundSeason = this.Seasons.SingleOrDefault( s => s!.Turn == turn && s.Timeline == timeline, @@ -282,7 +282,7 @@ public class World /// /// Returns a unit in a province. Throws if there are duplicate units. /// - public Unit GetUnitAt(string provinceName, (int turn, int timeline)? seasonCoord = null) + public Unit GetUnitAt(string provinceName, (Turn turn, int timeline)? seasonCoord = null) { Province province = GetProvince(provinceName); seasonCoord ??= (this.RootSeason.Turn, this.RootSeason.Timeline); diff --git a/MultiversalDiplomacyTests/MDATC_A.cs b/MultiversalDiplomacyTests/MDATC_A.cs index d9a6b3d..d0289be 100644 --- a/MultiversalDiplomacyTests/MDATC_A.cs +++ b/MultiversalDiplomacyTests/MDATC_A.cs @@ -42,7 +42,7 @@ public class TimeTravelTest "Failed to fork timeline when unit moved in"); // Confirm that there is a unit in Tyr 1:1 originating from Mun 1:0 - Season fork = world.GetSeason(1, 1); + Season fork = world.GetSeason(new(1), 1); Unit originalUnit = world.GetUnitAt("Mun", s0.Coord); Unit aMun0 = world.GetUnitAt("Mun", s1.Coord); Unit aTyr = world.GetUnitAt("Tyr", fork.Coord); @@ -91,7 +91,7 @@ public class TimeTravelTest // Confirm that an alternate future is created. World world = setup.UpdateWorld(); - Season fork = world.GetSeason(1, 1); + Season fork = world.GetSeason(new(1), 1); Unit tyr1 = world.GetUnitAt("Tyr", fork.Coord); Assert.That( tyr1.Past, @@ -140,7 +140,7 @@ public class TimeTravelTest Is.EqualTo(1), "A failed move incorrectly forked the timeline"); Assert.That(s1.Futures.Count(), Is.EqualTo(1)); - Season s2 = world.GetSeason(s1.Turn + 1, s1.Timeline); + Season s2 = world.GetSeason(s1.Turn.Next, s1.Timeline); Assert.That(s2.Futures.Count(), Is.EqualTo(0)); } @@ -182,7 +182,7 @@ public class TimeTravelTest Is.EqualTo(1), "A superfluous support incorrectly forked the timeline"); Assert.That(s1.Futures.Count(), Is.EqualTo(1)); - Season s2 = world.GetSeason(s1.Turn + 1, s1.Timeline); + Season s2 = world.GetSeason(s1.Turn.Next, s1.Timeline); Assert.That(s2.Futures.Count(), Is.EqualTo(0)); } @@ -233,9 +233,9 @@ public class TimeTravelTest s1_1.Futures.Count(), Is.EqualTo(1), "A cross-timeline support incorrectly forked the head of the timeline"); - Season s3_0 = world.GetSeason(s2_0.Turn + 1, s2_0.Timeline); + Season s3_0 = world.GetSeason(s2_0.Turn.Next, s2_0.Timeline); Assert.That(s3_0.Futures.Count(), Is.EqualTo(0)); - Season s2_1 = world.GetSeason(s1_1.Turn + 1, s1_1.Timeline); + Season s2_1 = world.GetSeason(s1_1.Turn.Next, s1_1.Timeline); Assert.That(s2_1.Futures.Count(), Is.EqualTo(0)); } diff --git a/MultiversalDiplomacyTests/MovementAdjudicatorTest.cs b/MultiversalDiplomacyTests/MovementAdjudicatorTest.cs index 267517a..9d6cd81 100644 --- a/MultiversalDiplomacyTests/MovementAdjudicatorTest.cs +++ b/MultiversalDiplomacyTests/MovementAdjudicatorTest.cs @@ -174,7 +174,7 @@ public class MovementAdjudicatorTest Assert.That(future.Past, Is.EqualTo(updated.RootSeason)); Assert.That(future.Futures, Is.Empty); Assert.That(future.Timeline, Is.EqualTo(updated.RootSeason.Timeline)); - Assert.That(future.Turn, Is.EqualTo(Season.FIRST_TURN + 1)); + Assert.That(future.Turn, Is.EqualTo(Turn.First.Next)); // Confirm the unit was created Assert.That(updated.Units.Count, Is.EqualTo(2)); @@ -199,11 +199,11 @@ public class MovementAdjudicatorTest World updated = setup.UpdateWorld(); // Confirm the future was created - Season s2 = updated.GetSeason(1, 0); + Season s2 = updated.GetSeason(new(1), 0); Assert.That(s2.Past, Is.EqualTo(s1)); Assert.That(s2.Futures, Is.Empty); Assert.That(s2.Timeline, Is.EqualTo(s1.Timeline)); - Assert.That(s2.Turn, Is.EqualTo(s1.Turn + 1)); + Assert.That(s2.Turn, Is.EqualTo(s1.Turn.Next)); // Confirm the unit was created in the future Unit u2 = updated.GetUnitAt("Mun", s2.Coord); @@ -227,7 +227,7 @@ public class MovementAdjudicatorTest // Update the world again updated = setup.UpdateWorld(); - Season s3 = updated.GetSeason(s2.Turn + 1, s2.Timeline); + Season s3 = updated.GetSeason(s2.Turn.Next, s2.Timeline); Unit u3 = updated.GetUnitAt("Mun", s3.Coord); Assert.That(u3.Past, Is.EqualTo(mun2.Order.Unit)); } @@ -249,11 +249,11 @@ public class MovementAdjudicatorTest World updated = setup.UpdateWorld(); // Confirm the future was created - Season s2 = updated.GetSeason(s1.Turn + 1, s1.Timeline); + Season s2 = updated.GetSeason(s1.Turn.Next, s1.Timeline); Assert.That(s2.Past, Is.EqualTo(s1)); Assert.That(s2.Futures, Is.Empty); Assert.That(s2.Timeline, Is.EqualTo(s1.Timeline)); - Assert.That(s2.Turn, Is.EqualTo(s1.Turn + 1)); + Assert.That(s2.Turn, Is.EqualTo(s1.Turn.Next)); // Confirm the unit was created in the future Unit u2 = updated.GetUnitAt("Tyr", s2.Coord); @@ -277,7 +277,7 @@ public class MovementAdjudicatorTest // Update the world again updated = setup.UpdateWorld(); - Season s3 = updated.GetSeason(s2.Turn + 1, s2.Timeline); + Season s3 = updated.GetSeason(s2.Turn.Next, s2.Timeline); Unit u3 = updated.GetUnitAt("Mun", s3.Coord); Assert.That(u3.Past, Is.EqualTo(u2)); } diff --git a/MultiversalDiplomacyTests/TestCaseBuilder.cs b/MultiversalDiplomacyTests/TestCaseBuilder.cs index 5d601ad..8018519 100644 --- a/MultiversalDiplomacyTests/TestCaseBuilder.cs +++ b/MultiversalDiplomacyTests/TestCaseBuilder.cs @@ -19,7 +19,7 @@ public class TestCaseBuilder /// /// Choose a new season to define orders for. /// - public ISeasonContext this[(int turn, int timeline) seasonCoord] { get; } + public ISeasonContext this[(Turn turn, int timeline) seasonCoord] { get; } /// /// Get the context for defining the orders for a power. @@ -40,7 +40,7 @@ public class TestCaseBuilder /// /// Choose a new season to define orders for. /// - public ISeasonContext this[(int turn, int timeline) seasonCoord] { get; } + public ISeasonContext this[(Turn turn, int timeline) seasonCoord] { get; } /// /// Get the context for defining the orders for another power. @@ -188,7 +188,7 @@ public class TestCaseBuilder /// /// Choose a new season to define orders for. /// - public ISeasonContext this[(int turn, int timeline) seasonCoord] { get; } + public ISeasonContext this[(Turn turn, int timeline) seasonCoord] { get; } /// /// Get the context for defining the orders for another power. @@ -234,12 +234,12 @@ public class TestCaseBuilder /// /// Get the context for defining the orders for a power. Defaults to the root season. /// - public IPowerContext this[string powerName] => this[(0, 0)][powerName]; + public IPowerContext this[string powerName] => this[(Turn.First, 0)][powerName]; /// /// Get the context for defining the orders for a season. /// - public ISeasonContext this[(int turn, int timeline) seasonCoord] + public ISeasonContext this[(Turn turn, int timeline) seasonCoord] => new SeasonContext(this, this.World.GetSeason(seasonCoord.turn, seasonCoord.timeline)); /// @@ -327,7 +327,7 @@ public class TestCaseBuilder this.Season = season; } - public ISeasonContext this[(int turn, int timeline) seasonCoord] + public ISeasonContext this[(Turn turn, int timeline) seasonCoord] => this.Builder[(seasonCoord.turn, seasonCoord.timeline)]; public IPowerContext this[string powerName] @@ -353,7 +353,7 @@ public class TestCaseBuilder this.Power = Power; } - public ISeasonContext this[(int turn, int timeline) seasonCoord] + public ISeasonContext this[(Turn turn, int timeline) seasonCoord] => this.SeasonContext[seasonCoord]; public IPowerContext this[string powerName] @@ -623,7 +623,7 @@ public class TestCaseBuilder return this.Builder; } - public ISeasonContext this[(int turn, int timeline) seasonCoord] + public ISeasonContext this[(Turn turn, int timeline) seasonCoord] => this.SeasonContext[seasonCoord]; public IPowerContext this[string powerName]