diff --git a/MultiversalDiplomacy/Model/Map.cs b/MultiversalDiplomacy/Model/Map.cs
new file mode 100644
index 0000000..1ce0c57
--- /dev/null
+++ b/MultiversalDiplomacy/Model/Map.cs
@@ -0,0 +1,560 @@
+using System.Collections.ObjectModel;
+
+namespace MultiversalDiplomacy.Model;
+
+///
+/// Encapsulation of the world map and playable powers constituting a Diplomacy variant.
+///
+public class Map {
+ ///
+ /// The game map.
+ ///
+ public ReadOnlyCollection Provinces { get; }
+
+ ///
+ /// The game powers.
+ ///
+ public ReadOnlyCollection Powers { get; }
+
+ private Map(IEnumerable provinces, IEnumerable powers)
+ {
+ Provinces = new(provinces.ToList());
+ Powers = new(powers.ToList());
+ }
+
+ ///
+ /// Get a province by name. Throws if the province is not found.
+ ///
+ public Province GetProvince(string provinceName)
+ => GetProvince(provinceName, this.Provinces);
+
+ ///
+ /// Get a province by name. Throws if the province is not found.
+ ///
+ private static Province GetProvince(string provinceName, IEnumerable provinces)
+ {
+ string provinceNameUpper = provinceName.ToUpperInvariant();
+ Province? foundProvince = provinces.SingleOrDefault(
+ p => p!.Name.ToUpperInvariant() == provinceNameUpper
+ || p.Abbreviations.Any(a => a.ToUpperInvariant() == provinceNameUpper),
+ null);
+ if (foundProvince == null) throw new KeyNotFoundException(
+ $"Province {provinceName} not found");
+ return foundProvince;
+ }
+
+ ///
+ /// Get the location in a province matching a predicate. Throws if there is not exactly one
+ /// such location.
+ ///
+ private Location GetLocation(string provinceName, Func predicate)
+ {
+ Location? foundLocation = GetProvince(provinceName).Locations.SingleOrDefault(
+ l => l != null && predicate(l), null);
+ if (foundLocation == null) throw new KeyNotFoundException(
+ $"No such location in {provinceName}");
+ return foundLocation;
+ }
+
+ ///
+ /// Get the sole land location of a province.
+ ///
+ public Location GetLand(string provinceName)
+ => GetLocation(provinceName, l => l.Type == LocationType.Land);
+
+ ///
+ /// Get the sole water location of a province, optionally specifying a named coast.
+ ///
+ public Location GetWater(string provinceName, string? coastName = null)
+ => coastName == null
+ ? GetLocation(provinceName, l => l.Type == LocationType.Water)
+ : GetLocation(provinceName, l => l.Name == coastName || l.Abbreviation == coastName);
+
+ ///
+ /// Get a power by name. Throws if there is not exactly one such power.
+ ///
+ public Power GetPower(string powerName)
+ => Powers.SingleOrDefault(p => p!.Name == powerName || p.Name.StartsWith(powerName), null)
+ ?? throw new KeyNotFoundException($"Power {powerName} not found");
+
+ public static Map FromType(MapType type)
+ => type switch {
+ MapType.Test => Test,
+ MapType.Classical => Test,
+ _ => throw new NotImplementedException($"Unknown variant {type}"),
+ };
+
+#region Variants
+
+ public static Map Test => _Test.Value;
+
+ private static readonly Lazy