Compare commits

...

7 Commits

Author SHA1 Message Date
Tim Van Baak c9bd8c8194 Delete Season.Coord 2024-08-13 18:56:21 -07:00
Tim Van Baak 5989970c42 Refactor timelines and season creation logic into World 2024-08-13 18:56:21 -07:00
Tim Van Baak cc2c29980a Add two more CLI verbs to implement 2024-08-13 18:56:21 -07:00
Tim Van Baak 984676f587 Add more JsonIgnores 2024-08-13 18:56:21 -07:00
Tim Van Baak fd8c725286 Store order history by timeline designation instead of reference 2024-08-12 21:58:24 -07:00
Tim Van Baak 0dec1e1eec Add a serialization round trip test
This currently fails because a lot of World still works on references instead of lookups
2024-08-12 21:47:28 -07:00
Tim Van Baak 27ffaccd20 Update Season ctor 2024-08-12 15:27:20 -07:00
18 changed files with 248 additions and 139 deletions

View File

@ -61,25 +61,25 @@ public class MovementDecisions
case MoveOrder move:
AdvanceTimeline.Ensure(
move.Season,
() => new(move.Season, world.OrderHistory[move.Season].Orders));
() => new(move.Season, world.OrderHistory[move.Season.Designation].Orders));
AdvanceTimeline[move.Season].Orders.Add(move);
break;
case SupportHoldOrder supportHold:
AdvanceTimeline.Ensure(
supportHold.Target.Season,
() => new(supportHold.Target.Season, world.OrderHistory[supportHold.Target.Season].Orders));
() => new(supportHold.Target.Season, world.OrderHistory[supportHold.Target.Season.Designation].Orders));
AdvanceTimeline[supportHold.Target.Season].Orders.Add(supportHold);
break;
case SupportMoveOrder supportMove:
AdvanceTimeline.Ensure(
supportMove.Target.Season,
() => new(supportMove.Target.Season, world.OrderHistory[supportMove.Target.Season].Orders));
() => new(supportMove.Target.Season, world.OrderHistory[supportMove.Target.Season.Designation].Orders));
AdvanceTimeline[supportMove.Target.Season].Orders.Add(supportMove);
AdvanceTimeline.Ensure(
supportMove.Season,
() => new(supportMove.Season, world.OrderHistory[supportMove.Season].Orders));
() => new(supportMove.Season, world.OrderHistory[supportMove.Season.Designation].Orders));
AdvanceTimeline[supportMove.Season].Orders.Add(supportMove);
break;
}

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
// record of when a future season has been created.
Dictionary<Season, Season> createdFutures = new();
List<Unit> createdUnits = new();
List<RetreatingUnit> retreats = new();
Dictionary<Season, Season> createdFutures = [];
List<Unit> createdUnits = [];
List<RetreatingUnit> retreats = [];
// Populate createdFutures with the timeline fork decisions
logger.Log(1, "Processing AdvanceTimeline decisions");
@ -324,9 +324,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
if (advanceTimeline.Outcome == true)
{
// A timeline that doesn't have a future yet simply continues. Otherwise, it forks.
createdFutures[advanceTimeline.Season] = !world.GetFutures(advanceTimeline.Season).Any()
? advanceTimeline.Season.MakeNext()
: advanceTimeline.Season.MakeFork();
createdFutures[advanceTimeline.Season] = world.ContinueOrFork(advanceTimeline.Season);
}
}
@ -386,11 +384,11 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
}
// Record the adjudication results to the season's order history
Dictionary<Season, OrderHistory> newHistory = new();
Dictionary<string, OrderHistory> newHistory = [];
foreach (UnitOrder unitOrder in decisions.OfType<IsDislodged>().Select(d => d.Order))
{
newHistory.Ensure(unitOrder.Unit.Season, () => new());
OrderHistory history = newHistory[unitOrder.Unit.Season];
newHistory.Ensure(unitOrder.Unit.Season.Designation, () => new());
OrderHistory history = newHistory[unitOrder.Unit.Season.Designation];
// TODO does this add every order to every season??
history.Orders.Add(unitOrder);
history.IsDislodgedOutcomes[unitOrder.Unit] = dislodges[unitOrder.Unit].Outcome == true;
@ -401,7 +399,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
}
// Log the new order history
foreach ((Season season, OrderHistory history) in newHistory)
foreach ((string season, OrderHistory history) in newHistory)
{
string verb = world.OrderHistory.ContainsKey(season) ? "Updating" : "Adding";
logger.Log(1, "{0} history for {1}", verb, season);
@ -411,7 +409,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
}
}
IEnumerable<KeyValuePair<Season, OrderHistory>> updatedHistory = world.OrderHistory
IEnumerable<KeyValuePair<string, OrderHistory>> updatedHistory = world.OrderHistory
.Where(kvp => !newHistory.ContainsKey(kvp.Key))
.Concat(newHistory);
@ -496,7 +494,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
IEnumerable<MoveOrder> newIncomingMoves = decision.Orders
.OfType<MoveOrder>()
.Where(order => order.Season == decision.Season
&& !world.OrderHistory[order.Season].DoesMoveOutcomes.ContainsKey(order));
&& !world.OrderHistory[order.Season.Designation].DoesMoveOutcomes.ContainsKey(order));
foreach (MoveOrder moveOrder in newIncomingMoves)
{
DoesMove doesMove = decisions.DoesMove[moveOrder];
@ -513,7 +511,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
// 1. The outcome of a dislodge decision is changed,
// 2. The outcome of an intra-timeline move decision is changed, or
// 3. The outcome of an inter-timeline move decision with that season as the destination is changed.
OrderHistory history = world.OrderHistory[decision.Season];
OrderHistory history = world.OrderHistory[decision.Season.Designation];
bool anyUnresolved = false;
foreach (UnitOrder order in decision.Orders)
{

View File

@ -0,0 +1,15 @@
using CommandLine;
namespace MultiversalDiplomacy.CommandLine;
[Verb("image", HelpText = "Generate an image of a game state.")]
public class ImageOptions
{
[Value(0, HelpText = "Input file describing the game state to visualize, or - to read from stdin.")]
public string? InputFile { get; set; }
public static void Execute(ImageOptions args)
{
throw new NotImplementedException();
}
}

View File

@ -0,0 +1,18 @@
using CommandLine;
namespace MultiversalDiplomacy.CommandLine;
[Verb("repl", HelpText = "Begin an interactive 5dplomacy session.")]
public class ReplOptions
{
[Option('i', "input", HelpText = "Begin the repl session by executing the commands in this file.")]
public string? InputFile { get; set; }
[Option('o', "output", HelpText = "Echo the repl session to this file. Specify a directory to autogenerate a filename.")]
public string? OutputFile { get; set; }
public static void Execute(ReplOptions args)
{
throw new NotImplementedException();
}
}

View File

@ -1,3 +1,5 @@
using System.Text.Json.Serialization;
namespace MultiversalDiplomacy.Model;
/// <summary>
@ -9,8 +11,12 @@ public class Location
{
/// <summary>
/// The province to which this location belongs.
/// </summary>
[JsonIgnore]
public Province Province { get; }
public string ProvinceName => Province.Name;
/// <summary>
/// The location's full human-readable name.
/// </summary>
@ -29,6 +35,7 @@ public class Location
/// <summary>
/// The locations that border this location.
/// </summary>
[JsonIgnore]
public IEnumerable<Location> Adjacents => this.AdjacentList;
private List<Location> AdjacentList { get; set; }

View File

@ -5,7 +5,13 @@ namespace MultiversalDiplomacy.Model;
/// <summary>
/// Encapsulation of the world map and playable powers constituting a Diplomacy variant.
/// </summary>
public class Map {
public class Map
{
/// <summary>
/// The map type.
/// </summary>
public MapType Type { get; }
/// <summary>
/// The game map.
/// </summary>
@ -16,7 +22,7 @@ public class Map {
/// </summary>
public ReadOnlyCollection<Power> Powers { get; }
private Map(IEnumerable<Province> provinces, IEnumerable<Power> powers)
private Map(MapType type, IEnumerable<Province> provinces, IEnumerable<Power> powers)
{
Provinces = new(provinces.ToList());
Powers = new(powers.ToList());
@ -102,7 +108,7 @@ public class Map {
Power a = new("Alpha");
Power b = new("Beta");
return new([lef, cen, rig], [a, b]);
return new(MapType.Test, [lef, cen, rig], [a, b]);
});
public static Map Classical => _Classical.Value;
@ -553,7 +559,7 @@ public class Map {
new("Turkey"),
];
return new(provinces, powers);
return new(MapType.Classical, provinces, powers);
});
#endregion Variants

View File

@ -20,4 +20,10 @@ public static class ModelExtensions
{
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

@ -13,7 +13,7 @@ public class OrderHistory
public Dictionary<MoveOrder, bool> DoesMoveOutcomes;
public OrderHistory()
: this(new(), new(), new())
: this([], [], [])
{}
public OrderHistory(

View File

@ -5,10 +5,10 @@ namespace MultiversalDiplomacy.Model;
/// <summary>
/// Represents a state of the map produced by a set of move orders on a previous season.
/// </summary>
public class Season
public class Season(string? past, int turn, string timeline)
{
/// <summary>
/// The first turn number.
/// The first turn number. This is defined to reduce confusion about whether the first turn is 0 or 1.
/// </summary>
public const int FIRST_TURN = 0;
@ -17,19 +17,19 @@ public class Season
/// If this season is an alternate timeline root, the past is from the origin timeline.
/// The initial season does not have a past.
/// </summary>
public string? Past { get; }
public string? Past { get; } = past;
/// <summary>
/// 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.
/// </summary>
public int Turn { get; }
public int Turn { get; } = turn;
/// <summary>
/// The timeline to which this season belongs.
/// </summary>
public string Timeline { get; }
public string Timeline { get; } = timeline;
/// <summary>
/// The multiversal designation of this season.
@ -37,50 +37,5 @@ public class Season
[JsonIgnore]
public string Designation => $"{this.Timeline}{this.Turn}";
/// <summary>
/// The season's multiversal location as a timeline-turn tuple for convenience.
/// </summary>
[JsonIgnore]
public (string Timeline, int Turn) Coord => (this.Timeline, this.Turn);
/// <summary>
/// The shared timeline number generator.
/// </summary>
[JsonIgnore]
private TimelineFactory Timelines { get; }
private Season(Season? past, int turn, string timeline, TimelineFactory factory)
{
this.Past = past?.ToString();
this.Turn = turn;
this.Timeline = timeline;
this.Timelines = factory;
}
public override string ToString() => Designation;
/// <summary>
/// Create a root season at the beginning of time.
/// </summary>
public static Season MakeRoot()
{
TimelineFactory factory = new TimelineFactory();
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, Turn + 1, Timeline, Timelines);
/// <summary>
/// Create a season immediately after this one in a new timeline.
/// </summary>
public Season MakeFork()
=> new(this, Turn + 1, Timelines.NextTimeline(), Timelines);
}

View File

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

View File

@ -1,3 +1,5 @@
using System.Text.Json.Serialization;
namespace MultiversalDiplomacy.Model;
/// <summary>
@ -18,6 +20,7 @@ public class Unit
/// <summary>
/// The province where the unit is.
/// </summary>
[JsonIgnore]
public Province Province => this.Location.Province;
/// <summary>
@ -59,11 +62,11 @@ public class Unit
/// method after accepting a build order.
/// </summary>
public static Unit Build(Location location, Season season, Power power, UnitType type)
=> new Unit(past: null, location, season, power, type);
=> new(past: null, location, season, power, type);
/// <summary>
/// Advance this unit's timeline to a new location and season.
/// </summary>
public Unit Next(Location location, Season season)
=> new Unit(past: this, location, season, this.Power, this.Type);
=> new(past: this, location, season, this.Power, this.Type);
}

View File

@ -1,6 +1,5 @@
using System.Collections.ObjectModel;
using MultiversalDiplomacy.Orders;
using System.Text.Json.Serialization;
namespace MultiversalDiplomacy.Model;
@ -12,16 +11,24 @@ public class World
/// <summary>
/// The map variant of the game.
/// </summary>
public readonly Map Map;
[JsonIgnore]
public Map Map { get; }
/// <summary>
/// The map variant of the game.
/// </summary>
public MapType MapType => this.Map.Type;
/// <summary>
/// The game map.
/// </summary>
[JsonIgnore]
public ReadOnlyCollection<Province> Provinces => this.Map.Provinces;
/// <summary>
/// The game powers.
/// </summary>
[JsonIgnore]
public ReadOnlyCollection<Power> Powers => this.Map.Powers;
/// <summary>
@ -32,11 +39,13 @@ public class World
/// <summary>
/// Lookup for seasons by designation.
/// </summary>
[JsonIgnore]
public ReadOnlyDictionary<string, Season> SeasonLookup { get; }
/// <summary>
/// The first season of the game.
/// </summary>
[JsonIgnore]
public Season RootSeason => GetSeason("a0");
/// <summary>
@ -52,7 +61,12 @@ public class World
/// <summary>
/// Orders given to units in each season.
/// </summary>
public ReadOnlyDictionary<Season, OrderHistory> OrderHistory { get; }
public ReadOnlyDictionary<string, OrderHistory> OrderHistory { get; }
/// <summary>
/// The shared timeline number generator.
/// </summary>
public TimelineFactory Timelines { get; }
/// <summary>
/// Immutable game options.
@ -67,7 +81,8 @@ public class World
ReadOnlyCollection<Season> seasons,
ReadOnlyCollection<Unit> units,
ReadOnlyCollection<RetreatingUnit> retreatingUnits,
ReadOnlyDictionary<Season, OrderHistory> orderHistory,
ReadOnlyDictionary<string, OrderHistory> orderHistory,
TimelineFactory timelines,
Options options)
{
this.Map = map;
@ -75,6 +90,7 @@ public class World
this.Units = units;
this.RetreatingUnits = retreatingUnits;
this.OrderHistory = orderHistory;
this.Timelines = timelines;
this.Options = options;
this.SeasonLookup = new(Seasons.ToDictionary(season => $"{season.Timeline}{season.Turn}"));
@ -88,7 +104,7 @@ public class World
ReadOnlyCollection<Season>? seasons = null,
ReadOnlyCollection<Unit>? units = null,
ReadOnlyCollection<RetreatingUnit>? retreatingUnits = null,
ReadOnlyDictionary<Season, OrderHistory>? orderHistory = null,
ReadOnlyDictionary<string, OrderHistory>? orderHistory = null,
Options? options = null)
: this(
previous.Map,
@ -96,6 +112,7 @@ public class World
units ?? previous.Units,
retreatingUnits ?? previous.RetreatingUnits,
orderHistory ?? previous.OrderHistory,
previous.Timelines,
options ?? previous.Options)
{
}
@ -105,12 +122,14 @@ public class World
/// </summary>
public static World WithMap(Map map)
{
TimelineFactory timelines = new();
return new World(
map,
new([Season.MakeRoot()]),
new([new(past: null, Season.FIRST_TURN, timelines.NextTimeline())]),
new([]),
new([]),
new(new Dictionary<Season, OrderHistory>()),
new(new Dictionary<string, OrderHistory>()),
timelines,
new Options());
}
@ -124,7 +143,7 @@ public class World
IEnumerable<Season>? seasons = null,
IEnumerable<Unit>? units = null,
IEnumerable<RetreatingUnit>? retreats = null,
IEnumerable<KeyValuePair<Season, OrderHistory>>? orders = null)
IEnumerable<KeyValuePair<string, OrderHistory>>? orders = null)
=> new(
previous: this,
seasons: seasons == null
@ -200,28 +219,17 @@ public class World
}
/// <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>
public World ContinueSeason(string season)
=> Update(seasons: Seasons.Append(SeasonLookup[season].MakeNext()));
/// <summary>
/// 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()));
public Season ContinueOrFork(Season season)
=> GetFutures(season).Any()
? new(season.Designation, season.Turn + 1, Timelines.NextTimeline())
: new(season.Designation, season.Turn + 1, season.Timeline);
/// <summary>
/// A standard Diplomacy game setup.
/// </summary>
public static World Standard => World
.WithStandardMap()
.AddStandardUnits();
public static World Standard => WithStandardMap().AddStandardUnits();
/// <summary>
/// Get a season by coordinate. Throws if the season is not found.
@ -288,11 +296,10 @@ public class World
/// <summary>
/// Returns a unit in a province. Throws if there are duplicate units.
/// </summary>
public Unit GetUnitAt(string provinceName, (string timeline, int turn)? seasonCoord = null)
public Unit GetUnitAt(string provinceName, Season? season = null)
{
Province province = Map.GetProvince(provinceName);
seasonCoord ??= (this.RootSeason.Timeline, this.RootSeason.Turn);
Season season = GetSeason(seasonCoord.Value.timeline, seasonCoord.Value.turn);
season ??= RootSeason;
Unit? foundUnit = this.Units.SingleOrDefault(
u => u!.Province == province && u.Season == season,
null)

View File

@ -9,9 +9,15 @@ internal class Program
static void Main(string[] args)
{
var parser = Parser.Default;
var parseResult = parser.ParseArguments<AdjudicateOptions>(args);
var parseResult = parser.ParseArguments(
args,
typeof(AdjudicateOptions),
typeof(ImageOptions),
typeof(ReplOptions));
parseResult
.WithParsed(AdjudicateOptions.Execute);
.WithParsed<AdjudicateOptions>(AdjudicateOptions.Execute)
.WithParsed<ImageOptions>(ImageOptions.Execute)
.WithParsed<ReplOptions>(ReplOptions.Execute);
}
}
}

View File

@ -43,14 +43,14 @@ public class TimeTravelTest
// Confirm that there is a unit in Tyr b1 originating from Mun a1
Season fork = world.GetSeason("b1");
Unit originalUnit = world.GetUnitAt("Mun", s0.Coord);
Unit aMun0 = world.GetUnitAt("Mun", s1.Coord);
Unit aTyr = world.GetUnitAt("Tyr", fork.Coord);
Unit originalUnit = world.GetUnitAt("Mun", s0);
Unit aMun0 = world.GetUnitAt("Mun", s1);
Unit aTyr = world.GetUnitAt("Tyr", fork);
Assert.That(aTyr.Past, Is.EqualTo(mun1.Order.Unit));
Assert.That(aTyr.Past?.Past, Is.EqualTo(mun0.Order.Unit));
// Confirm that there is a unit in Mun b1 originating from Mun a0
Unit aMun1 = world.GetUnitAt("Mun", fork.Coord);
Unit aMun1 = world.GetUnitAt("Mun", fork);
Assert.That(aMun1.Past, Is.EqualTo(originalUnit));
}
@ -92,7 +92,7 @@ public class TimeTravelTest
// Confirm that an alternate future is created.
World world = setup.UpdateWorld();
Season fork = world.GetSeason("b1");
Unit tyr1 = world.GetUnitAt("Tyr", fork.Coord);
Unit tyr1 = world.GetUnitAt("Tyr", fork);
Assert.That(
tyr1.Past,
Is.EqualTo(mun0.Order.Unit),

View File

@ -206,7 +206,7 @@ public class MovementAdjudicatorTest
Assert.That(s2.Turn, Is.EqualTo(s1.Turn + 1));
// Confirm the unit was created in the future
Unit u2 = updated.GetUnitAt("Mun", s2.Coord);
Unit u2 = updated.GetUnitAt("Mun", s2);
Assert.That(updated.Units.Count, Is.EqualTo(2));
Assert.That(u2, Is.Not.EqualTo(mun1.Order.Unit));
Assert.That(u2.Past, Is.EqualTo(mun1.Order.Unit));
@ -228,7 +228,7 @@ public class MovementAdjudicatorTest
// Update the world again
updated = setup.UpdateWorld();
Season s3 = updated.GetSeason(s2.Timeline, s2.Turn + 1);
Unit u3 = updated.GetUnitAt("Mun", s3.Coord);
Unit u3 = updated.GetUnitAt("Mun", s3);
Assert.That(u3.Past, Is.EqualTo(mun2.Order.Unit));
}
@ -256,7 +256,7 @@ public class MovementAdjudicatorTest
Assert.That(s2.Turn, Is.EqualTo(s1.Turn + 1));
// Confirm the unit was created in the future
Unit u2 = updated.GetUnitAt("Tyr", s2.Coord);
Unit u2 = updated.GetUnitAt("Tyr", s2);
Assert.That(updated.Units.Count, Is.EqualTo(2));
Assert.That(u2, Is.Not.EqualTo(mun1.Order.Unit));
Assert.That(u2.Past, Is.EqualTo(mun1.Order.Unit));
@ -278,7 +278,7 @@ public class MovementAdjudicatorTest
// Update the world again
updated = setup.UpdateWorld();
Season s3 = updated.GetSeason(s2.Timeline, s2.Turn + 1);
Unit u3 = updated.GetUnitAt("Mun", s3.Coord);
Unit u3 = updated.GetUnitAt("Mun", s3);
Assert.That(u3.Past, Is.EqualTo(u2));
}
}

