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
This commit is contained in:
1 parent
f067950f5e
commit
cab2f1c42d
26 files changed
+3001
-58
No files matched your search
@@ -0,0 +1,142 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user