diff --git a/README.md b/README.md index 1bea7b4..136e354 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ published cases is parsed and run. | Section | | Passing | | --- | --- | --- | -| 6.A - 6.G | movement | 133 / 139 | +| 6.A - 6.G | movement | 136 / 139 | | 6.H | retreating | 18 / 18 | | 6.I | building | 7 / 7 | | 6.J | civil disorder | 13 / 13 | @@ -121,14 +121,45 @@ Three cases are excluded rather than failed. 6.A.6, 6.B.10 and 6.B.11 state their setup in prose -- "Germany has a fleet in London" -- and the fixture is built from order lines, so it cannot carry a unit nobody ordered. -Six are known deviations, written down so they are not rediscovered as +Three are known deviations, written down so they are not rediscovered as surprises: | Case | What it is | | --- | --- | -| 6.A.5, 6.D.8 | The document distinguishes an `invalid` order from an `illegal` one, and the two cases disagree about whether a unit whose move was refused keeps its hold support. Guessing at the distinction cost four passing cases to buy one, so it is left until it can be read properly. | -| 6.F.22, 6.F.28, 6.F.29 | Second and sixth order paradoxes. The resolver settles them by Szykman's rule and reaches a different answer than the document's. These are positions nobody assembles by accident. | -| 6.G.19 | Whether an unnecessary fleet in a convoy chain signals intent. The document notes this one depends on which edition of the rules is used. | +| 6.F.28, 6.F.29 | Sixth order paradoxes: the same twenty-six orders with one extra support between them, which the document uses to show that the answer swings on it. The resolver gets five of the six sub-paradoxes right in both and the sixth wrong, and gives the same answer to both positions when the point is that they differ. These are positions nobody assembles by accident. | +| 6.G.19 | Whether an unnecessary fleet in a convoy chain signals intent. The document gives three different answers by edition, calls the question "a little bit pedantic", and advises that web adjudicators should not offer the order at all -- which is the line this one takes. | + +### What the failing cases were worth + +They found three real faults, which is the whole argument for running +somebody else's tests rather than only your own. + +**The illegal/invalid line (6.A.5, 6.D.8).** The document separates an order +that is impossible *in this position* from one this position allows that +merely did not come off. The first is ignored and the unit is holding like +any other; the second means the unit tried to leave and cannot be propped up +where it stands. An army sent across water with a fleet on the route and no +convoy ordered is the second (6.D.8); the identical order with nobody in the +sea at all is the first (6.D.32). Guessing at this once cost four passing +cases to buy one. Reading it properly cost nothing. + +**A support that only looked like a hold (6.A.5).** "Supports A Yorkshire" +and "Supports A Yorkshire - Yorkshire" are the same shape once parsed, and +the case turns on the difference: the first holds a unit down, the second +supports a move nobody can legally make. The extractor now keeps the +distinction; it was losing it. + +**The head of a cycle (6.F.22).** Kruijswijk's resolver guesses that an order +fails, sees what follows, and guesses the other way. Only the *outermost* +order in a cycle may do that, because its answer is the one everything else +was computed against. This engine let any order that reached the dependency +list first declare the cycle its own -- and an inner one takes both guesses +with its callers' provisional answers held fixed, gets the same result twice +for that reason, and records it as settled. The cycle then becomes +invisible: the backup rule never runs, and the position resolves to +whichever of its two consistent readings the search walked into first. It +had been doing that on every paradox and getting away with it, because on a +first-order paradox both readings agree. Sections H, I and J -- retreating and the winter -- have engine code but no harness yet; their cases are shaped differently and need one. diff --git a/src/game/adjudicate.ts b/src/game/adjudicate.ts index e5f1eae..b5336fb 100644 --- a/src/game/adjudicate.ts +++ b/src/game/adjudicate.ts @@ -83,7 +83,7 @@ function resolveAll( forced: ReadonlySet, ): Outcome { // A unit with no order holds, and so does a unit whose order was refused. - const { orders, illegal } = validate(board, orderList) + const { orders, illegal, orderedAway } = validate(board, orderList) const state = new Map() const result = new Map() @@ -236,14 +236,13 @@ function resolveAll( if (o?.type === 'move') return resolve(dest) ? 0 : 1 /* * A unit told to move cannot be supported where it stands, and the first - * branch above covers that. What is *not* settled here is the unit whose - * move order was refused outright: 6.D.8 says it loses its hold support - * too, while 6.D.28 to 6.D.32 say a unit in much the same position keeps - * it. The difference is the document's distinction between an order that - * is `invalid` and one that is `illegal`, which I have not pinned down, - * and guessing at it cost four passing cases to buy one. Left alone - * until it can be read properly rather than inferred. + * branch above covers the ones whose orders survived. This covers the + * rest: an order this position allowed and that merely did not come off + * still means the unit tried to leave. An order the position never + * allowed is ignored, and that unit is holding like any other and may be + * held down. See `orderedAway` for which is which. */ + if (orderedAway.has(dest)) return 1 return 1 + supportsToHold(dest) } @@ -316,6 +315,8 @@ function resolveAll( * agreeing answers are the answer, and disagreeing ones mean a genuine * cycle, which the backup rule below settles. */ + const stack: string[] = [] + function resolve(p: string): boolean { const s = state.get(p) if (s === 'resolved') return result.get(p)! @@ -325,18 +326,55 @@ function resolveAll( } const mark = dep.length + /* + * Who is already guessing further down the stack. + * + * This is the difference between settling a cycle and appearing to. Only + * the *outermost* order in a cycle may take the two guesses, because its + * answer is the one everything else was computed against. An inner order + * that finds itself first in the dependency list will otherwise declare + * the cycle its own, take both guesses with its callers' provisional + * answers held fixed, get the same result twice for that reason, and + * record it as settled. The cycle is then invisible: the backup rule + * never runs, and the position quietly resolves to whichever of its two + * consistent readings the search happened to walk into first. + * + * 6.F.22 is the case that found this. The English Channel put itself + * forward as the head while Edinburgh and London -- both in the same + * paradox -- were still on the stack below it. + */ + const below = new Set(stack) + stack.push(p) state.set(p, 'guessing') result.set(p, false) const first = adjudicateOne(p) if (dep.length === mark) { - state.set(p, 'resolved') - result.set(p, first) - return first + /* + * Nothing depended on the guess, so the answer stands -- unless the + * order resolved itself while we were away. A nested call can reach + * the backup rule, settle this very province, and return; writing the + * guess over that answer loses it, and the cycle it was settling + * quietly re-forms as a fixed point nobody detects. + */ + stack.pop() + if (state.get(p) !== 'resolved') { + state.set(p, 'resolved') + result.set(p, first) + } + return result.get(p)! } - if (dep[mark] !== p) { - // Somebody above us is the one really guessing; report and let them ask. + /* + * Am I the outermost order of this cycle? + * + * Only if the cycle came back round to me at all, and only if none of + * its other members is still waiting further down the stack. Anything + * else reports what it has and lets the one below ask. + */ + const cycle = dep.slice(mark) + if (!cycle.includes(p) || cycle.some((q) => below.has(q))) { + stack.pop() dep.push(p) result.set(p, first) return first @@ -350,11 +388,13 @@ function resolveAll( if (first === second) { while (dep.length > mark) state.set(dep.pop()!, 'unresolved') + stack.pop() state.set(p, 'resolved') result.set(p, first) return first } + stack.pop() backup(mark) return resolve(p) } diff --git a/src/game/datc.json b/src/game/datc.json index fa9115d..e2e7135 100644 --- a/src/game/datc.json +++ b/src/game/datc.json @@ -167,6 +167,7 @@ { "type": "support", "at": "lvp", + "ofMove": true, "from": "yor", "to": "yor", "power": "england" @@ -181,6 +182,7 @@ { "type": "support", "at": "wal", + "ofMove": true, "from": "lon", "to": "yor", "power": "germany" @@ -321,6 +323,7 @@ { "type": "support", "at": "tyr", + "ofMove": true, "from": "ven", "to": "tri", "power": "italy" @@ -410,6 +413,7 @@ { "type": "support", "at": "rom", + "ofMove": true, "from": "apu", "to": "ven", "power": "italy" @@ -665,6 +669,7 @@ { "type": "support", "at": "mar", + "ofMove": true, "from": "gas", "to": "spa/nc", "power": "france" @@ -725,6 +730,7 @@ { "type": "support", "at": "spa/nc", + "ofMove": true, "from": "mar", "to": "lyo", "power": "france" @@ -786,6 +792,7 @@ { "type": "support", "at": "iri", + "ofMove": true, "from": "nao", "to": "mao", "power": "england" @@ -869,6 +876,7 @@ { "type": "support", "at": "por", + "ofMove": true, "from": "mao", "to": "spa", "power": "france" @@ -883,6 +891,7 @@ { "type": "support", "at": "lyo", + "ofMove": true, "from": "wes", "to": "spa/sc", "power": "italy" @@ -944,6 +953,7 @@ { "type": "support", "at": "por", + "ofMove": true, "from": "gas", "to": "spa", "power": "france" @@ -958,6 +968,7 @@ { "type": "support", "at": "lyo", + "ofMove": true, "from": "wes", "to": "spa/sc", "power": "italy" @@ -1019,6 +1030,7 @@ { "type": "support", "at": "por", + "ofMove": true, "from": "mao", "to": "spa/nc", "power": "france" @@ -1033,6 +1045,7 @@ { "type": "support", "at": "lyo", + "ofMove": true, "from": "wes", "to": "spa/sc", "power": "italy" @@ -1264,6 +1277,7 @@ { "type": "support", "at": "por", + "ofMove": true, "from": "mao", "to": "spa", "power": "france" @@ -1278,6 +1292,7 @@ { "type": "support", "at": "lyo", + "ofMove": true, "from": "wes", "to": "spa/sc", "power": "italy" @@ -1346,6 +1361,7 @@ { "type": "support", "at": "mar", + "ofMove": true, "from": "gas", "to": "spa", "power": "france" @@ -1419,6 +1435,7 @@ { "type": "support", "at": "wes", + "ofMove": true, "from": "spa/nc", "to": "mao", "power": "italy" @@ -1634,6 +1651,7 @@ { "type": "support", "at": "bul", + "ofMove": true, "from": "ank", "to": "con", "power": "turkey" @@ -1959,6 +1977,7 @@ { "type": "support", "at": "tun", + "ofMove": true, "from": "nap", "to": "ion", "power": "italy" @@ -2210,6 +2229,7 @@ { "type": "support", "at": "smy", + "ofMove": true, "from": "bul", "to": "con", "power": "turkey" @@ -2293,6 +2313,7 @@ { "type": "support", "at": "smy", + "ofMove": true, "from": "bul", "to": "con", "power": "turkey" @@ -2371,6 +2392,7 @@ { "type": "support", "at": "adr", + "ofMove": true, "from": "tri", "to": "ven", "power": "austria" @@ -2449,6 +2471,7 @@ { "type": "support", "at": "adr", + "ofMove": true, "from": "tri", "to": "ven", "power": "austria" @@ -2532,6 +2555,7 @@ { "type": "support", "at": "adr", + "ofMove": true, "from": "tri", "to": "ven", "power": "austria" @@ -2619,6 +2643,7 @@ { "type": "support", "at": "bal", + "ofMove": true, "from": "pru", "to": "ber", "power": "russia" @@ -2685,6 +2710,7 @@ { "type": "support", "at": "ber", + "ofMove": true, "from": "mun", "to": "sil", "power": "germany" @@ -2706,6 +2732,7 @@ { "type": "support", "at": "bal", + "ofMove": true, "from": "pru", "to": "ber", "power": "russia" @@ -2803,6 +2830,7 @@ { "type": "support", "at": "bot", + "ofMove": true, "from": "lvn", "to": "bal", "power": "russia" @@ -2886,6 +2914,7 @@ { "type": "support", "at": "bot", + "ofMove": true, "from": "lvn", "to": "bal", "power": "russia" @@ -2961,6 +2990,7 @@ { "type": "support", "at": "ser", + "ofMove": true, "from": "alb", "to": "gre", "power": "austria" @@ -3047,6 +3077,7 @@ { "type": "support", "at": "tyr", + "ofMove": true, "from": "ven", "to": "tri", "power": "italy" @@ -3054,6 +3085,7 @@ { "type": "support", "at": "alb", + "ofMove": true, "from": "tri", "to": "ser", "power": "austria" @@ -3120,6 +3152,7 @@ { "type": "support", "at": "mun", + "ofMove": true, "from": "kie", "to": "ber", "power": "germany" @@ -3185,6 +3218,7 @@ { "type": "support", "at": "mun", + "ofMove": true, "from": "kie", "to": "ber", "power": "germany" @@ -3246,6 +3280,7 @@ { "type": "support", "at": "vie", + "ofMove": true, "from": "ven", "to": "tri", "power": "austria" @@ -3311,6 +3346,7 @@ { "type": "support", "at": "vie", + "ofMove": true, "from": "ven", "to": "tri", "power": "austria" @@ -3389,6 +3425,7 @@ { "type": "support", "at": "vie", + "ofMove": true, "from": "ven", "to": "tri", "power": "austria" @@ -3403,6 +3440,7 @@ { "type": "support", "at": "tyr", + "ofMove": true, "from": "ven", "to": "tri", "power": "italy" @@ -3410,6 +3448,7 @@ { "type": "support", "at": "adr", + "ofMove": true, "from": "ven", "to": "tri", "power": "italy" @@ -3462,6 +3501,7 @@ { "type": "support", "at": "con", + "ofMove": true, "from": "bla", "to": "ank", "power": "russia" @@ -3540,6 +3580,7 @@ { "type": "support", "at": "eng", + "ofMove": true, "from": "bel", "to": "lon", "power": "france" @@ -3606,6 +3647,7 @@ { "type": "support", "at": "con", + "ofMove": true, "from": "bla", "to": "ank", "power": "russia" @@ -3627,6 +3669,7 @@ { "type": "support", "at": "smy", + "ofMove": true, "from": "ank", "to": "con", "power": "turkey" @@ -3702,6 +3745,7 @@ { "type": "support", "at": "con", + "ofMove": true, "from": "bla", "to": "ank", "power": "russia" @@ -3730,6 +3774,7 @@ { "type": "support", "at": "smy", + "ofMove": true, "from": "ank", "to": "con", "power": "turkey" @@ -3798,6 +3843,7 @@ { "type": "support", "at": "con", + "ofMove": true, "from": "bla", "to": "ank", "power": "russia" @@ -3812,6 +3858,7 @@ { "type": "support", "at": "smy", + "ofMove": true, "from": "ank", "to": "con", "power": "russia" @@ -3874,6 +3921,7 @@ { "type": "support", "at": "lon", + "ofMove": true, "from": "nth", "to": "eng", "power": "england" @@ -3969,6 +4017,7 @@ { "type": "support", "at": "tyr", + "ofMove": true, "from": "ven", "to": "tri", "power": "italy" @@ -3990,6 +4039,7 @@ { "type": "support", "at": "ber", + "ofMove": true, "from": "sil", "to": "mun", "power": "russia" @@ -4058,6 +4108,7 @@ { "type": "support", "at": "bur", + "ofMove": true, "from": "kie", "to": "mun", "power": "germany" @@ -4072,6 +4123,7 @@ { "type": "support", "at": "ber", + "ofMove": true, "from": "mun", "to": "kie", "power": "russia" @@ -4134,6 +4186,7 @@ { "type": "support", "at": "wes", + "ofMove": true, "from": "lyo", "to": "spa/sc", "power": "italy" @@ -4148,6 +4201,7 @@ { "type": "support", "at": "mar", + "ofMove": true, "from": "spa/nc", "to": "lyo", "power": "france" @@ -4215,6 +4269,7 @@ { "type": "support", "at": "spa/sc", + "ofMove": true, "from": "mar", "to": "lyo", "power": "france" @@ -4227,6 +4282,7 @@ { "type": "support", "at": "tys", + "ofMove": true, "from": "wes", "to": "lyo", "power": "turkey" @@ -4305,6 +4361,7 @@ { "type": "support", "at": "bal", + "ofMove": true, "from": "pru", "to": "ber", "power": "russia" @@ -4366,6 +4423,7 @@ { "type": "support", "at": "ber", + "ofMove": true, "from": "pru", "to": "sil", "power": "germany" @@ -4380,6 +4438,7 @@ { "type": "support", "at": "bal", + "ofMove": true, "from": "pru", "to": "ber", "power": "russia" @@ -4453,6 +4512,7 @@ { "type": "support", "at": "den", + "ofMove": true, "from": "swe", "to": "bal", "power": "england" @@ -4550,6 +4610,7 @@ { "type": "support", "at": "bul", + "ofMove": true, "from": "bla", "to": "rum", "power": "turkey" @@ -4625,6 +4686,7 @@ { "type": "support", "at": "bul", + "ofMove": true, "from": "bla", "to": "rum", "power": "turkey" @@ -4700,6 +4762,7 @@ { "type": "support", "at": "bul", + "ofMove": true, "from": "bla", "to": "con", "power": "turkey" @@ -4751,6 +4814,7 @@ { "type": "support", "at": "bla", + "ofMove": true, "from": "rum", "to": "arm", "power": "turkey" @@ -4799,6 +4863,7 @@ { "type": "support", "at": "edi", + "ofMove": true, "from": "lvp", "to": "yor", "power": "england" @@ -4888,6 +4953,7 @@ { "type": "support", "at": "gal", + "ofMove": true, "from": "ser", "to": "bud", "power": "russia" @@ -4966,6 +5032,7 @@ { "type": "support", "at": "sil", + "ofMove": true, "from": "ber", "to": "pru", "power": "germany" @@ -4973,6 +5040,7 @@ { "type": "support", "at": "bal", + "ofMove": true, "from": "ber", "to": "pru", "power": "germany" @@ -4980,6 +5048,7 @@ { "type": "support", "at": "pru", + "ofMove": true, "from": "lvn", "to": "pru", "power": "italy" @@ -4987,6 +5056,7 @@ { "type": "support", "at": "war", + "ofMove": true, "from": "lvn", "to": "pru", "power": "russia" @@ -5072,6 +5142,7 @@ { "type": "support", "at": "con", + "ofMove": true, "from": "bla", "to": "ank", "power": "russia" @@ -5086,6 +5157,7 @@ { "type": "support", "at": "aeg", + "ofMove": true, "from": "ank", "to": "con", "power": "turkey" @@ -5100,6 +5172,7 @@ { "type": "support", "at": "smy", + "ofMove": true, "from": "arm", "to": "ank", "power": "turkey" @@ -5175,6 +5248,7 @@ { "type": "support", "at": "sil", + "ofMove": true, "from": "ber", "to": "pru", "power": "germany" @@ -5246,6 +5320,7 @@ { "type": "support", "at": "mun", + "ofMove": true, "from": "ber", "to": "kie", "power": "germany" @@ -5299,6 +5374,7 @@ { "type": "support", "at": "mun", + "ofMove": true, "from": "kie", "to": "ber", "power": "germany" @@ -5394,6 +5470,7 @@ { "type": "support", "at": "hel", + "ofMove": true, "from": "hol", "to": "nth", "power": "germany" @@ -5401,6 +5478,7 @@ { "type": "support", "at": "ska", + "ofMove": true, "from": "hol", "to": "nth", "power": "germany" @@ -5415,6 +5493,7 @@ { "type": "support", "at": "bel", + "ofMove": true, "from": "nth", "to": "hol", "power": "france" @@ -5422,6 +5501,7 @@ { "type": "support", "at": "edi", + "ofMove": true, "from": "nwg", "to": "nth", "power": "england" @@ -5429,6 +5509,7 @@ { "type": "support", "at": "yor", + "ofMove": true, "from": "nwg", "to": "nth", "power": "england" @@ -5443,6 +5524,7 @@ { "type": "support", "at": "kie", + "ofMove": true, "from": "ruh", "to": "hol", "power": "austria" @@ -5564,6 +5646,7 @@ { "type": "support", "at": "hel", + "ofMove": true, "from": "hol", "to": "nth", "power": "germany" @@ -5571,6 +5654,7 @@ { "type": "support", "at": "ska", + "ofMove": true, "from": "hol", "to": "nth", "power": "germany" @@ -5585,6 +5669,7 @@ { "type": "support", "at": "bel", + "ofMove": true, "from": "nth", "to": "hol", "power": "france" @@ -5592,6 +5677,7 @@ { "type": "support", "at": "edi", + "ofMove": true, "from": "nwg", "to": "nth", "power": "england" @@ -5599,6 +5685,7 @@ { "type": "support", "at": "yor", + "ofMove": true, "from": "nwg", "to": "nth", "power": "england" @@ -5613,6 +5700,7 @@ { "type": "support", "at": "lon", + "ofMove": true, "from": "nwg", "to": "nth", "power": "england" @@ -5620,6 +5708,7 @@ { "type": "support", "at": "kie", + "ofMove": true, "from": "ruh", "to": "hol", "power": "austria" @@ -5725,6 +5814,7 @@ { "type": "support", "at": "hel", + "ofMove": true, "from": "hol", "to": "nth", "power": "germany" @@ -5739,6 +5829,7 @@ { "type": "support", "at": "bel", + "ofMove": true, "from": "nth", "to": "hol", "power": "france" @@ -5746,6 +5837,7 @@ { "type": "support", "at": "eng", + "ofMove": true, "from": "hol", "to": "nth", "power": "france" @@ -5753,6 +5845,7 @@ { "type": "support", "at": "kie", + "ofMove": true, "from": "ruh", "to": "hol", "power": "austria" @@ -5838,6 +5931,7 @@ { "type": "support", "at": "yor", + "ofMove": true, "from": "nwy", "to": "nth", "power": "england" @@ -5845,6 +5939,7 @@ { "type": "support", "at": "hol", + "ofMove": true, "from": "hel", "to": "nth", "power": "germany" @@ -5859,6 +5954,7 @@ { "type": "support", "at": "ska", + "ofMove": true, "from": "nwy", "to": "nth", "power": "russia" @@ -5943,6 +6039,7 @@ { "type": "support", "at": "yor", + "ofMove": true, "from": "nwy", "to": "nth", "power": "england" @@ -5950,6 +6047,7 @@ { "type": "support", "at": "hol", + "ofMove": true, "from": "hel", "to": "nth", "power": "germany" @@ -5964,6 +6062,7 @@ { "type": "support", "at": "ska", + "ofMove": true, "from": "nwy", "to": "nth", "power": "russia" @@ -6048,6 +6147,7 @@ { "type": "support", "at": "yor", + "ofMove": true, "from": "nwy", "to": "nth", "power": "england" @@ -6055,6 +6155,7 @@ { "type": "support", "at": "hol", + "ofMove": true, "from": "hel", "to": "nth", "power": "germany" @@ -6069,6 +6170,7 @@ { "type": "support", "at": "ska", + "ofMove": true, "from": "nwy", "to": "nth", "power": "russia" @@ -6158,6 +6260,7 @@ { "type": "support", "at": "yor", + "ofMove": true, "from": "nwy", "to": "nth", "power": "england" @@ -6165,6 +6268,7 @@ { "type": "support", "at": "hol", + "ofMove": true, "from": "hel", "to": "nth", "power": "germany" @@ -6186,6 +6290,7 @@ { "type": "support", "at": "ska", + "ofMove": true, "from": "nwy", "to": "nth", "power": "russia" @@ -6285,6 +6390,7 @@ { "type": "support", "at": "lyo", + "ofMove": true, "from": "por", "to": "spa/nc", "power": "france" @@ -6292,6 +6398,7 @@ { "type": "support", "at": "mar", + "ofMove": true, "from": "gas", "to": "spa", "power": "germany" @@ -6313,6 +6420,7 @@ { "type": "support", "at": "wes", + "ofMove": true, "from": "por", "to": "spa/nc", "power": "italy" @@ -6388,6 +6496,7 @@ { "type": "support", "at": "ser", + "ofMove": true, "from": "vie", "to": "bud", "power": "austria" @@ -6409,6 +6518,7 @@ { "type": "support", "at": "rum", + "ofMove": true, "from": "gal", "to": "bud", "power": "russia" @@ -6481,6 +6591,7 @@ { "type": "support", "at": "edi", + "ofMove": true, "from": "yor", "to": "nth", "power": "england" @@ -6502,6 +6613,7 @@ { "type": "support", "at": "eng", + "ofMove": true, "from": "bel", "to": "nth", "power": "france" @@ -6521,6 +6633,7 @@ { "type": "support", "at": "nwy", + "ofMove": true, "from": "nwg", "to": "nth", "power": "russia" @@ -6659,6 +6772,7 @@ { "type": "support", "at": "hol", + "ofMove": true, "from": "ruh", "to": "kie", "power": "england" @@ -6680,6 +6794,7 @@ { "type": "support", "at": "mun", + "ofMove": true, "from": "kie", "to": "ber", "power": "france" @@ -6687,6 +6802,7 @@ { "type": "support", "at": "sil", + "ofMove": true, "from": "kie", "to": "ber", "power": "france" @@ -6701,6 +6817,7 @@ { "type": "support", "at": "den", + "ofMove": true, "from": "ber", "to": "kie", "power": "germany" @@ -6708,6 +6825,7 @@ { "type": "support", "at": "hel", + "ofMove": true, "from": "ber", "to": "kie", "power": "germany" @@ -6715,6 +6833,7 @@ { "type": "support", "at": "bal", + "ofMove": true, "from": "pru", "to": "ber", "power": "russia" @@ -6943,6 +7062,7 @@ { "type": "support", "at": "mao", + "ofMove": true, "from": "lon", "to": "bre", "power": "england" @@ -7095,6 +7215,7 @@ { "type": "support", "at": "bel", + "ofMove": true, "from": "eng", "to": "nth", "power": "france" @@ -7109,6 +7230,7 @@ { "type": "support", "at": "den", + "ofMove": true, "from": "ska", "to": "nth", "power": "germany" @@ -7217,6 +7339,7 @@ { "type": "support", "at": "hel", + "ofMove": true, "from": "ska", "to": "nth", "power": "germany" @@ -7238,6 +7361,7 @@ { "type": "support", "at": "bur", + "ofMove": true, "from": "pic", "to": "bel", "power": "france" @@ -7319,6 +7443,7 @@ { "type": "support", "at": "hel", + "ofMove": true, "from": "ska", "to": "nth", "power": "germany" @@ -7412,6 +7537,7 @@ { "type": "support", "at": "hel", + "ofMove": true, "from": "ska", "to": "nth", "power": "germany" @@ -7510,6 +7636,7 @@ { "type": "support", "at": "bre", + "ofMove": true, "from": "mao", "to": "eng", "power": "france" @@ -7601,6 +7728,7 @@ { "type": "support", "at": "bre", + "ofMove": true, "from": "mao", "to": "eng", "power": "france" @@ -7692,6 +7820,7 @@ { "type": "support", "at": "bre", + "ofMove": true, "from": "mao", "to": "eng", "power": "france" @@ -7783,6 +7912,7 @@ { "type": "support", "at": "nao", + "ofMove": true, "from": "mao", "to": "iri", "power": "france" @@ -7874,6 +8004,7 @@ { "type": "support", "at": "hol", + "ofMove": true, "from": "den", "to": "nth", "power": "germany" @@ -7939,6 +8070,7 @@ { "type": "support", "at": "lon", + "ofMove": true, "from": "wal", "to": "eng", "power": "england" @@ -8030,6 +8162,7 @@ { "type": "support", "at": "lon", + "ofMove": true, "from": "wal", "to": "eng", "power": "england" @@ -8146,6 +8279,7 @@ { "type": "support", "at": "lon", + "ofMove": true, "from": "wal", "to": "eng", "power": "england" @@ -8174,6 +8308,7 @@ { "type": "support", "at": "nth", + "ofMove": true, "from": "bel", "to": "eng", "power": "germany" @@ -8256,6 +8391,7 @@ { "type": "support", "at": "lon", + "ofMove": true, "from": "wal", "to": "eng", "power": "england" @@ -8284,6 +8420,7 @@ { "type": "support", "at": "yor", + "ofMove": true, "from": "bre", "to": "lon", "power": "france" @@ -8291,6 +8428,7 @@ { "type": "support", "at": "nth", + "ofMove": true, "from": "bel", "to": "eng", "power": "germany" @@ -8385,6 +8523,7 @@ { "type": "support", "at": "eng", + "ofMove": true, "from": "lon", "to": "bel", "power": "england" @@ -8399,6 +8538,7 @@ { "type": "support", "at": "hel", + "ofMove": true, "from": "ska", "to": "nth", "power": "germany" @@ -8492,6 +8632,7 @@ { "type": "support", "at": "nap", + "ofMove": true, "from": "rom", "to": "tys", "power": "italy" @@ -8594,6 +8735,7 @@ { "type": "support", "at": "aeg", + "ofMove": true, "from": "eas", "to": "ion", "power": "turkey" @@ -8682,6 +8824,7 @@ { "type": "support", "at": "edi", + "ofMove": true, "from": "nwy", "to": "cly", "power": "russia" @@ -8703,6 +8846,7 @@ { "type": "support", "at": "iri", + "ofMove": true, "from": "mao", "to": "nao", "power": "france" @@ -8826,6 +8970,7 @@ { "type": "support", "at": "lon", + "ofMove": true, "from": "edi", "to": "nth", "power": "england" @@ -8847,6 +8992,7 @@ { "type": "support", "at": "bel", + "ofMove": true, "from": "pic", "to": "eng", "power": "germany" @@ -8973,6 +9119,7 @@ { "type": "support", "at": "yor", + "ofMove": true, "from": "edi", "to": "nth", "power": "england" @@ -9015,6 +9162,7 @@ { "type": "support", "at": "iri", + "ofMove": true, "from": "mao", "to": "eng", "power": "italy" @@ -9133,6 +9281,7 @@ { "type": "support", "at": "lon", + "ofMove": true, "from": "edi", "to": "nth", "power": "england" @@ -9147,6 +9296,7 @@ { "type": "support", "at": "mao", + "ofMove": true, "from": "iri", "to": "eng", "power": "england" @@ -9299,6 +9449,7 @@ { "type": "support", "at": "hol", + "ofMove": true, "from": "ruh", "to": "bel", "power": "germany" @@ -9320,6 +9471,7 @@ { "type": "support", "at": "fin", + "ofMove": true, "from": "den", "to": "nwy", "power": "germany" @@ -9341,6 +9493,7 @@ { "type": "support", "at": "hel", + "ofMove": true, "from": "yor", "to": "hol", "power": "england" @@ -9360,6 +9513,7 @@ { "type": "support", "at": "nwy", + "ofMove": true, "from": "nwg", "to": "nth", "power": "russia" @@ -9477,6 +9631,7 @@ { "type": "support", "at": "lon", + "ofMove": true, "from": "wal", "to": "eng", "power": "england" @@ -9505,6 +9660,7 @@ { "type": "support", "at": "gas", + "ofMove": true, "from": "spa", "to": "bre", "power": "italy" @@ -9658,6 +9814,7 @@ { "type": "support", "at": "eng", + "ofMove": true, "from": "lon", "to": "bel", "power": "england" @@ -9672,6 +9829,7 @@ { "type": "support", "at": "hel", + "ofMove": true, "from": "ska", "to": "nth", "power": "germany" @@ -9707,6 +9865,7 @@ { "type": "support", "at": "gre", + "ofMove": true, "from": "aeg", "to": "ion", "power": "turkey" @@ -9897,6 +10056,7 @@ { "type": "support", "at": "stp/sc", + "ofMove": true, "from": "fin", "to": "bot", "power": "russia" @@ -9911,6 +10071,7 @@ { "type": "support", "at": "swe", + "ofMove": true, "from": "pru", "to": "bal", "power": "russia" @@ -9925,6 +10086,7 @@ { "type": "support", "at": "den", + "ofMove": true, "from": "ska", "to": "nth", "power": "russia" @@ -9939,6 +10101,7 @@ { "type": "support", "at": "cly", + "ofMove": true, "from": "nwy", "to": "nwg", "power": "russia" @@ -10009,6 +10172,7 @@ { "type": "support", "at": "lon", + "ofMove": true, "from": "wal", "to": "eng", "power": "england" @@ -10065,6 +10229,7 @@ { "type": "support", "at": "bre", + "ofMove": true, "from": "spa/nc", "to": "mao", "power": "france" @@ -10313,6 +10478,7 @@ { "type": "support", "at": "stp/sc", + "ofMove": true, "from": "fin", "to": "bot", "power": "russia" @@ -10327,6 +10493,7 @@ { "type": "support", "at": "swe", + "ofMove": true, "from": "pru", "to": "bal", "power": "russia" @@ -10341,6 +10508,7 @@ { "type": "support", "at": "den", + "ofMove": true, "from": "ska", "to": "nth", "power": "russia" @@ -10355,6 +10523,7 @@ { "type": "support", "at": "cly", + "ofMove": true, "from": "nwy", "to": "nwg", "power": "russia" @@ -10425,6 +10594,7 @@ { "type": "support", "at": "lon", + "ofMove": true, "from": "wal", "to": "eng", "power": "england" @@ -10488,6 +10658,7 @@ { "type": "support", "at": "bre", + "ofMove": true, "from": "spa/nc", "to": "mao", "power": "france" @@ -10630,6 +10801,7 @@ { "type": "support", "at": "nth", + "ofMove": true, "from": "lon", "to": "bel", "power": "england" @@ -10644,6 +10816,7 @@ { "type": "support", "at": "bre", + "ofMove": true, "from": "bel", "to": "eng", "power": "france" @@ -10841,6 +11014,7 @@ { "type": "support", "at": "bur", + "ofMove": true, "from": "pic", "to": "bel", "power": "france" @@ -10848,6 +11022,7 @@ { "type": "support", "at": "mao", + "ofMove": true, "from": "bre", "to": "eng", "power": "france" @@ -10937,6 +11112,7 @@ { "type": "support", "at": "bur", + "ofMove": true, "from": "pic", "to": "bel", "power": "france" @@ -10944,6 +11120,7 @@ { "type": "support", "at": "mao", + "ofMove": true, "from": "bre", "to": "eng", "power": "france" @@ -11360,6 +11537,7 @@ { "type": "support", "at": "fin", + "ofMove": true, "from": "nwy", "to": "swe", "power": "england" @@ -11448,6 +11626,7 @@ { "type": "support", "at": "den", + "ofMove": true, "from": "nwy", "to": "swe", "power": "england" @@ -11455,6 +11634,7 @@ { "type": "support", "at": "fin", + "ofMove": true, "from": "nwy", "to": "swe", "power": "england" @@ -11476,6 +11656,7 @@ { "type": "support", "at": "bar", + "ofMove": true, "from": "swe", "to": "nwy", "power": "russia" @@ -11490,6 +11671,7 @@ { "type": "support", "at": "nth", + "ofMove": true, "from": "nwg", "to": "nwy", "power": "france" @@ -11562,6 +11744,7 @@ { "type": "support", "at": "nwy", + "ofMove": true, "from": "nth", "to": "ska", "power": "england" @@ -11590,6 +11773,7 @@ { "type": "support", "at": "bar", + "ofMove": true, "from": "swe", "to": "nwy", "power": "russia" @@ -11782,6 +11966,7 @@ { "type": "support", "at": "ven", + "ofMove": true, "from": "alb", "to": "tri", "power": "italy" @@ -11871,6 +12056,7 @@ { "type": "support", "at": "den", + "ofMove": true, "from": "nwy", "to": "swe", "power": "england" @@ -11878,6 +12064,7 @@ { "type": "support", "at": "fin", + "ofMove": true, "from": "nwy", "to": "swe", "power": "england" @@ -11892,6 +12079,7 @@ { "type": "support", "at": "nth", + "ofMove": true, "from": "nwg", "to": "nwy", "power": "france" @@ -11913,6 +12101,7 @@ { "type": "support", "at": "bar", + "ofMove": true, "from": "swe", "to": "nwy", "power": "russia" @@ -11997,6 +12186,7 @@ { "type": "support", "at": "hol", + "ofMove": true, "from": "lon", "to": "bel", "power": "england" @@ -12108,6 +12298,7 @@ { "type": "support", "at": "den", + "ofMove": true, "from": "nwy", "to": "swe", "power": "england" @@ -12115,6 +12306,7 @@ { "type": "support", "at": "bal", + "ofMove": true, "from": "nwy", "to": "swe", "power": "england" @@ -12143,6 +12335,7 @@ { "type": "support", "at": "nwg", + "ofMove": true, "from": "swe", "to": "nwy", "power": "russia" @@ -12228,6 +12421,7 @@ { "type": "support", "at": "den", + "ofMove": true, "from": "nwy", "to": "swe", "power": "england" @@ -12235,6 +12429,7 @@ { "type": "support", "at": "bal", + "ofMove": true, "from": "nwy", "to": "swe", "power": "england" @@ -12263,6 +12458,7 @@ { "type": "support", "at": "nwg", + "ofMove": true, "from": "swe", "to": "nwy", "power": "russia" @@ -12353,6 +12549,7 @@ { "type": "support", "at": "hol", + "ofMove": true, "from": "lon", "to": "bel", "power": "england" @@ -12374,6 +12571,7 @@ { "type": "support", "at": "ruh", + "ofMove": true, "from": "lon", "to": "bel", "power": "england" @@ -12395,6 +12593,7 @@ { "type": "support", "at": "wal", + "ofMove": true, "from": "bel", "to": "lon", "power": "france" @@ -12555,6 +12754,7 @@ { "type": "support", "at": "bur", + "ofMove": true, "from": "pic", "to": "bel", "power": "france" @@ -12562,6 +12762,7 @@ { "type": "support", "at": "mao", + "ofMove": true, "from": "bre", "to": "eng", "power": "france" @@ -12657,6 +12858,7 @@ { "type": "support", "at": "ven", + "ofMove": true, "from": "tyr", "to": "tri", "power": "italy" @@ -12678,6 +12880,7 @@ { "type": "support", "at": "aeg", + "ofMove": true, "from": "ion", "to": "gre", "power": "italy" @@ -12717,6 +12920,7 @@ { "type": "support", "at": "ser", + "ofMove": true, "from": "tri", "to": "alb", "power": "austria" @@ -12805,6 +13009,7 @@ { "type": "support", "at": "yor", + "ofMove": true, "from": "lvp", "to": "edi", "power": "england" @@ -12817,6 +13022,7 @@ { "type": "support", "at": "kie", + "ofMove": true, "from": "ruh", "to": "hol", "power": "germany" @@ -12836,6 +13042,7 @@ { "type": "support", "at": "swe", + "ofMove": true, "from": "fin", "to": "nwy", "power": "russia" @@ -12900,6 +13107,7 @@ { "type": "support", "at": "hol", + "ofMove": true, "from": "edi", "to": "nth", "power": "russia" @@ -12959,6 +13167,7 @@ { "type": "support", "at": "kie", + "ofMove": true, "from": "ruh", "to": "hol", "power": "germany" @@ -13052,6 +13261,7 @@ { "type": "support", "at": "kie", + "ofMove": true, "from": "ruh", "to": "hol", "power": "germany" @@ -13130,6 +13340,7 @@ { "type": "support", "at": "con", + "ofMove": true, "from": "bla", "to": "ank", "power": "russia" @@ -13210,6 +13421,7 @@ { "type": "support", "at": "bud", + "ofMove": true, "from": "tri", "to": "vie", "power": "austria" @@ -13315,6 +13527,7 @@ { "type": "support", "at": "bud", + "ofMove": true, "from": "tri", "to": "vie", "power": "austria" @@ -13329,6 +13542,7 @@ { "type": "support", "at": "mun", + "ofMove": true, "from": "sil", "to": "boh", "power": "germany" @@ -13460,6 +13674,7 @@ { "type": "support", "at": "yor", + "ofMove": true, "from": "lvp", "to": "edi", "power": "england" @@ -13472,6 +13687,7 @@ { "type": "support", "at": "kie", + "ofMove": true, "from": "ruh", "to": "hol", "power": "germany" @@ -13491,6 +13707,7 @@ { "type": "support", "at": "swe", + "ofMove": true, "from": "fin", "to": "nwy", "power": "russia" @@ -13621,6 +13838,7 @@ { "type": "support", "at": "den", + "ofMove": true, "from": "hel", "to": "kie", "power": "england" @@ -13640,6 +13858,7 @@ { "type": "support", "at": "sil", + "ofMove": true, "from": "ber", "to": "pru", "power": "germany" @@ -13753,6 +13972,7 @@ { "type": "support", "at": "mun", + "ofMove": true, "from": "ber", "to": "kie", "power": "germany" @@ -13772,6 +13992,7 @@ { "type": "support", "at": "sil", + "ofMove": true, "from": "war", "to": "pru", "power": "russia" @@ -13871,6 +14092,7 @@ { "type": "support", "at": "bur", + "ofMove": true, "from": "gas", "to": "mar", "power": "france" @@ -14034,6 +14256,7 @@ { "type": "support", "at": "mao", + "ofMove": true, "from": "bre", "to": "eng", "power": "france" @@ -14062,6 +14285,7 @@ { "type": "support", "at": "cly", + "ofMove": true, "from": "edi", "to": "lvp", "power": "russia" @@ -14178,6 +14402,7 @@ { "type": "support", "at": "bre", + "ofMove": true, "from": "par", "to": "pic", "power": "france" @@ -14264,6 +14489,7 @@ { "type": "support", "at": "eng", + "ofMove": true, "from": "pic", "to": "bel", "power": "england" @@ -14278,6 +14504,7 @@ { "type": "support", "at": "bre", + "ofMove": true, "from": "par", "to": "pic", "power": "france" @@ -14290,6 +14517,7 @@ { "type": "support", "at": "mun", + "ofMove": true, "from": "mar", "to": "bur", "power": "germany" @@ -14389,6 +14617,7 @@ { "type": "support", "at": "bla", + "ofMove": true, "from": "bul/ec", "to": "con", "power": "russia" @@ -14459,6 +14688,7 @@ { "type": "support", "at": "mao", + "ofMove": true, "from": "spa/sc", "to": "por", "power": "france" @@ -14534,6 +14764,7 @@ { "type": "support", "at": "tun", + "ofMove": true, "from": "tys", "to": "wes", "power": "italy" diff --git a/src/game/orders.ts b/src/game/orders.ts index 9b74ef8..692704c 100644 --- a/src/game/orders.ts +++ b/src/game/orders.ts @@ -28,8 +28,14 @@ export interface Unit { export type Order = | { type: 'hold'; at: string; power?: Power } | { type: 'move'; at: string; to: string; viaConvoy?: boolean; power?: Power } - /** `from === to` is a support to hold. */ - | { type: 'support'; at: string; from: string; to: string; power?: Power } + /** + * `from === to` is a support to hold, unless `ofMove` says a destination + * was actually written out. The difference is invisible in the shape of + * the order and decides 6.A.5: "Supports A Yorkshire" holds a unit down, + * while "Supports A Yorkshire - Yorkshire" supports a move nobody can + * legally make, and is therefore no order at all. + */ + | { type: 'support'; at: string; from: string; to: string; ofMove?: boolean; power?: Power } | { type: 'convoy'; at: string; from: string; to: string; power?: Power } /** Units by province. One unit to a province, whatever its coast. */ @@ -133,6 +139,22 @@ export interface Validated { illegal: Set /** Provinces whose unit was told to move, legally or not. */ orderedToMove: Set + /** + * Provinces whose unit was told to leave and could have. + * + * The document draws a line here that decides two cases (issue 4.E.1). An + * order that is impossible *in this position* but would be possible in + * another is **illegal**: it is ignored, the unit is simply holding, and + * it may be held down by hold support. An order that this position allows + * and that merely did not come off is **invalid**: the unit tried to + * leave, and a unit trying to leave cannot be propped up where it stands. + * + * The two cases are the same order with one piece moved. An army sent + * across water with a fleet standing on the route and no convoy ordered + * was given a legal order that failed (6.D.8). The identical order with + * nobody in the sea at all was never an order (6.D.32). + */ + orderedAway: Set } export function validate(board: Board, given: readonly Order[]): Validated { @@ -197,16 +219,30 @@ export function validate(board: Board, given: readonly Order[]): Validated { } 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') { + // A destination written out makes this a support of a move, even when + // the destination is where the unit already is -- and there is no legal + // move to your own province, so there is nothing there to support. + const ofMove = order.ofMove === true || base(order.from) !== base(order.to) + if (ofMove && wasRefused && supported?.type !== 'move') { refused.add(at) continue } orders.set(at, ok) } + const orderedAway = new Set() + for (const order of given) { + if (order.type !== 'move') continue + const at = base(order.at) + const unit = board.get(at) + if (!unit || (order.power && order.power !== unit.power)) continue + if (orders.get(at)?.type === 'move') orderedAway.add(at) + else if (unit.type === 'army' && crewedRoute(board, unit.at, order.to)) orderedAway.add(at) + } + 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, orderedToMove } + return { orders, illegal, orderedToMove, orderedAway } } /** The order as it will be obeyed, with the coast filled in, or null. */ @@ -330,6 +366,49 @@ export function seaRouteExists( return false } +/** + * The coasts a chain of fleets standing on the board can reach from these + * seas. + * + * Whoever owns them and whatever they were ordered to do. This is a question + * about what the position makes possible, not about what anybody asked for, + * and it is the difference between an order that is illegal and one that is + * merely invalid. + */ +export function coastsThroughFleets(board: Board, from: readonly string[]): Set { + const crewed = (id: string) => board.get(base(id))?.type === 'fleet' + const out = new Set() + const seen = new Set() + const queue = [...from] + + while (queue.length > 0) { + 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 +} + +/** Could the fleets now on the board carry an army from here to there? */ +export function crewedRoute(board: Board, from: string, to: string): boolean { + const start = base(from) + const end = base(to) + if (start === end) return false + if (PROVINCES[start]?.terrain !== 'coast' || PROVINCES[end]?.terrain !== 'coast') return false + const seas = coastalSeas(start) + .map(base) + .filter((sea) => board.get(sea)?.type === 'fleet') + return coastsThroughFleets(board, seas).has(end) +} + /** The seas a coastal province touches, whichever coast they are on. */ export function coastalSeas(id: string): string[] { const coasts = PROVINCES[id]?.coasts diff --git a/src/game/targets.ts b/src/game/targets.ts index 10299cd..6bca6a5 100644 --- a/src/game/targets.ts +++ b/src/game/targets.ts @@ -1,6 +1,6 @@ import { reachableFrom } from './layout' -import { FLEET, PROVINCES, base } from './map' -import { coastalSeas, type Board, type Unit } from './orders' +import { PROVINCES, base } from './map' +import { coastalSeas, coastsThroughFleets, type Board, type Unit } from './orders' /** * What each half of an order may be clicked on. @@ -51,42 +51,6 @@ export function supportTargets(board: Board, unit: Unit, from: string): Set { - const crewed = (id: string) => board.get(base(id))?.type === 'fleet' - const out = new Set() - const seen = new Set() - const queue = [...from] - - while (queue.length > 0) { - 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 -} - /** The seas off this coast that have a fleet in them. */ const putToSea = (board: Board, coast: string): string[] => coastalSeas(coast) @@ -97,7 +61,7 @@ const putToSea = (board: Board, coast: string): string[] => export function convoyDestinations(board: Board, unit: Unit): Set { const here = base(unit.at) if (unit.type !== 'army' || PROVINCES[here]?.terrain !== 'coast') return new Set() - const out = coastsReached(board, putToSea(board, here)) + const out = coastsThroughFleets(board, putToSea(board, here)) out.delete(here) return out } @@ -112,7 +76,7 @@ export function convoyDestinations(board: Board, unit: Unit): Set { export function convoyable(board: Board, unit: Unit): Set { const sea = base(unit.at) if (unit.type !== 'fleet' || PROVINCES[sea]?.terrain !== 'sea') return new Set() - const ashore = coastsReached(board, [sea]) + const ashore = coastsThroughFleets(board, [sea]) const out = new Set() for (const coast of ashore) { if (board.get(coast)?.type === 'army') out.add(coast) @@ -124,7 +88,7 @@ export function convoyable(board: Board, unit: Unit): Set { export function convoyTargets(board: Board, unit: Unit, from: string): Set { const sea = base(unit.at) if (unit.type !== 'fleet' || PROVINCES[sea]?.terrain !== 'sea') return new Set() - const out = coastsReached(board, [sea]) + const out = coastsThroughFleets(board, [sea]) out.delete(base(from)) return out } diff --git a/tools/datc.py b/tools/datc.py index e84f13f..39d83a5 100644 --- a/tools/datc.py +++ b/tools/datc.py @@ -153,8 +153,14 @@ def main() -> int: order = {'type': 'convoy', 'at': at, 'from': place(m2.group(2)), 'to': place(m2.group(3))} elif m2 := re.match(r'^(.*?)\s+Supports\s+[AF]\s+(.*?)\s+-\s+(.*)$', rest, re.I): + # A destination was written out. Worth keeping even when it + # is the mover's own province: "Supports A Yorkshire" and + # "Supports A Yorkshire - Yorkshire" come out identically + # otherwise, and 6.A.5 turns on the difference between + # them -- the first holds a unit down, the second supports + # a move nobody can legally make. at = place(m2.group(1)) - order = {'type': 'support', 'at': at, + order = {'type': 'support', 'at': at, 'ofMove': True, 'from': place(m2.group(2)), 'to': place(m2.group(3))} elif m2 := re.match(r'^(.*?)\s+Supports\s+[AF]\s+(.*)$', rest, re.I): at = place(m2.group(1))