Files
Dune/tools/prototype-room.ts
T
Quintine e9830b6ced Accelerate rules-first feedback and integrate native Strongholds
Apply user priority: save/recovery/privacy/custody assurance after all rules. Add quick types/lint, concise failure-preserving reporter, verbose mode, actual empty-execution guard, signal handling and focused verify name selection; compact canonical context/handoffs without mass moves. Tooling15, six negative CLI, named/empty source-bound smokes,206 doc links; same15-case278B vs2455B,2.35s vs2.36s no compute speedclaim. Native Advanced Ix/CHOAM47-card Stronghold profile: actual firstMentat/HMS/sixeffects,18native/four legalpolicies,five genuine2..6games3129actions/no rejection, human390px LAUDR2WNv14 bank2/personal1 plan and real battle. Earlier162/6513 checks before assurance amendment; finalquick25/build/guide pass. No new preservation/privacy audit or public/deployed/full-mode claim.
2026-10-02 09:30:52 +10:00

165 lines
5.5 KiB
TypeScript

import type { DatabaseSync } from 'node:sqlite';
import {
initializeIxGameForAudit,
initializeDiscoveryGameForAudit,
initializeLeaderSkillsGameForAudit,
initializeSpiceBankerIncomeGameForAudit,
initializeFactionExpansionsGameForAudit,
initializeStrongholdFactionsGameForAudit,
initializeNexusGameForAudit,
initializeMoritaniAssassinateGameForAudit,
initializeEcazTreacheryGameForAudit,
initializeSemutaGameForAudit,
initializeKullGameForAudit,
initializeRicheseBetrayalGameForAudit,
initializeNexusKullGameForAudit,
initializeGuildBetrayalGameForAudit,
initializeIxianNexusReplacementGameForAudit,
initializeIxianNexusBetrayalGameForAudit,
initializeHarkonnenNexusBetrayalGameForAudit,
viewGame,
type Game,
} from '../game/engine';
export const PROTOTYPE_PROFILES = [
'ix',
'discovery',
'leader-skills',
'banker-income',
'factions',
'stronghold-factions',
'nexus',
'moritani-assassinate',
'ecaz-treachery',
'semuta',
'kull',
'richese-betrayal',
'nexus-kull',
'guild-betrayal',
'ixian-replacement',
'ixian-betrayal',
'harkonnen-betrayal',
] 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 === 'stronghold-factions'
? initializeStrongholdFactionsGameForAudit(initial)
: profile === 'ecaz-treachery'
? initializeEcazTreacheryGameForAudit(initial)
: profile === 'moritani-assassinate'
? initializeMoritaniAssassinateGameForAudit(initial)
: profile === 'semuta'
? initializeSemutaGameForAudit(initial)
: profile === 'kull'
? initializeKullGameForAudit(initial)
: profile === 'richese-betrayal'
? initializeRicheseBetrayalGameForAudit({
...initial,
nexusCards: initial.nexusCards ?? { cards: null, phase: null },
})
: profile === 'nexus-kull'
? initializeNexusKullGameForAudit({
...initial,
nexusCards: initial.nexusCards ?? { cards: null, phase: null },
})
: profile === 'guild-betrayal'
? initializeGuildBetrayalGameForAudit({
...initial,
nexusCards: initial.nexusCards ?? { cards: null, phase: null },
})
: profile === 'ixian-replacement'
? initializeIxianNexusReplacementGameForAudit({
...initial,
nexusCards: initial.nexusCards ?? { cards: null, phase: null },
})
: profile === 'ixian-betrayal'
? initializeIxianNexusBetrayalGameForAudit({
...initial,
nexusCards: initial.nexusCards ?? { cards: null, phase: null },
})
: profile === 'harkonnen-betrayal'
? initializeHarkonnenNexusBetrayalGameForAudit({
...initial,
nexusCards: initial.nexusCards ?? { cards: null, phase: null },
})
: profile === 'factions'
? initializeFactionExpansionsGameForAudit(initial)
: profile === 'nexus'
? initializeNexusGameForAudit({
...initial,
nexusCards: initial.nexusCards ?? { cards: null, phase: null },
})
: profile === 'leader-skills'
? initializeLeaderSkillsGameForAudit(initial)
: profile === 'banker-income'
? initializeSpiceBankerIncomeGameForAudit(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,
};
}