Enable basic World serialization

This commit is contained in:
Tim Van Baak 2024-08-13 22:14:55 -07:00
parent c9bd8c8194
commit b2ff8896b2
5 changed files with 109 additions and 54 deletions

View File

@ -1,5 +1,3 @@
using System.Collections.ObjectModel;
namespace MultiversalDiplomacy.Model; namespace MultiversalDiplomacy.Model;
/// <summary> /// <summary>
@ -15,17 +13,22 @@ public class Map
/// <summary> /// <summary>
/// The game map. /// The game map.
/// </summary> /// </summary>
public ReadOnlyCollection<Province> Provinces { get; } public IReadOnlyCollection<Province> Provinces => _Provinces.AsReadOnly();
private List<Province> _Provinces { get; }
/// <summary> /// <summary>
/// The game powers. /// The game powers.
/// </summary> /// </summary>
public ReadOnlyCollection<Power> Powers { get; } public IReadOnlyCollection<Power> Powers => _Powers.AsReadOnly();
private List<Power> _Powers { get; }
private Map(MapType type, IEnumerable<Province> provinces, IEnumerable<Power> powers) private Map(MapType type, IEnumerable<Province> provinces, IEnumerable<Power> powers)
{ {
Provinces = new(provinces.ToList()); Type = type;
Powers = new(powers.ToList()); _Provinces = provinces.ToList();
_Powers = powers.ToList();
} }
/// <summary> /// <summary>
@ -90,8 +93,6 @@ public class Map
_ => throw new NotImplementedException($"Unknown variant {type}"), _ => throw new NotImplementedException($"Unknown variant {type}"),
}; };
#region Variants
public static Map Test => _Test.Value; public static Map Test => _Test.Value;
private static readonly Lazy<Map> _Test = new(() => { private static readonly Lazy<Map> _Test = new(() => {
@ -117,6 +118,7 @@ public class Map
// Define the provinces of the standard world map. // Define the provinces of the standard world map.
List<Province> provinces = List<Province> provinces =
[ [
#region Provinces
Province.Empty("North Africa", "NAF") Province.Empty("North Africa", "NAF")
.AddLandLocation() .AddLandLocation()
.AddCoastLocation(), .AddCoastLocation(),
@ -313,6 +315,7 @@ public class Map
.AddOceanLocation(), .AddOceanLocation(),
Province.Empty("Western Mediterranean Sea", "WMS", "WES") Province.Empty("Western Mediterranean Sea", "WMS", "WES")
.AddOceanLocation(), .AddOceanLocation(),
#endregion
]; ];
// Declare some helpers for border definitions // Declare some helpers for border definitions
@ -334,6 +337,7 @@ public class Map
void AddBorders(string provinceName, Func<string, Location> LocationType, params string[] borders) void AddBorders(string provinceName, Func<string, Location> LocationType, params string[] borders)
=> AddBordersTo(LocationType(provinceName), LocationType, borders); => AddBordersTo(LocationType(provinceName), LocationType, borders);
#region Borders
AddBorders("NAF", Land, "TUN"); AddBorders("NAF", Land, "TUN");
AddBorders("NAF", Water, "MAO", "WES", "TUN"); AddBorders("NAF", Water, "MAO", "WES", "TUN");
@ -547,6 +551,7 @@ public class Map
AddBorders("WES", Water, "LYO", "TYS", "TUN", "NAF", "MAO"); AddBorders("WES", Water, "LYO", "TYS", "TUN", "NAF", "MAO");
Water("WES").AddBorder(Coast("SPA", "sc")); Water("WES").AddBorder(Coast("SPA", "sc"));
#endregion
List<Power> powers = List<Power> powers =
[ [
@ -561,6 +566,4 @@ public class Map
return new(MapType.Classical, provinces, powers); return new(MapType.Classical, provinces, powers);
}); });
#endregion Variants
} }

View File

@ -1,9 +1,12 @@
using System.Text.Json.Serialization;
namespace MultiversalDiplomacy.Model; 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>
public class TimelineFactory [JsonConverter(typeof(TimelineFactoryJsonConverter))]
public class TimelineFactory(int nextTimeline)
{ {
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',
@ -44,7 +47,9 @@ public class TimelineFactory
return new string(result.ToArray()); return new string(result.ToArray());
} }
private int nextTimeline = 0; public TimelineFactory() : this(0) { }
public int nextTimeline = nextTimeline;
public string NextTimeline() => IntToString(nextTimeline++); public string NextTimeline() => IntToString(nextTimeline++);
} }

