Initialize occupation benefits in original World module setups
This commit is contained in:
1 parent
396f9e0ccb
commit
09eb857583
18 files changed
+916
-12
No files matched your search
@@ -0,0 +1,105 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { applyAction, viewGame, type Game } from '../game/engine';
|
||||
import { leaders, spiceDeck, treacheryDeck } from '../game/cards';
|
||||
import { NEXUS_FACTIONS } from '../game/nexus-cards';
|
||||
import { DISCOVERY_SPICE_CARDS, DISCOVERY_TOKENS } from '../game/discoveries';
|
||||
import { LEADER_SKILL_CARDS } from '../game/leader-skill-cards';
|
||||
import { createOccupiedPercentageState } from '../game/homeworld-occupied-percentage';
|
||||
import {
|
||||
createMixedPhaseModulesFixture, mixedPhaseModulesInventory, type MixedPhaseModulesOptions,
|
||||
} from './fixture-mixed-phase-modules';
|
||||
|
||||
function originalOccupation(game: Game) {
|
||||
assert.equal(game.homeworldOccupationPreview, true);
|
||||
assert.equal(game.pendingHomeworldOccupiedIncome, null);
|
||||
assert.deepEqual(game.homeworldOccupiedIncomeHistory, []);
|
||||
assert.ok(game.homeworldOccupiedPercentageLedger);
|
||||
assert.deepEqual(game.homeworldOccupiedPercentageLedger.receipts, []);
|
||||
if (game.players.some(player => player.faction === 'ecaz')) {
|
||||
assert.equal(game.homeworldEcazVictoryPreview, true); assert.equal(game.ecazOccupyPreview, true);
|
||||
}
|
||||
}
|
||||
function originalModules(game: Game, options: MixedPhaseModulesOptions) {
|
||||
if (options.nexus) {
|
||||
const cards = game.nexusCards?.cards; assert.ok(cards);
|
||||
assert.deepEqual([...cards.deck, ...cards.discard, ...Object.values(cards.hands).filter(card => card !== null)].sort(),
|
||||
[...NEXUS_FACTIONS].sort());
|
||||
} else assert.ok(!game.nexusCards);
|
||||
if (options.discoveries) {
|
||||
assert.equal(game.discoveryEnabled, true); assert.ok(game.discoveries);
|
||||
assert.deepEqual(game.discoveries.tokens.map(token => token.face).sort(), DISCOVERY_TOKENS.map(token => token.id).sort());
|
||||
assert.equal(new Set(game.discoveries.tokens.map(token => token.id)).size, 8);
|
||||
const cards = [...game.spiceDeck, ...game.spiceDiscard.flat(),
|
||||
...(game.spiceResolution?.skipped ?? []), ...(game.spiceSequence?.skipped ?? [])];
|
||||
assert.equal(cards.length, spiceDeck(game.expansions.includes('ix')).length + 7);
|
||||
assert.deepEqual(cards.flatMap(card => 'territory' in card && card.discovery ? [card.discovery] : []).sort(),
|
||||
DISCOVERY_SPICE_CARDS.map(card => card.discovery).sort());
|
||||
assert.equal(cards.filter(card => 'worm' in card && card.greatMaker).length, 1);
|
||||
} else assert.ok(!game.discoveryEnabled && !game.discoveries);
|
||||
if (options.skills) {
|
||||
const skills = game.leaderSkills; assert.ok(skills);
|
||||
assert.deepEqual([...skills.deck, ...Object.values(skills.offers).flatMap(offer => offer.cards),
|
||||
...skills.assignments.map(assignment => assignment.skill)].sort(), LEADER_SKILL_CARDS.map(card => card.id).sort());
|
||||
} else assert.ok(!game.leaderSkills);
|
||||
}
|
||||
function entry(options: MixedPhaseModulesOptions) {
|
||||
const f = createMixedPhaseModulesFixture({ ...options, homeworlds: true });
|
||||
assert.equal(f.setup.status, 'setup'); originalOccupation(f.setup);
|
||||
assert.equal(f.game.status, 'playing'); assert.equal(f.game.turn, 1); assert.equal(f.game.phase, 1);
|
||||
originalModules(f.setup, options); originalModules(f.game, options);
|
||||
assert.equal(f.nexus, !!options.nexus); assert.equal(f.discoveries, !!options.discoveries);
|
||||
assert.equal(f.originalInventory.length, treacheryDeck(options.expansions).length);
|
||||
assert.deepEqual(mixedPhaseModulesInventory(f.game), f.originalInventory);
|
||||
const firstReady = f.actions.findIndex(step => step.action.type === 'ready');
|
||||
for (const [enabled, module] of [[options.nexus, 'nexusCards'], [options.discoveries, 'discoveries']] as const) {
|
||||
if (!enabled) continue;
|
||||
const selected = f.actions.findIndex(step => step.action.type === 'moduleOption' && step.action.module === module && step.action.enabled === true);
|
||||
assert.ok(selected >= 0 && selected < firstReady); assert.equal(f.actions[selected].actor, f.initial.host);
|
||||
}
|
||||
for (const player of f.afterSetup.players) {
|
||||
assert.deepEqual(player.leaders.map(leader => leader.id).sort(), leaders(player.faction).map(leader => leader.id).sort());
|
||||
assert.ok(viewGame(f.game, player.id).homeworlds?.worlds?.some(world => world.native === player.id));
|
||||
const requested = options.training?.[player.faction];
|
||||
if (requested) {
|
||||
const assignment = f.afterSetup.leaderSkills!.assignments.find(assignment => assignment.owner === player.id);
|
||||
assert.ok(assignment); assert.equal(assignment.skill, requested);
|
||||
assert.ok(player.leaders.some(leader => leader.id === assignment.leader));
|
||||
assert.ok(f.actions.some(step => step.actor === player.id && step.action.type === 'leaderSkill' &&
|
||||
step.action.skill === requested && step.action.leader === assignment.leader));
|
||||
}
|
||||
}
|
||||
assert.ok(!f.game.semutaPreview && !f.game.ecazTreachery);
|
||||
assert.deepEqual(f.staging, []); assert.equal(f.sourceActionCount, f.actions.length);
|
||||
return f;
|
||||
}
|
||||
|
||||
// These recipes exercise previously registered original World envelopes. They
|
||||
// do not assert new roster or overlay admission.
|
||||
void test('public original classic World Nexus and Discovery starts retain ordinary occupation and physical components', () => {
|
||||
entry({ factions: ['atreides', 'emperor', 'fremen'], expansions: [], nexus: true, tech: true });
|
||||
entry({ factions: ['atreides', 'emperor'], expansions: [], discoveries: true });
|
||||
});
|
||||
|
||||
void test('public original Advanced World Nexus plus Discovery keeps real fourteen-card training and phase modules', () => {
|
||||
entry({ factions: ['atreides', 'emperor', 'fremen'], expansions: [], advanced: true,
|
||||
nexus: true, discoveries: true, skills: true, tech: true, strongholds: true,
|
||||
training: { atreides: 'warmaster', emperor: 'mentat', fremen: 'swordmaster-of-ginaz' } });
|
||||
});
|
||||
|
||||
void test('public admitted paired E1 Nexus and standalone Ecaz Discovery retain original Worlds and occupation state', () => {
|
||||
entry({ factions: ['ixians', 'tleilaxu', 'guild'], expansions: ['ix'], nexus: true, tech: true });
|
||||
entry({ factions: ['ecaz', 'emperor', 'guild'], expansions: ['ecaz'], discoveries: true });
|
||||
});
|
||||
|
||||
void test('an original World Nexus lobby rejects a pre-existing occupation ledger without overwriting it', () => {
|
||||
const f = createMixedPhaseModulesFixture({ factions: ['atreides', 'emperor', 'fremen'], expansions: [],
|
||||
homeworlds: true, nexus: true });
|
||||
// Controlled negative input only: this otherwise valid original ready lobby
|
||||
// has an existing typed ledger before fresh setup is allowed to create one.
|
||||
const game = structuredClone(f.lobby);
|
||||
game.homeworldOccupiedPercentageLedger = createOccupiedPercentageState();
|
||||
const before = structuredClone(game);
|
||||
assert.throws(() => applyAction(game, game.host, { type: 'start' }));
|
||||
assert.deepEqual(game, before);
|
||||
});
|
||||
Reference in new issue
Block a user