Files
Dune/tests/homeworld-module-profile.test.ts

110 lines
7.1 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import {
homeworldPhaseModulesProfile as supported, type HomeworldPhaseModulesProfile,
} from '../game/homeworld-module-profile';
import { createTechTokens } from '../game/tech-tokens';
import { createStrongholdCards } from '../game/stronghold-cards';
function profile(factions: readonly string[] = ['atreides', 'emperor', 'guild'], expansions: readonly string[] = [],
extra: Partial<HomeworldPhaseModulesProfile> = {}): HomeworldPhaseModulesProfile {
return { advanced: false, expansions, players: factions.map(faction => ({ faction })),
homeworlds: { custody: null }, ...extra };
}
void test('classic empty-deck Homeworld starts permit both modes and independent cards without requiring phase modules', () => {
for (const advanced of [false, true]) for (const ecazTreachery of [undefined, false, true])
assert.equal(supported(profile(undefined, undefined, { advanced, ecazTreachery })), true);
assert.equal(supported(profile(undefined, undefined, { homeworldOccupationPreview: true })), true);
assert.equal(supported(profile(undefined, undefined, { homeworlds: null })), false);
assert.equal(supported(profile(undefined, undefined, { advanced: 'advanced' as unknown as boolean })), false);
});
void test('selected native, paired, mixed and unused family decks retain their actual Homeworld roster', () => {
for (const game of [
profile(['ecaz', 'moritani', 'guild'], ['ecaz']),
profile(['ecaz', 'ixians', 'emperor'], ['ix', 'ecaz']),
profile(['moritani', 'harkonnen', 'guild'], ['ecaz'], { advanced: true }),
profile(['ixians', 'tleilaxu', 'choam', 'richese', 'ecaz', 'moritani'], ['ix', 'choam', 'ecaz'], { advanced: true }),
profile(['atreides', 'emperor', 'guild'], ['ix', 'choam', 'ecaz']),
profile(['ixians', 'guild', 'emperor'], ['ix', 'choam', 'ecaz']),
]) assert.equal(supported(game), true);
});
void test('Homeworld Tech retains three-six seats and Strongholds retain Advanced two-six with both requiring three', () => {
const two = profile(['atreides', 'emperor']);
assert.equal(supported(two), true);
assert.equal(supported({ ...two, techTokens: createTechTokens() }), false);
assert.equal(supported({ ...two, strongholdCards: createStrongholdCards() }), false);
assert.equal(supported({ ...two, advanced: true, strongholdCards: createStrongholdCards() }), true);
assert.equal(supported({ ...two, advanced: true, strongholdCards: createStrongholdCards(), techTokens: createTechTokens() }), false);
assert.equal(supported(profile(undefined, undefined, { ecazTreachery: true, techTokens: createTechTokens() })), true);
assert.equal(supported(profile(undefined, undefined, { advanced: true, ecazTreachery: true,
techTokens: createTechTokens(), strongholdCards: createStrongholdCards() })), true);
});
void test('unknown, duplicate, missing-family and out-of-bounds rosters do not create a fictitious selected deck', () => {
for (const game of [
profile(['ecaz', 'guild', 'emperor']), profile(['ixians', 'guild', 'emperor'], ['choam']),
profile(undefined, ['ix', 'ix']), profile(undefined, ['unknown']),
profile(undefined, ['ix', 'choam', 'ecaz', 'ix']), profile(['unknown', 'guild']),
profile(['atreides', 'atreides']), profile(['atreides']),
profile(['atreides', 'emperor', 'guild', 'fremen', 'beneGesserit', 'harkonnen', 'ecaz'], ['ecaz']),
]) assert.equal(supported(game), false);
for (const value of [null, {}, { ...profile(), expansions: 'ix' },
{ ...profile(), players: 'atreides,emperor' }, { ...profile(), players: [{ faction: 'atreides' }, null] }])
assert.equal(supported(value as unknown as HomeworldPhaseModulesProfile), false);
});
void test('independent-card selection is an exact optional boolean rather than a truthy capability', () => {
for (const ecazTreachery of [undefined, false, true]) assert.equal(supported(profile(undefined, undefined, { ecazTreachery })), true);
for (const ecazTreachery of [null, 0, 1, 'true', {}]) assert.equal(supported(profile(undefined, undefined, { ecazTreachery })), false);
});
void test('Skills, Nexus, Discovery and unrelated reaction selectors remain outside the clean World scope', () => {
for (const field of [
'leaderSkills', 'nexusCards', 'discoveryEnabled', 'discoveries', 'discoveryStash', 'greatMaker',
'spiceBankerIncomePreview', 'semutaPreview', 'advancedPreview', 'kullPreview', 'nexusKullPreview',
'guildBetrayalPreview', 'richeseBetrayalPreview', 'nexusIxianReplacementPreview', 'nexusIxianBetrayalPreview',
'nexusHarkonnenBetrayalPreview',
] as const) assert.equal(supported({ ...profile(), [field]: true }), false, field);
});
void test('a pending original Sandtrout preserves the live World context without becoming a module selection', () => {
const game = profile(['ixians', 'emperor', 'atreides'], ['ix'], {
homeworldOccupationPreview: true, techTokens: createTechTokens(),
});
for (const sandtrout of [undefined, false, true]) assert.equal(supported({ ...game, sandtrout }), true);
for (const sandtrout of [null, 0, 1, 'true', {}]) assert.equal(supported({ ...game, sandtrout }), false);
});
void test('canonical occupation and native benefit stamps retain their actual seated source and required parent', () => {
const native = profile(['ecaz', 'moritani', 'choam'], ['choam', 'ecaz'], { advanced: true,
ecazOccupyPreview: true, moritaniAssassinatePreview: true, homeworldOccupationPreview: true,
homeworldTupilePreview: true, homeworldEcazVictoryPreview: true });
assert.equal(supported(native), true);
assert.equal(supported(profile(['ecaz', 'guild'], ['ecaz'], { ecazOccupyPreview: true })), true);
for (const game of [
profile(undefined, undefined, { ecazOccupyPreview: true }),
profile(undefined, undefined, { advanced: true, moritaniAssassinatePreview: true }),
{ ...native, advanced: false },
profile(undefined, undefined, { homeworldOccupationPreview: true, homeworldTupilePreview: true }),
profile(undefined, undefined, { homeworldOccupationPreview: true, homeworldEcazVictoryPreview: true }),
{ ...native, homeworldOccupationPreview: undefined },
]) assert.equal(supported(game), false);
for (const field of ['ecazOccupyPreview', 'moritaniAssassinatePreview', 'homeworldOccupationPreview',
'homeworldTupilePreview', 'homeworldEcazVictoryPreview'] as const)
assert.equal(supported({ ...native, [field]: 'forged' }), false, field);
});
void test('live occupied custody is configuration-neutral and the predicate never clones, resets or repairs physical state', () => {
const custody = { visitors: { 'homeworld:atreides': { emperor: { normal: 1, elite: 1 } } },
salusa: { normal: 0, elite: 4 } };
const game = profile(undefined, undefined, { advanced: true, homeworlds: { custody, historyVersion: 1 },
homeworldOccupationPreview: true, ecazTreachery: true, techTokens: createTechTokens(), strongholdCards: createStrongholdCards() });
const before = structuredClone(game);
Object.freeze(custody); Object.freeze(game.homeworlds); Object.freeze(game.players); Object.freeze(game);
assert.equal(supported(game), true); assert.deepEqual(game, before);
assert.equal((game.homeworlds as { custody: unknown }).custody, custody);
});