import test from 'node:test'; import assert from 'node:assert/strict'; import { TERRITORIES, MOBILE_STRONGHOLD } from '../game/board'; import { createTerrorState, placeTerror, revealTerror, type TerrorState, } from '../game/moritani-terror'; import { createNexusMoritani, validateNexusMoritani, quoteNexusMoritaniPlacement, quoteNexusMoritaniGrummanPlacement, type NexusMoritaniContext, type NexusMoritaniGrummanSource, type NexusMoritaniReceipt, } from '../game/nexus-moritani'; const context: NexusMoritaniContext = { turn: 3, players: [ { id: 'm', faction: 'moritani' }, { id: 'a', faction: 'atreides' }, ], }; const source: NexusMoritaniGrummanSource = { source: 'grumman', sourceEvent: JSON.stringify(['grummanCollection', 3, 'm']), prerequisite: 'arrakeen', }; const clone = (value: T): T => JSON.parse(JSON.stringify(value)); const fresh = () => createTerrorState(() => 0.3); const eligible = (turn = 1) => { const state = fresh(); return placeTerror(state, state.tokens[0].id, 'arrakeen', turn); }; const grummanEvent = (receipt: Extract) => JSON.stringify([ 'nexusMoritaniGrumman', receipt.turn, receipt.owner, receipt.token, receipt.territory, receipt.sourceEvent, receipt.prerequisite, ]); void test('Grumman Cunning receipts bind the native source and ordinary prerequisite to their event and signature', () => { const receipt = createNexusMoritani(context, 'm', 'supply-token', 'polar_sink', source); assert.equal(receipt.source, 'grumman'); if (receipt.source !== 'grumman') throw new Error('Expected Grumman receipt'); assert.equal(receipt.phase, 7); assert.equal(receipt.event, grummanEvent(receipt)); validateNexusMoritani({ ...context, turn: 8 }, clone(receipt)); assert.notEqual( receipt.event, createNexusMoritani(context, 'm', 'supply-token', 'polar_sink', { ...source, sourceEvent: 'another-native-operation', }).event, ); assert.notEqual( receipt.event, createNexusMoritani(context, 'm', 'supply-token', 'polar_sink', { ...source, prerequisite: 'carthag', }).event, ); const edits: ((r: Extract) => void)[] = [ (r) => { r.sourceEvent = 'other-source'; }, (r) => { r.prerequisite = 'carthag'; }, (r) => { r.sourceEvent = 'other-source'; r.event = grummanEvent(r); }, (r) => { r.prerequisite = 'carthag'; r.event = grummanEvent(r); }, (r) => { r.token = 'other-token'; }, (r) => { r.territory = 'hagga_basin'; }, (r) => { r.turn = 2; }, (r) => { r.owner = 'a'; }, (r) => { Object.assign(r, { source: 'mentat', phase: 8 }); }, (r) => { Object.assign(r, { phase: 8 }); }, (r) => { delete (r as Partial>).sourceEvent; }, (r) => { delete (r as Partial>).prerequisite; }, (r) => { r.signature = ''; }, ]; for (const edit of edits) { const bad: Extract = clone(receipt); edit(bad); const before: typeof bad = clone(bad); assert.throws(() => validateNexusMoritani(context, bad)); assert.deepEqual(bad, before); } assert.throws(() => validateNexusMoritani({ ...context, turn: 2 }, receipt)); for (const prerequisite of ['polar_sink', MOBILE_STRONGHOLD, 'homeworld:moritani', '']) assert.throws(() => createNexusMoritani(context, 'm', 'token', 'polar_sink', { ...source, prerequisite, })); assert.throws(() => createNexusMoritani(context, 'm', 'token', 'polar_sink', { ...source, sourceEvent: '', })); }); void test('Existing Mentat evidence authenticates independently of the new Collection source', () => { const legacy: NexusMoritaniReceipt = { version: 1, event: JSON.stringify(['nexusMoritani', 3, 'm', 'token', 'hagga_basin']), source: 'mentat', phase: 8, turn: 3, owner: 'm', token: 'token', territory: 'hagga_basin', roster: [{ id: 'a', faction: 'atreides' }, { id: 'm', faction: 'moritani' }], signature: '', }; legacy.signature = JSON.stringify([ 1, legacy.event, 'mentat', 8, 3, 'm', 'token', 'hagga_basin', [['a', 'atreides'], ['m', 'moritani']], ]); validateNexusMoritani(context, legacy); const current = createNexusMoritani(context, 'm', 'token', 'hagga_basin'); assert.equal(current.event, legacy.event); assert.equal(current.signature, legacy.signature); validateNexusMoritani(context, current); assert.throws(() => validateNexusMoritani(context, { ...legacy, source: 'grumman', phase: 7, sourceEvent: source.sourceEvent, prerequisite: source.prerequisite, })); }); void test('Grumman Cunning broadens an available ordinary addition to every printed static territory, including empty desert and stacks', () => { const state = eligible(); const before = clone(state); const id = state.tokens[1].id; for (const territory of TERRITORIES) { const placed = quoteNexusMoritaniGrummanPlacement(state, id, territory.id, 'arrakeen'); assert.equal(placed.tokens[1].status, 'placed'); assert.equal(placed.tokens[1].location, territory.id); assert.equal(placed.placementTurn, state.placementTurn); assert.deepEqual(placed.tokens.filter((token) => token.id !== id), state.tokens.filter((token) => token.id !== id)); if (territory.id === 'arrakeen') assert.equal(placed.tokens.filter((token) => token.location === 'arrakeen').length, 2); } assert.deepEqual(state, before); const desert = quoteNexusMoritaniGrummanPlacement(state, id, 'hagga_basin', 'arrakeen'); const stacked = quoteNexusMoritaniGrummanPlacement(desert, desert.tokens[2].id, 'hagga_basin', 'arrakeen'); assert.equal(stacked.tokens.filter((token) => token.location === 'hagga_basin').length, 2); }); void test('Grumman Cunning requires an actually eligible ordinary stronghold before broadening the destination', () => { const empty = fresh(); const desertOnly = quoteNexusMoritaniPlacement(empty, empty.tokens[0].id, 'hagga_basin', 1); const ordinary = eligible(); for (const [state, prerequisite] of [ [empty, 'arrakeen'], [desertOnly, 'arrakeen'], [desertOnly, 'hagga_basin'], [ordinary, 'carthag'], [ordinary, MOBILE_STRONGHOLD], [ordinary, 'homeworld:moritani'], ] as const) { const before = clone(state); assert.throws(() => quoteNexusMoritaniGrummanPlacement(state, state.tokens[1].id, 'polar_sink', prerequisite)); assert.deepEqual(state, before); } }); void test('Grumman Cunning cannot create supply, reuse revealed tokens or enhance relocation, and rejects excluded destinations', () => { const state = eligible(); const revealed = revealTerror(state, state.tokens[0].id); for (const [stock, id] of [ [state, 'invented-token'], [state, state.tokens[0].id], [revealed, revealed.tokens[0].id], ] as const) { const before = clone(stock); assert.throws(() => quoteNexusMoritaniGrummanPlacement(stock, id, 'polar_sink', 'arrakeen')); assert.deepEqual(stock, before); } for (const territory of [MOBILE_STRONGHOLD, 'homeworld:moritani', 'arrakeen:10', '', '__proto__']) { assert.throws(() => quoteNexusMoritaniGrummanPlacement(state, state.tokens[1].id, territory, 'arrakeen')); assert.throws(() => createNexusMoritani(context, 'm', state.tokens[1].id, territory, source)); } const edits: ((stock: TerrorState) => void)[] = [ (stock) => { stock.tokens.pop(); }, (stock) => { stock.tokens[2].id = stock.tokens[1].id; }, (stock) => { stock.tokens[2].kind = stock.tokens[1].kind; }, (stock) => { stock.tokens[1].location = 'arrakeen'; }, (stock) => { stock.supplyEpoch = -1; }, ]; for (const edit of edits) { const bad = clone(state); edit(bad); const before = clone(bad); assert.throws(() => quoteNexusMoritaniGrummanPlacement(bad, state.tokens[1].id, 'polar_sink', 'arrakeen')); assert.deepEqual(bad, before); } }); void test('Grumman Cunning preserves the physical faces, other placements, supply epoch and independent Mentat allowance', () => { let state = eligible(); state = quoteNexusMoritaniPlacement(state, state.tokens[1].id, 'hagga_basin', 2); state.supplyEpoch = 4; const before = clone(state); const placed = quoteNexusMoritaniGrummanPlacement(state, state.tokens[2].id, 'polar_sink', 'arrakeen'); assert.deepEqual(placed.tokens.map(({ id, kind }) => ({ id, kind })), state.tokens.map(({ id, kind }) => ({ id, kind }))); assert.deepEqual(placed.tokens.filter((token) => token.id !== state.tokens[2].id), state.tokens.filter((token) => token.id !== state.tokens[2].id)); assert.equal(placed.supplyEpoch, 4); assert.equal(placed.placementTurn, 2); const mentat = quoteNexusMoritaniPlacement(placed, placed.tokens[3].id, 'carthag', 3); assert.equal(mentat.placementTurn, 3); assert.equal(mentat.tokens[2].location, 'polar_sink'); placed.tokens[0].location = 'carthag'; placed.tokens[4].status = 'removed'; assert.deepEqual(state, before); const alreadySpent = eligible(3); const collection = quoteNexusMoritaniGrummanPlacement(alreadySpent, alreadySpent.tokens[1].id, 'hagga_basin', 'arrakeen'); assert.equal(collection.placementTurn, 3); assert.throws(() => quoteNexusMoritaniPlacement(collection, collection.tokens[2].id, 'carthag', 3)); });