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:
+56
-12
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user