Files
Dune/tests/ecaz-setup-controls.test.tsx
Quintine cab2f1c42d Connect expansion faction setup and saved Ixian auction decline
Add a local faction-only prototype profile for all seven nonempty expansion selections, genuine Basic/Advanced initialization and existing private setup, controls, AI and saved continuation. Ecaz allocates six forces across Imperial Basin sectors and retains fourteen reserves before later setup; preserve Fremen, Advanced BG and Moritani ordering with printed and inferred boundaries explicit. Keep optional Ecaz Treachery cards separate, ordinary deck counts 33/35/47, and Richese cache separate. Public start and publication gates remain closed.

Replace the Advanced Ixian/Richese pre-offer stop with an explicit per-lot Ixian decline. Bind the saved original offer to its round, turn, owner, independent event, exact card custody and terms before view/action/normalization. Decline resumes that unchanged lot once without using Technology; later normal lots still offer the exchange. Protect reserved cards across transfers, spending and random Harkonnen exchange, and preserve legitimate Gift/Truthtrance interruptions. The actual special-lot exchange still awaits replacement-custody interpretation.

Validation: focused setup, rules, UI/bot, private JSON/SQLite reload and concurrent CAS tests and independent custody/persistence review passed. Typecheck, lint, 4573 offline tests, production build and 43 HTTP tests passed. Browser Ecaz setup rejected an incomplete total, saved a 2/2/2 split, and refreshed to fourteen reserves and unchanged private cards. A separate genuine Advanced QA game restored the Ixian choice; explicit decline opened the unchanged cache lot with legal bidding and retained private hand, spice, pool count and unused Technology after refresh.

Five of six genuine four-profile samples finish: CHOAM Basic 1054 actions/28 JSON restores, CHOAM Advanced 225/6, Ecaz Basic 91/2, Ecaz Advanced 523/14, all-three Basic 476/12. Combined Advanced passes three former auction stops and reaches turn three/action614 with 16 restores, then hits an existing CHOAM movement/Ambassador arrival guard. Ecaz samples also include guarded arrival candidates with legal alternatives. Full combined play is not certified; arrival integration is the next gap. Retain initial failed logs, including a corrected private harness sold-card receipt alias.

After the outage, all 597 opening rooms and original seat/recovery records were preserved (632 after QA/HTTP). A stale development module cache required restart after a fresh 598-room backup; all 598 matched afterward and the QA seat restored. Final integrity and foreign keys passed. No database reset, recurring automation or publication.

Verified source tree: 5e2ff244a1167f2834bb46d609b8de914a34f7049ff7b06046243068c0935131
2026-09-13 22:51:19 +10:00

105 lines
3.3 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { EcazSetup } from '../components/ecaz-setup';
import {
applyAction,
createGame,
initializeFactionExpansionsGameForAudit,
joinGame,
newPlayer,
viewGame,
type Game,
} from '../game/engine';
import { ECAZ_START_LOCATIONS } from '../game/ecaz-setup';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
function pending(advanced = false) {
let game = createGame('ECAZSETU', newPlayer('e', 'Ecaz', 'ecaz'), advanced, [
'ecaz',
]);
joinGame(game, newPlayer('g', 'Guild', 'guild'));
for (const player of game.players)
game = applyAction(game, player.id, { type: 'ready' });
game = initializeFactionExpansionsGameForAudit(game);
while (game.setupStage === 'traitors') {
const player = game.players.find((p) => p.traitorChoices.length)!;
game = applyAction(game, player.id, {
type: 'traitor',
leader: player.traitorChoices[0],
});
}
assert.equal(game.setupStage, 'forces');
assert.deepEqual(
game.players.map((p) => p.hand.length),
[0, 0],
);
assert.deepEqual(game.setupStage && viewGame(game, 'e').setupPending, ['e']);
return game;
}
function markup(game: Game, viewer = 'e', busy = false) {
return renderToStaticMarkup(
createElement(EcazSetup, { game: viewGame(game, viewer), act() {}, busy }),
);
}
void test('Ecaz setup presents three sector allocations only to its pending owner, before cards are dealt', () => {
for (const advanced of [false, true]) {
const game = pending(advanced);
const html = markup(game);
assert.match(html, /Ecaz starting forces/);
assert.match(html, /fourteen forces remain in reserves/);
for (const location of ECAZ_START_LOCATIONS)
assert.ok(html.includes(`ecaz-start-${location}`));
assert.match(html, /6 \/ 6 forces assigned/);
assert.match(html, /Place Ecaz starting forces/);
assert.equal(markup(game, 'g'), '');
assert.equal(
(
markup(game, 'e', true).match(
/<(?:input|button)\b[^>]*\sdisabled=""/g,
) ?? []
).length,
4,
);
const placed = applyAction(JSON.parse(JSON.stringify(game)), 'e', {
type: 'ecazSetup',
placements: Object.fromEntries(
ECAZ_START_LOCATIONS.map((location) => [location, 2]),
),
});
assert.equal(markup(placed), '');
assert.equal(placed.players[0].reserves, 14);
assert.deepEqual(
placed.players.map((p) => p.hand.length),
[1, 1],
);
}
});
void test('every AI profile supplies a legal Ecaz allocation and other seats wait for placement', () => {
for (const difficulty of DIFFICULTIES) {
const game = pending(true);
const view = viewGame(game, 'e');
view.players[0].bot = difficulty;
const actions = botActions(view);
assert.equal(actions.length, 1);
assert.equal(actions[0].type, 'ecazSetup');
const result = applyAction(game, 'e', actions[0]);
assert.equal(result.players[0].reserves, 14);
assert.equal(
Object.values(result.players[0].forces).reduce(
(sum, count) => sum + count,
0,
),
6,
);
const other = viewGame(game, 'g');
other.players[1].bot = difficulty;
assert.deepEqual(botActions(other), []);
}
});