Territories, not circles
The board is regions again. Voronoi cells divide it with no gaps and no overlaps and look like a board, which is what this was always supposed to be. What they cannot do is reproduce every adjacency in the rules, because provinces interleave -- the Adriatic borders Venice with Trieste between their centres. The lesson I drew from that was to abandon regions and draw the adjacency graph, which is unimpeachable and is not a map. It is circles joined by lines. The right answer is to stop asking the picture to carry the rules. Clicking a province lights up exactly where its unit may legally go, taken from the graph rather than from which shapes happen to share an edge. Vienna lights Bohemia, Galicia, Budapest, Trieste and Tyrolia; a fleet on the north coast of Spain does not light the Gulf of Lyon. The picture is a picture and the rules answer for themselves when asked.
This commit is contained in:
@@ -42,17 +42,22 @@ other three games use, on a map generated from the topology in
|
|||||||
in Europe -- and everything else is drawn from those and from the adjacency
|
in Europe -- and everything else is drawn from those and from the adjacency
|
||||||
rules.
|
rules.
|
||||||
|
|
||||||
**It draws the graph, not regions, and that was not the first idea.** The
|
**Territories, with the rules kept out of the picture.** The regions are
|
||||||
first idea was Voronoi cells: every point on the board belonging to the
|
Voronoi cells -- every point belonging to the nearest province -- which
|
||||||
nearest province, which gives a handsome cut-paper board. It is also wrong.
|
divides the board with no gaps and no overlaps and looks like a board.
|
||||||
Sixty-five pairs that border each other in the rules came out with regions
|
|
||||||
that did not touch, and no amount of moving coordinates would fix it, because
|
What it cannot do is reproduce every adjacency in the rules, because
|
||||||
the fault is structural -- provinces here interleave. The Adriatic borders
|
provinces here interleave: the Adriatic borders Venice with Trieste sitting
|
||||||
Venice with Trieste sitting between their centres; the Atlantic borders North
|
between their centres, and the Atlantic borders North Africa round the
|
||||||
Africa around the outside of Spain. Convex cells cannot say that, and a map
|
outside of Spain. Convex cells cannot say that.
|
||||||
that shows two regions meeting when the rules say they do not is worse than
|
|
||||||
an ugly map. It is a map that loses you the game. So adjacency is drawn as
|
The wrong lesson to draw from that -- and the one I drew first -- is to give
|
||||||
lines, which say exactly what the rules say and cannot be misread.
|
up on regions and draw the adjacency graph instead. It is unimpeachable and
|
||||||
|
it is not a map; it is circles joined by lines. The right answer is to stop
|
||||||
|
asking the picture to carry the rules. **Click a province and exactly the
|
||||||
|
provinces its unit may legally reach light up**, taken from the adjacency
|
||||||
|
graph itself. The picture is a picture, and the rules answer for themselves
|
||||||
|
when they are asked.
|
||||||
|
|
||||||
**It gets the whole screen.** The other three games live in a four-by-three
|
**It gets the whole screen.** The other three games live in a four-by-three
|
||||||
cabinet because the machines they are rebuilding did. This one is not
|
cabinet because the machines they are rebuilding did. This one is not
|
||||||
|
|||||||
+20
-20
@@ -50,42 +50,42 @@ header p { margin: 2px 0 0; font-size: 0.9rem; }
|
|||||||
background: #6ea3c9;
|
background: #6ea3c9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sea-bed { fill: #6ea3c9; }
|
.ocean { fill: #4a7fa8; }
|
||||||
|
|
||||||
.edges line {
|
.region {
|
||||||
stroke: #241a10;
|
stroke: #241a10;
|
||||||
stroke-width: 2;
|
stroke-width: 2.5;
|
||||||
opacity: 0.28;
|
stroke-linejoin: round;
|
||||||
}
|
|
||||||
|
|
||||||
.province rect,
|
|
||||||
.province circle {
|
|
||||||
stroke: #241a10;
|
|
||||||
stroke-width: 3;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.province.sea circle { stroke-opacity: 0.55; }
|
/* Open water gets a lighter line: a coastline is a border, the middle of the
|
||||||
|
Atlantic is a convention. */
|
||||||
|
.region.sea { stroke-opacity: 0.3; stroke-width: 2; }
|
||||||
|
|
||||||
.province .pip {
|
.region.picked { stroke: #ffd479; stroke-width: 5; }
|
||||||
|
|
||||||
|
/* Where the selected unit may actually go, taken from the rules rather than
|
||||||
|
from which shapes happen to touch. */
|
||||||
|
.region.open { stroke: #fff6e0; stroke-width: 4; stroke-dasharray: 7 5; }
|
||||||
|
|
||||||
|
.pip {
|
||||||
fill: #fff6e0;
|
fill: #fff6e0;
|
||||||
stroke: #241a10;
|
stroke: #241a10;
|
||||||
stroke-width: 2;
|
stroke-width: 1.8;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.province .label {
|
.label {
|
||||||
font-size: 12px;
|
font-size: 11.5px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
text-anchor: middle;
|
text-anchor: middle;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
fill: #241a10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.province.picked rect,
|
.label.wet { fill: #dceaf5; }
|
||||||
.province.picked circle {
|
|
||||||
stroke: #ffd479;
|
|
||||||
stroke-width: 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.unit path {
|
.unit path {
|
||||||
stroke: #241a10;
|
stroke: #241a10;
|
||||||
|
|||||||
+58
-67
@@ -1,34 +1,38 @@
|
|||||||
import { PROVINCES, POWERS, base, type Power } from '../game/map'
|
import { PROVINCES, POWERS, base, type Power } from '../game/map'
|
||||||
import { CENTRES, borders, bounds, radius } from '../game/layout'
|
import { CENTRES, bounds, cell, path, radius, reachableFrom } from '../game/layout'
|
||||||
import type { Board as Units } from '../game/orders'
|
import type { Board as Units } from '../game/orders'
|
||||||
import type { Ownership } from '../game/turn'
|
import type { Ownership } from '../game/turn'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The board.
|
* The board.
|
||||||
*
|
*
|
||||||
* Everything is drawn from `layout.ts` and the rules, so nothing here is a
|
* Territories, drawn from coordinates I placed by hand and divided by the
|
||||||
* picture of anybody's map. Flat fills, one heavy outline, no gradients: the
|
* halfway line between them. Nothing here is a picture of anybody's map.
|
||||||
* same cel style the other three games use, on a board that has to stay
|
* Flat fills, one heavy outline, no gradients: the same cel style the other
|
||||||
* readable at a glance while somebody is arguing with you about Galicia.
|
* three games use.
|
||||||
*
|
*
|
||||||
* Borders are lines because in this game the adjacency *is* the rules. A
|
* The regions are what the map looks like; they are not what the rules are
|
||||||
* region map has to decide whether two shapes touch, and when it gets that
|
* read from. A Voronoi cell cannot reproduce every adjacency in this game --
|
||||||
* wrong it costs somebody the game. A line cannot be misread.
|
* provinces interleave, and the Adriatic borders Venice with Trieste sitting
|
||||||
|
* between their centres -- so clicking a province lights up exactly where its
|
||||||
|
* unit may legally go, taken from the adjacency graph itself. The picture is
|
||||||
|
* a picture; the rules answer for themselves when asked.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const COLOURS: Record<Power, string> = {
|
export const COLOURS: Record<Power, string> = {
|
||||||
austria: '#e4572e',
|
austria: '#e4572e',
|
||||||
england: '#3b5bdb',
|
england: '#3b5bdb',
|
||||||
france: '#4dabf7',
|
france: '#4dabf7',
|
||||||
germany: '#495057',
|
germany: '#4b545e',
|
||||||
italy: '#37b24d',
|
italy: '#37b24d',
|
||||||
russia: '#9775fa',
|
russia: '#9775fa',
|
||||||
turkey: '#f59f00',
|
turkey: '#f2a03d',
|
||||||
}
|
}
|
||||||
|
|
||||||
const SEA = '#4a7fa8'
|
const SEA = '#5b93bd'
|
||||||
const LAND = '#e8d9b5'
|
const SEA_DEEP = '#4a7fa8'
|
||||||
const NEUTRAL_SC = '#fff6e0'
|
const LAND = '#ded0ab'
|
||||||
|
const LAND_SC = '#efe3c2'
|
||||||
const OUTLINE = '#241a10'
|
const OUTLINE = '#241a10'
|
||||||
|
|
||||||
export function Board({
|
export function Board({
|
||||||
@@ -42,79 +46,73 @@ export function Board({
|
|||||||
selected?: string | null
|
selected?: string | null
|
||||||
onPick?: (province: string) => void
|
onPick?: (province: string) => void
|
||||||
}) {
|
}) {
|
||||||
const edges = borders()
|
const box = bounds(10)
|
||||||
const box = bounds()
|
const ids = Object.keys(PROVINCES)
|
||||||
|
|
||||||
|
const standing = selected ? units.get(selected) : undefined
|
||||||
|
const reachable = new Set(standing ? reachableFrom(standing) : [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
className="board"
|
className="board"
|
||||||
viewBox={`${box.x} ${box.y} ${box.w} ${box.h}`}
|
viewBox={`${box.x} ${box.y} ${box.w} ${box.h}`}
|
||||||
role="img"
|
role="img"
|
||||||
aria-label="The board: seventy-five provinces and the borders between them."
|
aria-label="The board: seventy-five provinces, coloured by who holds them."
|
||||||
>
|
>
|
||||||
<rect className="sea-bed" x={box.x} y={box.y} width={box.w} height={box.h} />
|
<rect className="ocean" x={box.x} y={box.y} width={box.w} height={box.h} />
|
||||||
|
|
||||||
{/* Borders first, so every province sits on top of its own edges. */}
|
{/* Territories. Sea first so the coastlines sit on top of the water. */}
|
||||||
<g className="edges">
|
{ids
|
||||||
{edges.map(([a, b]) => (
|
.sort((a, b) => Number(PROVINCES[b]!.terrain === 'sea') - Number(PROVINCES[a]!.terrain === 'sea'))
|
||||||
<line
|
.map((id) => {
|
||||||
key={`${a}-${b}`}
|
|
||||||
x1={CENTRES[a]!.x}
|
|
||||||
y1={CENTRES[a]!.y}
|
|
||||||
x2={CENTRES[b]!.x}
|
|
||||||
y2={CENTRES[b]!.y}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</g>
|
|
||||||
|
|
||||||
{Object.keys(PROVINCES).map((id) => {
|
|
||||||
const p = PROVINCES[id]!
|
const p = PROVINCES[id]!
|
||||||
const at = CENTRES[id]!
|
|
||||||
const r = radius(id)
|
|
||||||
const owner = own.get(id)
|
const owner = own.get(id)
|
||||||
const fill =
|
const fill =
|
||||||
p.terrain === 'sea' ? SEA : owner ? COLOURS[owner] : p.sc ? NEUTRAL_SC : LAND
|
p.terrain === 'sea' ? (p.sc ? SEA : SEA_DEEP) : owner ? COLOURS[owner] : p.sc ? LAND_SC : LAND
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<g
|
<path
|
||||||
className={`province ${p.terrain} ${p.sc ? 'centre' : ''} ${selected === id ? 'picked' : ''}`}
|
|
||||||
key={id}
|
key={id}
|
||||||
onClick={() => onPick?.(id)}
|
className={`region ${p.terrain} ${p.sc ? 'centre' : ''} ${
|
||||||
>
|
selected === id ? 'picked' : reachable.has(id) ? 'open' : ''
|
||||||
{p.terrain === 'sea' ? (
|
}`}
|
||||||
<circle cx={at.x} cy={at.y} r={r} fill={fill} />
|
d={path(cell(id))}
|
||||||
) : (
|
|
||||||
<rect
|
|
||||||
x={at.x - r}
|
|
||||||
y={at.y - r * 0.82}
|
|
||||||
width={r * 2}
|
|
||||||
height={r * 1.64}
|
|
||||||
rx={r * 0.45}
|
|
||||||
fill={fill}
|
fill={fill}
|
||||||
|
onClick={() => onPick?.(id)}
|
||||||
/>
|
/>
|
||||||
)}
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
{/* A supply centre is the only thing anybody is counting, so it
|
{/* Supply centres get a mark of their own: they are the only thing
|
||||||
gets a mark of its own rather than a different shade. */}
|
anybody is actually counting. */}
|
||||||
{p.sc && <circle className="pip" cx={at.x} cy={at.y - r * 0.86} r={4} />}
|
{ids
|
||||||
|
.filter((id) => PROVINCES[id]!.sc)
|
||||||
|
.map((id) => (
|
||||||
|
<circle
|
||||||
|
className="pip"
|
||||||
|
key={`pip-${id}`}
|
||||||
|
cx={CENTRES[id]!.x}
|
||||||
|
cy={CENTRES[id]!.y - radius(id) * 0.55}
|
||||||
|
r={4.4}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{ids.map((id) => (
|
||||||
<text
|
<text
|
||||||
className="label"
|
className={`label ${PROVINCES[id]!.terrain === 'sea' ? 'wet' : ''}`}
|
||||||
x={at.x}
|
key={`t-${id}`}
|
||||||
y={at.y + 5}
|
x={CENTRES[id]!.x}
|
||||||
style={{ fill: labelInk(p.terrain === 'sea' ? SEA : owner ? COLOURS[owner] : LAND) }}
|
y={CENTRES[id]!.y + 4}
|
||||||
>
|
>
|
||||||
{id.toUpperCase()}
|
{id.toUpperCase()}
|
||||||
</text>
|
</text>
|
||||||
</g>
|
))}
|
||||||
)
|
|
||||||
})}
|
|
||||||
|
|
||||||
{/* Units last: they are what you are actually looking at. */}
|
{/* Units last: they are what you are actually looking at. */}
|
||||||
{[...units.entries()].map(([at, unit]) => {
|
{[...units.entries()].map(([at, unit]) => {
|
||||||
const p = CENTRES[base(unit.at)] ?? CENTRES[at]!
|
const p = CENTRES[base(unit.at)] ?? CENTRES[at]!
|
||||||
return (
|
return (
|
||||||
<g className="unit" key={at} transform={`translate(${p.x} ${p.y + 16})`}>
|
<g className="unit" key={at} transform={`translate(${p.x} ${p.y + 17})`}>
|
||||||
{unit.type === 'army' ? (
|
{unit.type === 'army' ? (
|
||||||
<path d="M-11 6 L-11 -3 L0 -9 L11 -3 L11 6 Z" fill={COLOURS[unit.power]} />
|
<path d="M-11 6 L-11 -3 L0 -9 L11 -3 L11 6 Z" fill={COLOURS[unit.power]} />
|
||||||
) : (
|
) : (
|
||||||
@@ -127,13 +125,6 @@ export function Board({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Dark text on a light province, light on a dark one. */
|
|
||||||
function labelInk(background: string): string {
|
|
||||||
const n = parseInt(background.slice(1), 16)
|
|
||||||
const lum = (((n >> 16) & 255) * 299 + ((n >> 8) & 255) * 587 + (n & 255) * 114) / 1000
|
|
||||||
return lum > 140 ? OUTLINE : '#fff6e0'
|
|
||||||
}
|
|
||||||
|
|
||||||
export const POWER_NAMES: Record<Power, string> = {
|
export const POWER_NAMES: Record<Power, string> = {
|
||||||
austria: 'Austria',
|
austria: 'Austria',
|
||||||
england: 'England',
|
england: 'England',
|
||||||
@@ -144,4 +135,4 @@ export const POWER_NAMES: Record<Power, string> = {
|
|||||||
turkey: 'Turkey',
|
turkey: 'Turkey',
|
||||||
}
|
}
|
||||||
|
|
||||||
export { POWERS }
|
export { POWERS, OUTLINE }
|
||||||
|
|||||||
+51
-1
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import { ARMY, FLEET, PROVINCES, base } from './map'
|
import { ARMY, FLEET, PROVINCES, base } from './map'
|
||||||
import { CENTRES, HEIGHT, WIDTH, borders, radius } from './layout'
|
import { CENTRES, HEIGHT, WIDTH, borders, cell, radius, reachableFrom } from './layout'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Coordinates typed by hand are wrong somewhere, and a map is the one thing
|
* Coordinates typed by hand are wrong somewhere, and a map is the one thing
|
||||||
@@ -95,3 +95,53 @@ describe('the drawing', () => {
|
|||||||
expect(overlapping).toEqual([])
|
expect(overlapping).toEqual([])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('the regions', () => {
|
||||||
|
it('gives every province a shape with area in it', () => {
|
||||||
|
for (const id of ids) {
|
||||||
|
const poly = cell(id)
|
||||||
|
expect(poly.length, id).toBeGreaterThan(2)
|
||||||
|
// Shoelace: a region nobody can see is a region that is not there.
|
||||||
|
let area = 0
|
||||||
|
for (let i = 0; i < poly.length; i++) {
|
||||||
|
const a = poly[i]!
|
||||||
|
const b = poly[(i + 1) % poly.length]!
|
||||||
|
area += a.x * b.y - b.x * a.y
|
||||||
|
}
|
||||||
|
expect(Math.abs(area / 2), id).toBeGreaterThan(300)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('contains its own centre', () => {
|
||||||
|
for (const id of ids) {
|
||||||
|
const poly = cell(id, 0)
|
||||||
|
const me = CENTRES[id]!
|
||||||
|
for (let i = 0; i < poly.length; i++) {
|
||||||
|
const a = poly[i]!
|
||||||
|
const b = poly[(i + 1) % poly.length]!
|
||||||
|
const cross = (b.x - a.x) * (me.y - a.y) - (b.y - a.y) * (me.x - a.x)
|
||||||
|
expect(cross, `${id} edge ${i}`).toBeGreaterThan(-0.01)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('what a unit may reach', () => {
|
||||||
|
/**
|
||||||
|
* The point of this being separate from the drawing. The regions cannot
|
||||||
|
* express every adjacency in the rules, so the map never claims to: this
|
||||||
|
* is what lights up when a province is clicked, and it comes from the
|
||||||
|
* graph rather than from which shapes happen to share an edge.
|
||||||
|
*/
|
||||||
|
it('is the rules, not the picture', () => {
|
||||||
|
expect(reachableFrom({ type: 'army', at: 'vie' }).sort()).toEqual(
|
||||||
|
['boh', 'bud', 'gal', 'tri', 'tyr'],
|
||||||
|
)
|
||||||
|
expect(reachableFrom({ type: 'fleet', at: 'stp/sc' }).sort()).toEqual(['bot', 'fin', 'lvn'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('knows a fleet on one coast cannot use the other', () => {
|
||||||
|
expect(reachableFrom({ type: 'fleet', at: 'spa/nc' })).not.toContain('lyo')
|
||||||
|
expect(reachableFrom({ type: 'fleet', at: 'spa/sc' })).toContain('lyo')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -185,3 +185,77 @@ export function bounds(pad = 34): { x: number; y: number; w: number; h: number }
|
|||||||
|
|
||||||
return { x: minX - pad, y: minY - pad, w: maxX - minX + pad * 2, h: maxY - minY + pad * 2 }
|
return { x: minX - pad, y: minY - pad, w: maxX - minX + pad * 2, h: maxY - minY + pad * 2 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The region belonging to one province: the whole board, cut back by the
|
||||||
|
* halfway line between it and every other centre.
|
||||||
|
*
|
||||||
|
* This is a Voronoi diagram, and it is worth being straight about what it
|
||||||
|
* can and cannot do. It divides the plane with no gaps and no overlaps, and
|
||||||
|
* it looks like a board. It cannot reproduce every adjacency in the rules,
|
||||||
|
* because provinces here interleave -- the Adriatic borders Venice with
|
||||||
|
* Trieste between their centres -- and convex cells cannot say that.
|
||||||
|
*
|
||||||
|
* So the regions are what the map *looks* like, and they are not what the
|
||||||
|
* rules are read from. Clicking a province lights up exactly where its unit
|
||||||
|
* may legally go, taken from the adjacency graph. That is the honest split:
|
||||||
|
* the picture is a picture, and the rules answer for themselves when asked.
|
||||||
|
*/
|
||||||
|
export function cell(id: string, margin = 5): Point[] {
|
||||||
|
const me = CENTRES[id]
|
||||||
|
if (!me) return []
|
||||||
|
|
||||||
|
let poly: Point[] = [
|
||||||
|
{ x: -60, y: -60 },
|
||||||
|
{ x: WIDTH + 60, y: -60 },
|
||||||
|
{ x: WIDTH + 60, y: HEIGHT + 60 },
|
||||||
|
{ x: -60, y: HEIGHT + 60 },
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const [other, them] of Object.entries(CENTRES)) {
|
||||||
|
if (other === id) continue
|
||||||
|
const nx = them.x - me.x
|
||||||
|
const ny = them.y - me.y
|
||||||
|
const len = Math.hypot(nx, ny)
|
||||||
|
if (len === 0) continue
|
||||||
|
const on = {
|
||||||
|
x: (me.x + them.x) / 2 - (nx / len) * (margin / 2),
|
||||||
|
y: (me.y + them.y) / 2 - (ny / len) * (margin / 2),
|
||||||
|
}
|
||||||
|
poly = clip(poly, on, { x: nx / len, y: ny / len })
|
||||||
|
if (poly.length === 0) break
|
||||||
|
}
|
||||||
|
return poly
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Everything on the near side of a line through `on` facing `normal`. */
|
||||||
|
function clip(poly: Point[], on: Point, normal: Point): Point[] {
|
||||||
|
const side = (p: Point) => (p.x - on.x) * normal.x + (p.y - on.y) * normal.y
|
||||||
|
const out: Point[] = []
|
||||||
|
for (let i = 0; i < poly.length; i++) {
|
||||||
|
const a = poly[i]!
|
||||||
|
const b = poly[(i + 1) % poly.length]!
|
||||||
|
const sa = side(a)
|
||||||
|
const sb = side(b)
|
||||||
|
if (sa <= 0) out.push(a)
|
||||||
|
if ((sa < 0 && sb > 0) || (sa > 0 && sb < 0)) {
|
||||||
|
const t = sa / (sa - sb)
|
||||||
|
out.push({ x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
export const path = (poly: readonly Point[]): string =>
|
||||||
|
poly.length === 0 ? '' : `M${poly.map((p) => `${p.x.toFixed(1)} ${p.y.toFixed(1)}`).join('L')}Z`
|
||||||
|
|
||||||
|
/** Where a unit standing here may legally go. The rules, not the picture. */
|
||||||
|
export function reachableFrom(unit: { type: 'army' | 'fleet'; at: string }): string[] {
|
||||||
|
const out = new Set<string>()
|
||||||
|
if (unit.type === 'army') {
|
||||||
|
for (const to of ARMY[base(unit.at)] ?? []) out.add(base(to))
|
||||||
|
} else {
|
||||||
|
for (const to of FLEET[unit.at] ?? []) out.add(base(to))
|
||||||
|
}
|
||||||
|
return [...out]
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user