154 lines
8.5 KiB
TypeScript
154 lines
8.5 KiB
TypeScript
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);
|
|
}
|
|
});
|