Implement Bene Gesserit Nexus advisor conversion with durable responses
This commit is contained in:
1 parent
40912498b1
commit
b05125de9a
23 files changed
+2298
-16
No files matched your search
@@ -0,0 +1,174 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
applyAction,
|
||||
normalizeAutomaticGame,
|
||||
viewGame,
|
||||
type Game,
|
||||
} from '../game/engine';
|
||||
import {
|
||||
nexusAdvisorFixture,
|
||||
nexusAdvisorPlayer,
|
||||
holdAdvisorKarama,
|
||||
beginNexusAdvisorFlip,
|
||||
allowNexusAdvisorFlip,
|
||||
} from './fixture-nexus-advisors';
|
||||
import { nexusReload } from './fixture-nexus-cards';
|
||||
import { treacheryDeck } from '../game/cards';
|
||||
import { richeseCards } from '../game/richese-cards';
|
||||
|
||||
function inventory(g: Game) {
|
||||
assert.deepEqual(
|
||||
[
|
||||
...g.deck,
|
||||
...g.discard,
|
||||
...g.players.flatMap((p) => p.hand),
|
||||
...g.richeseCache!,
|
||||
]
|
||||
.map((card) => card.id)
|
||||
.sort(),
|
||||
[...treacheryDeck(g.expansions), ...richeseCards()]
|
||||
.map((card) => card.id)
|
||||
.sort(),
|
||||
);
|
||||
for (const p of g.players)
|
||||
assert.equal(
|
||||
p.reserves + p.tanks + Object.values(p.forces).reduce((a, b) => a + b, 0),
|
||||
20,
|
||||
);
|
||||
}
|
||||
|
||||
function paidSearch() {
|
||||
let g = nexusAdvisorFixture({ opponentFaction: 'richese' });
|
||||
const boxIndex = g.richeseCache!.findIndex(
|
||||
(card) => card.effect === 'nullentropyBox',
|
||||
);
|
||||
assert.ok(boxIndex >= 0);
|
||||
const box = g.richeseCache!.splice(boxIndex, 1)[0];
|
||||
g.players[1].hand.push(box);
|
||||
const karama = holdAdvisorKarama(g);
|
||||
for (const kind of ['shield', 'projectile']) {
|
||||
const index = g.deck.findIndex((card) => card.kind === kind);
|
||||
assert.ok(index >= 0);
|
||||
g.discard.push(g.deck.splice(index, 1)[0]);
|
||||
}
|
||||
g.players[1].spice = 10;
|
||||
const original = nexusReload(g);
|
||||
g = beginNexusAdvisorFlip(g, ['arrakeen', 'pasty_mesa']);
|
||||
const response = structuredClone(g.response);
|
||||
g = applyAction(g, 'q', { type: 'card', card: box.id });
|
||||
assert.equal(g.decision?.kind, 'nullentropy');
|
||||
assert.equal(g.pendingNullentropy?.resume.response?.kind, 'nexusAdvisorFlip');
|
||||
assert.deepEqual(g.pendingNullentropy!.resume.response, response);
|
||||
assert.equal(g.players[1].spice, 8);
|
||||
inventory(g);
|
||||
return { g, original, response, karama, box: box.id };
|
||||
}
|
||||
|
||||
function finishSearch(g: Game) {
|
||||
return applyAction(g, 'q', {
|
||||
type: 'decision',
|
||||
event: g.pendingNullentropy!.event,
|
||||
card: g.discard[0].id,
|
||||
});
|
||||
}
|
||||
|
||||
void test('paid Nullentropy suspends the advisor response and restores its exact batch once after a private JSON recovery', () => {
|
||||
const f = paidSearch();
|
||||
let g = nexusReload(f.g);
|
||||
const before = nexusReload(g);
|
||||
assert.deepEqual(normalizeAutomaticGame(g), before);
|
||||
for (const player of g.players) {
|
||||
assert.deepEqual(
|
||||
viewGame(nexusReload(g), player.id),
|
||||
viewGame(g, player.id),
|
||||
);
|
||||
if (player.id !== 'q')
|
||||
assert.equal(viewGame(g, player.id).nullentropy, null);
|
||||
}
|
||||
assert.deepEqual(
|
||||
nexusAdvisorPlayer(g).advisors,
|
||||
nexusAdvisorPlayer(f.original).advisors,
|
||||
);
|
||||
const selected = g.discard[0].id;
|
||||
g = finishSearch(g);
|
||||
assert.deepEqual(g.response, f.response);
|
||||
assert.equal(g.nexusAdvisorHistory!.at(-1)!.stage, 'pending');
|
||||
assert.equal(g.players[1].spice, 8);
|
||||
assert.ok(g.players[1].hand.some((card) => card.id === selected));
|
||||
assert.equal(g.discard.filter((card) => card.id === f.box).length, 1);
|
||||
g = allowNexusAdvisorFlip(nexusReload(g));
|
||||
assert.deepEqual(nexusAdvisorPlayer(g).advisors, { carthag: {} });
|
||||
assert.equal(g.nexusAdvisorHistory!.at(-1)!.stage, 'completed');
|
||||
assert.equal(g.nexusAdvisorHistory!.length, 1);
|
||||
assert.deepEqual(
|
||||
g.players.map((player) => player.forces),
|
||||
f.original.players.map((player) => player.forces),
|
||||
);
|
||||
assert.equal(g.players[1].spice, 8);
|
||||
assert.deepEqual(normalizeAutomaticGame(nexusReload(g)), nexusReload(g));
|
||||
inventory(g);
|
||||
});
|
||||
|
||||
void test('the restored advisor response still accepts one Karama after Box recovery without undoing the paid search', () => {
|
||||
const f = paidSearch();
|
||||
let g = finishSearch(nexusReload(f.g));
|
||||
const selected = f.g.discard[0].id;
|
||||
g = applyAction(g, 'q', { type: 'card', card: f.karama, mode: 'cancel' });
|
||||
g = allowNexusAdvisorFlip(nexusReload(g));
|
||||
assert.equal(g.nexusAdvisorHistory!.at(-1)!.stage, 'canceled');
|
||||
assert.deepEqual(
|
||||
nexusAdvisorPlayer(g).advisors,
|
||||
nexusAdvisorPlayer(f.original).advisors,
|
||||
);
|
||||
assert.equal(g.players[1].spice, 8);
|
||||
assert.ok(g.players[1].hand.some((card) => card.id === selected));
|
||||
assert.equal(g.discard.filter((card) => card.id === f.box).length, 1);
|
||||
assert.equal(g.discard.filter((card) => card.id === f.karama).length, 1);
|
||||
assert.equal(
|
||||
g.nexusCards!.cards!.discard.filter((card) => card === 'beneGesserit')
|
||||
.length,
|
||||
1,
|
||||
);
|
||||
inventory(g);
|
||||
});
|
||||
|
||||
void test('damaged advisor parents inside a paid search reject reads, automatic progress and card selection immutably', () => {
|
||||
const { g: paid } = paidSearch();
|
||||
const mutations: ((g: Game) => void)[] = [
|
||||
(g) => {
|
||||
g.pendingNullentropy!.resume.response = null;
|
||||
},
|
||||
(g) => {
|
||||
g.pendingNullentropy!.resume.response!.intent = 'stale';
|
||||
},
|
||||
(g) => {
|
||||
g.pendingNullentropy!.resume.response!.owner = 'q';
|
||||
},
|
||||
(g) => {
|
||||
delete g.nexusAdvisorLast;
|
||||
},
|
||||
(g) => {
|
||||
delete g.nexusAdvisorHistory;
|
||||
},
|
||||
(g) => {
|
||||
g.nexusAdvisorHistory![0].stage = 'completed';
|
||||
},
|
||||
(g) => {
|
||||
g.players[0].moved++;
|
||||
},
|
||||
(g) => {
|
||||
g.players[0].forces['arrakeen:10']++;
|
||||
g.players[0].reserves--;
|
||||
},
|
||||
];
|
||||
for (const mutate of mutations) {
|
||||
const g = nexusReload(paid);
|
||||
mutate(g);
|
||||
const before = nexusReload(g);
|
||||
for (const player of g.players) assert.throws(() => viewGame(g, player.id));
|
||||
assert.throws(() => normalizeAutomaticGame(g));
|
||||
assert.throws(() => finishSearch(g));
|
||||
assert.deepEqual(g, before);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user