Files
Dune/tests/tleilaxu-skills-revival.test.ts
T
Quintine d3559f65ad Connect Basic Tleilaxu to the full Leader Skills prototype
Admit genuine Basic Tleilaxu/base setup with the full fourteen-skill deck,
Ix card inventories and native three Face Dancers. Share public profile guards
across existing skill effects while retaining public-start and Advanced gates.

Record native own-leader skill offers at successful revival commitment so
automatic Tleilaxu response chains preserve the optional private draw. Connect
Rihani to unrevealed Face Dancer exchange with exact physical/status custody,
private controls and existing legal AI choices. Release completed Rihani's
live snapshot only after validated Face Dance admission; keep signed history
and current census checks. Zoal Smuggler uses opposing printed disc value at
reveal, excluding opposing skill bonuses and his separate revival value.

Add genuine setup, response/cancellation, private choice, custody, Face Dance
reset, Zoal, legal AI and authenticated concurrent recovery regressions. Add a
reproducible two-through-six-player sample profile and saved continuation.
Preserve unresolved skill effects, foreign-ghola boundaries and delayed AI tuning.

Validation: types, lint, 5,058 offline tests and production build passed.
Five full games completed with zero rejected actions and 74 JSON restores.
Browser QA CRY4DD4J used genuine setup and an explicitly conserved staged battle;
real Rihani draw/exchange and same-state refresh passed on mobile and desktop.
Screenshot capture timed out; full visual acceptance remains open. No HTTP
endpoint/auth/storage implementation changed; authenticated SQLite recovery was
verified. All 1,581 prior rooms, 3,349 seats and auxiliary records were preserved.
One new QA room/seat remains. No server restart, database reset or automation.
2026-09-21 14:08:30 +10:00

