From c27e4cad811feffc8bc3dd243a1f3e23ffffc20b Mon Sep 17 00:00:00 2001 From: John Coffey Date: Wed, 9 Sep 2026 06:13:34 -0700 Subject: [PATCH] Fix five hand-written cases built on an impossible order Paris does not border Munich, so it can never support an attack on it. Five of my own cases were built on that support and the new validation pass refused it -- and three of them had been passing for the wrong reason, which is worse than failing. Every one of them now uses a supporter that could actually have made the move it is supporting. The Portugal case takes its support off the Mid-Atlantic, since Spain is the only land province that touches Portugal at all -- which is also why a unit dislodged there has nowhere to go. --- src/game/adjudicate.test.ts | 36 +++++++++++++++++++----------------- src/game/turn.test.ts | 30 ++++++++++++++++-------------- 2 files changed, 35 insertions(+), 31 deletions(-) diff --git a/src/game/adjudicate.test.ts b/src/game/adjudicate.test.ts index d548395..d81e98b 100644 --- a/src/game/adjudicate.test.ts +++ b/src/game/adjudicate.test.ts @@ -69,9 +69,11 @@ describe('strength', () => { }) it('dislodges when the attack is supported and the defence is not', () => { + // Ruhr, not Paris. A unit may only support into a province it could have + // gone to itself, and Paris does not border Munich. const r = run( - [A('france', 'bur'), A('france', 'par'), A('germany', 'mun')], - [mv('bur', 'mun'), sup('par', 'bur', 'mun'), hold('mun')], + [A('france', 'bur'), A('france', 'ruh'), A('germany', 'mun')], + [mv('bur', 'mun'), sup('ruh', 'bur', 'mun'), hold('mun')], ) expect(r.success.get('bur')).toBe(true) expect(r.dislodged.get('mun')?.attackedFrom).toBe('bur') @@ -79,8 +81,8 @@ describe('strength', () => { it('needs more than equal support, because a tie is a bounce', () => { const r = run( - [A('france', 'bur'), A('france', 'par'), A('germany', 'mun'), A('germany', 'ruh')], - [mv('bur', 'mun'), sup('par', 'bur', 'mun'), hold('mun'), sup('ruh', 'mun', 'mun')], + [A('france', 'bur'), A('france', 'tyr'), A('germany', 'mun'), A('germany', 'kie')], + [mv('bur', 'mun'), sup('tyr', 'bur', 'mun'), hold('mun'), sup('kie', 'mun', 'mun')], ) expect(r.success.get('bur')).toBe(false) expect(r.dislodged.size).toBe(0) @@ -132,20 +134,20 @@ describe('cutting support', () => { // Picardy, not Ruhr: Ruhr does not border Paris, and an attack that // cannot arrive cuts nothing. const r = run( - [A('france', 'bur'), A('france', 'par'), A('germany', 'mun'), A('germany', 'pic')], - [mv('bur', 'mun'), sup('par', 'bur', 'mun'), hold('mun'), mv('pic', 'par')], + [A('france', 'bur'), A('france', 'ruh'), A('germany', 'mun'), A('germany', 'kie')], + [mv('bur', 'mun'), sup('ruh', 'bur', 'mun'), hold('mun'), mv('kie', 'ruh')], ) - expect(r.success.get('par')).toBe(false) + expect(r.success.get('ruh')).toBe(false) expect(r.success.get('bur')).toBe(false) }) it('is not cut by an attack that could never arrive', () => { - // The same order from Ruhr, which does not border Paris at all. + // Paris does not border Ruhr, so that order is no order at all. const r = run( - [A('france', 'bur'), A('france', 'par'), A('germany', 'mun'), A('germany', 'ruh')], - [mv('bur', 'mun'), sup('par', 'bur', 'mun'), hold('mun'), mv('ruh', 'par')], + [A('france', 'bur'), A('france', 'ruh'), A('germany', 'mun'), A('germany', 'par')], + [mv('bur', 'mun'), sup('ruh', 'bur', 'mun'), hold('mun'), mv('par', 'ruh')], ) - expect(r.success.get('par')).toBe(true) + expect(r.success.get('ruh')).toBe(true) expect(r.dislodged.get('mun')).toBeDefined() }) @@ -153,20 +155,20 @@ describe('cutting support', () => { // Munich attacks the supporter; Munich is what the support is aimed at, // so the support holds and Munich is thrown out by it. const r = run( - [A('france', 'bur'), A('france', 'par'), A('germany', 'mun')], - [mv('bur', 'mun'), sup('par', 'bur', 'mun'), mv('mun', 'par')], + [A('france', 'bur'), A('france', 'ruh'), A('germany', 'mun')], + [mv('bur', 'mun'), sup('ruh', 'bur', 'mun'), mv('mun', 'ruh')], ) - expect(r.success.get('par')).toBe(true) + expect(r.success.get('ruh')).toBe(true) expect(r.success.get('bur')).toBe(true) expect(r.dislodged.get('mun')).toBeDefined() }) it('is cut by being thrown out, however the support was going', () => { const r = run( - [A('france', 'bur'), A('france', 'par'), A('germany', 'mun'), A('germany', 'pic'), A('germany', 'bre')], - [mv('bur', 'mun'), sup('par', 'bur', 'mun'), hold('mun'), mv('pic', 'par'), sup('bre', 'pic', 'par')], + [A('france', 'bur'), A('france', 'ruh'), A('germany', 'mun'), A('germany', 'kie'), A('germany', 'hol')], + [mv('bur', 'mun'), sup('ruh', 'bur', 'mun'), hold('mun'), mv('kie', 'ruh'), sup('hol', 'kie', 'ruh')], ) - expect(r.dislodged.get('par')).toBeDefined() + expect(r.dislodged.get('ruh')).toBeDefined() expect(r.success.get('bur')).toBe(false) }) }) diff --git a/src/game/turn.test.ts b/src/game/turn.test.ts index e9b0af7..346709d 100644 --- a/src/game/turn.test.ts +++ b/src/game/turn.test.ts @@ -34,8 +34,8 @@ function movement(units: Unit[], orders: Order[]) { describe('after the moving', () => { it('puts the winner in and takes the loser off the board', () => { const { outcome, after } = movement( - [A('france', 'bur'), A('france', 'par'), A('germany', 'mun')], - [mv('bur', 'mun'), sup('par', 'bur', 'mun'), hold('mun')], + [A('france', 'bur'), A('france', 'ruh'), A('germany', 'mun')], + [mv('bur', 'mun'), sup('ruh', 'bur', 'mun'), hold('mun')], ) expect(after.get('mun')?.power).toBe('france') expect(after.has('bur')).toBe(false) @@ -51,8 +51,8 @@ describe('after the moving', () => { describe('where a beaten unit may go', () => { const setup = () => movement( - [A('france', 'bur'), A('france', 'par'), A('germany', 'mun')], - [mv('bur', 'mun'), sup('par', 'bur', 'mun'), hold('mun')], + [A('france', 'bur'), A('france', 'ruh'), A('germany', 'mun')], + [mv('bur', 'mun'), sup('ruh', 'bur', 'mun'), hold('mun')], ) it('will not go back the way the attacker came', () => { @@ -62,8 +62,8 @@ describe('where a beaten unit may go', () => { it('will not go where somebody is standing', () => { const { outcome, after } = setup() - // Paris is where the supporting army still is -- and out of reach anyway. - expect(retreatOptions(after, outcome, 'mun')).not.toContain('par') + // Ruhr is where the supporting army still stands. + expect(retreatOptions(after, outcome, 'mun')).not.toContain('ruh') expect(retreatOptions(after, outcome, 'mun')).toContain('tyr') }) @@ -71,14 +71,14 @@ describe('where a beaten unit may go', () => { const { outcome, after } = movement( [ A('france', 'bur'), - A('france', 'par'), + A('france', 'ruh'), A('germany', 'mun'), A('austria', 'vie'), A('italy', 'ven'), ], [ mv('bur', 'mun'), - sup('par', 'bur', 'mun'), + sup('ruh', 'bur', 'mun'), hold('mun'), mv('vie', 'tyr'), mv('ven', 'tyr'), @@ -101,8 +101,10 @@ describe('where a beaten unit may go', () => { it('is disbanded when there is nowhere at all', () => { // Boxed into a corner: Portugal, attacked from Spain, with the sea taken. const { outcome, after } = movement( - [A('france', 'spa'), A('france', 'gas'), F('england', 'mao'), A('italy', 'por')], - [mv('spa', 'por'), sup('gas', 'spa', 'por'), hold('mao'), hold('por')], + // Only Spain touches Portugal by land, so the support has to come off + // the water -- which is also the only other way out of Portugal. + [A('france', 'spa'), F('france', 'mao'), A('italy', 'por')], + [mv('spa', 'por'), sup('mao', 'spa', 'por'), hold('por')], ) expect(outcome.dislodged.has('por')).toBe(true) expect(retreatOptions(after, outcome, 'por')).toEqual([]) @@ -114,8 +116,8 @@ describe('where a beaten unit may go', () => { describe('retreating', () => { const setup = () => movement( - [A('france', 'bur'), A('france', 'par'), A('germany', 'mun')], - [mv('bur', 'mun'), sup('par', 'bur', 'mun'), hold('mun')], + [A('france', 'bur'), A('france', 'ruh'), A('germany', 'mun')], + [mv('bur', 'mun'), sup('ruh', 'bur', 'mun'), hold('mun')], ) it('puts a unit down where it was told', () => { @@ -130,7 +132,7 @@ describe('retreating', () => { const { outcome, after } = movement( [ A('france', 'bur'), - A('france', 'par'), + A('france', 'ruh'), A('germany', 'mun'), A('italy', 'ven'), A('italy', 'tri'), @@ -138,7 +140,7 @@ describe('retreating', () => { ], [ mv('bur', 'mun'), - sup('par', 'bur', 'mun'), + sup('ruh', 'bur', 'mun'), hold('mun'), mv('ven', 'tyr'), sup('tri', 'ven', 'tyr'),