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.Designation, 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 => Map.GetLocation(u!).Province == province && u!.Season == season, null) ?? throw new KeyNotFoundException($"Unit at {province} at {season} not found"); return foundUnit; } public Unit GetUnitByDesignation(string designation) => Units.SingleOrDefault(u => u!.Designation == designation, null) ?? throw new KeyNotFoundException($"Unit {designation} not found"); }