Convoy intent, coasts in support, and what a disrupted convoy is
122 of 139 movement cases now. Intent, which decides whether two units swapping bounce or sail past each other: an army goes by convoy if it was ordered to, or if its own power gave a legal convoy order for that move. Somebody else's fleets offering a route is not intent however good the route, and your own fleet offering from a sea it could never do it from is not intent either, because that was not an order. Once intent exists any ordered route may carry it, foreign or not. A convoy order is only an order if water could actually carry that army past that fleet, which is a question about the map and is now asked. Support is given to a province, not to a coast of one: a fleet in Marseilles may support an attack on the north coast of Spain although it could never sail there. And a support is only as legal as the move underneath it. Two bugs of my own. Convoy routing could not start from or arrive at a province with two coasts, because the fleet graph has no bare entry for one -- there is no 'bul', only 'bul/ec' and 'bul/sc' -- so an army boarding in Bulgaria found no sea and stayed home. And I had read 'disrupted' as the escort being sunk when it means the army not arriving; in Pandin's Paradox the fleet survives and the army still does not get there.
This commit is contained in:
+25
-3
@@ -117,14 +117,36 @@ function resolveAll(
|
||||
return dest
|
||||
}
|
||||
|
||||
/** Is this move going over water rather than across a border? */
|
||||
/**
|
||||
* Is this move going over water rather than across a border?
|
||||
*
|
||||
* Only interesting when both are possible, and then it is a question about
|
||||
* intent rather than about the map -- and intent decides whether two units
|
||||
* swapping places bounce off each other or sail past each other.
|
||||
*
|
||||
* The rule the published cases settle on: an army goes by convoy if it was
|
||||
* ordered to, or if its **own power** gave at least one convoy order for
|
||||
* that move that was a legal order. Somebody else's fleets offering to
|
||||
* carry you is not intent, however good the route -- and your own fleet
|
||||
* offering from a sea it could never do it from is not intent either,
|
||||
* because that was not an order. Once intent exists, any ordered route may
|
||||
* do the carrying, including a foreign one.
|
||||
*/
|
||||
function isConvoyed(p: string): boolean {
|
||||
const o = orderAt(p)
|
||||
const unit = unitAt(p)
|
||||
if (o?.type !== 'move' || unit?.type !== 'army') return false
|
||||
|
||||
const overland = (ARMY[base(unit.at)] ?? []).includes(base(o.to))
|
||||
if (overland && !o.viaConvoy) return false
|
||||
return true
|
||||
if (!overland) return true
|
||||
if (o.viaConvoy) return true
|
||||
|
||||
for (const [q, c] of orders) {
|
||||
if (c.type !== 'convoy') continue
|
||||
if (base(c.from) !== p || base(c.to) !== base(o.to)) continue
|
||||
if (unitAt(q)?.power === unit.power) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** Fleets convoying that are being thrown out of their sea as we speak. */
|
||||
|
||||
+14
-1
@@ -52,10 +52,23 @@ describe('DATC', () => {
|
||||
break
|
||||
case 'fails':
|
||||
case 'cut':
|
||||
case 'disrupted':
|
||||
case 'illegal':
|
||||
expect(outcome.success.get(province), `${province} ${mark}`).toBe(false)
|
||||
break
|
||||
case 'disrupted': {
|
||||
/*
|
||||
* A disrupted convoy is one that did not deliver its army. That
|
||||
* is not the same as its fleet being sunk -- in Pandin's Paradox
|
||||
* the fleet survives and the army still does not arrive -- so
|
||||
* the thing to check is the army, not the escort.
|
||||
*/
|
||||
const convoy = c.orders.find(
|
||||
(o) => o.type === 'convoy' && o.at.split('/')[0] === province,
|
||||
)
|
||||
const army = convoy && 'from' in convoy ? convoy.from.split('/')[0] : province
|
||||
expect(outcome.success.get(army), `${province} disrupted`).toBe(false)
|
||||
break
|
||||
}
|
||||
case 'dislodged':
|
||||
expect(outcome.dislodged.has(province), `${province} dislodged`).toBe(true)
|
||||
break
|
||||
|
||||
+86
-13
@@ -86,17 +86,23 @@ export function convoyRoute(
|
||||
}
|
||||
if (convoyers.size === 0) return null
|
||||
|
||||
// Breadth first, so the route found is the shortest one available.
|
||||
/*
|
||||
* Breadth first, so the route found is the shortest one available.
|
||||
*
|
||||
* Both ends are compared by province rather than by key. A two-coasted
|
||||
* province has no bare entry in the fleet graph at all -- there is no
|
||||
* 'bul', only 'bul/ec' and 'bul/sc' -- so an army walking aboard in
|
||||
* Bulgaria found no sea to start from and simply stayed there.
|
||||
*/
|
||||
const fromSeas = coastalSeas(start)
|
||||
const queue: string[][] = []
|
||||
for (const sea of convoyers) {
|
||||
if ((FLEET[start] ?? []).includes(sea)) queue.push([sea])
|
||||
}
|
||||
for (const sea of convoyers) if (fromSeas.includes(sea)) queue.push([sea])
|
||||
const seen = new Set<string>()
|
||||
|
||||
while (queue.length > 0) {
|
||||
const path = queue.shift()!
|
||||
const last = path[path.length - 1]!
|
||||
if ((FLEET[last] ?? []).includes(end)) return path
|
||||
if ((FLEET[last] ?? []).some((n) => base(n) === end)) return path
|
||||
if (seen.has(last)) continue
|
||||
seen.add(last)
|
||||
for (const next of FLEET[last] ?? []) {
|
||||
@@ -132,6 +138,7 @@ export function validate(board: Board, given: readonly Order[]): Validated {
|
||||
const illegal = new Set<string>()
|
||||
|
||||
const refused = new Set<string>()
|
||||
const supports: Extract<Order, { type: 'support' }>[] = []
|
||||
|
||||
for (const order of given) {
|
||||
const at = base(order.at)
|
||||
@@ -143,12 +150,38 @@ export function validate(board: Board, given: readonly Order[]): Validated {
|
||||
refused.add(at)
|
||||
continue
|
||||
}
|
||||
if (order.type === 'support') {
|
||||
supports.push(order)
|
||||
continue
|
||||
}
|
||||
|
||||
const ok = check(board, unit, order)
|
||||
if (ok) orders.set(at, ok)
|
||||
else refused.add(at)
|
||||
}
|
||||
|
||||
/*
|
||||
* Supports last, because a support is only as legal as the move underneath
|
||||
* it. You cannot support a fleet into an inland province by wishing: the
|
||||
* move was never an order, so neither was the support of it.
|
||||
*/
|
||||
for (const order of supports) {
|
||||
const at = base(order.at)
|
||||
const unit = board.get(at)!
|
||||
const ok = check(board, unit, order)
|
||||
if (!ok) {
|
||||
refused.add(at)
|
||||
continue
|
||||
}
|
||||
const supported = orders.get(base(order.from))
|
||||
const wasRefused = refused.has(base(order.from))
|
||||
if (base(order.from) !== base(order.to) && wasRefused && supported?.type !== 'move') {
|
||||
refused.add(at)
|
||||
continue
|
||||
}
|
||||
orders.set(at, ok)
|
||||
}
|
||||
|
||||
for (const at of refused) if (!orders.has(at)) illegal.add(at)
|
||||
for (const [p] of board) if (!orders.has(p)) orders.set(p, { type: 'hold', at: p })
|
||||
return { orders, illegal }
|
||||
@@ -181,7 +214,7 @@ function check(board: Board, unit: Unit, order: Order): Order | null {
|
||||
if (PROVINCES[base(unit.at)]!.terrain !== 'sea') return null
|
||||
const army = board.get(base(order.from))
|
||||
if (!army || army.type !== 'army') return null
|
||||
return convoyable(army, order.to) ? order : null
|
||||
return seaRouteExists(order.from, order.to, base(unit.at)) ? order : null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -196,15 +229,55 @@ function settleCoast(unit: Unit, to: string): string | null {
|
||||
return reachable.length === 1 ? reachable[0]! : null
|
||||
}
|
||||
|
||||
/** Could this unit ever step here, on any coast? Used for supports. */
|
||||
/**
|
||||
* Could this unit ever step here, on any coast?
|
||||
*
|
||||
* Support is given to a province rather than to a coast of one. A fleet in
|
||||
* Marseilles may support an attack on the north coast of Spain even though it
|
||||
* could never sail there itself -- it is holding the province down, not
|
||||
* sailing round it.
|
||||
*/
|
||||
function reaches(unit: Unit, to: string): boolean {
|
||||
if (unit.type === 'army') return (ARMY[base(unit.at)] ?? []).includes(base(to))
|
||||
if (to.includes('/')) return (FLEET[unit.at] ?? []).includes(to)
|
||||
return fleetCoasts(unit.at, to).length > 0
|
||||
return fleetCoasts(unit.at, base(to)).length > 0
|
||||
}
|
||||
|
||||
/** Both ends ashore, which is the most a convoy can be judged before it sails. */
|
||||
/**
|
||||
* Could water ever carry an army from here to there, whatever anybody has
|
||||
* ordered? A question about the map alone, and the one that makes a convoy
|
||||
* order from a fleet nowhere near the route no order at all.
|
||||
*/
|
||||
export function seaRouteExists(from: string, to: string, through?: string): boolean {
|
||||
const start = base(from)
|
||||
const end = base(to)
|
||||
if (PROVINCES[start]?.terrain !== 'coast' || PROVINCES[end]?.terrain !== 'coast') return false
|
||||
|
||||
const seas = (id: string) =>
|
||||
(FLEET[id] ?? []).filter((n) => PROVINCES[base(n)]!.terrain === 'sea')
|
||||
|
||||
// Walk the seas, remembering whether the required one has been used.
|
||||
const seen = new Set<string>()
|
||||
const queue: [string, boolean][] = []
|
||||
for (const sea of coastalSeas(start)) queue.push([sea, sea === through])
|
||||
|
||||
while (queue.length > 0) {
|
||||
const [sea, used] = queue.shift()!
|
||||
const key = `${sea}:${used}`
|
||||
if (seen.has(key)) continue
|
||||
seen.add(key)
|
||||
if ((FLEET[sea] ?? []).some((n) => base(n) === end) && (!through || used)) return true
|
||||
for (const next of seas(sea)) queue.push([next, used || next === through])
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** The seas a coastal province touches, whichever coast they are on. */
|
||||
function coastalSeas(id: string): string[] {
|
||||
const coasts = PROVINCES[id]?.coasts
|
||||
const keys = coasts ? coasts.map((c) => `${id}/${c}`) : [id]
|
||||
return keys.flatMap((k) => (FLEET[k] ?? []).filter((n) => PROVINCES[base(n)]!.terrain === 'sea'))
|
||||
}
|
||||
|
||||
/** Both ends ashore and some water between them. */
|
||||
const convoyable = (unit: Unit, to: string): boolean =>
|
||||
unit.type === 'army' &&
|
||||
PROVINCES[base(unit.at)]!.terrain === 'coast' &&
|
||||
PROVINCES[base(to)]!.terrain === 'coast'
|
||||
unit.type === 'army' && seaRouteExists(unit.at, to)
|
||||
|
||||
Reference in New Issue
Block a user