Align Advanced Ix starting cards with Harkonnen pool

This commit is contained in:
Quintine committed 2026-10-27 10:26:44 +11:00
1 parent e530d267a3
commit 84fdd404c9
14 files changed
+637 -12

No files matched your search

+2 -1
View File
@@ -258,7 +258,8 @@ export function createIxIndependentSkillsRevivalFixture(options: IxIndependentSk
const afterBattle = finishIxIndependentSkillsBattle(f, { rescue: 'none' });
const bribe: IxIndependentSkillsStep = { actor: f.opponent, action: { type: 'bribe', target: f.ixians, amount: 3 } };
let game = stepIxIndependentSkills(afterBattle, bribe);
game = advanceIxIndependentSkills(game, g => g.turn === 2 && g.phase === 4 && ixIndependentSkillsClean(g) && !g.pendingRevival);
game = advanceIxIndependentSkills(game, g => g.turn === 2 && g.phase === 4 && ixIndependentSkillsClean(g) && !g.pendingRevival &&
(!g.revivalOrder || g.revivalOrder.remaining[0] === f.ixians));
const staging = [...f.staging]; conserveCard(game, f.ixians, RECRUITS_CARD_ID, staging);
const beforeRecruits = structuredClone(game);
const priorReturn: IxIndependentSkillsStep = { actor: f.ixians, action: { type: 'revive', amount: 1, elite: 0 } };
+152
View File
@@ -0,0 +1,152 @@
import assert from 'node:assert/strict';
import { applyAction, createGame, initializeIxGameForAudit, initializeLeaderSkillsGameForAudit, joinGame,
newPlayer, viewGame, type Action, type Game } from '../game/engine';
import { treacheryDeck } from '../game/cards';
import { traitorDeck } from '../game/traitors';
import { LEADER_SKILL_CARDS } from '../game/leader-skill-cards';
import { validateLeaderSkills } from '../game/leader-skills';
import { nextStrongholdFactionsNativeStep } from './fixture-stronghold-factions';
export type IxStartingPoolStep = { actor: string; action: Action };
export type IxStartingPoolOptions = { advanced?: boolean; skills?: boolean; harkonnen?: boolean };
export const ixStartingPoolPlayer = (game: Game, actor: string) => {
const player = game.players.find(player => player.id === actor); assert.ok(player); return player;
};
export const ixStartingPoolReload = (game: Game): Game => JSON.parse(JSON.stringify(game)) as Game;
function permutation<T>(source: readonly T[], desired: readonly T[]): number[] {
const working = [...source], rolls: number[] = [];
for (let index = working.length - 1; index > 0; index--) {
const from = working.indexOf(desired[index]); assert.ok(from >= 0 && from <= index);
rolls.push(Math.floor((from + 0.5) * 0x100000000 / (index + 1)));
[working[index], working[from]] = [working[from], working[index]];
}
assert.deepEqual(working, desired); return rolls;
}
/** Only original shuffle draws are selected. UUID and byte entropy remain native. */
function entropy<T>(rolls: readonly number[], run: () => T): T {
const descriptor = Object.getOwnPropertyDescriptor(crypto, 'getRandomValues');
const native = crypto.getRandomValues.bind(crypto); let cursor = 0;
crypto.getRandomValues = <V extends ArrayBufferView | null>(array: V): V => {
if (array instanceof Uint32Array && array.length === 1) array[0] = rolls[cursor++] ?? 0xffffffff;
else Reflect.apply(native, crypto, [array]);
return array;
};
try { return run(); }
finally {
if (descriptor) Object.defineProperty(crypto, 'getRandomValues', descriptor);
else Reflect.deleteProperty(crypto, 'getRandomValues');
}
}
export function actIxStartingPool(game: Game, actor: string, action: Action,
actions: IxStartingPoolStep[] = []): Game {
const next = entropy([], () => applyAction(game, actor, action));
actions.push({ actor, action: structuredClone(action) }); return next;
}
/** Includes the still-private native draft; historical references are not slots. */
export function ixStartingPoolInventory(game: Game) {
const cards = [...game.deck, ...game.discard, ...(game.ixSetupCards ?? []),
...game.players.flatMap(player => player.hand), ...(game.auction?.cards.slice(game.auction.index) ?? [])]
.map(card => card.id).sort();
assert.deepEqual(cards, treacheryDeck(game.expansions).map(card => card.id).sort());
const traitors = [...(game.traitorReserve ?? []), ...game.players.flatMap(player =>
[...player.traitors, ...player.traitorChoices])].sort();
if (traitors.length) assert.deepEqual(traitors, traitorDeck(game.players, true).sort());
const skills = game.leaderSkills ? [...game.leaderSkills.deck,
...game.leaderSkills.assignments.map(row => row.skill),
...Object.values(game.leaderSkills.offers).flatMap(offer => offer.cards)].sort() : [];
if (game.leaderSkills) {
validateLeaderSkills(game.leaderSkills, game.players);
assert.deepEqual(skills, LEADER_SKILL_CARDS.map(card => card.id).sort());
}
for (const player of game.players) {
assert.equal(player.reserves + player.tanks + Object.values(player.forces).reduce((sum, amount) => sum + amount, 0), 20);
if (player.elites) assert.equal(player.elites.reserves + player.elites.tanks +
Object.values(player.elites.forces).reduce((sum, amount) => sum + amount, 0), player.faction === 'ixians' ? 7 : 5);
}
return { cards, traitors, skills };
}
/** The original Ix packet is deliberately caller-owned; no draft choice is guessed. */
export function nextIxStartingPoolStep(game: Game): IxStartingPoolStep | null {
if (game.decision?.kind === 'ixSetup') return null;
if (game.status === 'setup' && game.setupStage === 'leaderSkills') {
const actor = Object.keys(game.leaderSkills!.offers)[0], own = viewGame(game, actor).leaderSkills!;
const skill = own.offer!.cards.find(card => !own.unavailableSkills?.[card]);
const leader = own.eligibleLeaders[0]; assert.ok(skill && leader);
return { actor, action: { type: 'leaderSkill', event: own.offer!.event, skill, leader: leader.id } };
}
return nextStrongholdFactionsNativeStep(game);
}
export function advanceIxStartingPool(game: Game, until: (state: Game) => boolean,
actions: IxStartingPoolStep[] = []): Game {
for (let count = 0; count < 200; count++) {
if (until(game)) return game;
const step = nextIxStartingPoolStep(game); assert.ok(step, 'Choose the actual private Ix starting card before setup continues');
game = actIxStartingPool(game, step.actor, step.action, actions);
}
throw Error('Original Ix starting-card setup did not reach its finite boundary');
}
export function ixStartingPoolChoice(game: Game, card: string): Action {
const own = viewGame(game, 'ixians');
assert.equal(game.decision?.kind, 'ixSetup');
assert.ok(own.ixTechnology?.setup?.some(candidate => candidate.id === card));
return { type: 'decision', card };
}
export function finishIxStartingPool(game: Game, actions: IxStartingPoolStep[] = []): Game {
return advanceIxStartingPool(game, state => state.status === 'playing', actions);
}
/** Original fresh Ix/Hark/Atreides setup, or Emperor in the no-Hark control.
* The supported Ix or all14 preview initializer is used, not a public start.
* Only initial shuffle entropy is selected. No dealt hand, phase, force, purse,
* pending receipt or module flag is installed after initialization.
*/
export function createIxStartingPoolFixture(options: IxStartingPoolOptions = {}) {
const advanced = options.advanced ?? true, skills = options.skills ?? false, harkonnen = options.harkonnen ?? true;
const second = harkonnen ? 'harkonnen' : 'emperor';
let game = createGame('IXSTARTINGPOOL', newPlayer('ixians', 'Ixians', 'ixians'), advanced, ['ix']);
joinGame(game, newPlayer(second, second, second));
joinGame(game, newPlayer('atreides', 'Atreides', 'atreides'));
const initial = structuredClone(game), actions: IxStartingPoolStep[] = [];
for (const player of game.players) game = actIxStartingPool(game, player.id, { type: 'ready' }, actions);
const lobby = structuredClone(game), original = treacheryDeck(['ix']);
const prefix = [original.find(card => card.kind === 'projectile')!, original.find(card => card.kind === 'shield')!,
original.find(card => card.kind === 'snooper')!, original.find(card => card.effect === 'karama')!];
const deckOrder = [...prefix.map(card => card.id), ...original.filter(card => !prefix.some(first => first.id === card.id)).map(card => card.id)];
const rolls = [...(skills ? Array<number>(LEADER_SKILL_CARDS.length - 1).fill(0xffffffff) : []),
...permutation(original.map(card => card.id), deckOrder)];
game = entropy(rolls, () => skills ? initializeLeaderSkillsGameForAudit(game) : initializeIxGameForAudit(game));
const initialized = structuredClone(game);
game = advanceIxStartingPool(game, state => state.decision?.kind === 'ixSetup', actions);
assert.equal(game.status, 'setup'); assert.equal(game.decision?.player, 'ixians');
assert.ok(game.players.every(player => !player.hand.length));
assert.ok(!game.homeworlds && !game.nexusCards && !game.discoveryEnabled && !game.techTokens && !game.strongholdCards && !game.ecazTreachery);
assert.equal(!!game.leaderSkills, skills);
assert.deepEqual(game.ixSetupCards!.map(card => card.id), deckOrder.slice(0, game.ixSetupCards!.length));
const stock = ixStartingPoolInventory(game);
return { initial, lobby, initialized, game, advanced, skills, harkonnen, second,
pool: structuredClone(game.ixSetupCards!), prefix: prefix.map(card => card.id), extra: prefix[3].id,
nextAfterFour: deckOrder[4], deckOrder, stock, actions,
readyActions: actions.filter(step => step.action.type === 'ready'),
setupActions: actions.filter(step => step.action.type !== 'ready'),
sourceCounts: { ready: actions.filter(step => step.action.type === 'ready').length,
beforeDraft: actions.filter(step => step.action.type !== 'ready').length, source: actions.length },
sourceDisclosure: 'Genuine fresh Ix/Harkonnen/Atreides (or Emperor control) preview initialization, with optional original all14 Skills. Actual Ready/setup actions are recorded; initializer is not an action or public-start acceptance. Only original initial skill/Treachery shuffle entropy selected. Source stops before the actual Ix choice; no hand, army, purse, phase or receipt is installed.' };
}
/** Conserved old-count UNIT supplement, not a native historical save claim.
* Move only the untouched extra fourth card back to the undealt top, restoring
* the old Advanced N-card boundary without minting/dropping any physical card.
*/
export function createLegacyIxStartingPoolSupplement(skills = false) {
const base = createIxStartingPoolFixture({ advanced: true, skills });
const game = ixStartingPoolReload(base.game);
assert.equal(game.ixSetupCards!.length, game.players.length + 1);
const extra = game.ixSetupCards!.pop()!;
game.deck.unshift(extra);
assert.deepEqual(ixStartingPoolInventory(game), base.stock);
return { ...base, game, pool: structuredClone(game.ixSetupCards!),
sourceDisclosure: `${base.sourceDisclosure} Controlled conserved old-count unit supplement: the untouched fourth pool card returns to the undealt top to represent the earlier pending N-card contract. This is not claimed native old setup history.` };
}
+1 -1
View File
@@ -231,7 +231,7 @@ void test('all AI profiles complete genuine private Ix setup through JSON contin
assert.ok(g.players.every((p) => p.hand.length === 0));
g = reachIxChoice(g, difficulty);
assert.equal(g.ixSetupCards?.length, ROSTER.length);
assert.equal(g.ixSetupCards?.length, ROSTER.length + (advanced ? 1 : 0));
const ids = g.ixSetupCards!.map((card) => card.id);
assert.deepEqual(viewGame(g, 'i').ixTechnology?.setup, g.ixSetupCards);
for (const observer of ['t', 'a', 'h']) {
+153
View File
@@ -0,0 +1,153 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, viewGame, type Action, type Game } from '../game/engine';
import { botActions } from '../game/bots';
import type { Difficulty } from '../game/bot-profiles';
import {
actIxStartingPool as act, createIxStartingPoolFixture as fixture,
createLegacyIxStartingPoolSupplement as legacy, finishIxStartingPool as finish,
ixStartingPoolChoice as choice, ixStartingPoolInventory as inventory,
ixStartingPoolPlayer as p, ixStartingPoolReload as reload, type IxStartingPoolStep,
} from './fixture-ix-starting-pool';
function reject(game: Game, actor: string, action: Action) {
const before = reload(game);
assert.throws(() => applyAction(game, actor, action));
assert.deepEqual(game, before);
}
for (const advanced of [false, true]) for (const skills of [false, true]) {
void test(`genuine ${advanced ? 'Advanced' : 'Basic'} Ix starting pool ${skills ? 'with all14' : 'without Skills'} preserves original stock and native setup order`, () => {
const f = fixture({ advanced, skills });
assert.equal(f.sourceCounts.ready, f.readyActions.length);
assert.equal(f.sourceCounts.beforeDraft, f.setupActions.length);
assert.equal(f.sourceCounts.source, f.actions.length);
assert.equal(f.pool.length, f.game.players.length + (advanced ? 1 : 0));
assert.equal(f.game.players.every(player => player.hand.length === 0), true);
assert.deepEqual(viewGame(f.game, 'ixians').ixTechnology?.setup?.map(card => card.id), f.pool.map(card => card.id));
assert.deepEqual(inventory(f.game).cards, f.stock.cards);
if (skills) {
assert.equal(Object.keys(f.game.leaderSkills!.offers).length, 0);
assert.equal(f.game.leaderSkills!.assignments.length, 0);
assert.equal(f.actions.some(step => step.action.type === 'leaderSkill' || step.action.type === 'traitor'), false);
} else assert.ok(f.actions.some(step => step.action.type === 'traitor'));
});
}
void test('fresh Advanced Ix may choose the actual fourth card and Harkonnen receives two from that same inspected remainder', () => {
for (const skills of [false, true]) {
const f = fixture({ advanced: true, skills }), beforeTop = f.game.deck[0].id;
assert.equal(f.pool[3].id, f.extra);
const game = finish(act(reload(f.game), 'ixians', choice(f.game, f.extra)));
assert.deepEqual(p(game, 'ixians').hand.map(card => card.id), [f.extra]);
const remainder = f.pool.filter(card => card.id !== f.extra).map(card => card.id);
assert.equal(p(game, 'harkonnen').hand.length, 2);
assert.ok(p(game, 'harkonnen').hand.every(card => remainder.includes(card.id)));
assert.deepEqual([...p(game, 'harkonnen').hand, ...p(game, 'atreides').hand].map(card => card.id).sort(), remainder.sort());
assert.equal(game.deck[0].id, beforeTop);
assert.equal(game.deck[0].id, f.nextAfterFour, 'Fresh Advanced consumes no extra undealt card after the pool is dealt');
assert.deepEqual(inventory(game).cards, f.stock.cards);
assert.deepEqual(inventory(game).skills, f.stock.skills);
}
});
void test('Basic keeps the fourth card outside Ix inspection and actually deals it as the separate Harkonnen top-deck bonus', () => {
for (const skills of [false, true]) {
const f = fixture({ advanced: false, skills });
assert.equal(f.game.deck[0].id, f.extra);
assert.equal(f.pool.some(card => card.id === f.extra), false);
reject(f.game, 'ixians', { type: 'decision', card: f.extra });
const selected = f.pool[0].id;
const game = finish(act(reload(f.game), 'ixians', choice(f.game, selected)));
assert.deepEqual(p(game, 'ixians').hand.map(card => card.id), [selected]);
assert.equal(p(game, 'harkonnen').hand.length, 2);
assert.ok(p(game, 'harkonnen').hand.some(card => card.id === f.extra));
assert.equal(p(game, 'harkonnen').hand.filter(card => f.pool.some(source => source.id === card.id)).length, 1);
assert.equal(game.deck[0].id, f.nextAfterFour);
assert.deepEqual(inventory(game).cards, f.stock.cards);
}
});
void test('without a Harkonnen seat both Basic and Advanced retain the native one-card-per-faction draft', () => {
for (const advanced of [false, true]) for (const skills of [false, true]) {
const f = fixture({ advanced, skills, harkonnen: false });
assert.equal(f.pool.length, f.game.players.length);
const before = f.game.deck[0].id;
const game = finish(act(reload(f.game), 'ixians', choice(f.game, f.pool[0].id)));
assert.ok(game.players.every(player => player.hand.length === 1));
assert.equal(game.deck[0].id, before);
assert.deepEqual(inventory(game).cards, f.stock.cards);
}
});
void test('native Ix choice precedes all real skill offers assignments and Traitor selections in both modes', () => {
for (const advanced of [false, true]) {
const f = fixture({ advanced, skills: true }), actions: IxStartingPoolStep[] = [];
const selected = f.pool.at(-1)!.id;
let game = act(reload(f.game), 'ixians', choice(f.game, selected), actions);
assert.equal(game.setupStage, 'leaderSkills');
assert.equal(Object.keys(game.leaderSkills!.offers).length, game.players.length);
assert.equal(game.leaderSkills!.assignments.length, 0);
assert.equal(p(game, 'harkonnen').hand.length, 2);
game = finish(game, actions);
assert.equal(actions[0].action.card, selected);
const firstSkill = actions.findIndex(step => step.action.type === 'leaderSkill');
const firstTraitor = actions.findIndex(step => step.action.type === 'traitor');
assert.ok(firstSkill > 0 && firstTraitor > firstSkill);
assert.equal(game.leaderSkills!.assignments.length, game.players.length);
assert.deepEqual(inventory(game).cards, f.stock.cards);
assert.deepEqual(inventory(game).skills, f.stock.skills);
}
});
void test('native draft rejects another actor foreign card and replay after its actual one choice', () => {
const f = fixture({ advanced: true, skills: true }), packet = choice(f.game, f.extra);
reject(f.game, 'harkonnen', packet);
reject(f.game, 'ixians', { type: 'decision', card: f.game.deck[0].id });
const game = act(reload(f.game), 'ixians', packet);
reject(game, 'ixians', packet);
assert.equal(game.ixSetupCards, null);
assert.equal(p(game, 'ixians').hand.filter(card => card.id === f.extra).length, 1);
assert.deepEqual(inventory(game).cards, f.stock.cards);
});
void test('controlled conserved old Advanced N-card draft completes its old separate top-deck Harkonnen bonus', () => {
for (const skills of [false, true]) {
const f = legacy(skills);
assert.match(f.sourceDisclosure, /Controlled conserved old-count unit supplement/);
assert.equal(f.pool.length, f.game.players.length);
assert.equal(f.game.deck[0].id, f.extra);
const game = finish(act(reload(f.game), 'ixians', choice(f.game, f.pool[0].id)));
assert.equal(p(game, 'harkonnen').hand.length, 2);
assert.ok(p(game, 'harkonnen').hand.some(card => card.id === f.extra));
assert.equal(game.deck[0].id, f.nextAfterFour);
assert.deepEqual(inventory(game).cards, f.stock.cards);
assert.deepEqual(inventory(game).skills, f.stock.skills);
}
});
void test('all four minimal profiles legally choose from the fresh Advanced pool and finish original all14 setup', () => {
const f = fixture({ advanced: true, skills: true });
for (const difficulty of ['Easy', 'Medium', 'Hard', 'Brutal'] satisfies Difficulty[]) {
let game = reload(f.game), accepted = 0;
const own = viewGame(game, 'ixians'); own.players.find(player => player.id === 'ixians')!.bot = difficulty;
const proposal = botActions(own).find(action => action.type === 'decision' && f.pool.some(card => card.id === action.card));
assert.ok(proposal, `${difficulty} chooses one actual inspected card`);
game = act(game, 'ixians', proposal); accepted++;
for (let count = 0; game.status === 'setup' && count < 100; count++) {
let progressed = false;
for (const player of game.players) {
const view = viewGame(game, player.id); view.players.find(row => row.id === player.id)!.bot = difficulty;
const action = botActions(view)[0];
if (!action) continue;
game = act(game, player.id, action); accepted++; progressed = true; break;
}
assert.ok(progressed, `${difficulty} finishes actual original setup`);
}
assert.equal(game.status, 'playing'); assert.ok(accepted > 1);
assert.equal(p(game, 'harkonnen').hand.length, 2);
assert.equal(game.deck[0].id, f.nextAfterFour);
assert.deepEqual(inventory(game).cards, f.stock.cards);
assert.deepEqual(inventory(game).skills, f.stock.skills);
}
});
+107
View File
@@ -0,0 +1,107 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
IxStartingCardError,
ixStartingCardDealPlan,
ixStartingCardPoolSize,
type IxStartingCardGame,
} from '../game/ixian-starting-cards';
function game(advanced: boolean, factions: readonly string[]): IxStartingCardGame {
return { advanced, players: factions.map((faction, index) => ({ id: `seat-${index}`, faction })) };
}
const otherFactions = ['atreides', 'emperor', 'guild', 'fremen'] as const;
void test('fresh Ix pools add Harkonnen only in Advanced, from two through six seats', () => {
for (let count = 2; count <= 6; count++) {
for (const advanced of [false, true]) {
const harkonnen = game(advanced, ['ixians', 'harkonnen', ...otherFactions.slice(0, count - 2)]);
assert.equal(ixStartingCardPoolSize(harkonnen), count + (advanced ? 1 : 0));
const ordinary = game(advanced, ['ixians', ...otherFactions, 'beneGesserit'].slice(0, count));
assert.equal(ixStartingCardPoolSize(ordinary), count);
}
}
});
void test('new Advanced pools deal Harkonnen two from the shuffled remainder in original seat order', () => {
for (let count = 2; count <= 6; count++) {
for (const ixFirst of [false, true]) {
const factions = ixFirst ? ['ixians', 'harkonnen'] : ['harkonnen', 'ixians'];
const native = game(true, [...factions, ...otherFactions.slice(0, count - 2)]);
const before = structuredClone(native);
Object.freeze(native.players);
Object.freeze(native);
const plan = ixStartingCardDealPlan(native, count + 1);
assert.deepEqual(plan, {
kind: 'advanced-pool',
seats: native.players.filter(player => player.faction !== 'ixians').map(player => ({
player: player.id, amount: player.faction === 'harkonnen' ? 2 : 1,
})),
deckBonus: null,
});
assert.equal(plan.seats.reduce((amount, seat) => amount + seat.amount, 0), count);
assert.deepEqual(native, before);
plan.seats[0].amount = 99;
assert.deepEqual(native, before);
}
}
});
void test('Basic and old Advanced N-card or short pools keep one-per-other then the Harkonnen deck bonus', () => {
for (let count = 2; count <= 6; count++) {
for (const advanced of [false, true]) {
const native = game(advanced, ['harkonnen', ...otherFactions.slice(0, count - 2), 'ixians']);
for (const poolCount of new Set([1, count - 1, count])) {
assert.deepEqual(ixStartingCardDealPlan(native, poolCount), {
kind: 'legacy-deck-bonus',
seats: native.players.filter(player => player.faction !== 'ixians').map(player => ({ player: player.id, amount: 1 })),
deckBonus: 'seat-0',
});
}
}
}
});
void test('without Harkonnen both modes deal one per other seat with no extra deck draw', () => {
for (const advanced of [false, true]) {
const native = game(advanced, ['atreides', 'ixians', 'fremen', 'guild']);
for (const count of [1, 3, 4]) {
assert.deepEqual(ixStartingCardDealPlan(native, count), {
kind: 'legacy-deck-bonus',
seats: [{ player: 'seat-0', amount: 1 }, { player: 'seat-2', amount: 1 }, { player: 'seat-3', amount: 1 }],
deckBonus: null,
});
}
}
});
void test('a pool larger than its native source or an invalid count cannot select a new deal mode', () => {
const advanced = game(true, ['ixians', 'harkonnen']);
const basic = game(false, ['ixians', 'harkonnen']);
const noHarkonnen = game(true, ['ixians', 'atreides']);
for (const count of [0, -1, 1.5, Number.NaN, Number.POSITIVE_INFINITY, Number.MAX_SAFE_INTEGER + 1, 4])
assert.throws(() => ixStartingCardDealPlan(advanced, count), IxStartingCardError);
assert.throws(() => ixStartingCardDealPlan(basic, 3), IxStartingCardError);
assert.throws(() => ixStartingCardDealPlan(noHarkonnen, 3), IxStartingCardError);
});
void test('the planner rejects missing Ixians, unregistered or duplicate seats and invalid native roster bounds', () => {
const valid = game(true, ['ixians', 'harkonnen']);
const invalid: unknown[] = [
null,
{ ...valid, advanced: 'advanced' },
{ ...valid, players: undefined },
game(true, ['ixians']),
game(true, ['ixians', 'harkonnen', ...otherFactions, 'beneGesserit']),
game(true, ['atreides', 'harkonnen']),
game(true, ['ixians', 'ixians']),
game(true, ['ixians', 'unknown']),
{ ...valid, players: [{ id: 'seat', faction: 'ixians' }, { id: 'seat', faction: 'harkonnen' }] },
{ ...valid, players: [{ id: '', faction: 'ixians' }, valid.players[1]] },
{ ...valid, players: [valid.players[0], { id: ' seat ', faction: 'harkonnen' }] },
];
for (const native of invalid) {
assert.throws(() => ixStartingCardPoolSize(native as IxStartingCardGame), IxStartingCardError);
assert.throws(() => ixStartingCardDealPlan(native as IxStartingCardGame, 2), IxStartingCardError);
}
});