Enable basic World serialization
This commit is contained in:
parent
c9bd8c8194
commit
b2ff8896b2
|
@ -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
|
|
||||||
}
|
}
|
|
@ -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++);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -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];
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue