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.
153 lines
7.8 KiB
TypeScript
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);
|
|
});
|