176 lines
10 KiB
TypeScript
176 lines
10 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { applyAction, viewGame, type Action, type Game } from '../game/engine';
|
|
import { botActions } from '../game/bots';
|
|
import { DIFFICULTIES } from '../game/bot-profiles';
|
|
import { assertAdvancedNativeCustody } from './fixture-advanced-native-skills';
|
|
import {
|
|
createSemutaSkillsMentatFixture, semutaSkillsDiscardStep, reactSemutaSkillsDiscard,
|
|
openSemutaSkillsMentat, reloadSemutaSkills as reload, semutaSkillsPlayer,
|
|
semutaSkillsInventory, semutaSkillsPhysical, type SemutaSkillsMentatFixture,
|
|
} from './fixture-semuta-skills';
|
|
|
|
const profiles = [
|
|
{ advanced: false, tech: false, strongholds: false },
|
|
{ advanced: false, tech: true, strongholds: false },
|
|
{ advanced: true, tech: false, strongholds: false },
|
|
{ advanced: true, tech: true, strongholds: false },
|
|
{ advanced: true, tech: false, strongholds: true },
|
|
{ advanced: true, tech: true, strongholds: true },
|
|
];
|
|
function reject(game: Game, actor: string, action: Action): void {
|
|
const before = structuredClone(game);
|
|
assert.throws(() => applyAction(game, actor, action));
|
|
assert.deepEqual(game, before);
|
|
}
|
|
function questionResources(game: Game) {
|
|
return structuredClone({ players: game.players, deck: game.deck, discard: game.discard,
|
|
cache: game.richeseCache, removed: game.richeseRemoved, skills: game.leaderSkills,
|
|
tech: game.techTokens, strongholds: game.strongholdCards });
|
|
}
|
|
function pause(f: SemutaSkillsMentatFixture): Game {
|
|
const step = semutaSkillsDiscardStep(f), game = applyAction(reload(f.game), step.actor, step.action);
|
|
const pending = game.pendingTreacheryDiscard!;
|
|
assert.equal(pending.reaction?.stage, 'offer');
|
|
assert.equal(pending.batch.cause, 'ordinary:hajr');
|
|
assert.deepEqual(pending.batch.entries.map(e => ({ id: e.card.id, actor: e.discardedBy, publicFace: e.publicFace })),
|
|
[{ id: f.hajr, actor: f.spender, publicFace: true }]);
|
|
assert.equal(pending.continuation.kind, 'ordinaryCardDiscard');
|
|
if (pending.continuation.kind !== 'ordinaryCardDiscard') throw Error('Expected the original Hajr continuation');
|
|
assert.equal(pending.continuation.player, f.spender);
|
|
assert.equal(pending.continuation.effect, 'hajr');
|
|
assert.equal(game.active, f.spender);
|
|
assert.equal(game.phase, 5);
|
|
assert.equal(game.hajr.filter(id => id === f.spender).length, 1);
|
|
assert.deepEqual(semutaSkillsPhysical(game), semutaSkillsPhysical(f.game));
|
|
assert.deepEqual(semutaSkillsInventory(game), semutaSkillsInventory(f.game));
|
|
reject(game, f.spender, { type: 'endMovement' });
|
|
return game;
|
|
}
|
|
function resumedQuestion(pending: Game, f: SemutaSkillsMentatFixture, claim: boolean): Game {
|
|
const event = pending.pendingTreacheryDiscard!.batch.event;
|
|
const movement = reactSemutaSkillsDiscard(reload(pending), claim);
|
|
assert.equal(movement.pendingTreacheryDiscard, null);
|
|
assert.equal(movement.phase, 5);
|
|
assert.equal(movement.active, f.spender, 'The original Atreides closing owner resumes before Battle');
|
|
assert.deepEqual(movement.movementRemaining, f.game.movementRemaining);
|
|
assert.equal(movement.hajr.filter(id => id === f.spender).length, 1);
|
|
assert.equal(semutaSkillsPlayer(movement, f.owner).hand.some(c => c.id === f.hajr), claim);
|
|
assert.equal(semutaSkillsPlayer(movement, f.owner).hand.some(c => c.id === f.semuta), !claim);
|
|
assert.equal(movement.discard.filter(c => c.id === f.hajr).length, Number(!claim));
|
|
assert.equal(movement.discard.filter(c => c.id === f.semuta).length, Number(claim));
|
|
assert.deepEqual(semutaSkillsPhysical(movement), semutaSkillsPhysical(f.game));
|
|
assert.deepEqual(semutaSkillsInventory(movement), semutaSkillsInventory(f.game));
|
|
reject(movement, f.owner, { type: 'semutaCommit', event });
|
|
const game = openSemutaSkillsMentat(reload(movement), f);
|
|
assert.equal(game.decision?.kind, 'mentatQuestion', 'Semuta must retain the original native skill consumer');
|
|
assert.equal(game.decision?.player, f.owner);
|
|
assert.equal(game.battle?.mentatQuestion?.leader, f.trainer);
|
|
assert.equal(game.battle?.mentatQuestion?.stage, 'name');
|
|
assert.equal(game.battle?.mentatQuestion?.owner, f.owner);
|
|
assert.equal(game.battle?.mentatQuestion?.target, f.target);
|
|
assertAdvancedNativeCustody(game);
|
|
assert.deepEqual(semutaSkillsInventory(game), semutaSkillsInventory(f.game));
|
|
return game;
|
|
}
|
|
|
|
for (const profile of profiles) {
|
|
const label = `${profile.advanced ? 'Advanced' : 'Basic'}${profile.tech ? ' Tech' : ''}${profile.strongholds ? ' Strongholds' : ''}`;
|
|
void test(`${label} Semuta claim/pass resumes the native owner before a genuine private Mentat question and reply`, () => {
|
|
const f = createSemutaSkillsMentatFixture(profile.advanced, profile.tech, profile.strongholds);
|
|
const pending = pause(f);
|
|
for (const claim of [false, true]) for (const matched of [false, true]) {
|
|
const game = resumedQuestion(pending, f, claim), before = questionResources(game);
|
|
const event = game.battle!.mentatQuestionEvent!;
|
|
const weapon = matched ? 'Crysknife' : 'Lasgun';
|
|
const expected = matched ? f.weapon : f.fallback;
|
|
assert.ok(viewGame(game, f.owner).mentat.pending!.weapons.includes(weapon));
|
|
for (const id of [f.target, f.spender]) {
|
|
assert.deepEqual(viewGame(game, id).mentat.pending!.weapons, []);
|
|
assert.deepEqual(viewGame(game, id).mentat.pending!.cards, []);
|
|
reject(game, id, { type: 'decision', event, weapon });
|
|
}
|
|
const named = applyAction(reload(game), f.owner, { type: 'decision', event, weapon });
|
|
assert.equal(named.decision?.kind, 'mentatQuestion');
|
|
assert.equal(named.decision?.player, f.target, 'Both exact and fallback questions use the same target-owned reply');
|
|
assert.equal(named.battle!.mentatQuestion!.stage, 'reveal');
|
|
assert.equal(named.battle!.mentatQuestionEvent, event);
|
|
const choices = viewGame(named, f.target).mentat.pending!.cards;
|
|
assert.deepEqual(choices.map(c => c.id), matched ? [f.weapon] : [f.weapon, f.fallback]);
|
|
for (const id of [f.owner, f.spender]) {
|
|
assert.deepEqual(viewGame(named, id).mentat.pending!.cards, []);
|
|
assert.deepEqual(viewGame(named, id).mentat.history, []);
|
|
reject(named, id, { type: 'decision', event, card: expected });
|
|
}
|
|
if (matched) reject(named, f.target, { type: 'decision', event, card: f.fallback });
|
|
reject(named, f.target, { type: 'decision', event: 'stale', card: expected });
|
|
const shown = applyAction(reload(named), f.target, { type: 'decision', event, card: expected });
|
|
assert.equal(shown.battle!.mentatQuestion!.stage, 'answered');
|
|
assert.equal(shown.decision?.kind, 'leaderSkillVisibility');
|
|
assert.deepEqual(questionResources(shown), before, 'A normal question never transfers, reserves, spends or edits components');
|
|
assert.deepEqual(semutaSkillsPlayer(shown, f.target).hand.map(c => c.id), [f.weapon, f.fallback]);
|
|
for (const id of [f.owner, f.target]) {
|
|
const history = viewGame(shown, id).mentat.history;
|
|
assert.equal(history.length, 1);
|
|
assert.equal(history[0].event, event);
|
|
assert.equal(history[0].card.id, expected);
|
|
assert.equal(history[0].weapon, weapon);
|
|
}
|
|
assert.deepEqual(viewGame(shown, f.spender).mentat.history, []);
|
|
assert.ok(!JSON.stringify(viewGame(shown, f.spender)).includes(expected));
|
|
assert.ok(!JSON.stringify(shown.log).includes(expected));
|
|
reject(shown, f.owner, { type: 'decision', event, weapon });
|
|
reject(shown, f.target, { type: 'decision', event, card: expected });
|
|
let posture = reload(shown);
|
|
while (posture.decision?.kind === 'leaderSkillVisibility') {
|
|
const decision = posture.decision;
|
|
posture = applyAction(posture, decision.player,
|
|
{ type: 'leaderSkillVisibility', event: decision.event, hide: false });
|
|
}
|
|
assert.equal(posture.battle!.leaderSkillHidden!.r, false, 'The normal Mentat may stay face-up after the private question');
|
|
assert.ok(posture.battle?.preparation || posture.decision?.kind === 'fullPlanOffer' || posture.battle?.preLeader);
|
|
assertAdvancedNativeCustody(posture);
|
|
assert.deepEqual(semutaSkillsInventory(posture), semutaSkillsInventory(f.game));
|
|
}
|
|
});
|
|
}
|
|
|
|
void test('all four AI profiles participate legally in the original private Mentat contract after Semuta claim/pass', () => {
|
|
for (const profile of profiles) {
|
|
const f = createSemutaSkillsMentatFixture(profile.advanced, profile.tech, profile.strongholds);
|
|
const pending = pause(f);
|
|
for (const claim of [false, true]) {
|
|
const question = resumedQuestion(pending, f, claim);
|
|
for (const difficulty of DIFFICULTIES) {
|
|
const own = viewGame(question, f.owner);
|
|
own.players.find(p => p.id === f.owner)!.bot = difficulty;
|
|
const name = botActions(own)[0];
|
|
assert.ok(name && own.mentat.pending!.weapons.includes(name.weapon as string));
|
|
assert.equal(name.event, question.battle!.mentatQuestionEvent);
|
|
for (const id of [f.target, f.spender]) {
|
|
const other = viewGame(question, id);
|
|
other.players.find(p => p.id === id)!.bot = difficulty;
|
|
assert.deepEqual(botActions(other), []);
|
|
}
|
|
const named = applyAction(reload(question), f.owner, name), before = questionResources(named);
|
|
const target = viewGame(named, f.target);
|
|
target.players.find(p => p.id === f.target)!.bot = difficulty;
|
|
const answer = botActions(target)[0];
|
|
assert.ok(answer && target.mentat.pending!.cards.some(c => c.id === answer.card));
|
|
assert.equal(answer.event, name.event);
|
|
for (const id of [f.owner, f.spender]) {
|
|
const other = viewGame(named, id);
|
|
other.players.find(p => p.id === id)!.bot = difficulty;
|
|
assert.deepEqual(botActions(other), []);
|
|
}
|
|
const shown = applyAction(reload(named), f.target, answer);
|
|
assert.equal(shown.decision?.kind, 'leaderSkillVisibility');
|
|
assert.deepEqual(questionResources(shown), before);
|
|
assert.equal(viewGame(shown, f.owner).mentat.history[0].card.id, answer.card);
|
|
assert.deepEqual(viewGame(shown, f.spender).mentat.history, []);
|
|
assertAdvancedNativeCustody(shown);
|
|
}
|
|
}
|
|
}
|
|
});
|