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: case MoveOrder move:
AdvanceTimeline.Ensure( AdvanceTimeline.Ensure(
move.Season, 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); AdvanceTimeline[move.Season].Orders.Add(move);
break; break;
case SupportHoldOrder supportHold: case SupportHoldOrder supportHold:
AdvanceTimeline.Ensure( AdvanceTimeline.Ensure(
supportHold.Target.Season, 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); AdvanceTimeline[supportHold.Target.Season].Orders.Add(supportHold);
break; break;
case SupportMoveOrder supportMove: case SupportMoveOrder supportMove:
AdvanceTimeline.Ensure( AdvanceTimeline.Ensure(
supportMove.Target.Season, 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[supportMove.Target.Season].Orders.Add(supportMove);
AdvanceTimeline.Ensure( AdvanceTimeline.Ensure(
supportMove.Season, 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); AdvanceTimeline[supportMove.Season].Orders.Add(supportMove);
break; 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 // 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. // record of when a future season has been created.
Dictionary<Season, Season> createdFutures = new(); Dictionary<Season, Season> createdFutures = [];
List<Unit> createdUnits = new(); List<Unit> createdUnits = [];
List<RetreatingUnit> retreats = new(); List<RetreatingUnit> retreats = [];
// Populate createdFutures with the timeline fork decisions // Populate createdFutures with the timeline fork decisions
logger.Log(1, "Processing AdvanceTimeline decisions"); logger.Log(1, "Processing AdvanceTimeline decisions");
@ -324,9 +324,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
if (advanceTimeline.Outcome == true) if (advanceTimeline.Outcome == true)
{ {
// A timeline that doesn't have a future yet simply continues. Otherwise, it forks. // A timeline that doesn't have a future yet simply continues. Otherwise, it forks.
createdFutures[advanceTimeline.Season] = !world.GetFutures(advanceTimeline.Season).Any() createdFutures[advanceTimeline.Season] = world.ContinueOrFork(advanceTimeline.Season);
? advanceTimeline.Season.MakeNext()
: advanceTimeline.Season.MakeFork();
} }
} }
@ -386,11 +384,11 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
} }
// Record the adjudication results to the season's order history // 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)) foreach (UnitOrder unitOrder in decisions.OfType<IsDislodged>().Select(d => d.Order))
{ {
newHistory.Ensure(unitOrder.Unit.Season, () => new()); newHistory.Ensure(unitOrder.Unit.Season.Designation, () => new());
OrderHistory history = newHistory[unitOrder.Unit.Season]; OrderHistory history = newHistory[unitOrder.Unit.Season.Designation];
// TODO does this add every order to every season?? // TODO does this add every order to every season??
history.Orders.Add(unitOrder); history.Orders.Add(unitOrder);
history.IsDislodgedOutcomes[unitOrder.Unit] = dislodges[unitOrder.Unit].Outcome == true; history.IsDislodgedOutcomes[unitOrder.Unit] = dislodges[unitOrder.Unit].Outcome == true;
@ -401,7 +399,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
} }
// Log the new order history // 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"; string verb = world.OrderHistory.ContainsKey(season) ? "Updating" : "Adding";
logger.Log(1, "{0} history for {1}", verb, season); 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)) .Where(kvp => !newHistory.ContainsKey(kvp.Key))
.Concat(newHistory); .Concat(newHistory);
@ -496,7 +494,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
IEnumerable<MoveOrder> newIncomingMoves = decision.Orders IEnumerable<MoveOrder> newIncomingMoves = decision.Orders
.OfType<MoveOrder>() .OfType<MoveOrder>()
.Where(order => order.Season == decision.Season .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) foreach (MoveOrder moveOrder in newIncomingMoves)
{ {
DoesMove doesMove = decisions.DoesMove[moveOrder]; DoesMove doesMove = decisions.DoesMove[moveOrder];
@ -513,7 +511,7 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
// 1. The outcome of a dislodge decision is changed, // 1. The outcome of a dislodge decision is changed,
// 2. The outcome of an intra-timeline move decision is changed, or // 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. // 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; bool anyUnresolved = false;
foreach (UnitOrder order in decision.Orders) 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; namespace MultiversalDiplomacy.Model;
/// <summary> /// <summary>
@ -9,8 +11,12 @@ public class Location
{ {
/// <summary> /// <summary>
/// The province to which this location belongs. /// The province to which this location belongs.
/// </summary>
[JsonIgnore]
public Province Province { get; } public Province Province { get; }
public string ProvinceName => Province.Name;
/// <summary> /// <summary>
/// The location's full human-readable name. /// The location's full human-readable name.
/// </summary> /// </summary>
@ -29,6 +35,7 @@ public class Location
/// <summary> /// <summary>
/// The locations that border this location. /// The locations that border this location.
/// </summary> /// </summary>
[JsonIgnore]
public IEnumerable<Location> Adjacents => this.AdjacentList; public IEnumerable<Location> Adjacents => this.AdjacentList;
private List<Location> AdjacentList { get; set; } private List<Location> AdjacentList { get; set; }

View File

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

View File

@ -20,4 +20,10 @@ public static class ModelExtensions
{ {
return $"{coord.season.Timeline}-{coord.province.Abbreviations[0]}@{coord.season.Turn}"; 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 Dictionary<MoveOrder, bool> DoesMoveOutcomes;
public OrderHistory() public OrderHistory()
: this(new(), new(), new()) : this([], [], [])
{} {}
public OrderHistory( public OrderHistory(

View File

@ -5,10 +5,10 @@ namespace MultiversalDiplomacy.Model;
/// <summary> /// <summary>
/// Represents a state of the map produced by a set of move orders on a previous season. /// Represents a state of the map produced by a set of move orders on a previous season.
/// </summary> /// </summary>
public class Season public class Season(string? past, int turn, string timeline)
{ {
/// <summary> /// <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> /// </summary>
public const int FIRST_TURN = 0; 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. /// If this season is an alternate timeline root, the past is from the origin timeline.
/// The initial season does not have a past. /// The initial season does not have a past.
/// </summary> /// </summary>
public string? Past { get; } public string? Past { get; } = past;
/// <summary> /// <summary>
/// The current turn, beginning at 0. Each season (spring and fall) is one turn. /// 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. /// Phases that only occur after the fall phase occur when Turn % 2 == 1.
/// The current year is (Turn / 2) + 1901. /// The current year is (Turn / 2) + 1901.
/// </summary> /// </summary>
public int Turn { get; } public int Turn { get; } = turn;
/// <summary> /// <summary>
/// The timeline to which this season belongs. /// The timeline to which this season belongs.
/// </summary> /// </summary>
public string Timeline { get; } public string Timeline { get; } = timeline;
/// <summary> /// <summary>
/// The multiversal designation of this season. /// The multiversal designation of this season.
@ -37,50 +37,5 @@ public class Season
[JsonIgnore] [JsonIgnore]
public string Designation => $"{this.Timeline}{this.Turn}"; 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; 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> /// <summary>
/// A shared counter for handing out new timeline designations. /// A shared counter for handing out new timeline designations.
/// </summary> /// </summary>
internal class TimelineFactory public class TimelineFactory
{ {
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',

View File

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

View File

@ -9,9 +9,15 @@ internal class Program
static void Main(string[] args) static void Main(string[] args)
{ {
var parser = Parser.Default; var parser = Parser.Default;
var parseResult = parser.ParseArguments<AdjudicateOptions>(args); var parseResult = parser.ParseArguments(
args,
typeof(AdjudicateOptions),
typeof(ImageOptions),
typeof(ReplOptions));
parseResult 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 // Confirm that there is a unit in Tyr b1 originating from Mun a1
Season fork = world.GetSeason("b1"); Season fork = world.GetSeason("b1");
Unit originalUnit = world.GetUnitAt("Mun", s0.Coord); Unit originalUnit = world.GetUnitAt("Mun", s0);
Unit aMun0 = world.GetUnitAt("Mun", s1.Coord); Unit aMun0 = world.GetUnitAt("Mun", s1);
Unit aTyr = world.GetUnitAt("Tyr", fork.Coord); Unit aTyr = world.GetUnitAt("Tyr", fork);
Assert.That(aTyr.Past, Is.EqualTo(mun1.Order.Unit)); Assert.That(aTyr.Past, Is.EqualTo(mun1.Order.Unit));
Assert.That(aTyr.Past?.Past, Is.EqualTo(mun0.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 // 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)); Assert.That(aMun1.Past, Is.EqualTo(originalUnit));
} }
@ -92,7 +92,7 @@ public class TimeTravelTest
// Confirm that an alternate future is created. // Confirm that an alternate future is created.
World world = setup.UpdateWorld(); World world = setup.UpdateWorld();
Season fork = world.GetSeason("b1"); Season fork = world.GetSeason("b1");
Unit tyr1 = world.GetUnitAt("Tyr", fork.Coord); Unit tyr1 = world.GetUnitAt("Tyr", fork);
Assert.That( Assert.That(
tyr1.Past, tyr1.Past,
Is.EqualTo(mun0.Order.Unit), Is.EqualTo(mun0.Order.Unit),

View File

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

View File

@ -9,30 +9,22 @@ public class SeasonTests
[Test] [Test]
public void TimelineForking() public void TimelineForking()
{ {
World world = World World world = World.WithMap(Map.Test);
.WithMap(Map.Test) Season a0 = world.GetSeason("a0");
.ContinueSeason("a0") world = world
.ContinueSeason("a1") .ContinueOrFork(a0, out Season a1)
.ContinueSeason("a2") .ContinueOrFork(a1, out Season a2)
.ForkSeason("a1") .ContinueOrFork(a2, out Season a3)
.ContinueSeason("b2") .ContinueOrFork(a1, out Season b2)
.ForkSeason("a1") .ContinueOrFork(b2, out Season b3)
.ForkSeason("a2"); .ContinueOrFork(a1, out Season c2)
.ContinueOrFork(a2, out Season d3);
Assert.That( Assert.That(
world.Seasons.Select(season => season.ToString()), world.Seasons.Select(season => season.ToString()),
Is.EquivalentTo(new List<string> { "a0", "a1", "a2", "a3", "b2", "b3", "c2", "d3" }), Is.EquivalentTo(new List<string> { "a0", "a1", "a2", "a3", "b2", "b3", "c2", "d3" }),
"Unexpected seasons"); "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(a0.Timeline, Is.EqualTo("a"), "Unexpected trunk timeline");
Assert.That(a1.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"); 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; Season a0 = world.RootSeason;
Unit u1 = Unit.Build(Mun, a0, pw1, UnitType.Army); Unit u1 = Unit.Build(Mun, a0, pw1, UnitType.Army);
world = world.ContinueSeason(a0); world = world.ContinueOrFork(a0, out Season a1);
Season a1 = world.GetSeason("a1");
Unit u2 = u1.Next(Boh, a1); Unit u2 = u1.Next(Boh, a1);
world = world.ContinueSeason(a1); _ = world.ContinueOrFork(a1, out Season a2);
Season a2 = world.GetSeason("a2");
Unit u3 = u2.Next(Tyr, a2); Unit u3 = u2.Next(Tyr, a2);
Assert.That(u3.Past, Is.EqualTo(u2), "Missing unit past"); Assert.That(u3.Past, Is.EqualTo(u2), "Missing unit past");