Files
Dune/tests/ix-richese-technology-controls.test.tsx
T
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

143 lines
4.6 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { IxRicheseTechnology } from '../components/ix-richese-technology';
import { RicheseAuctionDecision } from '../components/richese-auctions';
import {
applyAction,
createGame,
initializeFactionExpansionsGameForAudit,
joinGame,
newPlayer,
viewGame,
type Game,
} from '../game/engine';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
function until(state: Game, kind: string) {
let game = state;
for (let step = 0; step < 100; step++) {
if (game.decision?.kind === kind) return game;
let next: Game | undefined;
for (const player of game.players) {
const action = botActions(viewGame(game, player.id))[0];
if (action) {
next = applyAction(game, player.id, action);
break;
}
}
assert.ok(next, `No action before ${kind}`);
game = next;
}
throw new Error(`Did not reach ${kind}`);
}
function offer(source: 'cache' | 'blackMarket') {
let game = createGame(
'IXRICHUI',
newPlayer('r', 'Richese', 'richese'),
true,
['ix', 'choam'],
);
joinGame(game, newPlayer('i', 'Ixians', 'ixians'));
joinGame(game, newPlayer('a', 'Atreides', 'atreides'));
for (const player of game.players) {
player.ready = true;
player.bot = 'Medium';
}
game = until(
initializeFactionExpansionsGameForAudit(game),
'richeseBlackMarket',
);
if (source === 'cache') {
game = applyAction(game, 'r', {
type: 'decision',
event: game.richeseBidding!.event,
decline: true,
});
game = applyAction(game, 'r', {
type: 'decision',
event: game.richeseBidding!.event,
position: 'first',
});
game = until(game, 'richeseCache');
}
const markup = renderToStaticMarkup(
createElement(RicheseAuctionDecision, {
game: viewGame(game, 'r'),
act() {},
busy: false,
}),
);
assert.match(markup, /Offer (?:cache|Black Market) card/);
assert.doesNotMatch(markup, /<button[^>]*\sdisabled=""[^>]*>Offer /);
const card =
source === 'cache' ? game.richeseCache![0] : game.players[0].hand[0];
game = applyAction(game, 'r', {
type: 'decision',
event: game.richeseBidding!.event,
card: card.id,
method: 'silent',
...(source === 'blackMarket' ? { claim: 'Private card claim' } : {}),
});
assert.equal(game.decision?.kind, 'ixRicheseTechnology');
return { game, card };
}
function render(game: Game, viewer = 'i', busy = false) {
return renderToStaticMarkup(
createElement(IxRicheseTechnology, {
game: viewGame(game, viewer),
act() {},
busy,
}),
);
}
void test('Ixian special-lot decline is owned, available after reload and disabled during submission', () => {
for (const source of ['cache', 'blackMarket'] as const) {
const { game, card } = offer(source);
const restored = JSON.parse(JSON.stringify(game)) as Game;
const html = render(restored);
assert.match(html, /Decline Technology for this lot/);
assert.match(html, /once-per-round Technology use available/);
assert.match(html, /custody/);
assert.doesNotMatch(html, new RegExp(card.id));
if (source === 'blackMarket') assert.ok(!html.includes(card.name));
assert.equal(render(restored, 'r'), '');
assert.equal(render(restored, 'a'), '');
assert.match(render(restored, 'i', true), /<button[^>]*\sdisabled=""/);
const completed = applyAction(restored, 'i', {
type: 'decision',
event: viewGame(restored, 'i').ixRicheseTechnology!.event,
decline: true,
});
assert.equal(render(completed), '');
assert.equal(completed.ixTechnologyTurn, game.ixTechnologyTurn);
assert.equal(completed.richeseAuction?.cardId, card.id);
}
});
void test('all four AI levels decline only this lot using the private owner view', () => {
for (const source of ['cache', 'blackMarket'] as const) {
const { game, card } = offer(source);
for (const difficulty of DIFFICULTIES) {
const view = viewGame(game, 'i');
view.players.find((player) => player.id === 'i')!.bot = difficulty;
const actions = botActions(view);
assert.deepEqual(actions, [
{
type: 'decision',
event: view.ixRicheseTechnology!.event,
decline: true,
},
]);
const result = applyAction(game, 'i', actions[0]);
assert.equal(result.richeseAuction?.cardId, card.id);
assert.equal(result.ixTechnologyTurn, game.ixTechnologyTurn);
}
}
});