Prototype Moritani Nexus Betrayal token return
This commit is contained in:
1 parent
066aeab207
commit
5c1c944a4f
10 files changed
+220
-7
No files matched your search
@@ -40,6 +40,7 @@ export function nexusMoritaniFixture(
|
||||
native?: number;
|
||||
karama?: boolean;
|
||||
stack?: boolean;
|
||||
cardOwner?: string;
|
||||
} = {},
|
||||
): NexusMoritaniFixture {
|
||||
const [owner, target, observer] = options.seatIds ?? ['p', 'q', 'r'];
|
||||
@@ -150,7 +151,7 @@ export function nexusMoritaniFixture(
|
||||
'moritani',
|
||||
...cards.deck.filter((card) => card !== 'moritani'),
|
||||
];
|
||||
g.nexusCards!.cards = drawNexusCard(cards, owner, g.players, () => 0);
|
||||
g.nexusCards!.cards = drawNexusCard(cards, options.cardOwner ?? owner, g.players, () => 0);
|
||||
const karama =
|
||||
options.karama === false
|
||||
? undefined
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createElement } from 'react';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import { applyAction, viewGame, type Game } from '../game/engine';
|
||||
import { moritaniBetrayalAction, moritaniBetrayalBotActions } from '../game/nexus-moritani-betrayal-options';
|
||||
import { NexusMoritaniBetrayal } from '../components/nexus-moritani-betrayal';
|
||||
import { grummanCollectionSignature } from '../game/grumman-collection-return';
|
||||
import { nexusMoritaniFixture, nexusMoritaniInventory, nexusMoritaniReload } from './fixture-nexus-moritani';
|
||||
|
||||
function position() {
|
||||
const f = nexusMoritaniFixture({ stack: true, karama: false, cardOwner: 'q' });
|
||||
const g = applyAction(f.g, f.owner, { type: 'decision', decline: true });
|
||||
return { ...f, g };
|
||||
}
|
||||
function reject(g: Game, owner: string, action: { type: string; [key: string]: unknown }) {
|
||||
const before = nexusMoritaniReload(g);
|
||||
assert.throws(() => applyAction(g, owner, action));
|
||||
assert.deepEqual(g, before);
|
||||
}
|
||||
|
||||
void test('Moritani Betrayal returns one actual placed token without disclosing its face or replaying Mentat placement', () => {
|
||||
const { g: initial, owner, target, observer } = position();
|
||||
const offer = viewGame(initial, target).nexusMoritaniBetrayal!;
|
||||
assert.equal(offer.blocked, null);
|
||||
assert.equal(offer.tokens.length, 1);
|
||||
const token = offer.tokens[0];
|
||||
const before = nexusMoritaniReload(initial);
|
||||
const action = moritaniBetrayalAction(viewGame(initial, target), token.id)!;
|
||||
assert.equal(moritaniBetrayalBotActions(viewGame(initial, target))[0].token, token.id);
|
||||
const g = applyAction(initial, target, action);
|
||||
nexusMoritaniInventory(g);
|
||||
assert.equal(g.nexusCards!.cards!.hands[target], null);
|
||||
assert.equal(g.nexusCards!.cards!.discard.filter(card => card === 'moritani').length, 1);
|
||||
assert.equal(g.moritaniTerror!.tokens.some(t => t.id === token.id), false);
|
||||
assert.equal(g.moritaniTerror!.tokens.filter(t => t.status === 'available').length,
|
||||
before.moritaniTerror!.tokens.filter(t => t.status === 'available').length + 1);
|
||||
assert.equal(g.moritaniTerror!.placementTurn, before.moritaniTerror!.placementTurn);
|
||||
assert.deepEqual(g.players.map(p => [p.forces, p.reserves, p.spice]), before.players.map(p => [p.forces, p.reserves, p.spice]));
|
||||
assert.deepEqual(g.nexusMoritaniLocations, before.nexusMoritaniLocations);
|
||||
assert.equal(viewGame(g, observer).nexusMoritaniBetrayal, null);
|
||||
assert.equal(viewGame(g, owner).nexusMoritaniBetrayal, null);
|
||||
assert.ok(g.log.at(-1)?.text.includes('without revealing'));
|
||||
reject(g, target, action);
|
||||
assert.deepEqual(viewGame(nexusMoritaniReload(g), target), viewGame(g, target));
|
||||
});
|
||||
|
||||
void test('Moritani Betrayal rejects foreign, stale, pending and nonexistent token choices before spending either physical component', () => {
|
||||
const { g, owner, target, observer } = position();
|
||||
const offer = viewGame(g, target).nexusMoritaniBetrayal!;
|
||||
const action = moritaniBetrayalAction(viewGame(g, target), offer.tokens[0].id)!;
|
||||
reject(g, observer, action);
|
||||
reject(g, owner, action);
|
||||
reject(g, target, { ...action, event: 'old' });
|
||||
reject(g, target, { ...action, token: 'unknown' });
|
||||
const pending = nexusMoritaniReload(g);
|
||||
pending.decision = { kind: 'moritaniPlacement', player: owner };
|
||||
reject(pending, target, action);
|
||||
assert.equal(g.nexusCards!.cards!.hands[target], 'moritani');
|
||||
});
|
||||
|
||||
void test('Betrayal offer and result depend on public token positions, not Moritani hidden faces', () => {
|
||||
const { g, target, observer } = position();
|
||||
const shuffled = nexusMoritaniReload(g);
|
||||
const placed = shuffled.moritaniTerror!.tokens.find(t => t.status === 'placed')!;
|
||||
const available = shuffled.moritaniTerror!.tokens.find(t => t.status === 'available')!;
|
||||
[placed.kind, available.kind] = [available.kind, placed.kind];
|
||||
assert.deepEqual(viewGame(g, target), viewGame(shuffled, target));
|
||||
assert.deepEqual(viewGame(g, observer), viewGame(shuffled, observer));
|
||||
});
|
||||
|
||||
void test('Betrayal controls name the chosen public token and disable during a request', () => {
|
||||
const { g, target, observer } = position();
|
||||
const view = viewGame(g, target);
|
||||
const html = renderToStaticMarkup(createElement(NexusMoritaniBetrayal, { game: view, act() {}, busy: false }));
|
||||
assert.match(html, /Moritani Nexus · Betrayal/);
|
||||
assert.match(html, /Arrakeen/);
|
||||
assert.match(html, /Return hidden Terror token/);
|
||||
assert.doesNotMatch(html, /Sabotage/);
|
||||
assert.match(renderToStaticMarkup(createElement(NexusMoritaniBetrayal, { game: view, act() {}, busy: true })), /disabled=""/);
|
||||
assert.equal(renderToStaticMarkup(createElement(NexusMoritaniBetrayal, { game: viewGame(g, observer), act() {}, busy: false })), '');
|
||||
});
|
||||
|
||||
void test('queued high-Grumman Collection resolves before a rival spends Moritani Betrayal', () => {
|
||||
const f = nexusMoritaniFixture({ stack: true, karama: false, cardOwner: 'q', homeworlds: true, native: 8 });
|
||||
const g = nexusMoritaniReload(f.g);
|
||||
// Reconstruct the saved automatic boundary just before opening the Grumman
|
||||
// choice; opening that choice only changes this frame and its decision.
|
||||
g.phase = 7;
|
||||
g.decision = null;
|
||||
g.grummanCollection!.stage = 'waiting';
|
||||
delete g.grummanCollection!.outcome;
|
||||
g.grummanCollection!.signature = grummanCollectionSignature(g.grummanCollection!);
|
||||
const view = viewGame(g, f.target);
|
||||
assert.equal(view.automaticContinuationPending, true);
|
||||
assert.match(view.nexusMoritaniBetrayal!.blocked!, /automatic phase continuation/);
|
||||
reject(g, f.target, { type: 'nexusMoritaniBetrayal',
|
||||
event: view.nexusMoritaniBetrayal!.event, token: view.nexusMoritaniBetrayal!.tokens[0].id });
|
||||
});
|
||||
Reference in new issue
Block a user