Convoys were in the rules and not on the board
Fifty of the DATC cases are about convoys, paradoxes included, and the adjudicator passes them. But the army's half of a convoy could not be ordered: the board offered a unit the provinces it could walk to, and a crossing is by definition not one of those. The fleet's half could be ordered, which made it worse -- the verb was there and did nothing anybody could reach. An army on a coast is now also offered the coasts a chain of crewed seas can reach, in blue with long dashes, so a crossing does not look like a march. A sea counts if a fleet is standing in it, whoever owns it; a fleet that has not been ordered to convoy still counts, because it is a thing that could be arranged and that is what the talking is for. This is deliberately not the question the rules ask when they judge a convoy order, which is whether water could ever get there. That one says yes to most of Europe: offering thirty provinces because a chain of fleets is conceivable is worse than offering none. Clicking a blue coast writes the move viaConvoy. Not a formality -- the rules let a unit be convoyed to a province it could have walked to, and the two orders resolve differently. The fleet's own offering was too loose in the other direction and is now on the same chain: the North Sea listed eleven armies it could never carry, one of them in Ankara, and wrote orders the adjudicator discarded without saying why. What needed nothing added: the order list already flags A Yorkshire → Norway in red while it stands alone, and clears it when F North Sea convoys Yorkshire → Norway joins it. The validator knew all along.
This commit is contained in:
@@ -271,6 +271,37 @@ the order takes you back to the start of it. And the unit you picked to
|
|||||||
support is marked on the board in a different colour from the unit giving
|
support is marked on the board in a different colour from the unit giving
|
||||||
the order, because otherwise the second half is written blind.
|
the order, because otherwise the second half is written blind.
|
||||||
|
|
||||||
|
## Ordering a convoy
|
||||||
|
|
||||||
|
The rules have had convoys all along -- fifty of the DATC cases are about
|
||||||
|
them, including the paradoxes -- but for a while you could not order one.
|
||||||
|
The board offered an army the provinces it could walk to, and a crossing is
|
||||||
|
by definition not one of those, so the army's half of a convoy could not be
|
||||||
|
written. The fleet's half could, which made it worse: the verb was there and
|
||||||
|
did nothing anybody could use.
|
||||||
|
|
||||||
|
An army on a coast is now also offered the coasts a chain of crewed seas can
|
||||||
|
reach, drawn in blue and with long dashes so a crossing does not look like a
|
||||||
|
march. A sea counts if there is a fleet standing in it, whoever owns it. A
|
||||||
|
fleet that has not been ordered to convoy still counts: it is a thing that
|
||||||
|
could be arranged, which is what the negotiation is for, and the adjudicator
|
||||||
|
will bounce the crossing if it was not.
|
||||||
|
|
||||||
|
That is deliberately not the question the rules ask when they *judge* a
|
||||||
|
convoy, which is whether water could ever get there. That one says yes to
|
||||||
|
most of Europe. Offering thirty provinces because a chain of fleets is
|
||||||
|
conceivable would be worse than offering none.
|
||||||
|
|
||||||
|
Clicking a blue coast writes the move `viaConvoy`, which is not a formality:
|
||||||
|
the rules allow a unit to be convoyed to a province it could have walked to,
|
||||||
|
and the two orders resolve differently.
|
||||||
|
|
||||||
|
The two halves are written separately, as they always have been, and the
|
||||||
|
order list says so while only one of them exists -- `A Yorkshire → Norway`
|
||||||
|
sits there in red until `F North Sea convoys Yorkshire → Norway` joins it,
|
||||||
|
and then both go black. Nothing was added to make that happen. The validator
|
||||||
|
already knew.
|
||||||
|
|
||||||
## Still to build
|
## Still to build
|
||||||
|
|
||||||
- bots strong enough to solo against each other, not only to draw
|
- bots strong enough to solo against each other, not only to draw
|
||||||
|
|||||||
@@ -102,6 +102,9 @@ header select {
|
|||||||
/* The unit you picked to support or to carry: solid, and not the colour of
|
/* The unit you picked to support or to carry: solid, and not the colour of
|
||||||
the unit giving the order, so the two halves are never confused. */
|
the unit giving the order, so the two halves are never confused. */
|
||||||
.region.helping { stroke: #1f6fb8; stroke-width: 7; }
|
.region.helping { stroke: #1f6fb8; stroke-width: 7; }
|
||||||
|
/* Offered, but only a convoy gets there. Long dashes rather than short:
|
||||||
|
the same invitation, made by sea. */
|
||||||
|
.region.open.ferry { stroke: #1f6fb8; stroke-dasharray: 22 10; }
|
||||||
|
|
||||||
.pip {
|
.pip {
|
||||||
fill: #fffaf0;
|
fill: #fffaf0;
|
||||||
|
|||||||
+45
-5
@@ -22,7 +22,13 @@ import { POWERS, PROVINCES, base, type Power } from './game/map'
|
|||||||
import { canStep, validate, type Order, type Unit } from './game/orders'
|
import { canStep, validate, type Order, type Unit } from './game/orders'
|
||||||
import type { Proposal } from './game/press'
|
import type { Proposal } from './game/press'
|
||||||
import { endingSounded, mergeCues, type Cue } from './game/sound'
|
import { endingSounded, mergeCues, type Cue } from './game/sound'
|
||||||
import { convoyTargets, convoyable, supportTargets, supportable } from './game/targets'
|
import {
|
||||||
|
convoyDestinations,
|
||||||
|
convoyTargets,
|
||||||
|
convoyable,
|
||||||
|
supportTargets,
|
||||||
|
supportable,
|
||||||
|
} from './game/targets'
|
||||||
import { adjustmentFor, centreCount, type AdjustOrder, type RetreatOrder } from './game/turn'
|
import { adjustmentFor, centreCount, type AdjustOrder, type RetreatOrder } from './game/turn'
|
||||||
import './App.css'
|
import './App.css'
|
||||||
|
|
||||||
@@ -104,18 +110,39 @@ export default function App() {
|
|||||||
const units = game.board
|
const units = game.board
|
||||||
const pos = useMemo(() => ({ board: units, own }), [units, own])
|
const pos = useMemo(() => ({ board: units, own }), [units, own])
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The coasts the selected army could be carried to.
|
||||||
|
*
|
||||||
|
* Kept separate from the rest of the offering because the board draws it
|
||||||
|
* differently -- a crossing is not a march, and a player who cannot tell
|
||||||
|
* them apart will order one meaning the other -- and because it is what
|
||||||
|
* decides whether the move is written `viaConvoy`.
|
||||||
|
*/
|
||||||
|
const bySea = useMemo(() => {
|
||||||
|
if (step.kind !== 'move') return new Set<string>()
|
||||||
|
const unit = units.get(step.at)
|
||||||
|
if (!unit) return new Set<string>()
|
||||||
|
const legs = new Set(reachableFrom(unit).map(base))
|
||||||
|
return new Set([...convoyDestinations(units, unit)].filter((p) => !legs.has(p)))
|
||||||
|
}, [step, units])
|
||||||
|
|
||||||
const offering = useMemo(() => {
|
const offering = useMemo(() => {
|
||||||
if (step.kind === 'idle') return new Set<string>()
|
if (step.kind === 'idle') return new Set<string>()
|
||||||
const unit = units.get(step.at)
|
const unit = units.get(step.at)
|
||||||
if (!unit) return new Set<string>()
|
if (!unit) return new Set<string>()
|
||||||
if (step.kind === 'move') return new Set(reachableFrom(unit).map(base))
|
if (step.kind === 'move') {
|
||||||
|
// Where its own legs go, and where somebody's fleets could take it.
|
||||||
|
return new Set([...reachableFrom(unit).map(base), ...bySea])
|
||||||
|
}
|
||||||
if (step.kind === 'support') {
|
if (step.kind === 'support') {
|
||||||
return step.from === undefined
|
return step.from === undefined
|
||||||
? supportable(units, unit)
|
? supportable(units, unit)
|
||||||
: supportTargets(units, unit, step.from)
|
: supportTargets(units, unit, step.from)
|
||||||
}
|
}
|
||||||
return step.from === undefined ? convoyable(units, unit) : convoyTargets(step.from)
|
return step.from === undefined
|
||||||
}, [step, units])
|
? convoyable(units, unit)
|
||||||
|
: convoyTargets(units, unit, step.from)
|
||||||
|
}, [step, units, bySea])
|
||||||
|
|
||||||
const write = useCallback((order: Order) => {
|
const write = useCallback((order: Order) => {
|
||||||
setOrders((prev) => new Map(prev).set(base(order.at), order))
|
setOrders((prev) => new Map(prev).set(base(order.at), order))
|
||||||
@@ -157,7 +184,18 @@ export default function App() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (step.kind === 'move') {
|
if (step.kind === 'move') {
|
||||||
write({ type: 'move', at: step.at, to: coastOf(units.get(step.at)!, province), power })
|
/*
|
||||||
|
* A destination its legs cannot reach is a crossing, and saying so is
|
||||||
|
* not a formality: the rules let a unit be convoyed to a province it
|
||||||
|
* could have walked to, and the two orders resolve differently.
|
||||||
|
*/
|
||||||
|
write({
|
||||||
|
type: 'move',
|
||||||
|
at: step.at,
|
||||||
|
to: coastOf(units.get(step.at)!, province),
|
||||||
|
power,
|
||||||
|
...(bySea.has(province) ? { viaConvoy: true } : {}),
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (step.kind === 'support') {
|
if (step.kind === 'support') {
|
||||||
@@ -319,6 +357,7 @@ export default function App() {
|
|||||||
selected={step.kind === 'idle' ? null : step.at}
|
selected={step.kind === 'idle' ? null : step.at}
|
||||||
helping={step.kind === 'support' || step.kind === 'convoy' ? step.from : null}
|
helping={step.kind === 'support' || step.kind === 'convoy' ? step.from : null}
|
||||||
offering={offering}
|
offering={offering}
|
||||||
|
bySea={bySea}
|
||||||
onPick={click}
|
onPick={click}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -376,6 +415,7 @@ export default function App() {
|
|||||||
orders={orders}
|
orders={orders}
|
||||||
step={step}
|
step={step}
|
||||||
illegal={illegal}
|
illegal={illegal}
|
||||||
|
bySea={bySea.size > 0}
|
||||||
onAsk={(kind) => setStep(step.kind === 'idle' ? step : ({ kind, at: step.at } as Step))}
|
onAsk={(kind) => setStep(step.kind === 'idle' ? step : ({ kind, at: step.at } as Step))}
|
||||||
onClear={clear}
|
onClear={clear}
|
||||||
onSubmit={submit}
|
onSubmit={submit}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export function Board({
|
|||||||
selected,
|
selected,
|
||||||
helping,
|
helping,
|
||||||
offering,
|
offering,
|
||||||
|
bySea,
|
||||||
onPick,
|
onPick,
|
||||||
}: {
|
}: {
|
||||||
units: Units
|
units: Units
|
||||||
@@ -54,6 +55,12 @@ export function Board({
|
|||||||
helping?: string | null
|
helping?: string | null
|
||||||
/** Provinces the current step will accept a click on. */
|
/** Provinces the current step will accept a click on. */
|
||||||
offering?: ReadonlySet<string>
|
offering?: ReadonlySet<string>
|
||||||
|
/**
|
||||||
|
* Of those, the ones only a convoy can reach. Drawn differently because a
|
||||||
|
* crossing is not a march: it needs fleets that have been asked, and it
|
||||||
|
* fails in ways a land move cannot.
|
||||||
|
*/
|
||||||
|
bySea?: ReadonlySet<string>
|
||||||
onPick?: (province: string) => void
|
onPick?: (province: string) => void
|
||||||
}) {
|
}) {
|
||||||
const ids = Object.keys(PROVINCES)
|
const ids = Object.keys(PROVINCES)
|
||||||
@@ -82,7 +89,9 @@ export function Board({
|
|||||||
: helping === id
|
: helping === id
|
||||||
? 'helping'
|
? 'helping'
|
||||||
: reachable.has(id)
|
: reachable.has(id)
|
||||||
? 'open'
|
? bySea?.has(id)
|
||||||
|
? 'open ferry'
|
||||||
|
: 'open'
|
||||||
: ''
|
: ''
|
||||||
}`}
|
}`}
|
||||||
d={SHAPES[id] ?? ''}
|
d={SHAPES[id] ?? ''}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export function OrderPanel({
|
|||||||
orders,
|
orders,
|
||||||
step,
|
step,
|
||||||
illegal,
|
illegal,
|
||||||
|
bySea,
|
||||||
onAsk,
|
onAsk,
|
||||||
onClear,
|
onClear,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
@@ -47,6 +48,8 @@ export function OrderPanel({
|
|||||||
orders: ReadonlyMap<string, Order>
|
orders: ReadonlyMap<string, Order>
|
||||||
step: Step
|
step: Step
|
||||||
illegal: ReadonlySet<string>
|
illegal: ReadonlySet<string>
|
||||||
|
/** Whether any of the offered destinations needs a fleet to get there. */
|
||||||
|
bySea?: boolean
|
||||||
onAsk: (kind: Step['kind']) => void
|
onAsk: (kind: Step['kind']) => void
|
||||||
onClear: (at: string) => void
|
onClear: (at: string) => void
|
||||||
onSubmit: () => void
|
onSubmit: () => void
|
||||||
@@ -75,7 +78,7 @@ export function OrderPanel({
|
|||||||
)}
|
)}
|
||||||
<button onClick={() => onClear(selected)}>Hold</button>
|
<button onClick={() => onClear(selected)}>Hold</button>
|
||||||
</div>
|
</div>
|
||||||
<p className="hint dim">{hint(step)}</p>
|
<p className="hint dim">{hint(step, bySea ?? false)}</p>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<p className="hint dim">Click one of your units.</p>
|
<p className="hint dim">Click one of your units.</p>
|
||||||
@@ -100,12 +103,14 @@ export function OrderPanel({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function hint(step: Step): string {
|
function hint(step: Step, bySea: boolean): string {
|
||||||
switch (step.kind) {
|
switch (step.kind) {
|
||||||
case 'idle':
|
case 'idle':
|
||||||
return ''
|
return ''
|
||||||
case 'move':
|
case 'move':
|
||||||
return 'Click where it should go.'
|
return bySea
|
||||||
|
? 'Click where it should go. The coasts in blue need a fleet to carry it — order the convoy too.'
|
||||||
|
: 'Click where it should go.'
|
||||||
case 'support':
|
case 'support':
|
||||||
return step.from === undefined
|
return step.from === undefined
|
||||||
? 'Click the unit to support — or its own province, to hold it there.'
|
? 'Click the unit to support — or its own province, to hold it there.'
|
||||||
|
|||||||
+1
-1
@@ -331,7 +331,7 @@ export function seaRouteExists(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The seas a coastal province touches, whichever coast they are on. */
|
/** The seas a coastal province touches, whichever coast they are on. */
|
||||||
function coastalSeas(id: string): string[] {
|
export function coastalSeas(id: string): string[] {
|
||||||
const coasts = PROVINCES[id]?.coasts
|
const coasts = PROVINCES[id]?.coasts
|
||||||
const keys = coasts ? coasts.map((c) => `${id}/${c}`) : [id]
|
const keys = coasts ? coasts.map((c) => `${id}/${c}`) : [id]
|
||||||
return keys.flatMap((k) => (FLEET[k] ?? []).filter((n) => PROVINCES[base(n)]!.terrain === 'sea'))
|
return keys.flatMap((k) => (FLEET[k] ?? []).filter((n) => PROVINCES[base(n)]!.terrain === 'sea'))
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import type { Power } from './map'
|
import type { Power } from './map'
|
||||||
import { boardFrom, type Unit } from './orders'
|
import { boardFrom, type Unit } from './orders'
|
||||||
import { convoyable, convoyTargets, supportTargets, supportable } from './targets'
|
import {
|
||||||
|
convoyDestinations,
|
||||||
|
convoyTargets,
|
||||||
|
convoyable,
|
||||||
|
supportTargets,
|
||||||
|
supportable,
|
||||||
|
} from './targets'
|
||||||
|
|
||||||
const A = (power: Power, at: string): Unit => ({ power, type: 'army', at })
|
const A = (power: Power, at: string): Unit => ({ power, type: 'army', at })
|
||||||
const F = (power: Power, at: string): Unit => ({ power, type: 'fleet', at })
|
const F = (power: Power, at: string): Unit => ({ power, type: 'fleet', at })
|
||||||
@@ -53,18 +59,83 @@ describe('who a unit may support', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('who a fleet may carry', () => {
|
describe('who a fleet may carry', () => {
|
||||||
it('offers armies on coasts, and nobody else', () => {
|
it('offers armies on the coasts it touches, and nobody else', () => {
|
||||||
const board = boardFrom([F('england', 'nth'), A('england', 'lon'), A('germany', 'mun')])
|
const board = boardFrom([F('england', 'nth'), A('england', 'lon'), A('germany', 'mun')])
|
||||||
const who = convoyable(board, board.get('nth')!)
|
const who = convoyable(board, board.get('nth')!)
|
||||||
expect(who.has('lon')).toBe(true)
|
expect(who.has('lon')).toBe(true)
|
||||||
expect(who.has('mun')).toBe(false)
|
expect(who.has('mun')).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('does not offer an army its own chain cannot reach', () => {
|
||||||
|
// The North Sea has no business being asked to carry Ankara. Offering it
|
||||||
|
// wrote an order the adjudicator threw away without saying why.
|
||||||
|
const board = boardFrom([F('england', 'nth'), A('turkey', 'ank')])
|
||||||
|
expect(convoyable(board, board.get('nth')!).has('ank')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('reaches further when there are fleets to reach with', () => {
|
||||||
|
const alone = boardFrom([F('england', 'nth'), A('france', 'spa')])
|
||||||
|
expect(convoyable(alone, alone.get('nth')!).has('spa')).toBe(false)
|
||||||
|
|
||||||
|
const chain = boardFrom([
|
||||||
|
F('england', 'nth'),
|
||||||
|
F('england', 'eng'),
|
||||||
|
F('england', 'mao'),
|
||||||
|
A('france', 'spa'),
|
||||||
|
])
|
||||||
|
expect(convoyable(chain, chain.get('nth')!).has('spa')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
it('lands them on a coast that is not the one they left', () => {
|
it('lands them on a coast that is not the one they left', () => {
|
||||||
const where = convoyTargets('lon')
|
const board = boardFrom([F('england', 'nth'), A('england', 'lon')])
|
||||||
|
const where = convoyTargets(board, board.get('nth')!, 'lon')
|
||||||
expect(where.has('nwy')).toBe(true)
|
expect(where.has('nwy')).toBe(true)
|
||||||
expect(where.has('lon')).toBe(false)
|
expect(where.has('lon')).toBe(false)
|
||||||
expect(where.has('mun')).toBe(false)
|
expect(where.has('mun')).toBe(false)
|
||||||
expect(where.has('nth')).toBe(false)
|
expect(where.has('nth')).toBe(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('where an army may be carried', () => {
|
||||||
|
it('offers a coast across one sea with a fleet in it', () => {
|
||||||
|
const board = boardFrom([A('england', 'lon'), F('england', 'nth')])
|
||||||
|
const where = convoyDestinations(board, board.get('lon')!)
|
||||||
|
expect(where.has('nwy')).toBe(true)
|
||||||
|
expect(where.has('bel')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('offers nothing when there is no fleet to carry it', () => {
|
||||||
|
const board = boardFrom([A('england', 'lon')])
|
||||||
|
expect(convoyDestinations(board, board.get('lon')!).size).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('counts a fleet of any power, because that is what talking is for', () => {
|
||||||
|
const board = boardFrom([A('england', 'lon'), F('germany', 'nth')])
|
||||||
|
expect(convoyDestinations(board, board.get('lon')!).has('nwy')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('walks a chain, and stops where the chain does', () => {
|
||||||
|
// London to Spain wants the Channel and the Mid-Atlantic. With only the
|
||||||
|
// Channel crewed it reaches Brest and no further.
|
||||||
|
const one = boardFrom([A('england', 'lon'), F('england', 'eng')])
|
||||||
|
expect(convoyDestinations(one, one.get('lon')!).has('bre')).toBe(true)
|
||||||
|
expect(convoyDestinations(one, one.get('lon')!).has('spa')).toBe(false)
|
||||||
|
|
||||||
|
const two = boardFrom([A('england', 'lon'), F('england', 'eng'), F('england', 'mao')])
|
||||||
|
expect(convoyDestinations(two, two.get('lon')!).has('spa')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('never offers the army its own province, or anywhere inland', () => {
|
||||||
|
const board = boardFrom([A('england', 'lon'), F('england', 'nth')])
|
||||||
|
const where = convoyDestinations(board, board.get('lon')!)
|
||||||
|
expect(where.has('lon')).toBe(false)
|
||||||
|
expect(where.has('mun')).toBe(false)
|
||||||
|
expect(where.has('nth')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has nothing to say about a fleet, or an army inland', () => {
|
||||||
|
const board = boardFrom([F('england', 'lon'), A('germany', 'mun'), F('england', 'nth')])
|
||||||
|
expect(convoyDestinations(board, board.get('lon')!).size).toBe(0)
|
||||||
|
expect(convoyDestinations(board, board.get('mun')!).size).toBe(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
+73
-17
@@ -1,6 +1,6 @@
|
|||||||
import { reachableFrom } from './layout'
|
import { reachableFrom } from './layout'
|
||||||
import { PROVINCES, base } from './map'
|
import { FLEET, PROVINCES, base } from './map'
|
||||||
import type { Board, Unit } from './orders'
|
import { coastalSeas, type Board, type Unit } from './orders'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* What each half of an order may be clicked on.
|
* What each half of an order may be clicked on.
|
||||||
@@ -51,24 +51,80 @@ export function supportTargets(board: Board, unit: Unit, from: string): Set<stri
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The armies a fleet at sea may carry: any army on a coast but its own. */
|
/**
|
||||||
export function convoyable(board: Board, unit: Unit): Set<string> {
|
* The coasts a chain of crewed seas touches, starting from these.
|
||||||
|
*
|
||||||
|
* The model the whole convoy offering is built on: a sea counts if there is
|
||||||
|
* a fleet standing in it, whoever owns it, and the chain runs as far as the
|
||||||
|
* fleets do. A fleet that has not been ordered to convoy still counts -- it
|
||||||
|
* is a thing that could be arranged, which is what the negotiation is for,
|
||||||
|
* and the adjudicator will bounce the crossing if it is not.
|
||||||
|
*
|
||||||
|
* This is deliberately not the question the rules ask when they *judge* a
|
||||||
|
* convoy order, which is whether water could ever get there. That one says
|
||||||
|
* yes to most of Europe: it would offer thirty provinces because a chain of
|
||||||
|
* fleets is conceivable, which is worse than offering none.
|
||||||
|
*/
|
||||||
|
function coastsReached(board: Board, from: readonly string[]): Set<string> {
|
||||||
|
const crewed = (id: string) => board.get(base(id))?.type === 'fleet'
|
||||||
const out = new Set<string>()
|
const out = new Set<string>()
|
||||||
for (const [at, other] of board) {
|
const seen = new Set<string>()
|
||||||
if (other.type !== 'army') continue
|
const queue = [...from]
|
||||||
if (PROVINCES[at]!.terrain !== 'coast') continue
|
|
||||||
if (at === base(unit.at)) continue
|
while (queue.length > 0) {
|
||||||
out.add(at)
|
const sea = queue.shift()!
|
||||||
|
if (seen.has(sea)) continue
|
||||||
|
seen.add(sea)
|
||||||
|
for (const next of FLEET[sea] ?? []) {
|
||||||
|
const p = base(next)
|
||||||
|
if (PROVINCES[p]!.terrain === 'sea') {
|
||||||
|
if (crewed(p)) queue.push(p)
|
||||||
|
} else if (PROVINCES[p]!.terrain === 'coast') {
|
||||||
|
out.add(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Where it may be put ashore: any other coast. Whether a chain of fleets
|
/** The seas off this coast that have a fleet in them. */
|
||||||
* actually exists is the adjudicator's business, not the map's. */
|
const putToSea = (board: Board, coast: string): string[] =>
|
||||||
export function convoyTargets(from: string): Set<string> {
|
coastalSeas(coast)
|
||||||
return new Set(
|
.map(base)
|
||||||
Object.keys(PROVINCES).filter(
|
.filter((sea) => board.get(sea)?.type === 'fleet')
|
||||||
(p) => PROVINCES[p]!.terrain === 'coast' && p !== base(from),
|
|
||||||
),
|
/** Where an army could be carried to, given the fleets actually on the board. */
|
||||||
)
|
export function convoyDestinations(board: Board, unit: Unit): Set<string> {
|
||||||
|
const here = base(unit.at)
|
||||||
|
if (unit.type !== 'army' || PROVINCES[here]?.terrain !== 'coast') return new Set()
|
||||||
|
const out = coastsReached(board, putToSea(board, here))
|
||||||
|
out.delete(here)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The armies this fleet could carry.
|
||||||
|
*
|
||||||
|
* Only the ones its own chain can actually reach. Offering every army on
|
||||||
|
* every coast wrote orders the adjudicator threw away: the North Sea has no
|
||||||
|
* business being asked to carry an army out of Ankara.
|
||||||
|
*/
|
||||||
|
export function convoyable(board: Board, unit: Unit): Set<string> {
|
||||||
|
const sea = base(unit.at)
|
||||||
|
if (unit.type !== 'fleet' || PROVINCES[sea]?.terrain !== 'sea') return new Set()
|
||||||
|
const ashore = coastsReached(board, [sea])
|
||||||
|
const out = new Set<string>()
|
||||||
|
for (const coast of ashore) {
|
||||||
|
if (board.get(coast)?.type === 'army') out.add(coast)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Where this fleet could put that army ashore: the same chain, less home. */
|
||||||
|
export function convoyTargets(board: Board, unit: Unit, from: string): Set<string> {
|
||||||
|
const sea = base(unit.at)
|
||||||
|
if (unit.type !== 'fleet' || PROVINCES[sea]?.terrain !== 'sea') return new Set()
|
||||||
|
const out = coastsReached(board, [sea])
|
||||||
|
out.delete(base(from))
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user