269 lines
8.4 KiB
TypeScript
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,
|
|
});
|
|
});
|