Files
Dune/tests/salusa-ghola-truth-controls.test.ts
T

92 lines
4.8 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, viewGame, type Game } from '../game/engine';
import { botActions } from '../game/bots';
import type { PlanClaim } from '../game/battle-promises';
import { homeworldRevivalActionBlock } from '../game/homeworld-revival-deployment-options';
import { createSalusaGholaTruthFixture } from './fixture-salusa-ghola-truth';
type Fixture = ReturnType<typeof createSalusaGholaTruthFixture>;
type Case = 'dial2' | 'dial1' | 'support';
const sources = new Map<Case, Fixture>();
function source(kind: Case): Fixture {
let fixture = sources.get(kind);
if (!fixture) {
const claim: PlanClaim = kind === 'support'
? { kind: 'support', compare: 'gte', value: 1 }
: { kind: 'dial', compare: 'eq', value: kind === 'dial1' ? 1 : 2 };
fixture = createSalusaGholaTruthFixture({ claim, answer: 'yes', inspectionDial: kind === 'dial1' ? 1 : 2 });
sources.set(kind, fixture);
}
return fixture;
}
const profiles = ['Easy', 'Medium', 'Hard', 'Brutal'] as const;
function ownPromise(game: Game, actor: string) {
const promise = game.battle?.truthPromises?.find(row => row.player === actor && !row.released);
assert.ok(promise, 'The actual answered Truthtrance promise remains live.');
return promise;
}
// These are shared control-adapter and minimal-policy checks over the earned
// native source. No Ghola amount selector or Battle Plan form is operated here;
// GameTable SSR is covered by the integration owner's separate runtime smoke.
void test('the exact owned Ghola group is blocked at Low Salusa while the native Yes dial2 remains', () => {
const f = source('dial2'), game = structuredClone(f.game), view = viewGame(game, f.actor);
assert.deepEqual(f.action, { type: 'card', card: f.card, amount: 1, elite: 1 });
assert.deepEqual(ownPromise(game, f.actor).claim, f.claim);
assert.match(homeworldRevivalActionBlock(view, f.action)!, /Salusa.*free Sardaukar/i);
assert.equal(view.ghola.available, false); assert.ok(view.ghola.cards.includes(f.card));
assert.equal(view.ghola.maxForces, 0); assert.equal(view.ghola.eliteRemaining, 0);
const before = structuredClone(game);
assert.throws(() => applyAction(game, f.actor, f.action), /Salusa.*free Sardaukar/i);
assert.deepEqual(game, before);
assert.equal(ownPromise(game, f.actor).answer, true);
assert.equal(game.discard.filter(card => card.id === f.card).length, 0);
assert.equal(game.players.find(player => player.id === f.actor)!.elites!.tanks, 1);
});
void test('the selected Ghola group block agrees with Low Salusa rejection for other Yes promises', () => {
for (const kind of ['dial1', 'support'] as const) {
const f = source(kind), game = structuredClone(f.game), view = viewGame(game, f.actor);
const blocked = homeworldRevivalActionBlock(view, f.action);
assert.ok(blocked, `${kind}: the actual selected return needs a visible block.`);
assert.match(blocked, /Salusa.*free Sardaukar/i);
assert.ok(view.homeworldRevivalBlocks?.some(row => row.source === 'ghola' &&
row.amount === f.action.amount && row.elite === f.action.elite && row.reason === blocked));
assert.equal(view.ghola.maxForces, 0); assert.equal(view.ghola.eliteRemaining, 0);
const before = structuredClone(game);
assert.throws(() => applyAction(game, f.actor, f.action), error => {
assert.ok(error instanceof Error);
assert.equal(error.message, blocked);
return true;
});
assert.deepEqual(game, before);
assert.equal(ownPromise(game, f.actor).answer, true);
assert.ok(game.players.find(player => player.id === f.actor)!.hand.some(card => card.id === f.card));
}
});
void test('all four minimal profiles omit blocked Ghola and retain legal required plans', () => {
for (const kind of ['dial1', 'support', 'dial2'] as const) {
const f = source(kind);
const start = f.game;
for (const profile of profiles) {
const game = structuredClone(start), view = viewGame(game, f.actor);
view.players.find(player => player.id === f.actor)!.bot = profile;
const actions = botActions(view);
assert.ok(actions.length, `${kind}/${profile}: current legal participation remains available.`);
assert.ok(actions.every(action => action.type !== 'card' || action.card !== f.card),
`${kind}/${profile}: the blocked Ghola is not offered.`);
const plans = actions.filter(action => action.type === 'battlePlan');
assert.ok(plans.length, `${kind}/${profile}: the live promise has a legal current plan.`);
for (const plan of plans) {
if (kind === 'support') assert.ok(Number(plan.support) >= 1);
else assert.equal(plan.dial, kind === 'dial1' ? 1 : 2);
const sealed = applyAction(game, f.actor, plan);
assert.ok(sealed.battle?.plans[f.actor]);
assert.equal(ownPromise(sealed, f.actor).answer, true);
}
}
}
});