130 lines
7.8 KiB
TypeScript
130 lines
7.8 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
import { createElement } from 'react';
|
|
import { applyAction, normalizeAutomaticGame, viewGame, type Action, type Game } from '../game/engine';
|
|
import { LEADER_SKILL_CARDS } from '../game/leader-skill-cards';
|
|
import { registerHooks } from 'node:module';
|
|
const aliases = registerHooks({resolve(specifier,context,next){return next(specifier === 'next/image' ? 'vinext/shims/image' : specifier,context);}});
|
|
const { RihaniChoice } = await import('../components/rihani-decipherer');
|
|
aliases.deregister();
|
|
import { sampleInventory, verifySampleCustody } from '../tools/sample-custody';
|
|
import { completedTleilaxuSkillsGame, assertTleilaxuSkillsCustody as custody, tleilaxuSkillsPlayer as player, reloadTleilaxuSkillsGame as reload, rejectTleilaxuSkillsAction as reject } from './tleilaxu-skills-fixture';
|
|
|
|
const act = (g: Game, owner: string, action: Action) => applyAction(reload(g), owner, action);
|
|
function stable(g: Game) {
|
|
custody(g); verifySampleCustody(g, sampleInventory(g));
|
|
assert.deepEqual(normalizeAutomaticGame(reload(g)), g);
|
|
}
|
|
// Explicit conserved positions after genuine setup; not claimed as played phase histories.
|
|
function stage(g: Game, owner = 't', opponent = 'e') {
|
|
for (const p of g.players) {
|
|
g.deck.push(...p.hand); p.hand = [];
|
|
p.forces = [owner, opponent].includes(p.id) ? { 'wind_pass:14': 6, 'carthag:11': 1 } : {};
|
|
p.reserves = [owner, opponent].includes(p.id) ? 13 : 20; p.tanks = 0; p.spice = 20;
|
|
}
|
|
Object.assign(g, { phase: 6, active: owner, storm: 18, order: [owner, opponent, ...g.players.map(p => p.id).filter(id => id !== owner && id !== opponent)],
|
|
ready: [], decision: null, response: null, phaseOpening: null, spice: {} });
|
|
return g;
|
|
}
|
|
function open(g: Game, owner = 't', opponent = 'e') {
|
|
g = act(g, owner, { type: 'chooseBattle', territory: 'wind_pass', target: opponent });
|
|
for (let i = 0; i < 30; i++) {
|
|
if (g.response) g = act(g, g.players.find(p => !g.response!.passed.includes(p.id))!.id, { type: 'passResponse' });
|
|
else if (g.decision?.kind === 'mentatQuestion') g = act(g, g.decision.player, { type: 'decision', event: g.decision.event, decline: true });
|
|
else if (g.decision?.kind === 'leaderSkillVisibility') g = act(g, g.decision.player, { type: 'leaderSkillVisibility', event: g.decision.event, hide: true });
|
|
else if (g.battle?.preparation) g = act(g, g.battle.preparation.owner, { type: 'declineBattlePower' });
|
|
else break;
|
|
}
|
|
assert.equal(g.decision, null); assert.equal(g.battle?.preparation, undefined);
|
|
return g;
|
|
}
|
|
function plans(g: Game, own: Partial<Action> = {}, other: Partial<Action> = {}, owner = 't', opponent = 'e') {
|
|
g = act(g, owner, { type: 'battlePlan', dial: 4, leader: player(g, owner).leaders[0].id, ...own });
|
|
return act(g, opponent, { type: 'battlePlan', dial: 0, leader: player(g, opponent).leaders[1].id, ...other });
|
|
}
|
|
function resolve(g: Game, owner = 't', opponent = 'e') {
|
|
g = act(g, owner, { type: 'traitorCall', call: false });
|
|
return act(g, opponent, { type: 'traitorCall', call: false });
|
|
}
|
|
function exchange(g: Game, owner: string) {
|
|
const event = g.rihaniHistory!.at(-1)!.event;
|
|
g = act(g, owner, { type: 'decision', event, draw: true });
|
|
const r = g.rihaniHistory!.at(-1)!;
|
|
return act(g, owner, { type: 'decision', event, cards: [r.drawn[0], r.eligible[0]] });
|
|
}
|
|
|
|
void test('all fourteen skills assign genuine Basic Tleilaxu discs through original setup', () => {
|
|
for (const card of LEADER_SKILL_CARDS) {
|
|
const game = completedTleilaxuSkillsGame({ requestedSkill: card.id });
|
|
assert.equal(game.leaderSkills!.assignments.find(a => a.owner === 't')!.skill, card.id);
|
|
stable(game);
|
|
}
|
|
});
|
|
|
|
void test('native Rihani keeps a private Face Dancer, returns only an old unrevealed card and survives JSON across every choice', () => {
|
|
let g = stage(completedTleilaxuSkillsGame({requestedSkill:'rihani-decipherer'}));
|
|
player(g,'t').faceDancers![0].revealed = true; // Conserved pre-existing public reveal.
|
|
const known = player(g,'t').faceDancers![0].leader;
|
|
g = resolve(plans(open(g)));
|
|
assert.equal(g.decision?.kind,'rihani');stable(g);
|
|
const offered = g.rihaniHistory!.at(-1)!;
|
|
assert.equal(offered.peeked.length,2);assert.equal(offered.drawn.length,0);assert.ok(!offered.eligible.includes(known));
|
|
assert.deepEqual(viewGame(g,'e').rihani!.history,[]);
|
|
reject(g,'e',{type:'decision',event:offered.event,draw:true});
|
|
const markup = renderToStaticMarkup(createElement(RihaniChoice,{game:viewGame(g,'t'),act:()=>{},busy:false}));
|
|
assert.match(markup,/Draw two Face Dancers/);assert.match(markup,/unrevealed/);
|
|
g = act(g,'t',{type:'decision',event:offered.event,draw:true});stable(g);
|
|
const drawn = g.rihaniHistory!.at(-1)!;
|
|
assert.equal(player(g,'t').faceDancers!.length,5);assert.equal(player(g,'t').traitors.length,0);
|
|
for (const observer of ['a','e']) {
|
|
assert.deepEqual(viewGame(g,observer).rihani!.history,[]);
|
|
assert.equal(viewGame(g,observer).rihani!.pending?.drawn,undefined);
|
|
}
|
|
reject(g,'t',{type:'decision',event:drawn.event,cards:[drawn.drawn[0],known]});
|
|
g=act(g,'t',{type:'decision',event:drawn.event,cards:[drawn.drawn[0],drawn.eligible[0]]});
|
|
assert.equal(player(g,'t').faceDancers!.length,3);
|
|
assert.ok(player(g,'t').faceDancers!.some(c=>c.leader===known&&c.revealed));
|
|
assert.ok(player(g,'t').faceDancers!.some(c=>c.leader===drawn.drawn[0]&&!c.revealed));
|
|
assert.ok(g.traitorReserve!.includes(drawn.drawn[1]));stable(g);
|
|
});
|
|
|
|
void test('completed winner Rihani cleanup releases custody for later Face Dance and returns the killed leader skill once', () => {
|
|
let g = stage(completedTleilaxuSkillsGame({requestedSkill:'rihani-decipherer',skillOwner:'atreides'}),'a','e');
|
|
const target='atreides-0', dancer=player(g,'t').faceDancers![0];
|
|
// Swap physical identities to target this battle without fabricating a card.
|
|
const index=g.traitorReserve!.indexOf(target);
|
|
if(index>=0) {g.traitorReserve![index]=dancer.leader;dancer.leader=target;}
|
|
else {
|
|
const held=g.players.find(p=>p.traitors.includes(target));
|
|
if(held) {held.traitors[held.traitors.indexOf(target)]=dancer.leader;dancer.leader=target;}
|
|
else assert.ok(player(g,'t').faceDancers!.some(c=>c.leader===target));
|
|
}
|
|
g=resolve(plans(open(g,'a','e'),{},{},'a','e'),'a','e');
|
|
g=exchange(g,'a');
|
|
assert.equal(g.decision?.kind,'faceDance');assert.equal(g.lastBattleContext!.rihani!.faceDanceStarted,true);stable(g);
|
|
const before=structuredClone(g.rihaniHistory), spice=player(g,'t').spice;
|
|
g=act(g,'t',{type:'decision',reveal:true,sources:{reserves:1},sector:14});
|
|
assert.equal(g.phase,6);assert.equal(player(g,'a').leaders[0].dead,true);
|
|
assert.ok(!g.leaderSkills!.assignments.some(a=>a.leader===target));
|
|
assert.equal(g.leaderSkills!.deck.filter(s=>s==='rihani-decipherer').length,1);
|
|
assert.deepEqual(g.rihaniHistory,before);assert.equal(player(g,'t').spice,spice);stable(g);
|
|
});
|
|
|
|
void test('Zoal Smuggler collects copied opposing printed strength at reveal, including zero for Cheap Hero', () => {
|
|
for (const strength of [5,3,0]) {
|
|
let g=stage(completedTleilaxuSkillsGame({requestedSkill:'smuggler'}));g.spice={'wind_pass:14':9};
|
|
let other:Partial<Action>;
|
|
if(strength===0) {
|
|
const i=g.deck.findIndex(c=>c.kind==='hero');assert.ok(i>=0);
|
|
const card=g.deck.splice(i,1)[0];player(g,'e').hand.push(card);other={leader:card.id};
|
|
} else {
|
|
const disc=player(g,'e').leaders.find(l=>l.strength===strength);assert.ok(disc);other={leader:disc.id};
|
|
}
|
|
g=plans(open(g),{},other);
|
|
assert.equal(g.battle!.smugglerCollection!.strength,strength);
|
|
assert.equal(player(g,'t').spice,20); // Sealing/reveal records, settlement collects.
|
|
g=resolve(g);assert.equal(player(g,'t').spice,20+strength);assert.equal(g.spice['wind_pass:14'],9-strength);stable(g);
|
|
}
|
|
});
|