Enable basic World serialization

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

View File

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

View File

@ -1,9 +1,12 @@
using System.Text.Json.Serialization;
namespace MultiversalDiplomacy.Model;
/// <summary>
/// A shared counter for handing out new timeline designations.
/// </summary>
public class TimelineFactory
[JsonConverter(typeof(TimelineFactoryJsonConverter))]
public class TimelineFactory(int nextTimeline)
{
private static readonly char[] Letters = [
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
@ -44,7 +47,9 @@ public class TimelineFactory
return new string(result.ToArray());
}
private int nextTimeline = 0;
public TimelineFactory() : this(0) { }
public int nextTimeline = 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>
/// The map variant of the game.
/// </summary>
/// <remarks>
/// While this is serialized to JSON, deserialization uses it to populate <see cref="Map"/>
/// </remarks>
public MapType MapType => this.Map.Type;
/// <summary>
/// The game map.
/// </summary>
[JsonIgnore]
public ReadOnlyCollection<Province> Provinces => this.Map.Provinces;
public IReadOnlyCollection<Province> Provinces => this.Map.Provinces;
/// <summary>
/// The game powers.
/// </summary>
[JsonIgnore]
public ReadOnlyCollection<Power> Powers => this.Map.Powers;
public IReadOnlyCollection<Power> Powers => this.Map.Powers;
/// <summary>
/// The state of the multiverse.
/// </summary>
public ReadOnlyCollection<Season> Seasons { get; }
public List<Season> Seasons { get; }
/// <summary>
/// Lookup for seasons by designation.
/// </summary>
[JsonIgnore]
public ReadOnlyDictionary<string, Season> SeasonLookup { get; }
public Dictionary<string, Season> SeasonLookup { get; }
/// <summary>
/// The first season of the game.
@ -51,17 +54,17 @@ public class World
/// <summary>
/// All units in the multiverse.
/// </summary>
public ReadOnlyCollection<Unit> Units { get; }
public List<Unit> Units { get; }
/// <summary>
/// All retreating units in the multiverse.
/// </summary>
public ReadOnlyCollection<RetreatingUnit> RetreatingUnits { get; }
public List<RetreatingUnit> RetreatingUnits { get; }
/// <summary>
/// Orders given to units in each season.
/// </summary>
public ReadOnlyDictionary<string, OrderHistory> OrderHistory { get; }
public Dictionary<string, OrderHistory> OrderHistory { get; }
/// <summary>
/// The shared timeline number generator.
@ -73,15 +76,36 @@ public class World
/// </summary>
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>
/// Create a new World, providing all state data.
/// </summary>
private World(
Map map,
ReadOnlyCollection<Season> seasons,
ReadOnlyCollection<Unit> units,
ReadOnlyCollection<RetreatingUnit> retreatingUnits,
ReadOnlyDictionary<string, OrderHistory> orderHistory,
List<Season> seasons,
List<Unit> units,
List<RetreatingUnit> retreatingUnits,
Dictionary<string, OrderHistory> orderHistory,
TimelineFactory timelines,
Options options)
{
@ -101,10 +125,10 @@ public class World
/// </summary>
private World(
World previous,
ReadOnlyCollection<Season>? seasons = null,
ReadOnlyCollection<Unit>? units = null,
ReadOnlyCollection<RetreatingUnit>? retreatingUnits = null,
ReadOnlyDictionary<string, OrderHistory>? orderHistory = null,
List<Season>? seasons = null,
List<Unit>? units = null,
List<RetreatingUnit>? retreatingUnits = null,
Dictionary<string, OrderHistory>? orderHistory = null,
Options? options = null)
: this(
previous.Map,
@ -219,7 +243,7 @@ public class World
}
/// <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>
public Season ContinueOrFork(Season season)
=> GetFutures(season).Any()
@ -237,6 +261,9 @@ public class World
public Season GetSeason(string timeline, int turn)
=> GetSeason($"{timeline}{turn}");
/// <summary>
/// Get a season by designation.
/// </summary>
public Season GetSeason(string designation)
=> SeasonLookup[designation];

View File

@ -9,19 +9,40 @@ namespace MultiversalDiplomacyTests;
public class SerializationTest
{
[Test]
public void SerializeRoundTrip_NewGame()
{
JsonSerializerOptions options = new() {
private JsonSerializerOptions Options = new() {
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
[Test]
public void SerializeRoundTrip_NewGame()
{
World world = World.WithStandardMap();
string serialized = JsonSerializer.Serialize(world, options);
Console.WriteLine(serialized);
World? deserialized = JsonSerializer.Deserialize<World>(serialized, options);
string serialized = JsonSerializer.Serialize(world, Options);
World? deserialized = JsonSerializer.Deserialize<World>(serialized, Options);
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]
@ -45,25 +66,11 @@ public class SerializationTest
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",
}));
string serialized = JsonSerializer.Serialize(setup.World, Options);
// 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");
// Resume the test case