View File

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

View File

@ -0,0 +1,98 @@
using System.Text.Json;
using MultiversalDiplomacy.Adjudicate;
using MultiversalDiplomacy.Model;
using NUnit.Framework;
namespace MultiversalDiplomacyTests;
public class SerializationTest
{
[Test]
public void SerializeRoundTrip_NewGame()
{
JsonSerializerOptions options = new() {
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
World world = World.WithStandardMap();
string serialized = JsonSerializer.Serialize(world, options);
Console.WriteLine(serialized);
World? deserialized = JsonSerializer.Deserialize<World>(serialized, options);
Assert.That(deserialized, Is.Not.Null, "Failed to deserialize");
}
[Test]
public void SerializeRoundTrip_MDATC_3_A_2()
{
// Set up MDATC 3.A.2
TestCaseBuilder setup = new(World.WithStandardMap(), MovementPhaseAdjudicator.Instance);
setup[("a", 0)]
.GetReference(out Season s0)
["Germany"]
.Army("Mun").MovesTo("Tyr").GetReference(out var mun0)
["Austria"]
.Army("Tyr").Holds().GetReference(out var tyr0);
setup.ValidateOrders();
Assert.That(mun0, Is.Valid);
Assert.That(tyr0, Is.Valid);
setup.AdjudicateOrders();
Assert.That(mun0, Is.Repelled);
Assert.That(tyr0, Is.NotDislodged);
setup.UpdateWorld();
// Serialize the world
JsonSerializerOptions options = new() {
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
JsonElement serialized = JsonSerializer.SerializeToDocument(setup.World, options).RootElement;
Assert.That(
serialized.EnumerateObject().Select(prop => prop.Name),
Is.EquivalentTo(new List<string> {
"mapType",
"seasons",
"units",
"retreatingUnits",
"orderHistory",
"options",
}));
// Deserialize the world
World reserialized = JsonSerializer.Deserialize<World>(serialized)
?? throw new AssertionException("Failed to reserialize world");
// Resume the test case
setup = new(reserialized, MovementPhaseAdjudicator.Instance);
setup[("a", 1)]
["Germany"]
.Army("Mun").Supports.Army("Mun", season: reserialized.GetSeason("a0")).MoveTo("Tyr").GetReference(out var mun1)
["Austria"]
.Army("Tyr").Holds();
setup.ValidateOrders();
Assert.That(mun1, Is.Valid);
setup.AdjudicateOrders();
Assert.That(mun1, Is.NotCut);
Assert.That(mun0, Is.Victorious);
Assert.That(tyr0, Is.Dislodged);
// Confirm that an alternate future is created.
World world = setup.UpdateWorld();
Season fork = world.GetSeason("b1");
Unit tyr1 = world.GetUnitAt("Tyr", fork);
Assert.That(
tyr1.Past,
Is.EqualTo(mun0.Order.Unit),
"Expected A Mun a0 to advance to Tyr b1");
Assert.That(
world.RetreatingUnits.Count,
Is.EqualTo(1),
"Expected A Tyr a0 to be in retreat");
Assert.That(world.RetreatingUnits.First().Unit, Is.EqualTo(tyr0.Order.Unit));
}
}

View File

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