User ruling 13 October 2026: the original faction keeps its existing Skill assignment while its trained leader is captured. Reviving a different eligible own leader is legal and simply draws no replacement card while that assignment exists (selected reading of authorized supplied Advanced physical p21 "if you have no Leader Skill Card"). Not a new publisher ruling; captor lower-band use, card movement and death-return unchanged. Remove the replacement-question veto that suppressed the entire ordinary/Ghola revival: leaderSkillRevivalBlocked/requireLeaderSkillRevival, the gholaLeaders early return, the view revival.leaders: [] filter and the beginRevival/Ghola/ Emperor-special-Karama call sites are gone. offerRevivedLeaderSkill is unchanged and still suppresses a second assignment or draw. Rewrote the two committed rejection assertions (tests/advanced-native-skills.test.ts, tests/leader-skills-capture.test.ts) and added native regressions covering ordinary paid, held-Ghola and Emperor special Karama revival from a genuinely captured skilled state: exact cost, no draw or offer, retained captive/card/controller and a conserved all14 deck. Docs: RULE_DECISIONS, LEADER_SKILLS_CAPTURE, LEADER_SKILLS_RULES, LEADER_SKILLS_RUNTIME, NEXUS_CARD_RULES, CURRENT_STATUS, IMPLEMENTATION_STATUS and the game/reference.ts leader-skills topic.
108 lines
6.4 KiB
TypeScript
108 lines
6.4 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { applyAction, viewGame } from '../game/engine';
|
|
import { botActions } from '../game/bots';
|
|
import { leaderRevivalOptions } from '../game/revival';
|
|
import {
|
|
advanceCapturedSkillSpecial, capturedSkillSpecialClean,
|
|
capturedSkillSpecialInventory, capturedSkillSpecialPlayer as player,
|
|
capturedSkillSpecialStock, createCapturedSkillSpecialRevivalFixture,
|
|
type CapturedSkillSpecialStep,
|
|
} from './fixture-captured-skill-special-revival';
|
|
|
|
void test('native Emperor Special revives a different own dead disc without replacing the captive Skill Card', () => {
|
|
const f = createCapturedSkillSpecialRevivalFixture();
|
|
const before = structuredClone(f.game);
|
|
const emperor = player(before, 'e');
|
|
const dead = emperor.leaders.find(l => l.id === f.dead)!;
|
|
const captive = emperor.leaders.find(l => l.id === f.captive)!;
|
|
assert.equal(dead.dead, true);
|
|
assert.equal(dead.deaths, 1);
|
|
assert.equal(dead.capturedBy, undefined);
|
|
assert.equal(captive.dead, false);
|
|
assert.equal(captive.capturedBy, 'h');
|
|
assert.equal(emperor.leaderRevived, false);
|
|
assert.equal(emperor.revived, 0);
|
|
assert.equal(emperor.specialKaramaUsed, undefined);
|
|
assert.equal(before.turn, 2);
|
|
assert.equal(before.phase, 4);
|
|
assert.ok(capturedSkillSpecialClean(before));
|
|
assert.equal(player(f.setup, 'h').forces['carthag:11'], 10);
|
|
assert.equal(player(f.afterShipment, 'e').forces['carthag:11'], 2);
|
|
assert.equal(player(f.afterShipment, 'e').spice, player(f.beforeShipment, 'e').spice - 2);
|
|
assert.ok(f.actions.some(step => step.actor === 'h' && step.action.type === 'decision' && step.action.mode === 'keep'));
|
|
|
|
const view = viewGame(before, 'e');
|
|
const original = view.leaderSkills!.assignments.find(a => a.owner === 'e')!;
|
|
assert.deepEqual(original, { skill: 'sandmaster', leader: f.captive, owner: 'e',
|
|
controller: 'h', captured: true, faceUp: false });
|
|
assert.equal(view.leaderSkills!.offer, null);
|
|
assert.deepEqual(view.revival.leaders, leaderRevivalOptions(before, emperor).leaders,
|
|
'Ordinary cycle eligibility remains native; Special uses the original dead roster, not a fabricated ordinary menu');
|
|
assert.equal(view.players.find(p => p.id === 'e')!.leaders.find(l => l.id === f.dead)!.dead, true);
|
|
assert.equal(view.players.find(p => p.id === 'e')!.leaders.find(l => l.id === f.captive)!.capturedBy, 'h');
|
|
view.players.find(p => p.id === 'e')!.bot = 'Easy';
|
|
const recommendations = botActions(view);
|
|
assert.equal(recommendations.length, 1);
|
|
assert.deepEqual(recommendations[0], f.action,
|
|
'Existing Easy policy already selects the one legal printed Special; no policy strategy is introduced');
|
|
|
|
assert.throws(() => applyAction(before, 'e', { ...f.action, leader: f.captive }), /dead leaders/);
|
|
assert.throws(() => applyAction(before, 'e', { ...f.action, target: 'h' }), /own forces or leaders/);
|
|
assert.throws(() => applyAction(f.beforeBattle, 'e', f.action), /during Revival/);
|
|
assert.throws(() => applyAction(f.initial, 'e', f.action), /setup|advanced game|phase opening/i);
|
|
assert.throws(() => applyAction(before, 'e', { ...f.action, card: f.weapon }), /Karama card in your hand/);
|
|
assert.throws(() => applyAction(before, 'h', f.action), /Karama card in your hand/);
|
|
assert.throws(() => applyAction(before, 'e', { ...f.action, amount: 1 }), /forces or one leader/);
|
|
assert.deepEqual(f.game, before, 'Rejected selections cannot consume the original right or physical card');
|
|
|
|
// This printed Special is immediate: it creates no generic paid-revival
|
|
// stop/cancellation observer or second suffix. One real declaration is enough.
|
|
const after = applyAction(before, 'e', recommendations[0]);
|
|
const revived = player(after, 'e');
|
|
assert.equal(revived.leaders.find(l => l.id === f.dead)!.dead, false);
|
|
assert.equal(revived.leaders.find(l => l.id === f.dead)!.deaths, dead.deaths);
|
|
assert.equal(revived.spice, emperor.spice, 'Full printed free revival never debits the leader strength');
|
|
assert.equal(revived.specialKaramaUsed, true);
|
|
assert.equal(revived.leaderRevived, emperor.leaderRevived);
|
|
assert.equal(revived.revived, emperor.revived);
|
|
assert.equal(revived.tanks, emperor.tanks);
|
|
assert.equal(revived.reserves, emperor.reserves);
|
|
assert.deepEqual(revived.forces, emperor.forces);
|
|
assert.deepEqual(revived.elites, emperor.elites);
|
|
assert.equal(revived.hand.filter(c => c.id === f.karama).length, 0);
|
|
assert.equal(after.discard.filter(c => c.id === f.karama).length, 1);
|
|
assert.deepEqual(capturedSkillSpecialStock(after), f.stock);
|
|
assert.deepEqual(after.leaderSkills, before.leaderSkills,
|
|
'One unique original assignment and unchanged physical all14 deck: no replacement offer or draw');
|
|
assert.deepEqual(revived.leaders.find(l => l.id === f.captive), captive);
|
|
assert.deepEqual(viewGame(after, 'e').leaderSkills!.assignments.find(a => a.owner === 'e'), original);
|
|
assert.equal(viewGame(after, 'e').leaderSkills!.offer, null);
|
|
assert.equal(after.pendingRevival, undefined);
|
|
assert.equal(after.response, null);
|
|
assert.equal(after.decision, null);
|
|
assert.equal(after.pendingTreacheryDiscard, undefined);
|
|
assert.equal(after.phase, before.phase);
|
|
assert.equal(after.turn, before.turn);
|
|
const receipt = `Emperor used special Karama to revive ${dead.name} for free.`;
|
|
assert.equal(after.log.filter(entry => entry.text === receipt).length, 1);
|
|
capturedSkillSpecialInventory(after);
|
|
assert.throws(() => applyAction(after, 'e', f.action), /already been used/);
|
|
assert.throws(() => applyAction(after, 'e', { ...f.action, amount: 1, leader: undefined }), /already been used/);
|
|
assert.equal(botActions(viewGame(after, 'e')).filter(a => a.type === 'card' && a.mode === 'special').length, 0);
|
|
|
|
const suffix: CapturedSkillSpecialStep[] = [];
|
|
const continued = advanceCapturedSkillSpecial(after,
|
|
g => g.phase === 5 && capturedSkillSpecialClean(g), suffix);
|
|
assert.equal(continued.turn, 2);
|
|
assert.equal(continued.phase, 5);
|
|
assert.ok(suffix.length > 0);
|
|
assert.ok(suffix.every(step => step.action.type !== 'card'));
|
|
assert.equal(continued.log.filter(entry => entry.text === receipt).length, 1);
|
|
assert.equal(continued.discard.filter(c => c.id === f.karama).length, 1);
|
|
assert.deepEqual(continued.leaderSkills, before.leaderSkills);
|
|
assert.deepEqual(player(continued, 'e').leaders.find(l => l.id === f.captive), captive);
|
|
assert.deepEqual(capturedSkillSpecialStock(continued), f.stock);
|
|
capturedSkillSpecialInventory(continued);
|
|
});
|