import assert from 'node:assert/strict'; import test from 'node:test'; import { createGame, newPlayer, type Game } from '../game/engine'; import { createRicheseNoField, deployRicheseNoField } from '../game/richese-no-field'; import { richeseRevealForeignMovementPending } from '../game/richese-reveal-foreign-movement'; /** Staged response packets exercise only the classifier. Native actions and * actual reveal/continuation are covered by the separate runtime programme. */ function movement(kind: 'fremen' | 'ixians' | 'choam'): Game { const richese = newPlayer('r', 'Richese', 'richese'); richese.noField = deployRicheseNoField(createRicheseNoField(['zero', 'three', 'five']), { tokenId: 'zero', controller: 'r', location: { territory: 'polar_sink', sector: 0 }, }); const g = createGame('MOVEQUOTE', richese, false, ['choam', 'ix']); const mover = newPlayer('m', 'Mover', kind === 'choam' ? 'emperor' : kind); mover.forces = { 'arrakeen:10': 3 }; if (mover.elites) mover.elites.forces = { 'arrakeen:10': 1 }; const choam = newPlayer('c', 'CHOAM', 'choam'); choam.forces = { 'carthag:11': 1 }; g.players.push(mover, choam); Object.assign(g, { status: 'playing', phase: 5, turn: 2, active: 'm', decision: null, response: null }); const order: NonNullable = { player: 'm', group: [['arrakeen:10', 3]], eliteGroup: { 'arrakeen:10': kind === 'ixians' ? 1 : 0 }, elite: kind === 'ixians' ? 1 : 0, origin: 'arrakeen', origins: ['arrakeen'], total: 3, to: 'carthag', sector: 11, advisors: false, wantsFighters: false, }; if (kind === 'fremen') { g.pendingFremenMove = { turn: g.turn, move: 0, order }; g.response = { kind: 'fremenMovement', owner: 'm', location: 'carthag:11', amount: 3, passed: [] }; } else if (kind === 'ixians') { g.pendingIxMove = order; g.response = { kind: 'ixMovement', owner: 'm', location: 'carthag:11', amount: 3, elite: 1, passed: [] }; } else { g.pendingChoamMove = order; g.decision = { kind: 'choamMovement', player: 'c', mover: 'm', territory: 'carthag', sector: 11, amount: 3 }; } return g; } void test('all three ordinary foreign-movement parents are classified without mutation', () => { for (const [kind, expected] of [ ['fremen', 'pendingFremenMove'], ['ixians', 'pendingIxMove'], ['choam', 'pendingChoamMove'], ] as const) { const g = movement(kind), before = structuredClone(g); assert.equal(richeseRevealForeignMovementPending(g), expected); assert.deepEqual(g, before); } }); void test('foreign movement requires the original actor, source, response and no unrelated child', () => { for (const kind of ['fremen', 'ixians', 'choam'] as const) { const g = movement(kind); const stale = structuredClone(g); stale.active = 'r'; assert.equal(richeseRevealForeignMovementPending(stale), null); const child = structuredClone(g); child.pendingShipment = {} as NonNullable; assert.equal(richeseRevealForeignMovementPending(child), null); const wrongPhase = structuredClone(g); wrongPhase.phase = 4; assert.equal(richeseRevealForeignMovementPending(wrongPhase), null); const noMarker = structuredClone(g); noMarker.players[0].noField!.deployed = null; assert.equal(richeseRevealForeignMovementPending(noMarker), null); const lostForces = structuredClone(g); lostForces.players[1].forces = {}; assert.equal(richeseRevealForeignMovementPending(lostForces), null); if (kind === 'choam') { const wrong = structuredClone(g); if (wrong.decision?.kind === 'choamMovement') wrong.decision.amount++; assert.equal(richeseRevealForeignMovementPending(wrong), null); } else { const wrong = structuredClone(g); wrong.response!.location = 'arrakeen:10'; assert.equal(richeseRevealForeignMovementPending(wrong), null); const duplicate = structuredClone(g); duplicate.response!.passed = ['r', 'r']; assert.equal(richeseRevealForeignMovementPending(duplicate), null); } } }); void test('marker identity and denomination are not needed to identify a foreign movement', () => { const g = movement('fremen'); g.players[0].noField = deployRicheseNoField(createRicheseNoField(['zero', 'three', 'five']), { tokenId: 'five', controller: 'r', location: { territory: 'arrakeen', sector: 10 }, }); assert.equal(richeseRevealForeignMovementPending(g), 'pendingFremenMove'); g.players[0].noField!.deployed!.controller = 'm'; assert.equal(richeseRevealForeignMovementPending(g), null); });