Files
Dune/tests/native-richese-skills.test.ts
T

172 lines
11 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, viewGame, type Game } from '../game/engine';
import { LEADER_SKILL_CARDS } from '../game/leader-skill-cards';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import {
advancedNativePlayer as player, advancedNativeStep, assertAdvancedNativeCustody as custody,
completedAdvancedNativeSkillsGame, finishAdvancedNativeSkillAftermath,
initializeAdvancedNativeSkillsSetup, openAdvancedNativeSkillBattle,
rejectAdvancedNativeAction as reject, stageAdvancedNativeSkillBattle,
} from './fixture-advanced-native-skills';
const reload = (g: Game): Game => JSON.parse(JSON.stringify(g)) as Game;
const trainer = (g: Game, id: string) => g.leaderSkills!.assignments.find(a => a.owner === id)!;
const untrained = (g: Game, id: string) => g.players.find(p => p.id === id)!.leaders
.filter(l => !l.dead && l.id !== trainer(g, id).leader).sort((a, b) => b.strength - a.strength)[0].id;
/** Genuine full native setup, then explicitly conserved shipment/battle position.
* Native ship creates the physical marker; no fabricated deployed token. */
function markerGame(value: 0 | 3 | 5, requestedSkill: 'suk-graduate' | 'mentat' | 'smuggler',
rules: 'basic' | 'advanced' = 'advanced', reserves = 20, companion = false) {
const initial = completedAdvancedNativeSkillsGame({ family: 'richese', requestedSkill, rules });
const actor = player(initial, 'richese').id, target = player(initial, 'emperor').id;
let game = stageAdvancedNativeSkillBattle(initial, actor, target);
const owner = player(game, 'richese');
owner.reserves += owner.forces['wind_pass:14'];
owner.forces = {};
owner.tanks += owner.reserves - reserves;
owner.reserves = reserves;
if (companion) {
const enemy = player(game, 'emperor');
enemy.reserves += enemy.forces['wind_pass:14'];
enemy.forces = {};
}
owner.shipped = false; owner.moved = 0;
Object.assign(game, { phase: 5, active: actor, ready: [], phaseOpening: null, response: null, decision: null });
const token = owner.noField!.tokens.find(t => t.value === value)!;
const before = owner.spice;
game = applyAction(game, actor, { type: 'ship', noField: token.id, event: owner.noFieldEvent,
territory: 'wind_pass', sector: 14, smuggler: companion });
for (let n = 0; (game.response || game.decision) && n < 100; n++) game = advancedNativeStep(game);
assert.equal(player(game, 'richese').spice, before - 2);
assert.ok(player(game, 'richese').noField!.deployed);
if (companion) {
const enemy = player(game, 'emperor');
enemy.reserves -= 8;
enemy.forces['wind_pass:14'] = 8;
} // Explicit later opposing position; the actual companion arrived in an empty territory.
const beforeBattle = reload(game);
Object.assign(game, { phase: 6, active: actor, ready: [], phaseOpening: null, response: null, decision: null });
custody(game);
return { game, beforeBattle, actor, target };
}
void test('native Basic and Advanced Richese retain source-ordered all14 and their separate original ten-card cache', () => {
for (const rules of ['basic', 'advanced'] as const) {
const offered = initializeAdvancedNativeSkillsSetup({ family: 'richese', requestedSkill: 'suk-graduate', rules });
assert.equal(offered.setupStage, 'leaderSkills');
assert.equal(offered.richeseCache!.length, 10);
for (const seat of offered.players) assert.equal(seat.hand.length, 1);
const physical = [...offered.leaderSkills!.deck, ...Object.values(offered.leaderSkills!.offers).flatMap(o => o.cards)];
assert.deepEqual([...physical].sort(), LEADER_SKILL_CARDS.map(c => c.id).sort());
const rival = offered.players.find(p => p.faction !== 'richese')!;
const own = player(offered, 'richese');
assert.deepEqual(viewGame(offered, rival.id).leaderSkills!.eligibleLeaders.map(l => l.id).sort(),
rival.leaders.filter(l => !l.dead && !l.capturedBy && !l.gholaBy).map(l => l.id).sort());
assert.equal(viewGame(offered, own.id).leaderSkills!.offer!.cards.includes('suk-graduate'), true);
custody(completedAdvancedNativeSkillsGame({ family: 'richese', requestedSkill: 'suk-graduate', rules }));
}
});
void test('scarce native No-Field five materializes only three after both plans and skilled Suk saves only real paid losses', () => {
const staged = markerGame(5, 'suk-graduate', 'advanced', 3);
let game = openAdvancedNativeSkillBattle(staged.game, staged.actor, staged.target);
const trained = trainer(game, staged.actor).leader;
reject(game, staged.actor, { type: 'battlePlan', dial: 4, support: 3, leader: trained });
game = applyAction(game, staged.actor, { type: 'battlePlan', dial: 3, support: 3, leader: trained });
assert.ok(player(game, 'richese').noField!.deployed);
assert.equal(player(game, 'richese').forces['wind_pass:14'] ?? 0, 0);
assert.equal(viewGame(game, staged.target).richeseNoField!.private, null);
game = applyAction(game, staged.target, { type: 'battlePlan', dial: 0, support: 0, leader: untrained(game, staged.target) });
assert.equal(player(game, 'richese').noField!.deployed, null);
assert.equal(player(game, 'richese').forces['wind_pass:14'], 3);
game = applyAction(game, staged.actor, { type: 'traitorCall', call: false });
game = applyAction(game, staged.target, { type: 'traitorCall', call: false });
assert.equal(game.decision?.kind, 'sukRescue');
if (game.decision?.kind !== 'sukRescue') throw new Error('Native real casualties must retain skilled rescue.');
const pending = game;
const choice = game.decision.options.findIndex(o => o.normal === 3 && o.elite === 0 && o.kept?.key === 'wind_pass:14');
assert.ok(choice >= 0);
game = applyAction(reload(game), staged.actor, { type: 'decision', event: game.decision.event, choice });
game = finishAdvancedNativeSkillAftermath(game);
assert.equal(player(game, 'richese').forces['wind_pass:14'], 1);
assert.equal(player(game, 'richese').reserves, 2);
assert.equal(player(game, 'richese').tanks, 17);
assert.equal(player(game, 'richese').spice, 0);
custody(game);
for (const difficulty of DIFFICULTIES) {
const view = viewGame(reload(pending), staged.actor);
view.players.find(p => p.id === staged.actor)!.bot = difficulty;
const rescued = applyAction(reload(pending), staged.actor, botActions(view)[0]);
assert.equal(player(rescued, 'richese').tanks, 17);
assert.equal(player(rescued, 'richese').spice, 0);
custody(finishAdvancedNativeSkillAftermath(rescued));
}
});
void test('normal native Suk returns one actual No-Field casualty under both rule-band support costs', () => {
for (const rules of ['basic', 'advanced'] as const) {
const staged = markerGame(3, 'suk-graduate', rules);
let game = openAdvancedNativeSkillBattle(staged.game, staged.actor, staged.target, false);
const balance = player(game, 'richese').spice;
game = applyAction(game, staged.actor, { type: 'battlePlan', dial: 2, support: rules === 'advanced' ? 2 : 0,
leader: untrained(game, staged.actor) });
game = applyAction(game, staged.target, { type: 'battlePlan', dial: 0, support: 0, leader: untrained(game, staged.target) });
game = applyAction(game, staged.actor, { type: 'traitorCall', call: false });
game = applyAction(game, staged.target, { type: 'traitorCall', call: false });
game = finishAdvancedNativeSkillAftermath(game);
assert.equal(game.lastBattleContext!.winner, staged.actor);
assert.equal(player(game, 'richese').forces['wind_pass:14'], 1);
assert.equal(player(game, 'richese').reserves, 18);
assert.equal(player(game, 'richese').tanks, 1);
assert.equal(player(game, 'richese').spice, balance - (rules === 'advanced' ? 2 : 0));
custody(game);
}
});
void test('zero native No-Field retains an actual skilled leader battle and no phantom forces or support', () => {
const staged = markerGame(0, 'mentat');
let game = openAdvancedNativeSkillBattle(staged.game, staged.actor, staged.target);
const trained = trainer(game, staged.actor).leader;
const before = player(game, 'richese').spice;
reject(game, staged.actor, { type: 'battlePlan', dial: 1, support: 0, leader: trained });
game = applyAction(game, staged.actor, { type: 'battlePlan', dial: 0, support: 0, leader: trained });
game = applyAction(game, staged.target, { type: 'battlePlan', dial: 0, support: 0, leader: untrained(game, staged.target) });
game = applyAction(game, staged.actor, { type: 'traitorCall', call: false });
game = applyAction(game, staged.target, { type: 'traitorCall', call: false });
game = finishAdvancedNativeSkillAftermath(game);
assert.equal(game.lastBattleContext!.winner, staged.actor);
assert.equal(player(game, 'richese').reserves, 20);
assert.equal(player(game, 'richese').tanks, 0);
assert.equal(player(game, 'richese').forces['wind_pass:14'] ?? 0, 0);
assert.equal(player(game, 'richese').spice, before);
custody(game);
});
void test('original Smuggler companion remains physical and mixed No-Field battle stays guarded until voluntary native reveal', () => {
const staged = markerGame(5, 'smuggler', 'advanced', 20, true);
const owner = player(staged.game, 'richese');
assert.equal(owner.reserves, 19);
assert.equal(owner.forces['wind_pass:14'], 1);
reject(staged.game, staged.actor, { type: 'chooseBattle', territory: 'wind_pass', target: staged.target });
const token = owner.noField!.deployed!.tokenId;
reject(staged.game, staged.actor, { type: 'revealNoField', token, event: owner.noFieldEvent });
let game = applyAction(staged.beforeBattle, staged.actor, { type: 'revealNoField', token, event: owner.noFieldEvent });
assert.equal(player(game, 'richese').forces['wind_pass:14'], 6);
assert.equal(player(game, 'richese').reserves, 14);
Object.assign(game, { phase: 6, active: staged.actor, ready: [], phaseOpening: null, response: null, decision: null });
game = openAdvancedNativeSkillBattle(game, staged.actor, staged.target);
game = applyAction(game, staged.actor, { type: 'battlePlan', dial: 0, support: 0, leader: untrained(game, staged.actor) });
game = applyAction(game, staged.target, { type: 'battlePlan', dial: 0, support: 0, leader: untrained(game, staged.target) });
game = applyAction(game, staged.actor, { type: 'traitorCall', call: false });
game = applyAction(game, staged.target, { type: 'traitorCall', call: false });
game = finishAdvancedNativeSkillAftermath(game);
assert.equal(game.lastBattleContext!.winner, staged.target);
assert.equal(player(game, 'richese').tanks, 6);
assert.equal(player(game, 'richese').forces['wind_pass:14'] ?? 0, 0);
assert.equal(player(game, 'richese').reserves, 14);
custody(game);
});