109 lines
5.7 KiB
TypeScript
109 lines
5.7 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { applyAction, createGame, initializeLeaderSkillsGameForAudit, joinGame, newPlayer, normalizeAutomaticGame, viewGame, type Game } from '../game/engine';
|
|
import { LEADER_SKILL_CARDS } from '../game/leader-skill-cards';
|
|
import { planetologistMoveDraft } from '../components/planetologist-movement';
|
|
import { TERRITORIES, distance, location, splitLocation } from '../game/board';
|
|
import { botActions } from '../game/bots';
|
|
import { DIFFICULTIES } from '../game/bot-profiles';
|
|
import { completedMoritaniSkillsGame, initializedMoritaniSkillsOffers, assertMoritaniSkillsCustody, moritaniSkillsPlayer as player, reloadMoritaniSkillsGame as reload, rejectMoritaniSkillsAction as reject } from './moritani-skills-fixture';
|
|
|
|
function movementPosition(game: Game): Game {
|
|
// Conserved focused position following genuine setup, not a played phase history.
|
|
for (const p of game.players) {
|
|
p.forces = {}; p.reserves = 20; p.tanks = 0;
|
|
p.spice = 20; p.moved = 0; p.shipped = false;
|
|
}
|
|
Object.assign(game, { phase: 5, active: 'm', order: ['m', 'a', 'e'], storm: 18,
|
|
ready: [], decision: null, response: null, phaseOpening: null });
|
|
return game;
|
|
}
|
|
|
|
void test('every canonical skill can be dealt and publicly assigned to Basic Moritani without reducing the deck', () => {
|
|
for (const card of LEADER_SKILL_CARDS) {
|
|
const game = completedMoritaniSkillsGame({ requestedSkill: card.id });
|
|
assert.equal(game.leaderSkills!.assignments.find(a => a.owner === 'm')!.skill, card.id);
|
|
assertMoritaniSkillsCustody(game);
|
|
for (const p of game.players) {
|
|
const view = viewGame(game, p.id);
|
|
assert.equal(view.leaderSkills!.offer, null);
|
|
assert.equal(view.leaderSkills!.assignments.length, 3);
|
|
assert.deepEqual(viewGame(reload(game), p.id), view);
|
|
}
|
|
}
|
|
});
|
|
|
|
void test('Moritani skill starts retain immutable Ecaz, optional-module and public-start rejection', () => {
|
|
for (const change of [
|
|
(g: Game) => { g.players[1] = newPlayer('a', 'Ecaz', 'ecaz'); },
|
|
(g: Game) => { g.expansions.push('choam'); },
|
|
(g: Game) => { g.discoveryEnabled = true; },
|
|
(g: Game) => { g.ecazTreachery = true; },
|
|
]) {
|
|
const lobby = createGame('MORIGATE', newPlayer('m', 'Moritani', 'moritani'), false, ['ecaz']);
|
|
joinGame(lobby, newPlayer('a', 'Atreides', 'atreides'));
|
|
lobby.players.forEach(p => { p.ready = true; });
|
|
change(lobby);
|
|
const before = JSON.stringify(lobby);
|
|
assert.throws(() => initializeLeaderSkillsGameForAudit(lobby));
|
|
assert.equal(JSON.stringify(lobby), before);
|
|
}
|
|
const publicLobby = createGame('MORIPUBL', newPlayer('m', 'Moritani', 'moritani'), false, ['ecaz']);
|
|
joinGame(publicLobby, newPlayer('a', 'Atreides', 'atreides'));
|
|
publicLobby.players.forEach(p => { p.ready = true; });
|
|
reject(publicLobby, 'm', { type: 'start' });
|
|
});
|
|
|
|
void test('all four profiles legally choose from the same private Moritani setup offer', () => {
|
|
const game = initializedMoritaniSkillsOffers({ requestedSkill: 'planetologist' });
|
|
for (const difficulty of DIFFICULTIES) {
|
|
const view = viewGame(reload(game), 'm');
|
|
view.players.find(p => p.id === 'm')!.bot = difficulty;
|
|
const action = botActions(view)[0];
|
|
assert.equal(action.type, 'leaderSkill');
|
|
const next = applyAction(game, 'm', action);
|
|
assert.equal(next.leaderSkills!.assignments.find(a => a.owner === 'm')?.leader, action.leader);
|
|
assertMoritaniSkillsCustody(next);
|
|
}
|
|
});
|
|
|
|
void test('Moritani Planetologist controls and bots use the same extended movement through saved views', () => {
|
|
const game = movementPosition(completedMoritaniSkillsGame({ requestedSkill: 'planetologist' }));
|
|
const from = 'red_chasm:7';
|
|
player(game, 'm').forces = { [from]: 3 }; player(game, 'm').reserves = 17;
|
|
const to = TERRITORIES.filter(t => t.type === 'sand').flatMap(t => t.sectors.map(s => location(t.id, s)))
|
|
.find(key => splitLocation(key).sector !== 18 && distance(from, key, k => splitLocation(k).sector === 18) === 2)!;
|
|
assert.ok(to);
|
|
const target = splitLocation(to);
|
|
const draft = planetologistMoveDraft(viewGame(game, 'm'), 'range', target.territory, target.sector, { [from]: 3 }, {});
|
|
assert.equal(draft.blocked, null); assert.ok(draft.action);
|
|
const done = applyAction(reload(game), 'm', draft.action);
|
|
assert.equal(player(done, 'm').forces[to], 3);
|
|
assert.equal(player(done, 'm').forces[from] ?? 0, 0);
|
|
assert.equal(player(done, 'm').moved, 1);
|
|
assertMoritaniSkillsCustody(done);
|
|
});
|
|
|
|
void test('Moritani Smuggler ships the free companion once and Bureaucrat redirects a real paid bribe after restore', () => {
|
|
let game = movementPosition(completedMoritaniSkillsGame({ requestedSkill: 'smuggler' }));
|
|
const ship = { type: 'ship', territory: 'carthag', sector: 11, amount: 3, smuggler: true };
|
|
game = applyAction(game, 'm', ship);
|
|
assert.equal(player(game, 'm').forces['carthag:11'], 3);
|
|
assert.equal(player(game, 'm').reserves, 17);
|
|
assert.equal(player(game, 'm').spice, 18);
|
|
assert.deepEqual(normalizeAutomaticGame(reload(game)), game);
|
|
reject(game, 'm', ship); assertMoritaniSkillsCustody(game);
|
|
|
|
game = movementPosition(completedMoritaniSkillsGame({ requestedSkill: 'bureaucrat' }));
|
|
game = applyAction(game, 'a', { type: 'bribe', target: 'e', amount: 5 });
|
|
assert.equal(game.decision?.kind, 'bureaucratPayment');
|
|
assert.equal(game.decision?.player, 'm');
|
|
const event = game.bureaucratPaymentEvent;
|
|
const done = applyAction(reload(game), 'm', { type: 'decision', event, redirect: true });
|
|
assert.equal(player(done, 'a').spice, 15);
|
|
assert.equal(player(done, 'e').bribes, 3);
|
|
assert.equal(done.bureaucratPayments!.used.length, 1);
|
|
reject(done, 'm', { type: 'decision', event, redirect: true });
|
|
assertMoritaniSkillsCustody(done);
|
|
});
|