Connect classic Nexus to normal Banker income
This commit is contained in:
1 parent
a9b26fd88c
commit
cd580d27c8
12 files changed
+290
-11
No files matched your search
@@ -0,0 +1,91 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { applyAction, initializeSpiceBankerIncomeGameForAudit, viewGame } from '../game/engine';
|
||||
import { botActions } from '../game/bots';
|
||||
import { validateLeaderSkills } from '../game/leader-skills';
|
||||
import { quoteSpiceCollection } from '../game/board-resolution-quote';
|
||||
import {
|
||||
collectClassicNexusBanker, createClassicNexusBankerFixture, nexusBankerPlayer,
|
||||
} from './fixture-classic-nexus-banker-income';
|
||||
|
||||
void test('classic Nexus Banker holds the actual four-spice bank grant until original Mentat', () => {
|
||||
const f = createClassicNexusBankerFixture();
|
||||
assert.equal(nexusBankerPlayer(f.paid, f.payer).spice, nexusBankerPlayer(f.beforePayment, f.payer).spice - 4);
|
||||
assert.equal(nexusBankerPlayer(f.paid, f.payer).forces['polar_sink:0'], f.amount);
|
||||
assert.equal(nexusBankerPlayer(f.paid, f.owner).spice, nexusBankerPlayer(f.beforePayment, f.owner).spice);
|
||||
assert.deepEqual(viewGame(f.paid, f.owner).spiceBankerIncome!.deferred, [{owner:'a',amount:1}]);
|
||||
const receipt = f.paid.spiceBankerIncome!.sources.at(-1)!;
|
||||
assert.deepEqual(receipt.source.bankLegs, [{payer:'g',amount:4}]);
|
||||
assert.equal(receipt.grant!.owner, 'a');
|
||||
assert.deepEqual(f.paid.nexusCards, f.beforePayment.nexusCards);
|
||||
const {beforeMentat,collected} = collectClassicNexusBanker(f.paid);
|
||||
assert.equal(nexusBankerPlayer(collected, f.owner).spice, nexusBankerPlayer(beforeMentat, f.owner).spice + 1);
|
||||
assert.deepEqual(viewGame(collected, f.owner).spiceBankerIncome!.deferred, []);
|
||||
assert.equal(collected.spiceBankerIncome!.collections.length, 1);
|
||||
assert.deepEqual(collected.spiceBankerIncome!.collections[0].credits, [{owner:'a',amount:1}]);
|
||||
validateLeaderSkills(collected.leaderSkills!, collected.players);
|
||||
});
|
||||
|
||||
void test('a three-spice bank shipment never turns into four through Nexus or front-shield accounting', () => {
|
||||
const f = createClassicNexusBankerFixture({amount:3});
|
||||
assert.equal(nexusBankerPlayer(f.paid, f.payer).spice, nexusBankerPlayer(f.beforePayment, f.payer).spice - 3);
|
||||
assert.deepEqual(f.paid.spiceBankerIncome!.sources.at(-1)!.source.bankLegs, [{payer:'g',amount:3}]);
|
||||
assert.equal(f.paid.spiceBankerIncome!.sources.at(-1)!.grant, null);
|
||||
assert.deepEqual(viewGame(f.paid, f.owner).spiceBankerIncome!.deferred, []);
|
||||
const {beforeMentat,collected} = collectClassicNexusBanker(f.paid);
|
||||
assert.equal(nexusBankerPlayer(collected, f.owner).spice, nexusBankerPlayer(beforeMentat, f.owner).spice);
|
||||
});
|
||||
|
||||
void test('original Guild player income remains a player payment rather than a qualifying bank leg', () => {
|
||||
const f = createClassicNexusBankerFixture({payer:'e'});
|
||||
assert.equal(nexusBankerPlayer(f.paid, 'e').spice, nexusBankerPlayer(f.beforePayment, 'e').spice - 4);
|
||||
assert.equal(nexusBankerPlayer(f.paid, 'g').spice, nexusBankerPlayer(f.beforePayment, 'g').spice + 4);
|
||||
assert.deepEqual(viewGame(f.paid, f.owner).spiceBankerIncome!.deferred, []);
|
||||
assert.equal(f.paid.spiceBankerIncome!.sources.length, 0);
|
||||
const {beforeMentat,collected} = collectClassicNexusBanker(f.paid);
|
||||
assert.equal(nexusBankerPlayer(collected, f.owner).spice, nexusBankerPlayer(beforeMentat, f.owner).spice);
|
||||
});
|
||||
|
||||
void test('Advanced flexible Guild timing retains deferred Nexus income with selected Tech and Strongholds', () => {
|
||||
const f = createClassicNexusBankerFixture({advanced:true,tech:true,strongholds:true});
|
||||
assert.equal(nexusBankerPlayer(f.paid, 'g').spice, nexusBankerPlayer(f.beforePayment, 'g').spice - 4);
|
||||
assert.deepEqual(viewGame(f.paid, f.owner).spiceBankerIncome!.deferred, [{owner:'a',amount:1}]);
|
||||
const {beforeMentat,collected} = collectClassicNexusBanker(f.paid);
|
||||
assert.equal(nexusBankerPlayer(collected, f.owner).spice, nexusBankerPlayer(beforeMentat, f.owner).spice + 1);
|
||||
assert.deepEqual(collected.spiceBankerIncome!.collections[0].credits, [{owner:'a',amount:1}]);
|
||||
assert.equal(collected.spiceBankerIncome!.sources.filter(source=>source.grant).length, 1);
|
||||
validateLeaderSkills(collected.leaderSkills!, collected.players);
|
||||
});
|
||||
|
||||
void test('actual Heighliners income pays at phase end without manufacturing a Banker bank payment', () => {
|
||||
const f = createClassicNexusBankerFixture({advanced:true,tech:true,strongholds:true,payer:'e'});
|
||||
const token = f.paid.techTokens!.heighliners;
|
||||
assert.ok(token.owner);
|
||||
assert.equal(token.spice, 1);
|
||||
assert.equal(nexusBankerPlayer(f.paid, 'e').spice, nexusBankerPlayer(f.beforePayment, 'e').spice - 4);
|
||||
assert.equal(nexusBankerPlayer(f.paid, 'g').spice, nexusBankerPlayer(f.beforePayment, 'g').spice + 4);
|
||||
assert.deepEqual(viewGame(f.paid, f.owner).spiceBankerIncome!.deferred, []);
|
||||
const ordinary = quoteSpiceCollection(f.paid).receipts.find(row => row.player === token.owner)!;
|
||||
const {beforeMentat,collected} = collectClassicNexusBanker(f.paid);
|
||||
assert.equal(nexusBankerPlayer(beforeMentat, token.owner).spice,
|
||||
nexusBankerPlayer(f.paid, token.owner).spice + 1 + ordinary.strongholds + ordinary.collected);
|
||||
assert.equal(collected.techTokens!.heighliners.spice, 0);
|
||||
assert.equal(nexusBankerPlayer(collected, f.owner).spice, nexusBankerPlayer(beforeMentat, f.owner).spice);
|
||||
assert.equal(collected.spiceBankerIncome!.sources.length, 0);
|
||||
});
|
||||
|
||||
void test('Nexus Banker keeps available wallet budgets and existing legal policy actions; setup cannot be redealt', () => {
|
||||
const f = createClassicNexusBankerFixture();
|
||||
const view = viewGame(f.paid, f.payer);
|
||||
view.players.find(p=>p.id===f.payer)!.bot = 'Easy';
|
||||
const action = botActions(view)[0];
|
||||
assert.ok(action);
|
||||
const next = applyAction(f.paid, f.payer, action);
|
||||
assert.equal(nexusBankerPlayer(next, f.owner).spice, nexusBankerPlayer(f.paid, f.owner).spice);
|
||||
assert.deepEqual(viewGame(next, f.owner).spiceBankerIncome!.deferred, [{owner:'a',amount:1}]);
|
||||
const snapshot = structuredClone(f.paid);
|
||||
assert.throws(()=>initializeSpiceBankerIncomeGameForAudit(f.paid));
|
||||
assert.deepEqual(f.paid,snapshot);
|
||||
assert.throws(()=>applyAction(f.paid,f.owner,{type:'collectSpiceBankerIncome'}));
|
||||
assert.deepEqual(f.paid,snapshot);
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
applyAction, createGame, initializeSpiceBankerIncomeGameForAudit,
|
||||
joinGame, newPlayer, viewGame, type Game, type Action,
|
||||
} from '../game/engine';
|
||||
import { validateLeaderSkills } from '../game/leader-skills';
|
||||
import { createTechTokens } from '../game/tech-tokens';
|
||||
import { createStrongholdCards } from '../game/stronghold-cards';
|
||||
import { withSpiceBankerIncomeSetupRandomness } from './fixture-spice-banker-income';
|
||||
import { nextClassicNexusSkillsPaymentsNativeStep } from './fixture-classic-nexus-skills-payments';
|
||||
|
||||
export const nexusBankerPlayer = (game: Game, id: string) => {
|
||||
const player = game.players.find(p => p.id === id);
|
||||
assert.ok(player);
|
||||
return player;
|
||||
};
|
||||
export const nexusBankerClean = (game: Game) => !game.phaseOpening && !game.response && !game.decision;
|
||||
export function nexusBankerStep(game: Game): Game {
|
||||
const step = nextClassicNexusSkillsPaymentsNativeStep(game);
|
||||
assert.ok(step, 'The original Nexus/Skills continuation must retain a legal participant');
|
||||
return applyAction(game, step.actor, step.action);
|
||||
}
|
||||
export function advanceNexusBanker(state: Game, until: (game: Game) => boolean): Game {
|
||||
let game = state;
|
||||
for (let n = 0; n < 1200; n++) {
|
||||
if (until(game)) return game;
|
||||
assert.notEqual(game.status, 'finished');
|
||||
game = nexusBankerStep(game);
|
||||
}
|
||||
throw Error('Native Nexus Banker continuation missed its requested boundary');
|
||||
}
|
||||
|
||||
/** Original fresh lobby selection, genuine offers and native shipment timing.
|
||||
* Only the original Skill shuffle is controlled to give the first seat Banker.
|
||||
* No wallet, force, phase, receipt, plan or Nexus hand is manufactured. */
|
||||
export function createClassicNexusBankerFixture(options: {
|
||||
advanced?: boolean; amount?: number; payer?: 'g' | 'e'; tech?: boolean; strongholds?: boolean;
|
||||
} = {}) {
|
||||
const { advanced = false, payer = 'g', amount = payer === 'g' ? 4 : 2, tech = false, strongholds = false } = options;
|
||||
let game = createGame('CLASSICNEXUSBANKER', newPlayer('a', 'Atreides', 'atreides'), advanced, []);
|
||||
joinGame(game, newPlayer('g', 'Guild', 'guild'));
|
||||
joinGame(game, newPlayer('e', 'Emperor', 'emperor'));
|
||||
game.nexusCards = { cards: null, phase: null };
|
||||
if (tech) game.techTokens = createTechTokens();
|
||||
if (strongholds) game.strongholdCards = createStrongholdCards();
|
||||
for (const player of game.players) game = applyAction(game, player.id, { type: 'ready' });
|
||||
const lobby = structuredClone(game);
|
||||
game = withSpiceBankerIncomeSetupRandomness(() => initializeSpiceBankerIncomeGameForAudit(game));
|
||||
const started = structuredClone(game);
|
||||
while (game.status === 'setup') {
|
||||
if (game.setupStage === 'leaderSkills') {
|
||||
const actor = Object.keys(game.leaderSkills!.offers)[0];
|
||||
const skills = viewGame(game, actor).leaderSkills!;
|
||||
const skill = actor === 'a' ? 'spice-banker' : skills.offer!.cards.find(id => !skills.unavailableSkills?.[id]);
|
||||
assert.ok(skill && skills.offer!.cards.includes(skill));
|
||||
game = applyAction(game, actor, { type: 'leaderSkill', event: skills.offer!.event,
|
||||
skill, leader: skills.eligibleLeaders[0].id });
|
||||
} else game = nexusBankerStep(game);
|
||||
}
|
||||
validateLeaderSkills(game.leaderSkills!, game.players);
|
||||
const setup = structuredClone(game);
|
||||
game = advanceNexusBanker(game, g => g.turn === 1 && g.phase === 5 && g.active === payer && nexusBankerClean(g));
|
||||
const action: Action = { type: 'ship', territory: 'polar_sink', sector: 0, amount, elite: 0 };
|
||||
const beforePayment = structuredClone(game);
|
||||
game = applyAction(game, payer, action);
|
||||
game = advanceNexusBanker(game, nexusBankerClean);
|
||||
return { lobby, started, setup, beforePayment, paid: game, payer, amount, action, owner: 'a' };
|
||||
}
|
||||
|
||||
/** Stop around the actual phase transition so the separate Mentat grant is
|
||||
* never mistaken for ordinary Arrakeen/Tech/Collection income. */
|
||||
export function collectClassicNexusBanker(state: Game) {
|
||||
let game = state;
|
||||
let beforeMentat: Game | undefined;
|
||||
for (let n = 0; n < 1200; n++) {
|
||||
if (game.phase === 8 && nexusBankerClean(game)) {
|
||||
assert.ok(beforeMentat);
|
||||
return { beforeMentat, collected: game };
|
||||
}
|
||||
const next = nexusBankerStep(game);
|
||||
if (game.phase !== 8 && next.phase === 8) beforeMentat = structuredClone(game);
|
||||
game = next;
|
||||
}
|
||||
throw Error('Native Nexus Banker did not reach actual Mentat');
|
||||
}
|
||||
@@ -866,7 +866,6 @@ void test('Banker income rejects incompatible profiles and public activation wit
|
||||
game => { game.discoveryEnabled = true; },
|
||||
game => { game.ecazTreachery = true; },
|
||||
game => { game.semutaPreview = true; },
|
||||
game => { game.nexusCards = { cards: null, phase: null }; },
|
||||
game => { game.nexusIxianReplacementPreview = true; },
|
||||
game => { game.nexusIxianBetrayalPreview = true; },
|
||||
game => { game.nexusHarkonnenBetrayalPreview = true; },
|
||||
|
||||
Reference in new issue
Block a user