Files
Dune/tests/sandmaster-movement-controls.test.tsx
T

160 lines
5.1 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import {
applyAction,
createGame,
joinGame,
newPlayer,
viewGame,
type Action,
type Game,
} from '../game/engine';
import {
SandmasterMovement,
sandmasterMoveDraft,
} from '../components/sandmaster-movement';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import { GRAPH, location } from '../game/board';
import { LEADER_SKILL_CARDS } from '../game/leader-skill-cards';
const source = location('false_wall_west', 16);
const destination = location('wind_pass', 16);
const spicePile = location('wind_pass', 15);
const move: Action = {
type: 'move',
from: source,
amount: 2,
elite: 0,
territory: 'wind_pass',
sector: 16,
};
function fixture(advanced = false): Game {
const game = createGame(
'SANDUI',
newPlayer('p', 'Mover', 'emperor'),
advanced,
);
joinGame(game, newPlayer('o', 'Observer', 'atreides'));
Object.assign(game, {
status: 'playing',
phase: 5,
turn: 2,
storm: 18,
order: ['p', 'o'],
active: 'p',
movementRemaining: ['p', 'o'],
spice: { [spicePile]: 3 },
});
for (const player of game.players) {
player.hand = [];
player.forces = {};
player.reserves = 20;
player.shipped = true;
}
game.players[0].forces[source] = 2;
game.players[0].reserves = 18;
game.leaderSkills = {
deck: LEADER_SKILL_CARDS.map((card) => card.id).filter(
(id) => id !== 'sandmaster',
),
offers: {},
assignments: [{ skill: 'sandmaster', leader: 'emperor-0', owner: 'p' }],
};
return game;
}
function markup(game: Game, viewer = 'p', busy = false) {
return renderToStaticMarkup(
createElement(SandmasterMovement, {
game: viewGame(game, viewer),
move,
act() {},
busy,
}),
);
}
void test('Sandmaster controls remain absent from other seats and unsupported module profiles', () => {
const game = fixture();
assert.equal(markup(game, 'o'), '');
const combined = fixture();
combined.expansions = ['ix'];
assert.equal(markup(combined), '');
});
void test('drafts preserve the ordinary move, accept a connected alternate and allow collecting or declining', () => {
const game = fixture();
const view = viewGame(game, 'p');
const standard = sandmasterMoveDraft(view, move);
const collected = applyAction(structuredClone(game), 'p', standard.action!);
assert.equal(collected.players[0].spice, game.players[0].spice + 1);
assert.equal(collected.spice[spicePile], 2);
assert.equal(collected.players[0].forces[source] ?? 0, 0);
assert.equal(collected.players[0].forces[destination], 2);
assert.equal(collected.players[0].moved, 1);
assert.ok(GRAPH[source].includes(location('wind_pass', 17)));
assert.ok(GRAPH[location('wind_pass', 17)].includes(destination));
const alternate = sandmasterMoveDraft(view, move, {
[source]: [source, location('wind_pass', 17), destination],
});
assert.equal(alternate.blocked, null);
assert.deepEqual(alternate.choice?.collect, [spicePile]);
const declined = sandmasterMoveDraft(view, move, undefined, [spicePile]);
assert.deepEqual(declined.choice?.collect, []);
const noCollection = applyAction(
structuredClone(game),
'p',
declined.action!,
);
assert.equal(noCollection.players[0].spice, game.players[0].spice);
assert.equal(noCollection.spice[spicePile], 3);
assert.match(
sandmasterMoveDraft(view, move, {
[source]: [source, location('arrakeen', 10), destination],
}).blocked!,
/connected route/,
);
});
void test('all four bot profiles add legal collection only to ordinary single-source candidates', () => {
for (const difficulty of DIFFICULTIES) {
const game = fixture();
const view = viewGame(game, 'p');
view.players.find((player) => player.id === 'p')!.bot = difficulty;
const candidates = botActions(view).filter(
(action) => action.type === 'move' && action.sandmaster,
);
assert.ok(candidates.length, `${difficulty} omitted Sandmaster collection`);
assert.ok(
candidates.every(
(action) =>
typeof action.from === 'string' &&
action.planetologist === undefined &&
action.movementCard === undefined &&
action.ornithopterEvent === undefined,
),
);
const completed = applyAction(game, 'p', candidates[0]);
const selected = (candidates[0].sandmaster as { collect: string[] }).collect;
assert.equal(completed.players[0].moved, 1, difficulty);
assert.equal(completed.players[0].spice, game.players[0].spice + selected.length, difficulty);
for (const key of selected) assert.equal(completed.spice[key], game.spice[key] - 1, difficulty);
const combined = viewGame(fixture(), 'p');
combined.players.find((player) => player.id === 'p')!.bot = difficulty;
combined.expansions = ['ix'];
assert.ok(
botActions(combined).every((action) => !action.sandmaster),
`${difficulty} emitted Sandmaster in a gated module combination`,
);
}
});