View File

@ -0,0 +1,13 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace MultiversalDiplomacy.Model;
internal class TimelineFactoryJsonConverter : JsonConverter<TimelineFactory>
{
public override TimelineFactory? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> new(reader.GetInt32());
public override void Write(Utf8JsonWriter writer, TimelineFactory value, JsonSerializerOptions options)
=> writer.WriteNumberValue(value.nextTimeline);
}

View File

@ -17,30 +17,33 @@ public class World
/// <summary> /// <summary>
/// The map variant of the game. /// The map variant of the game.
/// </summary> /// </summary>
/// <remarks>
/// While this is serialized to JSON, deserialization uses it to populate <see cref="Map"/>
/// </remarks>
public MapType MapType => this.Map.Type; public MapType MapType => this.Map.Type;
/// <summary> /// <summary>
/// The game map. /// The game map.
/// </summary> /// </summary>
[JsonIgnore] [JsonIgnore]
public ReadOnlyCollection<Province> Provinces => this.Map.Provinces; public IReadOnlyCollection<Province> Provinces => this.Map.Provinces;
/// <summary> /// <summary>
/// The game powers. /// The game powers.
/// </summary> /// </summary>
[JsonIgnore] [JsonIgnore]
public ReadOnlyCollection<Power> Powers => this.Map.Powers; public IReadOnlyCollection<Power> Powers => this.Map.Powers;
/// <summary> /// <summary>
/// The state of the multiverse. /// The state of the multiverse.
/// </summary> /// </summary>
public ReadOnlyCollection<Season> Seasons { get; } public List<Season> Seasons { get; }
/// <summary> /// <summary>
/// Lookup for seasons by designation. /// Lookup for seasons by designation.
/// </summary> /// </summary>
[JsonIgnore] [JsonIgnore]
public ReadOnlyDictionary<string, Season> SeasonLookup { get; } public Dictionary<string, Season> SeasonLookup { get; }
/// <summary> /// <summary>
/// The first season of the game. /// The first season of the game.
@ -51,17 +54,17 @@ public class World
/// <summary> /// <summary>
/// All units in the multiverse. /// All units in the multiverse.
/// </summary> /// </summary>
public ReadOnlyCollection<Unit> Units { get; } public List<Unit> Units { get; }
/// <summary> /// <summary>
/// All retreating units in the multiverse. /// All retreating units in the multiverse.
/// </summary> /// </summary>
public ReadOnlyCollection<RetreatingUnit> RetreatingUnits { get; } public List<RetreatingUnit> RetreatingUnits { get; }
/// <summary> /// <summary>
/// Orders given to units in each season. /// Orders given to units in each season.
/// </summary> /// </summary>
public ReadOnlyDictionary<string, OrderHistory> OrderHistory { get; } public Dictionary<string, OrderHistory> OrderHistory { get; }
/// <summary> /// <summary>
/// The shared timeline number generator. /// The shared timeline number generator.
@ -73,15 +76,36 @@ public class World
/// </summary> /// </summary>
public Options Options { get; } public Options Options { get; }
[JsonConstructor]
public World(
MapType mapType,
List<Season> seasons,
List<Unit> units,
List<RetreatingUnit> retreatingUnits,
Dictionary<string, OrderHistory> orderHistory,
TimelineFactory timelines,
Options options)
{
this.Map = Map.FromType(mapType);
this.Seasons = seasons;
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}"));
}
/// <summary> /// <summary>
/// Create a new World, providing all state data. /// Create a new World, providing all state data.
/// </summary> /// </summary>
private World( private World(
Map map, Map map,
ReadOnlyCollection<Season> seasons, List<Season> seasons,
ReadOnlyCollection<Unit> units, List<Unit> units,
ReadOnlyCollection<RetreatingUnit> retreatingUnits, List<RetreatingUnit> retreatingUnits,
ReadOnlyDictionary<string, OrderHistory> orderHistory, Dictionary<string, OrderHistory> orderHistory,
TimelineFactory timelines, TimelineFactory timelines,
Options options) Options options)
{ {
@ -101,10 +125,10 @@ public class World
/// </summary> /// </summary>
private World( private World(
World previous, World previous,
ReadOnlyCollection<Season>? seasons = null, List<Season>? seasons = null,
ReadOnlyCollection<Unit>? units = null, List<Unit>? units = null,
ReadOnlyCollection<RetreatingUnit>? retreatingUnits = null, List<RetreatingUnit>? retreatingUnits = null,
ReadOnlyDictionary<string, OrderHistory>? orderHistory = null, Dictionary<string, OrderHistory>? orderHistory = null,
Options? options = null) Options? options = null)
: this( : this(
previous.Map, previous.Map,
@ -219,7 +243,7 @@ public class World
} }
/// <summary> /// <summary>
/// Create a continuation of this season if it has no futures, otherwise ceate a fork. /// Create a continuation of this season if it has no futures, otherwise create a fork.
/// </summary> /// </summary>
public Season ContinueOrFork(Season season) public Season ContinueOrFork(Season season)
=> GetFutures(season).Any() => GetFutures(season).Any()
@ -237,6 +261,9 @@ public class World
public Season GetSeason(string timeline, int turn) public Season GetSeason(string timeline, int turn)
=> GetSeason($"{timeline}{turn}"); => GetSeason($"{timeline}{turn}");
/// <summary>
/// Get a season by designation.
/// </summary>
public Season GetSeason(string designation) public Season GetSeason(string designation)
=> SeasonLookup[designation]; => SeasonLookup[designation];

View File

@ -9,19 +9,40 @@ namespace MultiversalDiplomacyTests;
public class SerializationTest public class SerializationTest
{ {
[Test] private JsonSerializerOptions Options = new() {
public void SerializeRoundTrip_NewGame()
{
JsonSerializerOptions options = new() {
WriteIndented = true, WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
}; };
[Test]
public void SerializeRoundTrip_NewGame()
{
World world = World.WithStandardMap(); World world = World.WithStandardMap();
string serialized = JsonSerializer.Serialize(world, options); string serialized = JsonSerializer.Serialize(world, Options);
Console.WriteLine(serialized); World? deserialized = JsonSerializer.Deserialize<World>(serialized, Options);
World? deserialized = JsonSerializer.Deserialize<World>(serialized, options);
Assert.That(deserialized, Is.Not.Null, "Failed to deserialize"); Assert.That(deserialized, Is.Not.Null, "Failed to deserialize");
Assert.That(deserialized!.Map, Is.Not.Null, "Failed to deserialize map");
Assert.That(deserialized!.Seasons, Is.Not.Null, "Failed to deserialize seasons");
Assert.That(deserialized!.Units, Is.Not.Null, "Failed to deserialize units");
Assert.That(deserialized!.RetreatingUnits, Is.Not.Null, "Failed to deserialize retreats");
Assert.That(deserialized!.OrderHistory, Is.Not.Null, "Failed to deserialize history");
Assert.That(deserialized!.Timelines, Is.Not.Null, "Failed to deserialize timelines");
Assert.That(deserialized!.Options, Is.Not.Null, "Failed to deserialize options");
Assert.That(deserialized.Timelines.nextTimeline, Is.EqualTo(world.Timelines.nextTimeline));
JsonElement document = JsonSerializer.SerializeToDocument(world, Options).RootElement;
Assert.That(
document.EnumerateObject().Select(prop => prop.Name),
Is.EquivalentTo(new List<string> {
"mapType",
"seasons",
"units",
"retreatingUnits",
"orderHistory",
"options",
"timelines",
}));
} }
[Test] [Test]
@ -45,25 +66,11 @@ public class SerializationTest
setup.UpdateWorld(); setup.UpdateWorld();
// Serialize the world // Serialize the world
JsonSerializerOptions options = new() { string serialized = JsonSerializer.Serialize(setup.World, Options);
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 // Deserialize the world
World reserialized = JsonSerializer.Deserialize<World>(serialized) Console.WriteLine(serialized);
World reserialized = JsonSerializer.Deserialize<World>(serialized, Options)
?? throw new AssertionException("Failed to reserialize world"); ?? throw new AssertionException("Failed to reserialize world");
// Resume the test case // Resume the test case