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
92 lines
2.8 KiB
TypeScript
92 lines
2.8 KiB
TypeScript
import type { DatabaseSync } from 'node:sqlite';
|
|
import {
|
|
initializeIxGameForAudit,
|
|
initializeDiscoveryGameForAudit,
|
|
initializeLeaderSkillsGameForAudit,
|
|
initializeFactionExpansionsGameForAudit,
|
|
viewGame,
|
|
type Game,
|
|
} from '../game/engine';
|
|
|
|
export const PROTOTYPE_PROFILES = [
|
|
'ix',
|
|
'discovery',
|
|
'leader-skills',
|
|
'factions',
|
|
] as const;
|
|
export type PrototypeProfile = (typeof PROTOTYPE_PROFILES)[number];
|
|
export function isPrototypeProfile(value: string): value is PrototypeProfile {
|
|
return PROTOTYPE_PROFILES.some((profile) => profile === value);
|
|
}
|
|
|
|
/** Local development only. This cannot create a seat, change its owner, or
|
|
* redeal a started game. The caller backs up the database before invoking it. */
|
|
export function startIxPrototypeRoom(
|
|
db: DatabaseSync,
|
|
code: string,
|
|
expectedVersion: number,
|
|
) {
|
|
return startPrototypeRoom(db, code, expectedVersion, 'ix');
|
|
}
|
|
export function startPrototypeRoom(
|
|
db: DatabaseSync,
|
|
code: string,
|
|
expectedVersion: number,
|
|
profile: PrototypeProfile,
|
|
) {
|
|
if (
|
|
!/^[A-Z0-9]{8}$/.test(code) ||
|
|
!Number.isSafeInteger(expectedVersion) ||
|
|
expectedVersion < 0
|
|
)
|
|
throw new Error('Provide an eight-character room and its current version.');
|
|
const row = db
|
|
.prepare('SELECT state, version FROM rooms WHERE code = ?')
|
|
.get(code);
|
|
if (!row || typeof row.state !== 'string' || row.version !== expectedVersion)
|
|
throw new Error(
|
|
'The room is missing or changed. Refresh before starting the prototype.',
|
|
);
|
|
const initial = JSON.parse(row.state) as Game;
|
|
if (initial.code !== code)
|
|
throw new Error('The room identity is inconsistent.');
|
|
if (!isPrototypeProfile(profile))
|
|
throw new Error('Unknown prototype profile.');
|
|
const game =
|
|
profile === 'ix'
|
|
? initializeIxGameForAudit(initial)
|
|
: profile === 'factions'
|
|
? initializeFactionExpansionsGameForAudit(initial)
|
|
: profile === 'leader-skills'
|
|
? initializeLeaderSkillsGameForAudit(initial)
|
|
: initializeDiscoveryGameForAudit({
|
|
...initial,
|
|
discoveryEnabled: true,
|
|
});
|
|
// Exercise the same player projection before accepting the new saved state.
|
|
for (const player of game.players) viewGame(game, player.id);
|
|
const version = expectedVersion + 1;
|
|
game.version = version;
|
|
const result = db
|
|
.prepare(
|
|
'UPDATE rooms SET state = ?, version = ?, updated_at = ? WHERE code = ? AND version = ? AND state = ?',
|
|
)
|
|
.run(
|
|
JSON.stringify(game),
|
|
version,
|
|
Date.now(),
|
|
code,
|
|
expectedVersion,
|
|
row.state,
|
|
);
|
|
if (result.changes !== 1)
|
|
throw new Error('The lobby changed during setup. No prototype was saved.');
|
|
return {
|
|
code,
|
|
version,
|
|
status: game.status,
|
|
advanced: game.advanced,
|
|
players: game.players.length,
|
|
};
|
|
}
|