Compare commits

...

1 Commits

Author SHA1 Message Date
Jaculabilis dae0658492 tmp 2024-08-09 07:10:29 -07:00
9 changed files with 290 additions and 59 deletions

View File

@ -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<MoveOrder> 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))
{

View File

@ -5,21 +5,6 @@ namespace MultiversalDiplomacy.Model;
/// </summary>
public class Season
{
/// <summary>
/// A shared counter for handing out new timeline numbers.
/// </summary>
private class TimelineFactory
{
private int nextTimeline = 0;
public int NextTimeline() => nextTimeline++;
}
/// <summary>
/// The first turn number.
/// </summary>
public const int FIRST_TURN = 0;
/// <summary>
/// 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.
/// </summary>
public int Turn { get; }
public Turn Turn { get; }
/// <summary>
/// The timeline to which this season belongs.
@ -42,10 +27,10 @@ public class Season
/// <summary>
/// The season's spatial location as a turn-timeline tuple.
/// </summary>
public (int Turn, int Timeline) Coord => (this.Turn, this.Timeline);
public (Turn Turn, int Timeline) Coord => (this.Turn, this.Timeline);
/// <summary>
/// The shared timeline number generator.
/// The shared timeline number generator created by the root season.
/// </summary>
private TimelineFactory Timelines { get; }
@ -55,13 +40,13 @@ public class Season
public IEnumerable<Season> Futures => this.FutureList;
private List<Season> 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()}";
/// <summary>
/// Create a root season at the beginning of time.
/// </summary>
@ -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.
/// </summary>
public Season MakeNext()
=> new Season(this, this.Turn + 1, this.Timeline, this.Timelines);
=> new Season(this, Turn.Next, Timeline, Timelines);
/// <summary>
/// Create a season immediately after this one in a new timeline.
/// </summary>
public Season MakeFork()
=> new Season(this, this.Turn + 1, this.Timelines.NextTimeline(), this.Timelines);
=> new Season(this, Turn.Next, Timelines.Next().number, Timelines);
/// <summary>
/// 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.
/// </summary>
public Season TimelineRoot()
=> this.Past != null && this.Timeline == this.Past.Timeline
? this.Past.TimelineRoot()
=> Past != null && Timeline == Past.Timeline
? Past.TimelineRoot()
: this;
/// <summary>
@ -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))
{

View File

@ -0,0 +1,139 @@
using System.Diagnostics.CodeAnalysis;
namespace MultiversalDiplomacy.Model;
/// <summary>
/// A timeline of a game. Essentially a wrapper around <see cref="int"/> for tracking the multiversal dimension.
/// </summary>
public struct Timeline
{
public readonly int number;
/// <summary>
/// Timeline designations for differentiating timelines. Shortenable to the first character.
/// </summary>
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"
};
/// <summary>
/// Disambiguators used once all primary timeline designations have been used.
/// </summary>
private static readonly string[] secondaryDesignations = new string[]
{
"",
"-prime",
"-second",
"-third",
"-fourth",
"-fifth",
"-sixth",
"-seventh",
"-eighth",
"-ninth",
};
/// <summary>
/// Short versions of the secondary timeline disambiguators.
/// </summary>
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;
}
/// <summary>
/// The first timeline number.
/// </summary>
public const int FIRST_TIMELINE = 0;
/// <summary>
/// Which primary designation the timeline has.
/// </summary>
private int primaryIndex => number % primaryDesignations.Length;
/// <summary>
/// Which secondary designation the timeline has.
/// </summary>
private int secondaryIndex => number / primaryDesignations.Length;
/// <summary>
/// Returns the timeline's full string representation.
/// </summary>
/// <returns>The timeline designation as a string, such as "bravo" or "delta-prime".</returns>
public override string ToString()
=> primaryDesignations[primaryIndex] + secondaryDesignations[secondaryIndex];
/// <summary>
/// Returns a shorter string representation of the timeline.
/// </summary>
/// <returns>The timeline's designation as a string, such as "b" or "d'".</returns>
public string ToShort()
=> primaryDesignations[primaryIndex][0] + secondaryDesignationsShort[secondaryIndex];
/// <summary>
/// Returns a value indicating whether this instance is equal to a specified <see cref="Timeline"/> value.
/// </summary>
/// <returns>true if obj has the same value as this instance; otherwise, false.</returns>
public override bool Equals([NotNullWhen(true)] object? obj)
=> obj is Timeline other
? number.Equals(other.number)
: false;
/// <summary>
/// Returns the hash code for this instance.
/// </summary>
/// <returns>A 32-bit signed integer hash code.</returns>
public override int GetHashCode()
=> number.GetHashCode();
public static int operator -(Timeline first, Timeline second)
=> Math.Abs(first.number - second.number);
}

