Retain trained Ixian response inside paused Semuta cost

This commit is contained in:
Quintine committed 2026-10-12 08:51:20 +11:00
1 parent 90c6f0020c
commit 5b3729e2db
7 files changed
+92 -8

No files matched your search

+4 -4
View File
@@ -26,12 +26,12 @@ export function semutaIxPhysical(game: Game) {
}
/** Original native lobby, private Ix card choice, Traitors and actual first Storm/HMS placement. */
export function initializeSemutaIxGame(advanced = false): Game {
export function initializeSemutaIxGame(advanced = false, skills = false): Game {
let game = createGame('SEMUTAIX', newPlayer('r', 'Richese', 'richese'), advanced, ['choam', 'ix']);
joinGame(game, newPlayer('i', 'Ixians', 'ixians'));
joinGame(game, newPlayer('e', 'Emperor', 'emperor'));
for (const p of game.players) { p.bot = 'Medium'; p.ready = true; }
game = initializeSemutaGameForAudit(game);
game = initializeSemutaGameForAudit(game, skills);
for (let step = 0; (game.status === 'setup' || game.phase === 1 || !game.mobileStronghold?.location) && step < 160; step++) {
let next: Game | undefined;
for (const p of game.players) {
@@ -49,8 +49,8 @@ export function initializeSemutaIxGame(advanced = false): Game {
}
/** Later rule position uses conserved native pieces, not a synthetic Game or replacement players. */
export function semutaIxBattleFixture(advanced = false): SemutaIxFixture {
const game = initializeSemutaIxGame(advanced);
export function semutaIxBattleFixture(advanced = false, skills = false): SemutaIxFixture {
const game = initializeSemutaIxGame(advanced, skills);
const cards = semutaIxInventory(game);
assert.equal(new Set(cards).size, cards.length);
for (const p of game.players) {
+6 -2
View File
@@ -24,8 +24,8 @@ export function semutaNativeKaramaAct(game: Game, step: SemutaNativeKaramaStep,
}
/** Original lobby and conserved later battle; only the existing printed Karama changes hands. */
export function semutaNativeIxKaramaFixture(advanced = false): SemutaNativeKaramaFixture {
const ix = semutaIxBattleFixture(advanced);
export function semutaNativeIxKaramaFixture(advanced = false, skills = false): SemutaNativeKaramaFixture {
const ix = semutaIxBattleFixture(advanced, skills);
const richese = semutaIxPlayer(ix.game, ix.holder);
const index = richese.hand.findIndex(card => card.effect === 'karama');
assert.ok(index >= 0);
@@ -85,6 +85,10 @@ export function semutaNativeKaramaPreCost(f: SemutaNativeKaramaFixture, actions?
step = { actor: game.battle.preparation.owner, action: { type: 'declineBattlePower' } };
else if (game.decision?.kind === 'fullPlanOffer')
step = { actor: game.decision.player, action: { type: 'decision', decline: true } };
else if (game.decision?.kind === 'leaderSkillVisibility')
step = { actor: game.decision.player, action: { type: 'leaderSkillVisibility', event: game.decision.event, hide: true } };
else if (game.decision?.kind === 'mentatQuestion')
step = { actor: game.decision.player, action: { type: 'decision', event: game.decision.event, decline: true } };
else break;
game = semutaNativeKaramaAct(game, step, actions);
}
+26
View File
@@ -203,3 +203,29 @@ void test('All four profiles legally retire native printed costs and their origi
}
}
});
for (const claim of [false, true]) void test(`Skill-trained Ix cancellation cost ${claim ? 'claim' : 'pass'} preserves the declared response while Semuta owns controls`, () => {
const f = semutaNativeIxKaramaFixture(true, true); assert.ok(f.ix);
const before = semutaNativeKaramaPreCost(f);
assert.ok(before.pendingIxSubstitution?.receipt);
const pending = semutaNativeKaramaCost(before, f);
const event = pending.pendingTreacheryDiscard!.batch.event;
assert.deepEqual(pending.pendingIxSubstitution!.sources, { [f.ix.location]: 1 });
assert.deepEqual(pending.pendingIxSubstitution!.recover, { [f.ix.location]: 1 });
for (const player of pending.players)
assert.equal(viewGame(reload(pending), player.id).semutaReaction!.event, event);
let game = semutaNativeKaramaReact(reload(pending), f, claim);
assert.equal(game.pendingIxSubstitution, null);
assert.equal(game.lastBattleContext!.ixSubstitution!.completed, true);
assert.equal(game.pendingTreacheryDiscard!.continuation.kind, 'winnerMandatoryDiscard');
game = semutaNativeKaramaRemainder(game, f);
assert.equal(game.phase, 7);
assert.equal(semutaIxPlayer(game, f.owner).forces[f.ix.location], 2);
assert.equal(semutaIxPlayer(game, f.owner).elites!.forces[f.ix.location] ?? 0, 0);
assert.equal(semutaIxPlayer(game, f.owner).elites!.tanks, 1);
assert.equal(semutaIxPlayer(game, f.holder).hand.some(card => card.id === f.karama), claim);
assert.deepEqual(semutaIxInventory(game), f.cards);
for (const player of game.players) viewGame(reload(game), player.id);
const cost = semutaNativeKaramaCostStep(f); rejected(game, cost.actor, cost.action);
rejected(game, f.holder, { type: 'semutaCommit', event });
});