85 lines
4.5 KiB
TypeScript
85 lines
4.5 KiB
TypeScript
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<Game['pendingIxMove']> = {
|
|
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<Game['pendingShipment']>;
|
|
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);
|
|
});
|