View File

@ -0,0 +1,16 @@
namespace MultiversalDiplomacy.Model;
/// <summary>
/// A ratchet counter for creating timelines.
/// </summary>
public class TimelineFactory
{
private int nextTimeline;
public TimelineFactory()
{
nextTimeline = 0;
}
public Timeline Next() => new(nextTimeline++);
}

View File

@ -0,0 +1,74 @@
using System.Diagnostics.CodeAnalysis;
namespace MultiversalDiplomacy.Model;
/// <summary>
/// A turn of a game. Essentially a wrapper around <see cref="int"/> for tracking the temporal dimension.
/// </summary>
public struct Turn
{
/// <summary>
/// The first turn number.
/// </summary>
public const int FIRST_TURN = 0;
/// <summary>
/// The first turn.
/// </summary>
public static readonly Turn First = new(FIRST_TURN);
/// <summary>
/// This turn's number.
/// </summary>
private readonly int number;
public Turn(int number)
{
if (number < FIRST_TURN)
{
throw new ArgumentException($"Invalid turn number: {number}", nameof(number));
}
this.number = number;
}
/// <summary>
/// Returns the turn's string representation as a Diplomacy season.
/// </summary>
/// <returns>The season and year as a string, e.g. "Spring 1901".</returns>
public override string ToString()
=> $"{(number % 2 == 0 ? "Spring" : "Fall")} {1901 + (number / 2)}";
/// <summary>
/// Returns a shorter string representation of the turn.
/// </summary>
/// <returns>The season and year as a string, e.g. "S'01".</returns>
public string ToShort()
=> $"{(number % 2 == 0 ? "S" : "F")}'{number / 2:D2}";
/// <summary>
/// Returns a value indicating whether this instance is equal to a specified <see cref="Turn"/> value.
/// </summary>
/// <returns>true if obj has the same value as this instance; otherwise, false.</returns>
public override bool Equals([NotNullWhen(true)] object? obj)
=> obj is Turn other
? number.Equals(other.number)
: false;
/// <summary>
/// Returns the hash code for this instance.
/// </summary>
/// <returns>A 32-bit signed integer hash code.</returns>
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);
}

View File

@ -256,7 +256,7 @@ public class World
/// <summary>
/// Get a season by coordinate. Throws if the season is not found.
/// </summary>
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
/// <summary>
/// Returns a unit in a province. Throws if there are duplicate units.
/// </summary>
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);

View File

@ -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));
}

View File

@ -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));
}

View File

@ -19,7 +19,7 @@ public class TestCaseBuilder
/// <summary>
/// Choose a new season to define orders for.
/// </summary>
public ISeasonContext this[(int turn, int timeline) seasonCoord] { get; }
public ISeasonContext this[(Turn turn, int timeline) seasonCoord] { get; }
/// <summary>
/// Get the context for defining the orders for a power.
@ -40,7 +40,7 @@ public class TestCaseBuilder
/// <summary>
/// Choose a new season to define orders for.
/// </summary>
public ISeasonContext this[(int turn, int timeline) seasonCoord] { get; }
public ISeasonContext this[(Turn turn, int timeline) seasonCoord] { get; }
/// <summary>
/// Get the context for defining the orders for another power.
@ -188,7 +188,7 @@ public class TestCaseBuilder
/// <summary>
/// Choose a new season to define orders for.
/// </summary>
public ISeasonContext this[(int turn, int timeline) seasonCoord] { get; }
public ISeasonContext this[(Turn turn, int timeline) seasonCoord] { get; }
/// <summary>
/// Get the context for defining the orders for another power.
@ -234,12 +234,12 @@ public class TestCaseBuilder
/// <summary>
/// Get the context for defining the orders for a power. Defaults to the root season.
/// </summary>
public IPowerContext this[string powerName] => this[(0, 0)][powerName];
public IPowerContext this[string powerName] => this[(Turn.First, 0)][powerName];
/// <summary>
/// Get the context for defining the orders for a season.
/// </summary>
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));
/// <summary>
@ -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]