Refactor timelines and season creation logic into World

This commit is contained in:
Tim Van Baak 2024-08-13 15:24:40 -07:00
parent 58f877425a
commit 345d54f960
7 changed files with 41 additions and 78 deletions

View File

@ -312,9 +312,9 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
// All moves to a particular season in a single phase result in the same future. Keep a // All moves to a particular season in a single phase result in the same future. Keep a
// record of when a future season has been created. // record of when a future season has been created.
Dictionary<Season, Season> createdFutures = new(); Dictionary<Season, Season> createdFutures = [];
List<Unit> createdUnits = new(); List<Unit> createdUnits = [];
List<RetreatingUnit> retreats = new(); List<RetreatingUnit> retreats = [];
// Populate createdFutures with the timeline fork decisions // Populate createdFutures with the timeline fork decisions
logger.Log(1, "Processing AdvanceTimeline decisions"); logger.Log(1, "Processing AdvanceTimeline decisions");
@ -324,9 +324,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
if (advanceTimeline.Outcome == true) if (advanceTimeline.Outcome == true)
{ {
// A timeline that doesn't have a future yet simply continues. Otherwise, it forks. // A timeline that doesn't have a future yet simply continues. Otherwise, it forks.
createdFutures[advanceTimeline.Season] = !world.GetFutures(advanceTimeline.Season).Any() createdFutures[advanceTimeline.Season] = world.ContinueOrFork(advanceTimeline.Season);
? advanceTimeline.Season.MakeNext()
: advanceTimeline.Season.MakeFork();
} }
} }

View File

@ -20,4 +20,10 @@ public static class ModelExtensions
{ {
return $"{coord.season.Timeline}-{coord.province.Abbreviations[0]}@{coord.season.Turn}"; return $"{coord.season.Timeline}-{coord.province.Abbreviations[0]}@{coord.season.Turn}";
} }
public static World ContinueOrFork(this World world, Season season, out Season future)
{
future = world.ContinueOrFork(season);
return world.Update(seasons: world.Seasons.Append(future));
}
} }

View File

@ -43,44 +43,12 @@ public class Season
[JsonIgnore] [JsonIgnore]
public (string Timeline, int Turn) Coord => (this.Timeline, this.Turn); public (string Timeline, int Turn) Coord => (this.Timeline, this.Turn);
/// <summary> public Season(string? past, int turn, string timeline)
/// The shared timeline number generator.
/// </summary>
[JsonIgnore]
private TimelineFactory Timelines { get; }
private Season(string? past, int turn, string timeline, TimelineFactory factory)
{ {
this.Past = past; this.Past = past;
this.Turn = turn; this.Turn = turn;
this.Timeline = timeline; this.Timeline = timeline;
this.Timelines = factory;
} }
public override string ToString() => Designation; public override string ToString() => Designation;
/// <summary>
/// Create a root season at the beginning of time.
/// </summary>
public static Season MakeRoot()
{
TimelineFactory factory = new();
return new Season(
past: null,
turn: FIRST_TURN,
timeline: factory.NextTimeline(),
factory: factory);
}
/// <summary>
/// Create a season immediately after this one in the same timeline.
/// </summary>
public Season MakeNext()
=> new(this.Designation, Turn + 1, Timeline, Timelines);
/// <summary>
/// Create a season immediately after this one in a new timeline.
/// </summary>
public Season MakeFork()
=> new(this.Designation, Turn + 1, Timelines.NextTimeline(), Timelines);
} }

View File

