Fresh Richese-roster battles support clean aggressor changes after plans and revelation. Both combatants explicitly finish a public final opportunity regardless of held cards or result; an actual Sapho use clears both marks with a fresh event. Close priority before physical Stone allocation and other outcome settlement. Preserve plans, votes, participants, Habbanya priority and reserved physical cards. Keep old unstamped timing and discard signature bytes. Route all final vote exits through native Homeworld defense first. Add persistent controls and finite minimal bot readiness. Validation:16-file177/177, types/lint/build and eight original-action/rendered-table programmes. Actual cache1 and paid forces use40 Basic/47 Advanced source actions; genuine Stone1 turn1/Sapho1 turn2 uses86. Ordinary and mixed count2 ties change through sealed-plan/final-window Sapho, explicit revised acknowledgements and native losses. Sixteen minimal proposals are legal; controlled reservation and pure window/control cases are separate. Bounded review fixed the Harkonnen Nexus final-vote route; frozen feedback corrected four historical fixture assumptions. All checks then passed together. Final-window protocol is provisional. Direct late Semuta claim/capture, combined World/Nexus/Sapho, wider combinations, full modes, final assurance and browser/deployed acceptance remain open. No saved-room/database/reset/restart/deployment operation.
84 lines
4.4 KiB
TypeScript
84 lines
4.4 KiB
TypeScript
import test from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { registerHooks } from 'node:module';
|
||
import { readFileSync } from 'node:fs';
|
||
import { createElement } from 'react';
|
||
import { renderToStaticMarkup } from 'react-dom/server';
|
||
import { applyAction, viewGame, type GameView } from '../game/engine';
|
||
import { JuiceOfSapho } from '../components/juice-of-sapho';
|
||
import { botBattleChoices } from '../game/bot-battle-choices';
|
||
import { saphoBattleOrderGame, saphoBattleOrderAction } from './fixture-sapho-battle-order';
|
||
import { saphoAggressorGame, aggressorAction } from './fixture-sapho-aggressor';
|
||
|
||
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 };
|
||
},
|
||
});
|
||
const { GameTable } = await import('../components/game-table');
|
||
aliases.deregister();
|
||
const panel = (game: GameView, busy = false) => renderToStaticMarkup(createElement(JuiceOfSapho, { game, act() {}, busy }));
|
||
const table = (game: GameView) => renderToStaticMarkup(createElement(GameTable, { game, send: async () => {}, onExit() {}, busy: false }));
|
||
|
||
void test('early aggressor control explains ties and the table keeps its public physical identities', () => {
|
||
const g = saphoAggressorGame();
|
||
const view = viewGame(g, 'b');
|
||
assert.match(panel(view), /Become aggressor and discard Juice of Sapho/);
|
||
assert.match(panel(view), /Habbanya Stronghold advantage still takes precedence/);
|
||
assert.match(panel(view, true), /<button[^>]*disabled=""[^>]*>Become aggressor/);
|
||
assert.equal(panel(viewGame(g, 'r')), '');
|
||
const changed = applyAction(g, 'b', aggressorAction(g));
|
||
const html = table(viewGame(changed, 'a'));
|
||
assert.match(html, /Aggressor: guild/);
|
||
assert.match(html, /Tied battle: guild/);
|
||
assert.equal(changed.battle!.attacker, 'a');
|
||
assert.equal(changed.battle!.chooser, 'a');
|
||
});
|
||
|
||
void test('held Sapho exposes the legal battle-order action with its actual timing and limits', () => {
|
||
for (const [holder, mode] of [['c', 'first'], ['a', 'last']] as const) {
|
||
const game = saphoBattleOrderGame({ holder, geometry: 'shared' });
|
||
const view = viewGame(game, holder);
|
||
const html = panel(view);
|
||
assert.match(html, new RegExp(`Choose battles ${mode} and`));
|
||
assert.match(html, /Other players may still choose battles against you/);
|
||
assert.match(html, /not the aggressor or tie advantage/);
|
||
assert.match(html, /Late aggressor timing[\s\S]*provisional shared final opportunity/);
|
||
assert.match(panel(view, true), /<button[^>]*disabled=""[^>]*>Choose battles/);
|
||
const observer = game.players.find(player => player.id !== holder)!;
|
||
assert.equal(panel(viewGame(game, observer.id)), '');
|
||
}
|
||
});
|
||
|
||
void test('the actual table offers an earlier physical attacker as the reordered chooser’s opponent', () => {
|
||
const original = saphoBattleOrderGame({ holder: 'c', geometry: 'shared' });
|
||
const game = applyAction(original, 'c', saphoBattleOrderAction(original, 'c', 'first'));
|
||
const view = viewGame(game, 'c');
|
||
const html = table(view);
|
||
const opponent = game.players.find(player => player.id === 'a')!;
|
||
assert.match(html, new RegExp(`Fight ${opponent.name} · Arrakeen`));
|
||
assert.match(html, /Remaining battle-choice order/);
|
||
assert.doesNotMatch(html, /Choose battles first and/);
|
||
const action = botBattleChoices(view).find(candidate => candidate.target === 'a' && candidate.territory === 'arrakeen');
|
||
assert.ok(action);
|
||
const opened = applyAction(game, 'c', action);
|
||
assert.equal(opened.battle?.attacker, 'a');
|
||
assert.equal(opened.battle?.defender, 'c');
|
||
});
|
||
|
||
void test('a held Sapho explains why ordering must wait through the current battle', () => {
|
||
const game = saphoBattleOrderGame({ holder: 'c', geometry: 'shared' });
|
||
const action = botBattleChoices(viewGame(game, game.active!))[0];
|
||
assert.ok(action);
|
||
const opened = applyAction(game, game.active!, action);
|
||
const html = panel(viewGame(opened, 'c'));
|
||
assert.doesNotMatch(html, /Choose battles (first|last) and/);
|
||
assert.match(html, /Finish the current (battle|interaction)/);
|
||
});
|