diff --git a/MultiversalDiplomacy/Model/Map.cs b/MultiversalDiplomacy/Model/Map.cs index ba1ae80..36a4914 100644 --- a/MultiversalDiplomacy/Model/Map.cs +++ b/MultiversalDiplomacy/Model/Map.cs @@ -1,5 +1,3 @@ -using System.Collections.ObjectModel; - namespace MultiversalDiplomacy.Model; /// @@ -15,17 +13,22 @@ public class Map /// /// The game map. /// - public ReadOnlyCollection Provinces { get; } + public IReadOnlyCollection Provinces => _Provinces.AsReadOnly(); + + private List _Provinces { get; } /// /// The game powers. /// - public ReadOnlyCollection Powers { get; } + public IReadOnlyCollection Powers => _Powers.AsReadOnly(); + + private List _Powers { get; } private Map(MapType type, IEnumerable provinces, IEnumerable powers) { - Provinces = new(provinces.ToList()); - Powers = new(powers.ToList()); + Type = type; + _Provinces = provinces.ToList(); + _Powers = powers.ToList(); } /// @@ -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 _Test = new(() => { @@ -117,6 +118,7 @@ public class Map // Define the provinces of the standard world map. List 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 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 powers = [ @@ -561,6 +566,4 @@ public class Map return new(MapType.Classical, provinces, powers); }); - -#endregion Variants -} \ No newline at end of file +} diff --git a/MultiversalDiplomacy/Model/TimelineFactory.cs b/MultiversalDiplomacy/Model/TimelineFactory.cs index 1c33f0f..63c48b3 100644 --- a/MultiversalDiplomacy/Model/TimelineFactory.cs +++ b/MultiversalDiplomacy/Model/TimelineFactory.cs @@ -1,9 +1,12 @@ +using System.Text.Json.Serialization; + namespace MultiversalDiplomacy.Model; /// /// A shared counter for handing out new timeline designations. /// -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++); } diff --git a/MultiversalDiplomacy/Model/TimelineFactoryJsonConverter.cs b/MultiversalDiplomacy/Model/TimelineFactoryJsonConverter.cs new file mode 100644 index 0000000..684097b --- /dev/null +++ b/MultiversalDiplomacy/Model/TimelineFactoryJsonConverter.cs @@ -0,0 +1,13 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MultiversalDiplomacy.Model; + +internal class TimelineFactoryJsonConverter : JsonConverter +{ + 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); +} diff --git a/MultiversalDiplomacy/Model/World.cs b/MultiversalDiplomacy/Model/World.cs index 5138529..abc5169 100644 --- a/MultiversalDiplomacy/Model/World.cs +++ b/MultiversalDiplomacy/Model/World.cs @@ -17,30 +17,33 @@ public class World /// /// The map variant of the game. /// + /// + /// While this is serialized to JSON, deserialization uses it to populate + /// public MapType MapType => this.Map.Type; /// /// The game map. /// [JsonIgnore] - public ReadOnlyCollection Provinces => this.Map.Provinces; + public IReadOnlyCollection Provinces => this.Map.Provinces; /// /// The game powers. /// [JsonIgnore] - public ReadOnlyCollection Powers => this.Map.Powers; + public IReadOnlyCollection Powers => this.Map.Powers; /// /// The state of the multiverse. /// - public ReadOnlyCollection Seasons { get; } + public List Seasons { get; } /// /// Lookup for seasons by designation. /// [JsonIgnore] - public ReadOnlyDictionary SeasonLookup { get; } + public Dictionary SeasonLookup { get; } /// /// The first season of the game. @@ -51,17 +54,17 @@ public class World /// /// All units in the multiverse. /// - public ReadOnlyCollection Units { get; } + public List Units { get; } /// /// All retreating units in the multiverse. /// - public ReadOnlyCollection RetreatingUnits { get; } + public List RetreatingUnits { get; } /// /// Orders given to units in each season. /// - public ReadOnlyDictionary OrderHistory { get; } + public Dictionary OrderHistory { get; } /// /// The shared timeline number generator. @@ -73,15 +76,36 @@ public class World /// public Options Options { get; } + [JsonConstructor] + public World( + MapType mapType, + List seasons, + List units, + List retreatingUnits, + Dictionary 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}")); + } + /// /// Create a new World, providing all state data. /// private World( Map map, - ReadOnlyCollection seasons, - ReadOnlyCollection units, - ReadOnlyCollection retreatingUnits, - ReadOnlyDictionary orderHistory, + List seasons, + List units, + List retreatingUnits, + Dictionary orderHistory, TimelineFactory timelines, Options options) { @@ -101,10 +125,10 @@ public class World /// private World( World previous, - ReadOnlyCollection? seasons = null, - ReadOnlyCollection? units = null, - ReadOnlyCollection? retreatingUnits = null, - ReadOnlyDictionary? orderHistory = null, + List? seasons = null, + List? units = null, + List? retreatingUnits = null, + Dictionary? orderHistory = null, Options? options = null) : this( previous.Map, @@ -219,7 +243,7 @@ public class World } /// - /// 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. /// 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}"); + /// + /// Get a season by designation. + /// public Season GetSeason(string designation) => SeasonLookup[designation]; diff --git a/MultiversalDiplomacyTests/SerializationTest.cs b/MultiversalDiplomacyTests/SerializationTest.cs index c1f41b1..44be89c 100644 --- a/MultiversalDiplomacyTests/SerializationTest.cs +++ b/MultiversalDiplomacyTests/SerializationTest.cs @@ -9,19 +9,40 @@ namespace MultiversalDiplomacyTests; public class SerializationTest { + private JsonSerializerOptions Options = new() { + WriteIndented = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; + [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(serialized, options); + string serialized = JsonSerializer.Serialize(world, Options); + World? deserialized = JsonSerializer.Deserialize(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 { + "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 { - "mapType", - "seasons", - "units", - "retreatingUnits", - "orderHistory", - "options", - })); + string serialized = JsonSerializer.Serialize(setup.World, Options); // Deserialize the world - World reserialized = JsonSerializer.Deserialize(serialized) + Console.WriteLine(serialized); + World reserialized = JsonSerializer.Deserialize(serialized, Options) ?? throw new AssertionException("Failed to reserialize world"); // Resume the test case