Files
Dune/tests/nexus-suboids-engine.test.ts

133 lines
7.2 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { applyAction, normalizeAutomaticGame, viewGame, type Game } from '../game/engine';
import { casualtyOptions } from '../game/combat';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import { suboidFixture, boostSuboids } from './fixture-nexus-cunning';
import { nexusAllow, nexusReload, nexusReject } from './fixture-nexus-cards';
function prepare(g: Game) {
while (g.battle!.preparation) g = applyAction(g, g.battle!.preparation.owner, { type: 'declineBattlePower' });
if (g.decision?.kind === 'fullPlanOffer') g = applyAction(g, g.decision.player, { type: 'decision', decline: true });
return g;
}
void test('Ixian Cunning supplies full free Suboid strength in Basic and Advanced without altering Cyborgs or custody', () => {
for (const advanced of [false, true]) {
let g = suboidFixture(advanced);
const before = structuredClone(g.players);
const ordinary = viewGame(g, 'p').battle!.ownForces!;
assert.equal(ordinary.normalFixedHalf, true);
g = boostSuboids(g);
const boosted = viewGame(g, 'p').battle!.ownForces!;
assert.equal(boosted.normalFixedHalf, false);
assert.equal(boosted.normalFreeSupport, true);
assert.equal(boosted.eliteStrength, ordinary.eliteStrength);
assert.equal(boosted.freeSupport, ordinary.freeSupport);
assert.deepEqual(g.players, before);
assert.ok(casualtyOptions(boosted, 3, 0).some(c => c.normal === 3 && c.elite === 0));
if (advanced) {
assert.equal(casualtyOptions(boosted, 7, 0).length, 0);
assert.ok(casualtyOptions(boosted, 7, 2).length);
assert.equal(casualtyOptions(boosted, 7, 3).length, 0);
}
for (const p of g.players) assert.equal(viewGame(g, p.id).nexusSuboids!.active, true);
assert.deepEqual(normalizeAutomaticGame(nexusReload(g)), nexusReload(g));
}
});
void test('Ixian Cunning keeps disclosed half-dial commitments binding and rejects impossible activation without spending the card', () => {
let g = suboidFixture(true, true);
g = applyAction(g, 'q', { type: 'prescience', field: 'dial' });
g = nexusAllow(g);
g = applyAction(g, 'p', { type: 'prescienceAnswer', value: 0.5 });
const offer = viewGame(g, 'p').nexusSuboids!.offer!;
assert.match(offer.blocked!, /commitment/);
nexusReject(g, 'p', { type: 'nexusSuboids', event: offer.event }, /commitment/);
assert.equal(g.nexusCards!.cards!.hands.p, 'ixians');
});
void test('Cunning is allowed after compatible Prescience and before own plan even when the opposing plan is sealed', () => {
let g = suboidFixture(true, true);
g = applyAction(g, 'q', { type: 'prescience', field: 'dial' }); g = nexusAllow(g);
g = applyAction(g, 'p', { type: 'prescienceAnswer', value: 1 });
g = prepare(g);
g = applyAction(g, 'q', { type: 'battlePlan', dial: 0, leader: g.players[1].leaders[0].id });
const sealed = structuredClone(g.battle!.plans.q);
g = boostSuboids(g);
assert.deepEqual(g.battle!.plans.q, sealed);
assert.equal(g.battle!.prescience!.value, 1);
g = applyAction(g, 'p', { type: 'battlePlan', dial: 1, support: 0, leader: g.players[0].leaders[0].id });
assert.equal(g.battle!.revealed, true);
});
void test('Suboid effect survives a second battle in the same turn and expires without discarding history next turn', () => {
let g = prepare(boostSuboids(suboidFixture()));
const first = g.battle!.event;
for (const id of ['p', 'q']) g = applyAction(g, id, { type: 'battlePlan', dial: 0, leader: g.players.find(p => p.id === id)!.leaders[0].id });
for (const id of ['p', 'q']) g = applyAction(g, id, { type: 'traitorCall', call: false });
for (let i = 0; g.battle && i < 25; i++) {
let acted = false;
for (const p of g.players) {
const view = viewGame(g, p.id); view.players.find(x => x.id === p.id)!.bot = 'Easy';
const action = botActions(view)[0]; if (action) { g = applyAction(g, p.id, action); acted = true; break; }
}
assert.ok(acted, `Battle stalled at ${g.decision?.kind}/${g.response?.kind}`);
}
assert.equal(g.battle, null);
assert.ok(g.decision?.kind === 'nexusChoamInspection');
g = applyAction(g, g.decision.player, {
type: 'decision', event: g.decision.event, inspect: false,
});
g = applyAction(g, 'p', { type: 'chooseBattle', territory: 'hagga_basin', target: 'q' });
assert.notEqual(g.battle!.event, first);
assert.equal(viewGame(g, 'p').battle!.ownForces!.normalFreeSupport, true);
const turn = g.turn;
for (let i = 0; g.turn === turn && i < 150; i++) {
let acted = false;
for (const p of g.players) {
const view = viewGame(g, p.id); view.players.find(x => x.id === p.id)!.bot = 'Easy';
const action = botActions(view)[0];
if (action) { g = applyAction(g, p.id, action); acted = true; break; }
}
assert.ok(acted, `Next-turn continuation stalled at ${g.phase}/${g.decision?.kind}/${g.response?.kind}`);
}
assert.equal(g.turn, turn + 1);
assert.equal(viewGame(nexusReload(g), 'p').nexusSuboids!.active, false);
assert.equal(g.battle, null);
assert.equal(g.nexusSuboidHistory!.length, 1);
});
void test('stale, foreign, repeated and post-submission Cunning attempts reject unchanged', () => {
const initial = suboidFixture(), offer = viewGame(initial, 'p').nexusSuboids!.offer!;
nexusReject(initial, 'q', { type: 'nexusSuboids', event: offer.event }, /Cunning/);
nexusReject(initial, 'p', { type: 'nexusSuboids', event: 'old' }, /Cunning/);
nexusReject(initial, 'p', { type: 'nexusSuboids', event: offer.event, support: 0 }, /Cunning/);
const played = boostSuboids(initial);
nexusReject(played, 'p', { type: 'nexusSuboids', event: offer.event }, /Cunning/);
let sealed = prepare(initial);
sealed = applyAction(sealed, 'p', { type: 'battlePlan', dial: 0, leader: sealed.players[0].leaders[0].id });
nexusReject(sealed, 'p', { type: 'nexusSuboids', event: offer.event }, /submitted/);
});
void test('edited Suboid receipts reject reads, normalization and actions without changing the saved game', () => {
const original = boostSuboids(suboidFixture());
for (const mutate of [
(g: Game) => { delete g.nexusSuboidHistory; },
(g: Game) => { delete g.nexusSuboidLast; },
(g: Game) => { g.nexusSuboidHistory![0].turn++; },
(g: Game) => { g.nexusSuboidHistory![0].owner = 'q'; },
(g: Game) => { g.nexusSuboidHistory![0].battle = 'other'; },
(g: Game) => { g.nexusSuboidHistory!.push(structuredClone(g.nexusSuboidHistory![0])); },
]) {
const bad = nexusReload(original); mutate(bad); const before = JSON.stringify(bad);
assert.throws(() => viewGame(bad, 'p')); assert.throws(() => normalizeAutomaticGame(bad));
assert.throws(() => applyAction(bad, 'p', { type: 'advanceBots' })); assert.equal(JSON.stringify(bad), before);
}
});
void test('all four bot profiles use the actual Cunning offer and submit legal boosted plans', () => {
for (const level of DIFFICULTIES) {
let g = prepare(suboidFixture());
for (let i = 0; !g.battle!.plans.p && i < 8; i++) {
const v = viewGame(g, 'p'); v.players[0].bot = level;
const action = botActions(v)[0]; assert.ok(action); g = applyAction(g, 'p', action);
}
assert.equal(g.nexusSuboidHistory!.length, 1);
assert.ok(g.battle!.plans.p);
}
});