The board, and what this game is going to be

Seventy-five provinces as two graphs, because an army and a fleet do not move
on the same one, and three provinces whose two coasts do not connect.

The topology is a fact about a published game and free to use; the artwork is
not, and there is none here -- the map will be generated from this data the
way the cave game's dodecahedron is. The name is a working one for the same
reason the starship game is not called what you expect.

The tests check the board against itself, and symmetry is the one that pays:
it found the Skagerrak wired to the Baltic, which would have opened a back
door into the Baltic and made Denmark worth nothing.
This commit is contained in:
2026-09-09 00:13:30 -07:00
commit c5cd0a6351
13 changed files with 4279 additions and 0 deletions
+302
View File
@@ -0,0 +1,302 @@
/**
* The board.
*
* Seventy-five provinces, drawn from the standard 1901 map. The topology is
* a fact about a published game and free to use -- an adjacency list is not
* expression, and every implementation of this game since 1959 has used the
* same one. The *artwork* is Avalon Hill's and is not here: the map this
* game draws is generated from the data below, the way the cave game's
* dodecahedron and the starship game's grid are.
*
* Two adjacency lists, because the two unit types do not move on the same
* graph. An army walks the land; a fleet follows water and hugs coastlines,
* which is why Wales and Yorkshire are neighbours for one and strangers for
* the other. Three provinces have two coasts that do not connect to each
* other, and a fleet in one of them has to say which -- those are keyed
* `stp/nc` and so on, and the bare province id is never a fleet position.
*/
export type Power = 'austria' | 'england' | 'france' | 'germany' | 'italy' | 'russia' | 'turkey'
export const POWERS: readonly Power[] = [
'austria',
'england',
'france',
'germany',
'italy',
'russia',
'turkey',
]
/** `land` is inland, `coast` touches water, `sea` is water. */
export type Terrain = 'land' | 'sea' | 'coast'
export interface Province {
name: string
terrain: Terrain
/** A supply centre: the only thing anybody is actually counting. */
sc: boolean
/** Whose home centre it is, for builds. */
home?: Power
/** Named coasts, when a fleet has to choose one. */
coasts?: readonly string[]
}
const P = (
name: string,
terrain: Terrain,
sc = false,
home?: Power,
coasts?: readonly string[],
): Province => ({ name, terrain, sc, home, coasts })
export const PROVINCES: Record<string, Province> = {
// --- seas ---------------------------------------------------------------
adr: P('Adriatic Sea', 'sea'),
aeg: P('Aegean Sea', 'sea'),
bal: P('Baltic Sea', 'sea'),
bar: P('Barents Sea', 'sea'),
bla: P('Black Sea', 'sea'),
bot: P('Gulf of Bothnia', 'sea'),
eas: P('Eastern Mediterranean', 'sea'),
eng: P('English Channel', 'sea'),
hel: P('Helgoland Bight', 'sea'),
ion: P('Ionian Sea', 'sea'),
iri: P('Irish Sea', 'sea'),
lyo: P('Gulf of Lyon', 'sea'),
mao: P('Mid-Atlantic Ocean', 'sea'),
nao: P('North Atlantic Ocean', 'sea'),
nth: P('North Sea', 'sea'),
nwg: P('Norwegian Sea', 'sea'),
ska: P('Skagerrak', 'sea'),
tys: P('Tyrrhenian Sea', 'sea'),
wes: P('Western Mediterranean', 'sea'),
// --- home centres -------------------------------------------------------
vie: P('Vienna', 'land', true, 'austria'),
bud: P('Budapest', 'land', true, 'austria'),
tri: P('Trieste', 'coast', true, 'austria'),
lon: P('London', 'coast', true, 'england'),
edi: P('Edinburgh', 'coast', true, 'england'),
lvp: P('Liverpool', 'coast', true, 'england'),
par: P('Paris', 'land', true, 'france'),
mar: P('Marseilles', 'coast', true, 'france'),
bre: P('Brest', 'coast', true, 'france'),
ber: P('Berlin', 'coast', true, 'germany'),
mun: P('Munich', 'land', true, 'germany'),
kie: P('Kiel', 'coast', true, 'germany'),
rom: P('Rome', 'coast', true, 'italy'),
ven: P('Venice', 'coast', true, 'italy'),
nap: P('Naples', 'coast', true, 'italy'),
mos: P('Moscow', 'land', true, 'russia'),
sev: P('Sevastopol', 'coast', true, 'russia'),
stp: P('St Petersburg', 'coast', true, 'russia', ['nc', 'sc']),
war: P('Warsaw', 'land', true, 'russia'),
ank: P('Ankara', 'coast', true, 'turkey'),
con: P('Constantinople', 'coast', true, 'turkey'),
smy: P('Smyrna', 'coast', true, 'turkey'),
// --- neutral centres ----------------------------------------------------
bel: P('Belgium', 'coast', true),
bul: P('Bulgaria', 'coast', true, undefined, ['ec', 'sc']),
den: P('Denmark', 'coast', true),
gre: P('Greece', 'coast', true),
hol: P('Holland', 'coast', true),
nwy: P('Norway', 'coast', true),
por: P('Portugal', 'coast', true),
rum: P('Rumania', 'coast', true),
ser: P('Serbia', 'land', true),
spa: P('Spain', 'coast', true, undefined, ['nc', 'sc']),
swe: P('Sweden', 'coast', true),
tun: P('Tunis', 'coast', true),
// --- the rest -----------------------------------------------------------
alb: P('Albania', 'coast'),
apu: P('Apulia', 'coast'),
arm: P('Armenia', 'coast'),
boh: P('Bohemia', 'land'),
bur: P('Burgundy', 'land'),
cly: P('Clyde', 'coast'),
fin: P('Finland', 'coast'),
gal: P('Galicia', 'land'),
gas: P('Gascony', 'coast'),
lvn: P('Livonia', 'coast'),
naf: P('North Africa', 'coast'),
pic: P('Picardy', 'coast'),
pie: P('Piedmont', 'coast'),
pru: P('Prussia', 'coast'),
ruh: P('Ruhr', 'land'),
sil: P('Silesia', 'land'),
syr: P('Syria', 'coast'),
tus: P('Tuscany', 'coast'),
tyr: P('Tyrolia', 'land'),
ukr: P('Ukraine', 'land'),
wal: P('Wales', 'coast'),
yor: P('Yorkshire', 'coast'),
}
/** Where an army may walk. Seas do not appear. */
export const ARMY: Record<string, readonly string[]> = {
cly: ['edi', 'lvp'],
edi: ['cly', 'lvp', 'yor'],
lvp: ['cly', 'edi', 'yor', 'wal'],
yor: ['edi', 'lvp', 'wal', 'lon'],
wal: ['lvp', 'yor', 'lon'],
lon: ['yor', 'wal'],
nwy: ['swe', 'fin', 'stp'],
swe: ['nwy', 'fin', 'den'],
fin: ['nwy', 'swe', 'stp'],
den: ['swe', 'kie'],
stp: ['nwy', 'fin', 'mos', 'lvn'],
mos: ['stp', 'lvn', 'war', 'ukr', 'sev'],
lvn: ['stp', 'mos', 'war', 'pru'],
war: ['lvn', 'mos', 'ukr', 'gal', 'sil', 'pru'],
ukr: ['mos', 'war', 'gal', 'rum', 'sev'],
sev: ['mos', 'ukr', 'rum', 'arm'],
pru: ['lvn', 'war', 'sil', 'ber'],
ber: ['pru', 'sil', 'mun', 'kie'],
kie: ['ber', 'mun', 'ruh', 'hol', 'den'],
mun: ['kie', 'ber', 'sil', 'boh', 'tyr', 'bur', 'ruh'],
ruh: ['kie', 'mun', 'bur', 'bel', 'hol'],
sil: ['pru', 'ber', 'mun', 'boh', 'gal', 'war'],
hol: ['kie', 'ruh', 'bel'],
bel: ['hol', 'ruh', 'bur', 'pic'],
pic: ['bel', 'bur', 'par', 'bre'],
bur: ['bel', 'ruh', 'mun', 'mar', 'gas', 'par', 'pic'],
par: ['pic', 'bur', 'gas', 'bre'],
bre: ['pic', 'par', 'gas'],
gas: ['bre', 'par', 'bur', 'mar', 'spa'],
mar: ['bur', 'gas', 'spa', 'pie'],
spa: ['gas', 'mar', 'por'],
por: ['spa'],
pie: ['mar', 'tyr', 'ven', 'tus'],
tus: ['pie', 'ven', 'rom'],
ven: ['pie', 'tyr', 'tri', 'apu', 'rom', 'tus'],
rom: ['tus', 'ven', 'apu', 'nap'],
nap: ['rom', 'apu'],
apu: ['nap', 'rom', 'ven'],
tyr: ['mun', 'boh', 'vie', 'tri', 'ven', 'pie'],
boh: ['mun', 'sil', 'gal', 'vie', 'tyr'],
vie: ['boh', 'gal', 'bud', 'tri', 'tyr'],
bud: ['vie', 'gal', 'rum', 'ser', 'tri'],
tri: ['tyr', 'vie', 'bud', 'ser', 'alb', 'ven'],
gal: ['war', 'sil', 'boh', 'vie', 'bud', 'rum', 'ukr'],
ser: ['tri', 'bud', 'rum', 'bul', 'gre', 'alb'],
alb: ['tri', 'ser', 'gre'],
gre: ['alb', 'ser', 'bul'],
bul: ['ser', 'rum', 'con', 'gre'],
rum: ['bud', 'gal', 'ukr', 'sev', 'bul', 'ser'],
con: ['bul', 'ank', 'smy'],
ank: ['con', 'smy', 'arm'],
smy: ['con', 'ank', 'arm', 'syr'],
arm: ['ank', 'smy', 'syr', 'sev'],
syr: ['smy', 'arm'],
naf: ['tun'],
tun: ['naf'],
}
/**
* Where a fleet may sail. Inland provinces do not appear, and the three
* two-coasted provinces appear only under their named coasts -- a fleet is
* never simply "in Spain".
*/
export const FLEET: Record<string, readonly string[]> = {
// --- open water ---------------------------------------------------------
nao: ['nwg', 'cly', 'lvp', 'iri', 'mao'],
nwg: ['bar', 'nwy', 'nth', 'edi', 'cly', 'nao'],
bar: ['nwg', 'nwy', 'stp/nc'],
nth: ['nwg', 'nwy', 'ska', 'den', 'hel', 'hol', 'bel', 'lon', 'yor', 'edi', 'eng'],
// No Baltic. The Danish straits are the only way in, which is the whole
// reason Denmark is worth fighting over.
ska: ['nwy', 'swe', 'den', 'nth'],
bal: ['swe', 'den', 'kie', 'ber', 'pru', 'lvn', 'bot'],
bot: ['swe', 'fin', 'stp/sc', 'lvn', 'bal'],
hel: ['nth', 'den', 'kie', 'hol'],
iri: ['nao', 'lvp', 'wal', 'eng', 'mao'],
eng: ['iri', 'wal', 'lon', 'nth', 'bel', 'pic', 'bre', 'mao'],
mao: ['nao', 'iri', 'eng', 'bre', 'gas', 'spa/nc', 'por', 'spa/sc', 'naf', 'wes'],
wes: ['mao', 'spa/sc', 'lyo', 'tys', 'tun', 'naf'],
lyo: ['spa/sc', 'mar', 'pie', 'tus', 'tys', 'wes'],
tys: ['lyo', 'tus', 'rom', 'nap', 'ion', 'tun', 'wes'],
ion: ['tys', 'nap', 'apu', 'adr', 'alb', 'gre', 'aeg', 'eas', 'tun'],
adr: ['ven', 'tri', 'alb', 'ion', 'apu'],
aeg: ['gre', 'bul/sc', 'con', 'smy', 'eas', 'ion'],
eas: ['aeg', 'smy', 'syr', 'ion'],
bla: ['sev', 'arm', 'ank', 'con', 'bul/ec', 'rum'],
// --- coasts -------------------------------------------------------------
cly: ['nao', 'nwg', 'edi', 'lvp'],
edi: ['nwg', 'nth', 'yor', 'cly'],
lvp: ['nao', 'iri', 'wal', 'cly'],
yor: ['edi', 'nth', 'lon'],
wal: ['lvp', 'iri', 'eng', 'lon'],
lon: ['yor', 'nth', 'eng', 'wal'],
nwy: ['nwg', 'bar', 'stp/nc', 'swe', 'ska', 'nth'],
swe: ['nwy', 'ska', 'den', 'bal', 'bot', 'fin'],
fin: ['swe', 'bot', 'stp/sc'],
'stp/nc': ['bar', 'nwy'],
'stp/sc': ['bot', 'fin', 'lvn'],
lvn: ['bot', 'bal', 'pru', 'stp/sc'],
pru: ['lvn', 'bal', 'ber'],
ber: ['pru', 'bal', 'kie'],
kie: ['ber', 'bal', 'den', 'hel', 'hol'],
den: ['swe', 'ska', 'bal', 'kie', 'hel', 'nth'],
hol: ['nth', 'hel', 'kie', 'bel'],
bel: ['hol', 'nth', 'eng', 'pic'],
pic: ['bel', 'eng', 'bre'],
bre: ['pic', 'eng', 'mao', 'gas'],
gas: ['bre', 'mao', 'spa/nc'],
'spa/nc': ['gas', 'mao', 'por'],
'spa/sc': ['por', 'mao', 'wes', 'lyo', 'mar'],
por: ['spa/nc', 'mao', 'spa/sc'],
mar: ['spa/sc', 'lyo', 'pie'],
pie: ['mar', 'lyo', 'tus'],
tus: ['pie', 'lyo', 'tys', 'rom'],
rom: ['tus', 'tys', 'nap'],
nap: ['rom', 'tys', 'ion', 'apu'],
apu: ['nap', 'ion', 'adr', 'ven'],
ven: ['apu', 'adr', 'tri'],
tri: ['ven', 'adr', 'alb'],
alb: ['tri', 'adr', 'ion', 'gre'],
gre: ['alb', 'ion', 'aeg', 'bul/sc'],
'bul/ec': ['rum', 'bla', 'con'],
'bul/sc': ['gre', 'aeg', 'con'],
con: ['bul/sc', 'aeg', 'smy', 'ank', 'bla', 'bul/ec'],
smy: ['con', 'aeg', 'eas', 'syr'],
syr: ['smy', 'eas'],
ank: ['con', 'bla', 'arm'],
arm: ['ank', 'bla', 'sev'],
sev: ['arm', 'bla', 'rum'],
rum: ['sev', 'bla', 'bul/ec'],
naf: ['mao', 'wes', 'tun'],
tun: ['naf', 'wes', 'tys', 'ion'],
}
/** The province a fleet key belongs to: `bul/sc` is in Bulgaria. */
export const base = (id: string): string => id.split('/')[0]!
/** Whose armies and fleets start where, in Spring 1901. */
export const OPENING: Record<Power, { armies: readonly string[]; fleets: readonly string[] }> = {
austria: { armies: ['vie', 'bud'], fleets: ['tri'] },
england: { armies: ['lvp'], fleets: ['lon', 'edi'] },
france: { armies: ['par', 'mar'], fleets: ['bre'] },
germany: { armies: ['ber', 'mun'], fleets: ['kie'] },
italy: { armies: ['rom', 'ven'], fleets: ['nap'] },
russia: { armies: ['mos', 'war'], fleets: ['stp/sc', 'sev'] },
turkey: { armies: ['con', 'smy'], fleets: ['ank'] },
}
/** Eighteen of the thirty-four centres ends it. */
export const SOLO = 18