using System.Collections.ObjectModel;
using System.Text.Json.Serialization;
namespace MultiversalDiplomacy.Model;
///
/// The global game state.
///
public class World
{
///
/// The map variant of the game.
///
[JsonIgnore]
public Map Map { get; }
///
/// 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 IReadOnlyCollection Provinces => this.Map.Provinces;
///
/// The game powers.
///
[JsonIgnore]
public IReadOnlyCollection Powers => this.Map.Powers;
///
/// The state of the multiverse.
///
public List Seasons { get; }
///
/// Lookup for seasons by designation.
///
[JsonIgnore]
public Dictionary SeasonLookup { get; }
///
/// The first season of the game.
///
[JsonIgnore]
public Season RootSeason => GetSeason("a0");
///
/// All units in the multiverse.
///
public List Units { get; }
///
/// All retreating units in the multiverse.
///
public List RetreatingUnits { get; }
///
/// Orders given to units in each season.
///
public Dictionary OrderHistory { get; }
///
/// The shared timeline number generator.
///
public TimelineFactory Timelines { get; }
///
/// Immutable game options.
///
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,
List seasons,
List units,
List retreatingUnits,
Dictionary orderHistory,
TimelineFactory timelines,
Options options)
{
this.Map = map;
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 from a previous one, replacing some state data.
///
private World(
World previous,
List? seasons = null,
List? units = null,
List? retreatingUnits = null,
Dictionary? orderHistory = null,
Options? options = null)
: this(
previous.Map,
seasons ?? previous.Seasons,
units ?? previous.Units,
retreatingUnits ?? previous.RetreatingUnits,
orderHistory ?? previous.OrderHistory,
previous.Timelines,
options ?? previous.Options)
{
}
///
/// Create a new world with specified provinces and powers and an initial season.
///
public static World WithMap(Map map)
{
TimelineFactory timelines = new();
return new World(
map,
new([new(past: null, Season.FIRST_TURN, timelines.NextTimeline())]),
new([]),
new([]),
new(new Dictionary()),
timelines,
new Options());
}
///
/// Create a new world with the standard Diplomacy provinces and powers.
///
public static World WithStandardMap()
=> WithMap(Map.Classical);
public World Update(
IEnumerable? seasons = null,
IEnumerable? units = null,
IEnumerable? retreats = null,
IEnumerable>? orders = null)
=> new(
previous: this,
seasons: seasons == null
? this.Seasons
: new(seasons.ToList()),
units: units == null
? this.Units
: new(units.ToList()),
retreatingUnits: retreats == null
? this.RetreatingUnits
: new(retreats.ToList()),
orderHistory: orders == null
? this.OrderHistory
: new(orders.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)));
///
/// Create a new world with new units created from unit specs. Units specs are in the format
/// " []". If the province or coast name has a space in it, the
/// abbreviation should be used. Unit specs always describe units in the root season.
///
public World AddUnits(params string[] unitSpecs)
{
IEnumerable units = unitSpecs.Select(spec =>
{
string[] splits = spec.Split(' ', 4);
Power power = Map.GetPower(splits[0]);
UnitType type = splits[1] switch
{
"A" => UnitType.Army,
"F" => UnitType.Fleet,
_ => throw new ApplicationException($"Unknown unit type {splits[1]}")
};
Location location = type == UnitType.Army
? Map.GetLand(splits[2])
: splits.Length == 3
? Map.GetWater(splits[2])
: Map.GetWater(splits[2], splits[3]);
Unit unit = Unit.Build(location, this.RootSeason, power, type);
return unit;
});
return this.Update(units: units);
}
///
/// Create a new world with standard Diplomacy initial unit placements.
///
public World AddStandardUnits()
{
return this.AddUnits(
"Austria A Bud",
"Austria A Vir",
"Austria F Tri",
"England A Lvp",
"England F Edi",
"England F Lon",
"France A Mar",
"France A Par",
"France F Bre",
"Germany A Ber",
"Germany A Mun",
"Germany F Kie",
"Italy A Rom",
"Italy A Ven",
"Italy F Nap",
"Russia A Mos",
"Russia A War",
"Russia F Sev",
"Russia F Stp wc",
"Turkey A Con",
"Turkey A Smy",
"Turkey F Ank"
);
}
///
/// Create a continuation of this season if it has no futures, otherwise create a fork.
///
public Season ContinueOrFork(Season season)
=> GetFutures(season).Any()
? new(season.Designation, season.Turn + 1, Timelines.NextTimeline())
: new(season.Designation, season.Turn + 1, season.Timeline);
///
/// A standard Diplomacy game setup.
///
public static World Standard => WithStandardMap().AddStandardUnits();
///
/// Get a season by coordinate. Throws if the season is not found.
///
public Season GetSeason(string timeline, int turn)
=> GetSeason($"{timeline}{turn}");
///
/// Get a season by designation.
///
public Season GetSeason(string designation)
=> SeasonLookup[designation];
///
/// Get all seasons that are immediate futures of a season.
///
/// A season designation.
/// The immediate futures of the designated season.
public IEnumerable GetFutures(string present)
=> Seasons.Where(future => future.Past == present);
///
/// Get all seasons that are immediate futures of a season.
///
/// A season.
/// The immediate futures of the season.
public IEnumerable GetFutures(Season present) => GetFutures(present.Designation);
///
/// Returns the first season in this season's timeline. The first season is the
/// root of the first timeline. The earliest season in each alternate timeline is
/// the root of that timeline.
///
public Season GetTimelineRoot(Season season)
{
if (season.Past is null) {
return season;
}
Season past = SeasonLookup[season.Past];
return season.Timeline == past.Timeline
? GetTimelineRoot(past)
: season;
}
///
/// Returns whether this season is in an adjacent timeline to another season.
/// Seasons are considered to be in adjacent timelines if they are in the same timeline,
/// one is in a timeline that branched from the other's timeline, or both are in timelines
/// that branched from the same point.
///
public bool InAdjacentTimeline(Season one, Season two)
{
// Timelines are adjacent to themselves. Early out in that case.
if (one == two) return true;
// If the timelines aren't identical, one of them isn't the initial trunk.
// They can still be adjacent if one of them branched off of the other, or
// if they both branched off of the same point.
Season rootOne = GetTimelineRoot(one);
Season rootTwo = GetTimelineRoot(two);
bool oneForked = rootOne.Past != null && GetSeason(rootOne.Past).Timeline == two.Timeline;
bool twoForked = rootTwo.Past != null && GetSeason(rootTwo.Past).Timeline == one.Timeline;
bool bothForked = rootOne.Past == rootTwo.Past;
return oneForked || twoForked || bothForked;
}
///
/// Returns a unit in a province. Throws if there are duplicate units.
///
public Unit GetUnitAt(string provinceName, Season? season = null)
{
Province province = Map.GetProvince(provinceName);
season ??= RootSeason;
Unit? foundUnit = this.Units.SingleOrDefault(
u => u!.Province == province && u.Season == season,
null)
?? throw new KeyNotFoundException($"Unit at {province} at {season} not found");
return foundUnit;
}
}