@ -3,7 +3,7 @@ namespace MultiversalDiplomacy.Model;
/// <summary> /// <summary>
/// A shared counter for handing out new timeline designations. /// A shared counter for handing out new timeline designations.
/// </summary> /// </summary>
internal class TimelineFactory public class TimelineFactory
{ {
private static readonly char[] Letters = [ private static readonly char[] Letters = [
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',

View File

@ -63,6 +63,11 @@ public class World
/// </summary> /// </summary>
public ReadOnlyDictionary<string, OrderHistory> OrderHistory { get; } public ReadOnlyDictionary<string, OrderHistory> OrderHistory { get; }
/// <summary>
/// The shared timeline number generator.
/// </summary>
public TimelineFactory Timelines { get; }
/// <summary> /// <summary>
/// Immutable game options. /// Immutable game options.
/// </summary> /// </summary>
@ -77,6 +82,7 @@ public class World
ReadOnlyCollection<Unit> units, ReadOnlyCollection<Unit> units,
ReadOnlyCollection<RetreatingUnit> retreatingUnits, ReadOnlyCollection<RetreatingUnit> retreatingUnits,
ReadOnlyDictionary<string, OrderHistory> orderHistory, ReadOnlyDictionary<string, OrderHistory> orderHistory,
TimelineFactory timelines,
Options options) Options options)
{ {
this.Map = map; this.Map = map;
@ -84,6 +90,7 @@ public class World
this.Units = units; this.Units = units;
this.RetreatingUnits = retreatingUnits; this.RetreatingUnits = retreatingUnits;
this.OrderHistory = orderHistory; this.OrderHistory = orderHistory;
this.Timelines = timelines;
this.Options = options; this.Options = options;
this.SeasonLookup = new(Seasons.ToDictionary(season => $"{season.Timeline}{season.Turn}")); this.SeasonLookup = new(Seasons.ToDictionary(season => $"{season.Timeline}{season.Turn}"));
@ -105,6 +112,7 @@ public class World
units ?? previous.Units, units ?? previous.Units,
retreatingUnits ?? previous.RetreatingUnits, retreatingUnits ?? previous.RetreatingUnits,
orderHistory ?? previous.OrderHistory, orderHistory ?? previous.OrderHistory,
previous.Timelines,
options ?? previous.Options) options ?? previous.Options)
{ {
} }
@ -114,12 +122,14 @@ public class World
/// </summary> /// </summary>
public static World WithMap(Map map) public static World WithMap(Map map)
{ {
TimelineFactory timelines = new();
return new World( return new World(
map, map,
new([Season.MakeRoot()]), new([new(past: null, Season.FIRST_TURN, timelines.NextTimeline())]),
new([]), new([]),
new([]), new([]),
new(new Dictionary<string, OrderHistory>()), new(new Dictionary<string, OrderHistory>()),
timelines,
new Options()); new Options());
} }
@ -209,21 +219,12 @@ public class World
} }
/// <summary> /// <summary>
/// Create a season immediately after this one in the same timeline. /// Create a continuation of this season if it has no futures, otherwise ceate a fork.
/// </summary> /// </summary>
public World ContinueSeason(string season) public Season ContinueOrFork(Season season)
=> Update(seasons: Seasons.Append(SeasonLookup[season].MakeNext())); => GetFutures(season).Any()
? new(season.Designation, season.Turn + 1, Timelines.NextTimeline())
/// <summary> : new(season.Designation, season.Turn + 1, season.Timeline);
/// Create a season immediately after this one in the same timeline.
/// </summary>
public World ContinueSeason(Season season) => ContinueSeason(season.ToString());
/// <summary>
/// Create a season immediately after this one in a new timeline.
/// </summary>
public World ForkSeason(string season)
=> Update(seasons: Seasons.Append(SeasonLookup[season].MakeFork()));
/// <summary> /// <summary>
/// A standard Diplomacy game setup. /// A standard Diplomacy game setup.

View File

@ -9,30 +9,22 @@ public class SeasonTests
[Test] [Test]
public void TimelineForking() public void TimelineForking()
{ {
World world = World World world = World.WithMap(Map.Test);
.WithMap(Map.Test) Season a0 = world.GetSeason("a0");
.ContinueSeason("a0") world = world
.ContinueSeason("a1") .ContinueOrFork(a0, out Season a1)
.ContinueSeason("a2") .ContinueOrFork(a1, out Season a2)
.ForkSeason("a1") .ContinueOrFork(a2, out Season a3)
.ContinueSeason("b2") .ContinueOrFork(a1, out Season b2)
.ForkSeason("a1") .ContinueOrFork(b2, out Season b3)
.ForkSeason("a2"); .ContinueOrFork(a1, out Season c2)
.ContinueOrFork(a2, out Season d3);
Assert.That( Assert.That(
world.Seasons.Select(season => season.ToString()), world.Seasons.Select(season => season.ToString()),
Is.EquivalentTo(new List<string> { "a0", "a1", "a2", "a3", "b2", "b3", "c2", "d3" }), Is.EquivalentTo(new List<string> { "a0", "a1", "a2", "a3", "b2", "b3", "c2", "d3" }),
"Unexpected seasons"); "Unexpected seasons");
Season a0 = world.GetSeason("a0");
Season a1 = world.GetSeason("a1");
Season a2 = world.GetSeason("a2");
Season a3 = world.GetSeason("a3");
Season b2 = world.GetSeason("b2");
Season b3 = world.GetSeason("b3");
Season c2 = world.GetSeason("c2");
Season d3 = world.GetSeason("d3");
Assert.That(a0.Timeline, Is.EqualTo("a"), "Unexpected trunk timeline"); Assert.That(a0.Timeline, Is.EqualTo("a"), "Unexpected trunk timeline");
Assert.That(a1.Timeline, Is.EqualTo("a"), "Unexpected trunk timeline"); Assert.That(a1.Timeline, Is.EqualTo("a"), "Unexpected trunk timeline");
Assert.That(a2.Timeline, Is.EqualTo("a"), "Unexpected trunk timeline"); Assert.That(a2.Timeline, Is.EqualTo("a"), "Unexpected trunk timeline");

View File

@ -17,12 +17,10 @@ public class UnitTests
Season a0 = world.RootSeason; Season a0 = world.RootSeason;
Unit u1 = Unit.Build(Mun, a0, pw1, UnitType.Army); Unit u1 = Unit.Build(Mun, a0, pw1, UnitType.Army);
world = world.ContinueSeason(a0); world = world.ContinueOrFork(a0, out Season a1);
Season a1 = world.GetSeason("a1");
Unit u2 = u1.Next(Boh, a1); Unit u2 = u1.Next(Boh, a1);
world = world.ContinueSeason(a1); _ = world.ContinueOrFork(a1, out Season a2);
Season a2 = world.GetSeason("a2");
Unit u3 = u2.Next(Tyr, a2); Unit u3 = u2.Next(Tyr, a2);
Assert.That(u3.Past, Is.EqualTo(u2), "Missing unit past"); Assert.That(u3.Past, Is.EqualTo(u2), "Missing unit past");