From 9dc28e0d7ab6f69c4b2146d89fe276b3cc371921 Mon Sep 17 00:00:00 2001 From: John Coffey Date: Wed, 9 Sep 2026 10:04:15 -0700 Subject: [PATCH] A convoy order from a fleet with nothing to do 6.G.19. The document's rule, from issue 4.E.1: for a convoy order the fleet must be part of at least one route for which the fleets are available, and it must not be superfluous -- there has to be a route, ordered or not, for which that fleet is necessary. The path finder implements it by considering only proper routes, where the chain never jumps to a province an earlier fleet already reached. Tunis - Ionian - Adriatic - Albania is not proper, because the Ionian touches Albania and the Adriatic is doing nothing. In graph terms the chain has no chords. It matters more than a tidy-up. In 6.G.19 France orders a convoy from the Western Mediterranean for Marseilles to Spain when the Gulf of Lyon already touches both. If that order counts, France has expressed an intent to go by sea and the two units swap places; if it does not, they walk into each other and bounce. The document's own advice is that an interactive system should never have offered the order. Properness is asked only when a particular fleet is being judged, which is the only question the document uses it for. Asking it of the plain question -- could water get an army there at all -- is a different and wrong claim, and doing so broke five cases where a convoy legitimately runs the long way round because the short way has no fleet in it. The rewrite from breadth-first over seas to depth-first over chains needed the first step back: a two-coasted province has no bare entry in the fleet graph, so an army boarding in Bulgaria found no water to start from until both coasts' seas were offered to it. --- src/game/orders.ts | 70 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 57 insertions(+), 13 deletions(-) diff --git a/src/game/orders.ts b/src/game/orders.ts index 692704c..89b056f 100644 --- a/src/game/orders.ts +++ b/src/game/orders.ts @@ -334,6 +334,17 @@ function reaches(unit: Unit, to: string): boolean { * 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. + * + * Only **proper** routes count, which is the document's word (issue 4.E.1) + * and is what makes a superfluous convoy order illegal rather than merely + * pointless. A proper route never jumps to a province an earlier fleet in + * the chain could already have reached: Tunis - Ionian - Adriatic - Albania + * is not one, because the Ionian touches Albania and the Adriatic is doing + * nothing. In graph terms the chain has no chords. + * + * Without it a fleet can claim to be carrying an army it is standing beside + * the point of, and that claim carries weight -- in 6.G.19 it is the + * difference between two units swapping places and bouncing off each other. */ export function seaRouteExists( from: string, @@ -347,23 +358,56 @@ export function seaRouteExists( if (start === end) return false if (PROVINCES[start]?.terrain !== 'coast' || PROVINCES[end]?.terrain !== 'coast') return false + /* + * The seas out of a node. The first step is its own case: 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 boarding in Bulgaria + * has to be given both coasts' seas or it finds no water to start from. + */ const seas = (id: string) => - (FLEET[id] ?? []).filter((n) => PROVINCES[base(n)]!.terrain === 'sea' && n !== avoiding) + (id === start ? coastalSeas(start) : (FLEET[id] ?? [])).filter( + (n) => PROVINCES[base(n)]!.terrain === 'sea' && n !== avoiding, + ) - // Walk the seas, remembering whether the required one has been used. - const seen = new Set() - const queue: [string, boolean][] = [] - for (const sea of coastalSeas(start)) if (sea !== avoiding) queue.push([sea, sea === through]) + /** Does this node of the chain touch that one? */ + const touches = (a: string, b: string): boolean => + a === start + ? coastalSeas(start).includes(b) + : b === end + ? (FLEET[a] ?? []).some((n) => base(n) === end) + : (FLEET[a] ?? []).includes(b) - 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]) + /* + * Depth first over chains rather than breadth first over seas, because + * properness is a fact about the whole chain so far and cannot be decided + * one sea at a time. The map has nineteen seas and the chains are short. + */ + const walk = (path: string[], used: boolean): boolean => { + const last = path[path.length - 1]! + /* + * A jump is proper if nothing before the current tail already reached + * where we are going. + * + * Only asked when a particular fleet is being judged, which is the only + * question the document uses it for: whether *that* fleet is superfluous + * to every route it could belong to. Asking it of the plain question -- + * could water get an army there at all -- is a different and wrong + * claim, and it broke five cases where a convoy legitimately runs the + * long way round because the short way has no fleet in it. + */ + const proper = (next: string) => + through === undefined || + !path.slice(0, -1).some((earlier) => touches(earlier, next)) + + if (touches(last, end) && proper(end) && (!through || used)) return true + for (const next of seas(last)) { + if (path.includes(next) || !proper(next)) continue + if (walk([...path, next], used || next === through)) return true + } + return false } - return false + + return walk([start], false) } /**