Files
Dune/tests/moritani-skills-custody.test.ts
T

269 lines
8.4 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { applyAction, viewGame, type Game } from '../game/engine';
import { newRevivalRules } from '../game/revival';
import {
assertMoritaniSkillsCustody,
completedMoritaniSkillsGame,
enterMoritaniSkillsTerror,
initializedMoritaniSkillsOffers,
moritaniSkillsPlayer,
placeMoritaniSkillsTerror,
rejectMoritaniSkillsAction,
reloadMoritaniSkillsGame,
} from './moritani-skills-fixture';
const forceTotal = (game: Game, id: string) => {
const player = moritaniSkillsPlayer(game, id);
return (
player.reserves +
player.tanks +
Object.values(player.forces).reduce((sum, count) => sum + count, 0)
);
};
void test('genuine Basic Moritani setup keeps all fourteen skill cards and each two-card offer private', () => {
const game = initializedMoritaniSkillsOffers({
requestedSkill: 'warmaster',
skillOwner: 'moritani',
});
assert.equal(game.advanced, false);
assert.deepEqual(game.expansions, ['ecaz']);
assert.deepEqual(
new Set(game.players.map((player) => player.faction)),
new Set(['moritani', 'atreides', 'emperor']),
);
assert.equal(game.leaderSkills!.deck.length, 8);
assert.equal(Object.keys(game.leaderSkills!.offers).length, 3);
assert.equal(game.leaderSkills!.assignments.length, 0);
assertMoritaniSkillsCustody(game);
for (const viewer of game.players) {
const view = viewGame(reloadMoritaniSkillsGame(game), viewer.id);
assert.deepEqual(
view.leaderSkills!.offer,
game.leaderSkills!.offers[viewer.id],
);
assert.equal(view.leaderSkills!.offer!.cards.length, 2);
assert.equal('deck' in view.leaderSkills!, false);
assert.equal('offers' in view.leaderSkills!, false);
}
});
void test('Terror Assassination returns exactly the killed leader skill once and real revival offers one private optional replacement after JSON restore', () => {
let game = completedMoritaniSkillsGame({
requestedSkill: 'warmaster',
skillOwner: 'emperor',
});
const assignment = game.leaderSkills!.assignments.find(
(candidate) => candidate.owner === 'e',
)!;
assert.deepEqual(assignment, {
owner: 'e',
leader: 'emperor-0',
skill: 'warmaster',
});
const emperor = moritaniSkillsPlayer(game, 'e');
for (const leader of emperor.leaders)
if (leader.id !== assignment.leader) {
leader.dead = true;
leader.deaths = 1;
}
const forceTotals = Object.fromEntries(
game.players.map((player) => [player.id, forceTotal(game, player.id)]),
);
game = placeMoritaniSkillsTerror(game, 'assassination');
assert.deepEqual(
Object.fromEntries(
game.players.map((player) => [player.id, forceTotal(game, player.id)]),
),
forceTotals,
);
const beforeEntry = reloadMoritaniSkillsGame(game);
game = enterMoritaniSkillsTerror(game, 'e');
assert.equal(moritaniSkillsPlayer(game, 'e').reserves,
moritaniSkillsPlayer(beforeEntry, 'e').reserves - 1);
assert.equal(moritaniSkillsPlayer(game, 'e').forces['carthag:11'], 1);
assert.equal(forceTotal(game, 'e'), forceTotals.e);
assert.equal(viewGame(game, 'm').terrorEntry!.kind, 'assassination');
assert.equal('kind' in viewGame(game, 'e').terrorEntry!, false);
assert.equal(game.leaderSkills!.deck.includes('warmaster'), false);
const killed = applyAction(game, 'm', {
type: 'decision',
reveal: true,
});
const killedLeader = moritaniSkillsPlayer(killed, 'e').leaders.find(
(leader) => leader.id === assignment.leader,
)!;
assert.equal(killedLeader.dead, true);
assert.equal(killedLeader.deaths, 1);
assert.equal(
killed.leaderSkills!.assignments.some(
(candidate) => candidate.owner === 'e',
),
false,
);
assert.equal(
killed.leaderSkills!.deck.filter((skill) => skill === 'warmaster').length,
1,
);
assertMoritaniSkillsCustody(killed);
rejectMoritaniSkillsAction(killed, 'm', {
type: 'decision',
reveal: true,
});
const restored = reloadMoritaniSkillsGame(killed);
Object.assign(restored, {
turn: restored.turn + 1,
phase: 4,
active: null,
ready: [],
decision: null,
response: null,
phaseOpening: null,
revivalRules: newRevivalRules(),
});
const restoredEmperor = moritaniSkillsPlayer(restored, 'e');
restoredEmperor.spice = 20;
restoredEmperor.leaderRevived = false;
const offered = applyAction(restored, 'e', {
type: 'reviveLeader',
leader: assignment.leader,
});
assert.equal(offered.decision?.kind, 'leaderSkillRevival');
assert.equal(
moritaniSkillsPlayer(offered, 'e').leaders.find(
(leader) => leader.id === assignment.leader,
)!.dead,
false,
);
assert.deepEqual(viewGame(offered, 'e').leaderSkills!.offer!.cards, []);
assert.equal(viewGame(offered, 'm').leaderSkills!.offer, null);
assert.equal(viewGame(offered, 'a').leaderSkills!.offer, null);
assertMoritaniSkillsCustody(offered);
const event = offered.leaderSkills!.offers.e.event;
rejectMoritaniSkillsAction(
offered,
'm',
{ type: 'leaderSkill', event, mode: 'draw' },
/not waiting for you/,
);
rejectMoritaniSkillsAction(
offered,
'e',
{ type: 'leaderSkill', event: `${event}:stale`, mode: 'draw' },
/not waiting for you/,
);
const declined = applyAction(reloadMoritaniSkillsGame(offered), 'e', {
type: 'leaderSkill',
event,
mode: 'decline',
});
assert.equal(declined.decision, null);
assert.equal(declined.leaderSkills!.offers.e, undefined);
assert.equal(
declined.leaderSkills!.assignments.some(
(candidate) => candidate.owner === 'e',
),
false,
);
assertMoritaniSkillsCustody(declined);
const drawn = applyAction(reloadMoritaniSkillsGame(offered), 'e', {
type: 'leaderSkill',
event,
mode: 'draw',
});
const privateOffer = viewGame(reloadMoritaniSkillsGame(drawn), 'e')
.leaderSkills!.offer!;
assert.equal(privateOffer.cards.length, 2);
assert.equal(viewGame(drawn, 'm').leaderSkills!.offer, null);
assert.equal(viewGame(drawn, 'a').leaderSkills!.offer, null);
assertMoritaniSkillsCustody(drawn);
const choice = {
type: 'leaderSkill',
event,
skill: privateOffer.cards[0],
leader: assignment.leader,
} as const;
const selected = applyAction(drawn, 'e', choice);
assert.deepEqual(
selected.leaderSkills!.assignments.find(
(candidate) => candidate.owner === 'e',
),
{
owner: 'e',
leader: assignment.leader,
skill: privateOffer.cards[0],
},
);
assert.equal(selected.leaderSkills!.offers.e, undefined);
assertMoritaniSkillsCustody(selected);
rejectMoritaniSkillsAction(
selected,
'e',
choice,
/not waiting for you/,
);
});
void test('Sabotage discards only Treachery cards while skill custody and shared projections stay unchanged', () => {
let game = completedMoritaniSkillsGame({
requestedSkill: 'warmaster',
skillOwner: 'emperor',
});
game = placeMoritaniSkillsTerror(game, 'sabotage');
const beforeEntry = reloadMoritaniSkillsGame(game);
const entrantCard = moritaniSkillsPlayer(beforeEntry, 'e').hand[0];
assert.ok(entrantCard);
game = enterMoritaniSkillsTerror(game, 'e');
const revealed = applyAction(game, 'm', {
type: 'decision',
reveal: true,
});
assert.equal(revealed.pendingTerrorEntry?.stage, 'gift');
assert.equal(
moritaniSkillsPlayer(revealed, 'e').hand.some(
(card) => card.id === entrantCard.id,
),
false,
);
assert.equal(
revealed.discard.filter((card) => card.id === entrantCard.id).length,
1,
);
assert.deepEqual(revealed.leaderSkills, beforeEntry.leaderSkills);
assertMoritaniSkillsCustody(revealed);
const projections = revealed.players.map(
(player) => viewGame(reloadMoritaniSkillsGame(revealed), player.id).leaderSkills!,
);
for (const projection of projections) {
assert.deepEqual(projection.assignments, projections[0].assignments);
assert.equal(projection.offer, null);
assert.equal('deck' in projection, false);
}
const opponentView = viewGame(revealed, 'a');
assert.equal('discard' in opponentView, false);
assert.equal(JSON.stringify(opponentView).includes(entrantCard.id), false);
const done = applyAction(reloadMoritaniSkillsGame(revealed), 'm', {
type: 'decision',
decline: true,
});
assert.equal(done.pendingTerrorEntry, null);
assert.equal(done.decision, null);
assert.deepEqual(done.leaderSkills, beforeEntry.leaderSkills);
assertMoritaniSkillsCustody(done);
rejectMoritaniSkillsAction(done, 'm', {
type: 'decision',
decline: true,
});
});