312 lines
15 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import {
applyAction,
normalizeAutomaticGame,
viewGame,
type Action,
type Game,
} from '../game/engine';
import type { Card } from '../game/cards';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import { newRevivalRules } from '../game/revival';
import {
assertTleilaxuSkillsCustody as conserved,
completedTleilaxuSkillsGame,
reloadTleilaxuSkillsGame as reload,
rejectTleilaxuSkillsAction as reject,
tleilaxuSkillsPlayer as player,
} from './tleilaxu-skills-fixture';
const act = (game: Game, owner: string, action: Action): Game =>
applyAction(reload(game), owner, action);
function take(game: Game, owner: string, predicate: (card: Card) => boolean): Card {
const index = game.deck.findIndex(predicate);
assert.ok(index >= 0, 'The selected physical Treachery Card must exist in the real deck.');
const [card] = game.deck.splice(index, 1);
player(game, owner).hand.push(card);
return card;
}
function passWindow(state: Game): Game {
let game = state;
const kind = game.response!.kind;
for (let step = 0; game.response?.kind === kind && step < 8; step++) {
const responder = game.players.find((candidate) => !game.response!.passed.includes(candidate.id));
assert.ok(responder);
game = act(game, responder.id, { type: 'passResponse' });
}
assert.notEqual(game.response?.kind, kind);
return game;
}
/** Stage conserved armies, then kill the original skilled leader in a real battle. */
function killedOriginalSkill(owner: 't' | 'a' = 't'): Game {
let game = completedTleilaxuSkillsGame({ skillOwner: owner === 't' ? 'tleilaxu' : 'atreides' });
for (const p of game.players) {
game.deck.push(...p.hand);
p.hand = [];
p.forces = [owner, 'e'].includes(p.id)
? { 'wind_pass:14': 5, 'carthag:11': 1 }
: { 'polar_sink:0': 1 };
p.reserves = [owner, 'e'].includes(p.id) ? 14 : 19;
p.tanks = 0;
p.spice = 20;
}
Object.assign(game, {
phase: 6, storm: 18, order: [owner, 'e', owner === 't' ? 'a' : 't'],
active: owner, ready: [], decision: null, response: null, phaseOpening: null,
});
const poison = take(game, 'e', (card) => card.kind === 'poison');
game = act(game, owner, { type: 'chooseBattle', territory: 'wind_pass', target: 'e' });
for (let step = 0; step < 20; step++) {
if (game.response) game = passWindow(game);
else if (game.decision?.kind === 'leaderSkillVisibility') {
const decision = game.decision;
game = act(game, decision.player, {
type: 'leaderSkillVisibility', event: decision.event, hide: true,
});
} else if (game.battle?.preparation)
game = act(game, game.battle.preparation.owner, { type: 'declineBattlePower' });
else break;
}
assert.equal(game.decision, null);
assert.equal(game.battle?.preparation, undefined);
const leader = player(game, owner).leaders[0].id;
game = act(game, owner, { type: 'battlePlan', dial: 0, leader });
game = act(game, 'e', {
type: 'battlePlan', dial: 1, leader: 'emperor-1', weapon: poison.id,
});
game = act(game, owner, { type: 'traitorCall', call: false });
game = act(game, 'e', { type: 'traitorCall', call: false });
for (let step = 0; game.decision && step < 10; step++) {
const decision = game.decision;
if (decision.kind === 'battleCards')
game = act(game, decision.player, { type: 'decision', discard: [] });
else if (decision.kind === 'faceDance')
game = act(game, decision.player, { type: 'decision', reveal: false });
else assert.fail(`Unexpected postbattle choice: ${decision.kind}`);
}
assert.equal(player(game, owner).leaders[0].dead, true);
assert.equal(game.leaderSkills!.assignments.some((assignment) => assignment.owner === owner), false);
assert.equal(game.leaderSkills!.deck.filter((skill) => skill === 'warmaster').length, 1);
assert.equal(game.leaderSkills!.offers[owner], undefined);
conserved(game);
return game;
}
/** A clean Revival boundary retains every card, disc and force from the battle. */
function revivalGame(owner: 't' | 'a' = 't', heldKarama = false): Game {
const game = killedOriginalSkill(owner);
Object.assign(game, {
phase: 4, active: null, ready: [], decision: null, response: null,
phaseOpening: null, revivalRules: newRevivalRules(),
});
for (const p of game.players) p.leaderRevived = false;
if (heldKarama) take(game, 'e', (card) => card.effect === 'karama');
conserved(game);
return game;
}
function reviveOwn(game: Game): Game {
return act(game, 't', { type: 'reviveLeader', leader: 'tleilaxu-0' });
}
function negotiatedRevival(game: Game, amount = 7): Game {
game = act(game, 'a', { type: 'requestLeaderRevival', leader: 'atreides-0' });
game = act(game, 't', { type: 'quoteLeaderRevival', target: 'a', amount });
return act(game, 'a', { type: 'acceptLeaderRevival' });
}
function assertOffer(game: Game, owner: 't' | 'a'): string {
const offer = game.leaderSkills!.offers[owner];
assert.ok(offer);
assert.equal(offer.leader, player(game, owner).leaders[0].id);
assert.deepEqual(offer.cards, []);
for (const viewer of game.players) {
const projection = viewGame(reload(game), viewer.id);
assert.deepEqual(projection.leaderSkills!.offer, viewer.id === owner ? offer : null);
assert.equal('deck' in projection.leaderSkills!, false);
assert.equal('offers' in projection.leaderSkills!, false);
}
conserved(game);
return offer.event;
}
void test('genuine Basic Tleilaxu battle death returns the original physical skill exactly once', () => {
const game = killedOriginalSkill();
const restored = normalizeAutomaticGame(reload(game));
assert.deepEqual(restored, game);
assert.equal(game.log.filter((entry) => entry.text.includes('Warmaster returned')).length, 1);
assert.equal(player(game, 't').faceDancers!.length, 3);
assert.deepEqual(player(game, 't').traitors, []);
conserved(restored);
});
void test('automatic Tleilaxu early permission and discount keep a private undrawn skill offer after own revival', () => {
const before = revivalGame();
const previousDeck = [...before.leaderSkills!.deck];
const original = player(before, 't').leaders[0];
const cost = Math.ceil(original.strength / 2);
const game = reviveOwn(before);
assert.equal(game.response, null);
assert.equal(game.pendingRevival, null);
assert.equal(player(game, 't').leaders[0].dead, false);
assert.equal(player(game, 't').spice, player(before, 't').spice - cost);
assert.equal(player(game, 'e').spice, player(before, 'e').spice);
assert.equal(game.decision?.kind, 'leaderSkillRevival');
assertOffer(game, 't');
assert.deepEqual(game.leaderSkills!.deck, previousDeck, 'An optional draw must not preview or remove cards.');
assert.deepEqual(normalizeAutomaticGame(reload(game)), game);
assert.equal(before.leaderSkills!.offers.t, undefined, 'Applying revival must leave its input unchanged.');
});
void test('held Karama preserves early permission and discount as separate saved windows before the own-leader offer', () => {
const before = revivalGame('t', true);
let game = reviveOwn(before);
assert.equal(game.response?.kind, 'earlyRevival');
assert.equal(player(game, 't').leaders[0].dead, true);
assert.equal(game.leaderSkills!.offers.t, undefined);
assert.equal(player(game, 't').spice, player(before, 't').spice);
reject(game, 't', { type: 'leaderSkill', event: 'not-yet-created', mode: 'draw' });
game = passWindow(game);
assert.equal(game.response?.kind, 'revivalDiscount');
assert.equal(player(game, 't').leaders[0].dead, true);
assert.equal(game.leaderSkills!.offers.t, undefined);
assert.deepEqual(normalizeAutomaticGame(reload(game)), game);
game = passWindow(game);
assert.equal(game.response, null);
assert.equal(game.decision?.kind, 'leaderSkillRevival');
const event = assertOffer(game, 't');
const restored = normalizeAutomaticGame(normalizeAutomaticGame(reload(game)));
assert.equal(restored.leaderSkills!.offers.t.event, event);
assert.deepEqual(restored, game);
});
void test('declining the optional own-leader skill draw keeps deck order and never reopens after JSON normalization', () => {
let game = reviveOwn(revivalGame());
const event = assertOffer(game, 't');
const deck = [...game.leaderSkills!.deck];
game = act(game, 't', { type: 'leaderSkill', event, mode: 'decline' });
assert.equal(game.decision, null);
assert.equal(game.leaderSkills!.offers.t, undefined);
assert.deepEqual(game.leaderSkills!.deck, deck);
assert.equal(game.leaderSkills!.assignments.some((assignment) => assignment.owner === 't'), false);
assert.deepEqual(normalizeAutomaticGame(reload(game)), game);
reject(game, 't', { type: 'leaderSkill', event, mode: 'draw' });
conserved(game);
});
void test('saved optional draw rejects stale, alien and wrong-leader choices and binds one dealt card to the same revived disc', () => {
let game = reviveOwn(revivalGame());
const event = assertOffer(game, 't');
reject(game, 'e', { type: 'leaderSkill', event, mode: 'draw' });
reject(game, 't', { type: 'leaderSkill', event: `${event}-old`, mode: 'draw' });
reject(game, 't', { type: 'leaderSkill', event, skill: game.leaderSkills!.deck[0], leader: 'tleilaxu-0' });
const deck = [...game.leaderSkills!.deck];
game = act(game, 't', { type: 'leaderSkill', event, mode: 'draw' });
assert.deepEqual(game.leaderSkills!.offers.t.cards, deck.slice(0, 2));
assert.deepEqual(game.leaderSkills!.deck, deck.slice(2));
assert.equal(viewGame(reload(game), 'e').leaderSkills!.offer, null);
assert.deepEqual(normalizeAutomaticGame(reload(game)), game);
const [chosen, returned] = game.leaderSkills!.offers.t.cards;
reject(game, 't', { type: 'leaderSkill', event, mode: 'draw' });
reject(game, 't', { type: 'leaderSkill', event, mode: 'decline' });
reject(game, 't', { type: 'leaderSkill', event, skill: chosen, leader: 'tleilaxu-1' });
reject(game, 't', { type: 'leaderSkill', event, skill: chosen, leader: 'emperor-0' });
reject(game, 't', { type: 'leaderSkill', event, skill: game.leaderSkills!.deck[0], leader: 'tleilaxu-0' });
game = act(game, 't', { type: 'leaderSkill', event, skill: chosen, leader: 'tleilaxu-0' });
assert.equal(game.decision, null);
assert.equal(game.leaderSkills!.offers.t, undefined);
assert.deepEqual(game.leaderSkills!.assignments.find((assignment) => assignment.owner === 't'), {
owner: 't', leader: 'tleilaxu-0', skill: chosen,
});
assert.equal(game.leaderSkills!.deck.filter((skill) => skill === returned).length, 1);
assert.equal(game.leaderSkills!.deck.includes(chosen), false);
assert.deepEqual(normalizeAutomaticGame(reload(game)), game);
reject(game, 't', { type: 'leaderSkill', event, skill: chosen, leader: 'tleilaxu-0' });
conserved(game);
});
for (const heldKarama of [false, true])
void test(`negotiated original-leader revival retains one offer through native Tleilaxu income ${heldKarama ? 'with held Karama' : 'automatically'}`, () => {
const before = revivalGame('a', heldKarama);
let game = negotiatedRevival(before);
if (heldKarama) {
assert.equal(game.response?.kind, 'earlyRevival');
assert.equal(game.leaderSkills!.offers.a, undefined);
assert.equal(player(game, 'a').leaders[0].dead, true);
game = passWindow(game);
assert.equal(game.response?.kind, 'revivalIncome');
assert.equal(player(game, 't').spice, player(before, 't').spice);
const event = assertOffer(game, 'a');
reject(game, 'a', { type: 'leaderSkill', event, mode: 'draw' });
game = passWindow(game);
assert.equal(assertOffer(game, 'a'), event);
}
assert.equal(game.response, null);
assert.equal(game.pendingRevival, null);
assert.equal(game.decision?.kind, 'leaderSkillRevival');
assert.equal(player(game, 'a').leaders[0].dead, false);
assert.equal(player(game, 'a').spice, player(before, 'a').spice - 7);
assert.equal(player(game, 't').spice, player(before, 't').spice + 7);
assertOffer(game, 'a');
assert.deepEqual(normalizeAutomaticGame(reload(game)), game);
conserved(game);
});
for (const owner of ['t', 'a'] as const)
void test(`Karama cancellation of ${owner === 't' ? 'native Tleilaxu' : 'negotiated opponent'} early permission leaves the leader dead and creates no skill offer`, () => {
const before = revivalGame(owner, true);
let game = owner === 't' ? reviveOwn(before) : negotiatedRevival(before);
assert.equal(game.response?.kind, 'earlyRevival');
const karama = player(game, 'e').hand.find((card) => card.effect === 'karama')!;
game = act(game, 'e', { type: 'card', card: karama.id, mode: 'cancel' });
assert.equal(game.pendingRevival, null);
assert.equal(game.response, null);
assert.equal(game.decision, null);
assert.equal(player(game, owner).leaders[0].dead, true);
assert.equal(game.leaderSkills!.offers[owner], undefined);
assert.equal(player(game, owner).spice, player(before, owner).spice);
assert.equal(player(game, 't').spice, player(before, 't').spice);
assert.equal(game.discard.filter((card) => card.id === karama.id).length, 1);
assert.deepEqual(normalizeAutomaticGame(reload(game)), game);
conserved(game);
});
void test('canceling an unaffordable Tleilaxu discount aborts before revival and cannot create a skill offer', () => {
const before = revivalGame('t', true);
player(before, 't').spice = Math.ceil(player(before, 't').leaders[0].strength / 2);
let game = passWindow(reviveOwn(before));
assert.equal(game.response?.kind, 'revivalDiscount');
const karama = player(game, 'e').hand.find((card) => card.effect === 'karama')!;
game = act(game, 'e', { type: 'card', card: karama.id, mode: 'cancel' });
assert.equal(game.pendingRevival, null);
assert.equal(player(game, 't').leaders[0].dead, true);
assert.equal(player(game, 't').spice, player(before, 't').spice);
assert.equal(game.leaderSkills!.offers.t, undefined);
assert.equal(game.decision, null);
assert.deepEqual(normalizeAutomaticGame(reload(game)), game);
conserved(game);
});
void test('each AI level can complete the private original-leader draw with legal saved actions', () => {
const revived = reviveOwn(revivalGame());
for (const difficulty of DIFFICULTIES) {
let game = reload(revived);
player(game, 't').bot = difficulty;
for (let step = 0; game.leaderSkills!.offers.t && step < 3; step++) {
const action = botActions(viewGame(reload(game), 't'))[0];
assert.ok(action);
assert.equal(action.type, 'leaderSkill');
game = act(game, 't', action);
}
assert.equal(game.leaderSkills!.offers.t, undefined, difficulty);
assert.equal(game.decision, null);
assert.deepEqual(normalizeAutomaticGame(reload(game)), game);
conserved(game);
}
});