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.
This commit is contained in:
2026-09-09 10:04:15 -07:00
parent 35b52f3453
commit 9dc28e0d7a
+57 -13
View File
@@ -334,6 +334,17 @@ function reaches(unit: Unit, to: string): boolean {
* Could water ever carry an army from here to there, whatever anybody has * 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 * 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. * 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( export function seaRouteExists(
from: string, from: string,
@@ -347,23 +358,56 @@ export function seaRouteExists(
if (start === end) return false if (start === end) return false
if (PROVINCES[start]?.terrain !== 'coast' || PROVINCES[end]?.terrain !== 'coast') 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) => 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. /** Does this node of the chain touch that one? */
const seen = new Set<string>() const touches = (a: string, b: string): boolean =>
const queue: [string, boolean][] = [] a === start
for (const sea of coastalSeas(start)) if (sea !== avoiding) queue.push([sea, sea === through]) ? 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()! * Depth first over chains rather than breadth first over seas, because
const key = `${sea}:${used}` * properness is a fact about the whole chain so far and cannot be decided
if (seen.has(key)) continue * one sea at a time. The map has nineteen seas and the chains are short.
seen.add(key) */
if ((FLEET[sea] ?? []).some((n) => base(n) === end) && (!through || used)) return true const walk = (path: string[], used: boolean): boolean => {
for (const next of seas(sea)) queue.push([next, used || next === through]) 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)
} }
/** /**