Match original Ecaz sample snapshots to their native Occupy and Advanced assassination state. Preserve older absent-power samples and resume the original scenario instead of rejecting fresh saved output. Update stale fixture reactions and module expectations while retaining meaningful Banker guidance and positive legal commitments. Correct the unsupported rules-completion claim. Validation: typecheck/lint, 173 affected tests across 16 files, bounded independent source review, and one direct Ecaz continuation completed through 178 accepted actions. Runtime fingerprint 95d009d3957650b08bb24826b70aa5f00a74cdd9992b6a6c222f0665d3e3759c; documentation-only proof update a91ab248109bee74a74373fb544027a2e7cc8f178e26d2c9dad475a9618eb64d. Full rules, comprehensive assurance and deployed acceptance remain open. No saved-room or provider changes.
175 lines
9.4 KiB
TypeScript
175 lines
9.4 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { registerHooks } from 'node:module';
|
|
import { readFileSync } from 'node:fs';
|
|
import { Children, createElement, isValidElement } from 'react';
|
|
import type { ReactNode, ComponentProps } from 'react';
|
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
import { applyAction, viewGame } from '../game/engine';
|
|
import type { Action, Game, GameView } from '../game/engine';
|
|
import { createGuildBetrayalFixture } from './fixture-nexus-guild-betrayal';
|
|
import { DEFAULT_ROOM_CONTROL } from '../lib/room-control';
|
|
|
|
const aliases = registerHooks({
|
|
resolve(specifier, context, next) {
|
|
return next(specifier === 'next/image' ? 'vinext/shims/image' : specifier === 'next/link' ? 'vinext/shims/link' : specifier, context);
|
|
},
|
|
load(url, context, next) {
|
|
if (!url.endsWith('.module.css')) return next(url, context);
|
|
const css = readFileSync(new URL(url), 'utf8');
|
|
const classes = Object.fromEntries([...css.matchAll(/\.([a-zA-Z_][\w-]*)/g)].map(match => [match[1], match[1]]));
|
|
return { format: 'module', source: `export default ${JSON.stringify(classes)}`, shortCircuit: true };
|
|
},
|
|
});
|
|
// These UI modules require the Node-only Next/CSS loading hooks above;
|
|
// static imports would resolve them before the hooks are registered.
|
|
const { NexusGuildBetrayal } = await import('../components/nexus-guild-betrayal');
|
|
const { GameTable } = await import('../components/game-table');
|
|
|
|
function markup(game: GameView, busy = false): string {
|
|
return renderToStaticMarkup(createElement(NexusGuildBetrayal, { game, act() {}, busy }));
|
|
}
|
|
|
|
function click(node: ReactNode, prefix: string): boolean {
|
|
for (const child of Children.toArray(node)) {
|
|
if (!isValidElement<{ children?: ReactNode; onClick?: () => void }>(child)) continue;
|
|
if (typeof child.props.children === 'string' && child.props.children.startsWith(prefix) && child.props.onClick) {
|
|
child.props.onClick();
|
|
return true;
|
|
}
|
|
if (click(child.props.children, prefix)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function select(game: GameView, prefix: string, busy = false): Action | undefined {
|
|
let selected: Action | undefined;
|
|
click(NexusGuildBetrayal({ game, busy, act(action) { selected = action; } }), prefix);
|
|
return selected;
|
|
}
|
|
|
|
function allPass(original: Game): Game {
|
|
let game = structuredClone(original);
|
|
for (const player of game.players) {
|
|
const action = select(viewGame(game, player.id), 'Pass');
|
|
if (action) game = applyAction(game, player.id, action);
|
|
}
|
|
return game;
|
|
}
|
|
|
|
void test('private eligibility grants Use and own Guild inspector without revealing a rival face or source', () => {
|
|
const fixture = createGuildBetrayalFixture();
|
|
const own = viewGame(fixture.game, fixture.holder);
|
|
assert.match(markup(own), /<button[^>]*>Use Guild Nexus/);
|
|
assert.match(markup(own), /Inspect Spacing Guild Nexus card/);
|
|
const nonholder = createGuildBetrayalFixture({ nexusFace: 'choam' });
|
|
const neutral = viewGame(nonholder.game, nonholder.holder);
|
|
assert.ok(select(neutral, 'Pass'));
|
|
assert.equal(select(neutral, 'Use Guild Nexus'), undefined);
|
|
assert.doesNotMatch(markup(neutral), /Inspect Spacing Guild Nexus card/);
|
|
const outsider = viewGame(fixture.game, fixture.guild);
|
|
assert.equal(select(outsider, 'Pass'), undefined);
|
|
assert.equal(select(outsider, 'Use Guild Nexus'), undefined);
|
|
assert.doesNotMatch(markup(outsider), /Inspect Spacing Guild Nexus card/);
|
|
// The response itself gets only the fixed private projection, not a quote or
|
|
// an extra entitlement to a shipper's hand, balance, source or route.
|
|
assert.deepEqual(Object.keys(own.guildBetrayalReaction!).sort(), ['blocked', 'canPass', 'canUse', 'event', 'hasPassed', 'shipper']);
|
|
});
|
|
|
|
void test('actual event-only human Use replaces other and own funded reserve payments with identical force delivery', () => {
|
|
for (const payer of ['other', 'holder'] as const) {
|
|
const fixture = createGuildBetrayalFixture({ payer });
|
|
const before = structuredClone(fixture.game);
|
|
const action = select(viewGame(before, fixture.holder), 'Use Guild Nexus');
|
|
assert.deepEqual(action, { type: 'guildBetrayalUse', event: fixture.event });
|
|
assert.ok(action);
|
|
const result = applyAction(before, fixture.holder, action);
|
|
const passed = allPass(fixture.game);
|
|
assert.equal(viewGame(result, fixture.holder).guildBetrayalReaction, null);
|
|
const holderBefore = fixture.game.players.find(player => player.id === fixture.holder)!;
|
|
const holderAfter = result.players.find(player => player.id === fixture.holder)!;
|
|
const shipperBefore = fixture.game.players.find(player => player.id === fixture.shipper)!;
|
|
const shipperAfter = result.players.find(player => player.id === fixture.shipper)!;
|
|
assert.equal(holderAfter.spice, holderBefore.spice + (payer === 'holder' ? 0 : fixture.price));
|
|
assert.equal(shipperAfter.spice, shipperBefore.spice - (payer === 'holder' ? 0 : fixture.price));
|
|
assert.equal(result.players.find(player => player.id === fixture.guild)!.spice,
|
|
fixture.game.players.find(player => player.id === fixture.guild)!.spice);
|
|
assert.equal(shipperAfter.shipped, true);
|
|
assert.equal(shipperAfter.reserves, shipperBefore.reserves - 2);
|
|
assert.deepEqual(result.players.map(player => ({ id: player.id, reserves: player.reserves, forces: player.forces, elites: player.elites })),
|
|
passed.players.map(player => ({ id: player.id, reserves: player.reserves, forces: player.forces, elites: player.elites })));
|
|
assert.equal(result.nexusCards?.cards?.hands[fixture.holder], null);
|
|
assert.ok(result.nexusCards?.cards?.discard.includes('guild'));
|
|
assert.equal(passed.nexusCards?.cards?.hands[fixture.holder], 'guild');
|
|
assert.equal(passed.players.find(player => player.id === fixture.guild)!.spice,
|
|
fixture.game.players.find(player => player.id === fixture.guild)!.spice + fixture.price);
|
|
}
|
|
});
|
|
|
|
void test('busy, stopped, passed, ineligible and privately blocked responses cannot emit Use', () => {
|
|
const fixture = createGuildBetrayalFixture();
|
|
const view = viewGame(fixture.game, fixture.holder);
|
|
assert.equal(select(view, 'Use Guild Nexus', true), undefined);
|
|
assert.equal(select(view, 'Pass', true), undefined);
|
|
view.guildBetrayalReaction!.blocked = 'An existing promise reserves this Nexus card.';
|
|
assert.equal(select(view, 'Use Guild Nexus'), undefined);
|
|
assert.deepEqual(select(view, 'Pass'), { type: 'guildBetrayalPass', event: fixture.event });
|
|
assert.match(markup(view), /Inspect Spacing Guild Nexus card/);
|
|
view.guildBetrayalReaction!.blocked = null;
|
|
view.guildBetrayalReaction!.canUse = false;
|
|
assert.equal(select(view, 'Use Guild Nexus'), undefined);
|
|
view.guildBetrayalReaction!.canUse = true;
|
|
view.guildBetrayalReaction!.hasPassed = true;
|
|
assert.equal(select(view, 'Use Guild Nexus'), undefined);
|
|
assert.equal(select(view, 'Pass'), undefined);
|
|
view.guildBetrayalReaction!.hasPassed = false;
|
|
view.guildBetrayalReaction!.canPass = false;
|
|
assert.equal(select(view, 'Use Guild Nexus'), undefined);
|
|
view.guildBetrayalReaction!.canPass = true;
|
|
view.status = 'finished';
|
|
assert.equal(select(view, 'Use Guild Nexus'), undefined);
|
|
assert.equal(select(view, 'Pass'), undefined);
|
|
view.guildBetrayalReaction = null;
|
|
assert.equal(markup(view), '');
|
|
});
|
|
|
|
void test('saved human actions cannot spend against future, foreign or completed shipment events', () => {
|
|
const fixture = createGuildBetrayalFixture();
|
|
const action = select(viewGame(fixture.game, fixture.holder), 'Use Guild Nexus');
|
|
assert.ok(action);
|
|
for (const event of ['foreign-shipment', `${fixture.event}:future`]) {
|
|
const game = structuredClone(fixture.game);
|
|
const before = JSON.stringify(game);
|
|
assert.throws(() => applyAction(game, fixture.holder, { ...action, event }));
|
|
assert.equal(JSON.stringify(game), before);
|
|
}
|
|
const settled = applyAction(structuredClone(fixture.game), fixture.holder, action);
|
|
const before = JSON.stringify(settled);
|
|
assert.throws(() => applyAction(settled, fixture.holder, action));
|
|
assert.equal(JSON.stringify(settled), before);
|
|
});
|
|
|
|
void test('actual GameTable reserves underlying shipment, voluntary and hand controls and respects room ownership', () => {
|
|
const fixture = createGuildBetrayalFixture({ payer: 'holder' });
|
|
const own = viewGame(fixture.game, fixture.holder);
|
|
const table = (game: ComponentProps<typeof GameTable>['game'], busy = false) => renderToStaticMarkup(createElement(GameTable, {
|
|
game, send: async () => {}, busy, onExit() {},
|
|
}));
|
|
const html = table(own);
|
|
assert.match(html, /aria-label="Guild Nexus Betrayal response"/);
|
|
assert.doesNotMatch(html, /<button[^>]*>[^<]*(?:Ship |Confirm shipment|End movement|Play Karama|Make a bribe|Pledge|Search)/i);
|
|
assert.match(table(own, true), /disabled=""[^>]*>Use Guild Nexus/);
|
|
for (const restriction of ['paused', 'closed', 'autopilot'] as const) {
|
|
const restricted: ComponentProps<typeof GameTable>['game'] = structuredClone(own);
|
|
if (restriction === 'autopilot') restricted.players.find(player => player.id === own.me)!.autopilot = 'Easy';
|
|
else restricted.roomControl = { ...DEFAULT_ROOM_CONTROL, [restriction]: true };
|
|
assert.match(table(restricted), /disabled=""[^>]*>Use Guild Nexus/);
|
|
assert.match(table(restricted), /disabled=""[^>]*>Pass/);
|
|
}
|
|
const continued = viewGame(allPass(fixture.game), fixture.holder);
|
|
assert.doesNotMatch(table(continued), /aria-label="Guild Nexus Betrayal response"/);
|
|
assert.equal(continued.players.find(player => player.id === own.me)!.shipped, true);
|
|
});
|
|
|
|
aliases.deregister();
|