Files
Dune/tests/leader-skill-combat.test.ts
T
Quintine 6e6fecc9d4 Prototype Leader Skills setup, battle disciplines and capture
Connect all fourteen physical skills and private setup to five card-role battle
modifiers, public posture, Harkonnen skilled capture, death return and optional
own-leader revival through controls, legal AI and persisted room actions.
Nine skill effects and combined-mode/publication acceptance remain unfinished.
Captured replacement entitlement and foreign ghola integration stay guarded.

Fresh publisher/designer research supports skilled-capture publicity through
its unique known physical card; this is recorded as a component inference.
Independent review found and fixed Voice/must-use-leader and invalid saved
assignment/offer cases. A pre-existing Discovery privacy assertion confused
card ID prefixes; the corrected deterministic physical-pair scenario was
independently reviewed. The first broad run was 4410/4411; corrected final
verification passes types/lint, all 4411 offline tests, build and 43 HTTP tests.
Final durations: check 149741ms, build 6659ms, integration 28274ms.
Source fingerprint: aef655ac1ebc0eb2f64294e13d3d27337167312cf8ab17568a166ee9c39f6a85.
Private final report: 20260913-leader-skills/checks-final/report.json.

Three seeded prototype games completed with no rejected bot candidates. Browser
setup/card inspection/refresh passed. Another user-reported power outage stopped
the server; backup and necessary restart restored the same private seat. All
292 games at restart and their original application records remain unchanged;
33 isolated HTTP-test rooms bring the current total to 325. No database reset.

Efficiency: one shared setup/lifecycle implementation supports five connected
role effects; selective existing agents handled catalog/UI/tests/research and
independent reviews. One repeated broad check was required by the reproduced
ID-prefix assertion failure. Aggregate goal counter rose from 65529766 to
67313673 through this checkpoint (1783907), including inherited context and
coordination; this is not a billing or savings estimate. Full goal remains active.
2026-09-13 17:31:37 +10:00

