Files
Dune/tests/moritani-assassinate-target.test.ts
Quintine eff668b202 Support Moritani assassination of captured and foreign-ghola targets
Retain printed native identity and valid original custody while projecting only
uniform potential bounty before commitment. Resolve living/dead targets once,
clear selected capture/concealment and retain ordinary Ghola return custody.
Selected concealed-outcome disclosure is explicitly provisional.

Verified: 14 affected offline files100/100, types/lint and production build;
six actual choice/gameplay/GameTable SSR journeys and twelve profile paths.
Native capture source47 actions; actual paid Ghola prefix140 actions followed
by explicit conserved seven-action battle. No saved-room/D1/reset/restart or
deployment operation. Runtime-only driver fixes reused unchanged passed checks.
2026-10-24 19:03:03 +11:00

153 lines
7.8 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, viewGame, type Action, type Game } from '../game/engine';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import { DUKE_VIDAL_ID } from '../game/duke-vidal';
import {
createMoritaniAssassinateTargetFixture as fixture,
actAssassinateTarget as act,
assassinateTargetPlayer as player,
assassinateTargetInventory as inventory,
assassinateTargetTraitors as traitors,
finishMoritaniAssassinateTarget as finish,
} from './fixture-moritani-assassinate-target';
function rejected(game: Game, actor: string, action: Action) {
const before = structuredClone(game);
assert.throws(() => applyAction(game, actor, action));
assert.deepEqual(game, before);
}
void test('a genuinely captured native target is publicly assassinated once for its printed bounty and replaced at Mentat', () => {
const f = fixture();
const target = player(f.game, f.opponent).leaders.find(l => l.id === f.target)!;
assert.equal(target.capturedBy, f.captor);
assert.equal(target.dead, false);
assert.ok(target.concealed);
assert.notEqual(f.game.moritaniAssassinate!.opportunities.at(-1)!.opposingLeader, f.target);
const choice = viewGame(f.game, f.owner).moritaniAssassinate!.pending!;
assert.equal(choice.blocked, null);
assert.deepEqual(choice.cards.find(card => card.card === f.target), {
card: f.target, name: target.name, potentialBounty: target.strength,
});
const money = player(f.game, f.owner).spice;
const captorMoney = player(f.game, f.captor).spice;
const done = act(f.game, f.owner, f.action);
const dead = player(done, f.opponent).leaders.find(l => l.id === f.target)!;
assert.equal(dead.dead, true);
assert.equal(dead.deaths, target.deaths + 1);
assert.equal(dead.capturedBy, undefined);
assert.equal(dead.concealed, undefined);
assert.equal(dead.usedAt, undefined);
assert.equal(player(done, f.owner).spice, money + f.potentialBounty);
assert.equal(done.phase, 7);
assert.equal(player(done, f.captor).spice, captorMoney + 2);
assert.ok(done.log.some(row => row.text === 'Harkonnen received 2 spice from strongholds.'));
assert.equal(done.moritaniAssassinate!.opportunities.at(-1)!.bounty, f.potentialBounty);
assert.equal(player(done, f.opponent).leaders.find(l => l.id === f.winningDisc)!.dead, false);
rejected(done, f.owner, f.action);
const mentat = finish(done, f);
const receipt = mentat.moritaniAssassinate!.opportunities.find(r => r.event === f.event)!;
assert.equal(receipt.stage, 'replaced');
assert.ok(receipt.replacement);
assert.notEqual(receipt.replacement, f.target);
assert.equal(player(mentat, f.owner).traitors.includes(f.target), false);
assert.deepEqual(inventory(mentat), f.originalInventory);
assert.deepEqual(traitors(mentat), f.originalTraitors);
});
void test('a genuinely concealed execution has the same pending choices and resolves publicly with zero bounty and no repeated death', () => {
const live = fixture();
const f = fixture({ execute: true });
const target = player(f.game, f.opponent).leaders.find(l => l.id === f.target)!;
assert.equal(target.dead, true);
assert.equal(target.deaths, 2);
assert.ok(target.concealed);
const visible = viewGame(f.game, f.owner).moritaniAssassinate!.pending!;
const liveVisible = viewGame(live.game, live.owner).moritaniAssassinate!.pending!;
assert.deepEqual(visible.cards, liveVisible.cards);
assert.equal(visible.blocked, liveVisible.blocked);
for (const card of visible.cards)
assert.deepEqual(Object.keys(card).sort(), ['card', 'name', 'potentialBounty']);
const money = player(f.game, f.owner).spice;
const done = act(f.game, f.owner, f.action);
const dead = player(done, f.opponent).leaders.find(l => l.id === f.target)!;
assert.equal(dead.dead, true);
assert.equal(dead.deaths, target.deaths);
assert.equal(dead.concealed, undefined);
assert.equal(dead.capturedBy, undefined);
assert.equal(player(done, f.owner).spice, money);
assert.equal(done.moritaniAssassinate!.opportunities.at(-1)!.bounty, 0);
assert.ok(done.log.some(row => row.text.includes('already in the Tanks; no new death or spice')));
for (const actor of [f.owner, f.opponent, f.captor]) {
const actual = viewGame(done, actor).players.find(p => p.id === f.opponent)!.leaders.find(l => l.id === f.target)!;
assert.equal(actual.dead, true);
assert.equal(viewGame(done, actor).moritaniAssassinate!.history.at(-1)!.bounty, 0);
}
const mentat = finish(done, f);
assert.equal(mentat.moritaniAssassinate!.opportunities.at(-1)!.stage, 'replaced');
assert.deepEqual(traitors(mentat), f.originalTraitors);
});
for (const execute of [false, true]) {
void test(`declining the ${execute ? 'concealed-executed' : 'living captive'} target preserves its original custody and physical Traitor`, () => {
const f = fixture({ execute });
const target = structuredClone(player(f.game, f.opponent).leaders.find(l => l.id === f.target)!);
const money = player(f.game, f.owner).spice;
const done = act(f.game, f.owner, f.decline);
assert.deepEqual(player(done, f.opponent).leaders.find(l => l.id === f.target), target);
assert.equal(player(done, f.owner).spice, money);
const mentat = finish(done, f);
assert.equal(mentat.moritaniAssassinate!.opportunities.at(-1)!.stage, 'declined');
assert.ok(player(mentat, f.owner).traitors.includes(f.target));
assert.deepEqual(player(mentat, f.opponent).leaders.find(l => l.id === f.target), target);
assert.deepEqual(traitors(mentat), f.originalTraitors);
});
}
void test('the target remains event-bound and owner-only with the original winning-disc restriction', () => {
const f = fixture();
rejected(f.game, f.captor, f.action);
rejected(f.game, f.owner, { ...f.action, event: 'other-battle' });
rejected(f.game, f.owner, { ...f.action, card: f.winningDisc });
rejected(f.game, f.owner, { ...f.action, card: DUKE_VIDAL_ID });
for (const actor of [f.opponent, f.captor])
assert.deepEqual(viewGame(f.game, actor).moritaniAssassinate!.pending!.cards, []);
});
for (const profile of DIFFICULTIES) {
void test(`${profile} legally uses the uniform potential-bounty choice for both genuine capture outcomes`, () => {
for (const execute of [false, true]) {
const f = fixture({ execute });
const own = viewGame(f.game, f.owner);
own.players.find(p => p.id === f.owner)!.bot = profile;
const actions = botActions(own);
assert.deepEqual(actions, [f.action]);
const done = act(f.game, f.owner, actions[0]);
assert.equal(done.moritaniAssassinate!.opportunities.at(-1)!.bounty, execute ? 0 : f.potentialBounty);
assert.equal(finish(done, f).moritaniAssassinate!.opportunities.at(-1)!.stage, 'replaced');
}
});
}
void test('an actual paid foreign-ghola target keeps native identity and Tleilaxu return custody after the controlled later battle', async () => {
const { createMoritaniAssassinateGholaTargetFixture } = await import('./fixture-moritani-assassinate-ghola-target');
const f = createMoritaniAssassinateGholaTargetFixture();
const target = player(f.game, f.opponent).leaders.find(l => l.id === f.target)!;
assert.equal(target.dead, false);
assert.ok(target.gholaBy);
const controller = target.gholaBy;
const money = player(f.game, f.owner).spice;
const controllerMoney = player(f.game, controller).spice;
const done = applyAction(f.game, f.owner, f.action);
const killed = player(done, f.opponent).leaders.find(l => l.id === f.target)!;
assert.equal(killed.faction, target.faction);
assert.equal(killed.dead, true);
assert.equal(killed.deaths, target.deaths + 1);
assert.equal(killed.gholaBy, controller);
assert.equal(killed.concealed, undefined);
assert.equal(player(done, f.owner).spice, money + f.potentialBounty);
assert.equal(player(done, controller).spice, controllerMoney);
});