Connect the GF9 FAQ combination: one free physical reserve force accompanies an owned concealed No-Field at its unchanged one-marker price. Preserve zero/three/five token values, actual supply at later reveal, optional human choice and all four legal AI paths. Genuine CHOAM & Richese Leader Skills development setup adds the two sourced battle cards without duplicating their Ix identities. Public mode and publication gates stay closed; allied and remaining combined routes are unfinished. Bind the private declaration across initial No-Field cancellation and later Advanced Guild interception, including saved interruptions. Validate before projection/actions/normalization, hide the internal proof, reject altered declarations immutably, and withdraw the companion once only on successful commitment. Independent rules/privacy/persistence review found no blocker. Preserve other audit setup gates despite expanded deck assembly. Repair the existing randomized recovery test with a genuine seeded Rihani offer and explicit human choice, replacing an inappropriate AI-policy call on a human seat. Initial Karama and later Guild stop retain their distinct existing opportunity outcomes. Validation: 72 focused regressions plus 10 deck tests; typecheck, lint, 4520 offline tests, production build and 43 HTTP tests passed. SQLite restart, concurrent allowance/cancellation and exact physical custody passed. Browser genuine assignment, private token selection, opt-in, one-spice shipment, refresh, five-force reveal and final refresh preserved six physical Arrakeen forces, 14 reserves, zero Tanks, four spice, Crysknife and Soo-Soo Sook. Genuine four-profile Basic/Advanced games completed in 77/116 actions with 3/4 JSON restores and no rejected candidates; Advanced exercised one companion shipment and later reveal. After the outage the existing server was healthy. Preserved all 529 opening rooms and original seat/recovery records (563 after QA/HTTP). SQLite integrity and foreign keys passed. No restart, database reset or automation. Verified source tree: c2aee4138bcb46a05814a340d62eadace3470247ca620844eb0375e78ea4d6c4
129 lines
4.8 KiB
TypeScript
129 lines
4.8 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { createElement } from 'react';
|
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
import {
|
|
RicheseNoFieldControls,
|
|
SmugglerNoFieldChoice,
|
|
} from '../components/richese-no-field';
|
|
import { botActions } from '../game/bots';
|
|
import { DIFFICULTIES } from '../game/bot-profiles';
|
|
import { applyAction, viewGame, type Game } from '../game/engine';
|
|
import { presenceAt } from '../game/force-presence';
|
|
import {
|
|
smugglerNoFieldAction,
|
|
smugglerNoFieldGame,
|
|
} from './smuggler-no-field-fixture';
|
|
|
|
function markup(game: Game, viewer = 'r', destination = 'arrakeen') {
|
|
return renderToStaticMarkup(
|
|
createElement(RicheseNoFieldControls, {
|
|
game: viewGame(game, viewer),
|
|
act() {},
|
|
busy: false,
|
|
destination,
|
|
sector: destination === 'arrakeen' ? 10 : 9,
|
|
allyPayment: '',
|
|
}),
|
|
);
|
|
}
|
|
|
|
void test('the private No-Field panel offers an unchecked free real-force companion without changing the marker price', () => {
|
|
const html = markup(smugglerNoFieldGame());
|
|
assert.match(html, /Private No-Field inventory/);
|
|
for (const value of [0, 3, 5])
|
|
assert.match(html, new RegExp(`No-Field ${value}`));
|
|
assert.match(
|
|
html,
|
|
/Use Smuggler to add 1 real reserve force at no added cost/,
|
|
);
|
|
assert.doesNotMatch(html, /type="checkbox" checked=""/);
|
|
assert.match(
|
|
html,
|
|
/No-Field token still uses the normal one-force shipment price/,
|
|
);
|
|
assert.match(html, /Ship concealed No-Field [035] · 1 spice/);
|
|
assert.match(
|
|
html,
|
|
/Smuggler companion is a separate real force that leaves reserves immediately/,
|
|
);
|
|
|
|
const optedIn = renderToStaticMarkup(
|
|
createElement(SmugglerNoFieldChoice, {
|
|
quote: { leader: 'richese-0', amount: 1 },
|
|
use: true,
|
|
busy: false,
|
|
onChange() {},
|
|
}),
|
|
);
|
|
assert.match(optedIn, /type="checkbox" checked=""/);
|
|
assert.match(optedIn, /hidden forces remain in reserves until revelation/);
|
|
assert.match(markup(smugglerNoFieldGame(true)), /Use Smuggler/);
|
|
});
|
|
|
|
void test('the companion choice follows empty-territory and real-reserve gates and remains owner-private', () => {
|
|
const occupied = smugglerNoFieldGame();
|
|
occupied.players.find((player) => player.id === 'h')!.forces = {
|
|
'arrakeen:9': 1,
|
|
};
|
|
occupied.players.find((player) => player.id === 'h')!.reserves = 19;
|
|
assert.doesNotMatch(markup(occupied), /Use Smuggler/);
|
|
|
|
const emptyReserves = smugglerNoFieldGame();
|
|
emptyReserves.players.find((player) => player.id === 'r')!.reserves = 0;
|
|
assert.doesNotMatch(markup(emptyReserves), /Use Smuggler/);
|
|
|
|
const privateGame = smugglerNoFieldGame();
|
|
assert.equal(markup(privateGame, 'h'), '');
|
|
assert.equal(viewGame(privateGame, 'h').richeseNoField!.private, null);
|
|
assert.doesNotMatch(markup(privateGame, 'h'), /No-Field [035]/);
|
|
});
|
|
|
|
void test('all four bot profiles add the legal companion to owned No-Field shipments', () => {
|
|
for (const advanced of [false, true])
|
|
for (const difficulty of DIFFICULTIES) {
|
|
const game = smugglerNoFieldGame(advanced);
|
|
const view = viewGame(game, 'r');
|
|
view.players.find((player) => player.id === 'r')!.bot = difficulty;
|
|
const noFieldActions = botActions(view).filter(
|
|
(action) => action.type === 'ship' && action.noField,
|
|
);
|
|
const candidates = noFieldActions.filter(
|
|
(action) => action.smuggler === true,
|
|
);
|
|
assert.ok(
|
|
candidates.length,
|
|
`${advanced ? 'Advanced' : 'Basic'} ${difficulty} omitted the companion`,
|
|
);
|
|
assert.ok(
|
|
noFieldActions.every((action) => action.smuggler === true),
|
|
`${advanced ? 'Advanced' : 'Basic'} ${difficulty} left a legal owned No-Field candidate unaugmented`,
|
|
);
|
|
const destination = String(candidates[0].territory);
|
|
let settled = applyAction(game, 'r', candidates[0]);
|
|
if (settled.decision?.kind === 'guildShipment')
|
|
settled = applyAction(settled, settled.decision.player, {
|
|
type: 'decision',
|
|
allow: true,
|
|
});
|
|
assert.equal(settled.players[0].reserves, 19);
|
|
assert.equal(presenceAt(settled.players[0], destination), 2);
|
|
}
|
|
});
|
|
|
|
void test('all profiles reveal a zero No-Field before moving when its real companion is alongside it', () => {
|
|
for (const difficulty of DIFFICULTIES) {
|
|
let game = smugglerNoFieldGame();
|
|
game = applyAction(game, 'r', smugglerNoFieldAction(game, 0, true));
|
|
game.active = 'r';
|
|
const view = viewGame(game, 'r');
|
|
view.players.find((player) => player.id === 'r')!.bot = difficulty;
|
|
const reveal = botActions(view)[0];
|
|
assert.equal(reveal.type, 'revealNoField', difficulty);
|
|
game = applyAction(game, 'r', reveal);
|
|
assert.equal(game.players[0].noField!.deployed, null);
|
|
assert.equal(game.players[0].reserves, 19);
|
|
assert.equal(presenceAt(game.players[0], 'arrakeen'), 1);
|
|
}
|
|
});
|