296 lines
9.2 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { baseDeck, ixBattleCards, leaders, type Card } from '../game/cards';
import { richeseCards } from '../game/richese-cards';
import { CHEAP_HERO_TRAITOR } from '../game/traitors';
import {
leaderSkillBattleBonus,
type BattleLeaderSkill,
type DirectLeaderSkillId,
} from '../game/leader-skill-combat';
import {
quoteBattleResolution,
type BattleResolutionInput,
type ResolutionCombatant,
} from '../game/battle-resolution-quote';
const catalog = () => [...baseDeck(), ...ixBattleCards(), ...richeseCards()];
function one(predicate: (card: Card) => boolean) {
const card = catalog().find(predicate);
assert.ok(card);
return card;
}
const selected = { id: 'selected-disc', kind: 'disc' as const };
const assignment = (
skill: DirectLeaderSkillId,
leader = selected.id,
faceUp = false,
captured = false,
): BattleLeaderSkill => ({ skill, leader, faceUp, captured });
void test('all five direct families use the physical selected card role for normal +1 or skilled +3 without +4', () => {
const cases: [DirectLeaderSkillId, Card | undefined, Card | undefined][] = [
['warmaster', one((card) => card.kind === 'worthless'), undefined],
[
'master-of-assassins',
one((card) => card.kind === 'chemistry'),
undefined,
],
[
'swordmaster-of-ginaz',
one((card) => card.kind === 'weirdingWay'),
undefined,
],
['killer-medic', undefined, one((card) => card.kind === 'chemistry')],
[
'prana-bindu-adept',
undefined,
one((card) => card.kind === 'weirdingWay'),
],
];
for (const [skill, weapon, defense] of cases) {
assert.deepEqual(
leaderSkillBattleBonus({
assignments: [assignment(skill)],
selectedLeader: selected,
weapon,
defense,
skilledLeaderSurvives: true,
}),
{ bonus: 3, applied: [{ skill, amount: 3, mode: 'skilled' }] },
);
assert.deepEqual(
leaderSkillBattleBonus({
assignments: [assignment(skill, 'trainer', true)],
selectedLeader: selected,
weapon,
defense,
skilledLeaderSurvives: true,
}),
{ bonus: 1, applied: [{ skill, amount: 1, mode: 'normal' }] },
);
}
});
void test('dead, bluffed, face-up selected and captured assignments preserve their distinct effect bands', () => {
const weapon = one((card) => card.kind === 'projectile');
const quote = (skill: BattleLeaderSkill, survives = true) =>
leaderSkillBattleBonus({
assignments: [skill],
selectedLeader: selected,
weapon,
defense: undefined,
skilledLeaderSurvives: survives,
});
assert.equal(quote(assignment('swordmaster-of-ginaz'), false).bonus, 0);
assert.equal(
quote(assignment('swordmaster-of-ginaz', 'trainer', false)).bonus,
0,
'A concealed bluff supplies neither effect band.',
);
assert.equal(
quote(assignment('swordmaster-of-ginaz', selected.id, true)).bonus,
0,
'A still-public skilled disc cannot receive its lower battle effect.',
);
assert.equal(
quote(assignment('swordmaster-of-ginaz', 'trainer', true, true)).bonus,
0,
'A captured assignment cannot train another leader.',
);
assert.equal(
quote(assignment('swordmaster-of-ginaz', selected.id, false, true)).bonus,
3,
'A surviving captured skilled leader retains only its lower effect.',
);
assert.equal(
leaderSkillBattleBonus({
assignments: [assignment('swordmaster-of-ginaz', 'trainer', true)],
selectedLeader: { id: 'cheap-hero', kind: 'hero' },
weapon,
defense: undefined,
skilledLeaderSurvives: true,
}).bonus,
1,
'Cheap Hero substitutes for another leader for the public normal band.',
);
assert.equal(
leaderSkillBattleBonus({
assignments: [assignment('swordmaster-of-ginaz', 'trainer', true)],
selectedLeader: undefined,
weapon,
defense: undefined,
skilledLeaderSurvives: true,
}).bonus,
0,
);
});
function battle(): BattleResolutionInput {
const side = (
id: string,
faction: 'emperor' | 'guild',
strength: number,
): ResolutionCombatant => {
const leader = { ...leaders(faction)[0], strength };
return {
id,
faction,
spice: 10,
hand: [],
leader,
plan: {
dial: 2,
support: 0,
leader: leader.id,
weapon: null,
defense: null,
},
forces: {
normal: 5,
elite: 0,
eliteStrength: 2,
freeSupport: false,
},
};
};
return {
advanced: false,
turn: 2,
territory: 'arrakeen',
attacker: side('a', 'emperor', 3),
defender: side('d', 'guild', 2),
voters: [
{ id: 'a', beneficiary: 'a', called: false, traitors: [] },
{ id: 'd', beneficiary: 'd', called: false, traitors: [] },
],
participants: [
{ id: 'a', faction: 'emperor' },
{ id: 'd', faction: 'guild' },
],
physicalCards: catalog(),
pendingAuditorPresent: false,
pendingRetentionPresent: false,
};
}
function play(
input: BattleResolutionInput,
side: 'attacker' | 'defender',
slot: 'leader' | 'weapon' | 'defense' | 'lateDefense',
predicate: (card: Card) => boolean,
) {
const card = one(predicate);
input[side].hand = [...input[side].hand, card];
if (slot === 'lateDefense') input[side].lateDefense = card.id;
else input[side].plan[slot] = card.id;
if (slot === 'leader') delete input[side].leader;
return card;
}
void test('battle quote adds only surviving skill receipts to scores while preserving printed strength and bounty', () => {
const input = battle();
const worthless = play(
input,
'attacker',
'weapon',
(card) => card.kind === 'worthless',
);
input.attacker.leaderSkills = [
assignment('warmaster', input.attacker.leader!.id),
];
const before = structuredClone(input);
const quote = quoteBattleResolution(input);
assert.deepEqual(quote.leaderStrengths, { attacker: 3, defender: 2 });
assert.deepEqual(quote.leaderSkillBonuses.attacker, {
bonus: 3,
applied: [{ skill: 'warmaster', amount: 3, mode: 'skilled' }],
});
assert.deepEqual(quote.scores, { attacker: 8, defender: 4 });
assert.deepEqual(input, before);
const killed = structuredClone(input);
play(killed, 'defender', 'weapon', (card) => card.kind === 'poison');
const resolved = quoteBattleResolution(killed);
assert.equal(resolved.winner, 'd');
assert.equal(resolved.leaderSkillBonuses.attacker.bonus, 0);
assert.equal(resolved.scores!.attacker, 2);
assert.equal(resolved.bounty!.amount, 3);
assert.equal(killed.attacker.plan.weapon, worthless.id);
});
void test('a supplemental physical defense activates its matching surviving skill role', () => {
const input = battle();
const snooper = play(
input,
'attacker',
'lateDefense',
(card) => card.kind === 'snooper',
);
input.attacker.leaderSkills = [
assignment('killer-medic', input.attacker.leader!.id),
];
const quote = quoteBattleResolution(input);
assert.equal(input.attacker.plan.defense, null);
assert.equal(input.attacker.lateDefense, snooper.id);
assert.deepEqual(quote.leaderSkillBonuses.attacker, {
bonus: 3,
applied: [{ skill: 'killer-medic', amount: 3, mode: 'skilled' }],
});
assert.deepEqual(quote.scores, { attacker: 8, defender: 4 });
});
void test('normal training follows a Cheap Hero substitution but traitor precedence suppresses skill scoring', () => {
const input = battle();
const hero = play(
input,
'attacker',
'leader',
(card) => card.kind === 'hero',
);
play(input, 'attacker', 'weapon', (card) => card.kind === 'projectile');
input.attacker.leaderSkills = [
assignment('swordmaster-of-ginaz', 'public-trainer', true),
];
let quote = quoteBattleResolution(input);
assert.equal(quote.leaderStrengths.attacker, 0);
assert.equal(quote.leaderSkillBonuses.attacker.bonus, 1);
assert.equal(quote.scores!.attacker, 3);
input.voters = [
input.voters[0],
{
id: 'd',
beneficiary: 'd',
called: true,
traitors: [CHEAP_HERO_TRAITOR],
},
];
quote = quoteBattleResolution(input);
assert.equal(quote.result, 'traitor');
assert.equal(quote.winner, 'd');
assert.equal(quote.scores, null);
assert.equal(quote.leaderSkillBonuses.attacker.bonus, 0);
assert.equal(quote.bounty!.amount, 0);
assert.equal(hero.kind, 'hero');
});
void test('Stone Burner comparison remains independent of a surviving skilled bonus', () => {
const input = battle();
input.attacker.forces.freeSupport = true;
input.defender.forces.freeSupport = true;
input.attacker.plan.dial = 4;
input.defender.plan.dial = 1;
play(input, 'attacker', 'weapon', (card) => card.effect === 'stoneBurner');
play(input, 'attacker', 'defense', (card) => card.kind === 'snooper');
input.attacker.stoneMode = 'ignore';
input.attacker.leaderSkills = [
assignment('killer-medic', input.attacker.leader!.id),
];
const quote = quoteBattleResolution(input);
assert.equal(quote.result, 'normal');
assert.equal(quote.leaderSkillBonuses.attacker.bonus, 3);
assert.deepEqual(quote.scores, { attacker: 10, defender: 3 });
assert.equal(quote.stone?.winner, 'defender');
assert.equal(quote.winner, 'd');
});