Files
Dune/tests/discovery-recovery.test.ts

248 lines
9.2 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { unitStore } from './fixture-nexus-room-store';
import {
discoveryFixture,
enterDiscoveryCollection,
putDiscovery,
} from './fixture-discovery';
import { applyAction, viewGame, type Game } from '../game/engine';
import { revealDiscoveryToken } from '../game/discoveries';
import { discoveryStashSignature } from '../game/discovery-actions';
import { startPrototypeRoom } from '../tools/prototype-room';
import type { RoomsClock } from '../db/rooms';
const clock: RoomsClock = { now: () => 10000, sleep: async () => {} };
void test('SQLite Discovery prototype preserves seats and competing stash reveals pay once; saved overflow discard resumes exactly once', async () => {
const store = unitStore();
try {
const made = await store.rooms.createRoom(
'Discovery SQL',
'atreides',
false,
[],
),
code = made.view.code;
const tokens = [made.token];
for (const faction of ['guild', 'fremen'] as const)
tokens.push((await store.rooms.joinRoom(code, faction, faction)).token!);
const auths = await Promise.all(
tokens.map((token) => store.rooms.authenticate(code, token)),
);
const ids = auths.map((auth) => auth.playerId) as [string, string, string];
let lobby = await store.rooms.readRoom(code);
for (const p of lobby.players)
lobby = applyAction(lobby, p.id, { type: 'ready' });
store.sqlite
.prepare('UPDATE rooms SET state = ? WHERE code = ?')
.run(JSON.stringify(lobby), code);
const seats = store.sqlite
.prepare('SELECT * FROM seats ORDER BY player_id')
.all();
const begun = startPrototypeRoom(
store.sqlite,
code,
lobby.version,
'discovery',
);
assert.equal(begun.status, 'setup');
assert.equal(begun.version, lobby.version + 1);
assert.throws(() =>
startPrototypeRoom(store.sqlite, code, begun.version, 'discovery'),
);
assert.deepEqual(
store.sqlite.prepare('SELECT * FROM seats ORDER BY player_id').all(),
seats,
);
let g = discoveryFixture(false, ids);
g.code = code;
g.version = begun.version;
const stash = enterDiscoveryCollection(g, 'spice-stash');
g = applyAction(g, ids[0], {
type: 'discovery',
token: stash.id,
reveal: false,
});
store.sqlite
.prepare('UPDATE rooms SET state = ?, version = ? WHERE code = ?')
.run(JSON.stringify(g), g.version, code);
let arrivals = 0,
release!: () => void;
const gate = new Promise<void>((resolve) => {
release = resolve;
});
store.hooks.beforeWrite = async () => {
if (++arrivals === 2) release();
await gate;
};
const timer = setTimeout(release, 2000);
const action = { type: 'discovery', token: stash.id, reveal: true };
try {
const results = await Promise.allSettled(
[0, 1].map(() =>
store.restart().act(code, auths[0], g.version, action, clock),
),
);
assert.equal(arrivals, 2);
assert.equal(results.filter((r) => r.status === 'fulfilled').length, 1);
} finally {
clearTimeout(timer);
delete store.hooks.beforeWrite;
}
const done = await store.restart().readRoom(code);
assert.equal(done.players[0].spice, g.players[0].spice + 7);
assert.equal(done.version, g.version + 1);
await assert.rejects(
store.restart().act(code, auths[0], done.version, action, clock),
);
for (const [i, token] of tokens.entries()) {
const rooms = store.restart(),
auth = await rooms.authenticate(code, token);
assert.deepEqual(
await rooms.readSeatView(code, auth),
viewGame(done, ids[i]),
);
}
g = done;
const cardToken = enterDiscoveryCollection(g, 'treachery-card-stash');
while (g.players[0].hand.length < 4)
g.players[0].hand.push(g.deck.shift()!);
g = applyAction(g, ids[0], {
type: 'discovery',
token: cardToken.id,
reveal: false,
});
g = applyAction(g, ids[0], {
type: 'discovery',
token: cardToken.id,
reveal: true,
});
store.sqlite
.prepare('UPDATE rooms SET state = ? WHERE code = ?')
.run(JSON.stringify(g), code);
const restored = await store.restart().readRoom(code);
assert.equal(restored.players[0].hand.length, 5);
assert.equal(restored.decision?.kind, 'discoveryDiscard');
const discard = restored.players[0].hand.at(-1)!;
await store
.restart()
.act(
code,
auths[0],
restored.version,
{
type: 'decision',
event: restored.discoveryStash!.event,
card: discard.id,
},
clock,
);
const finished = await store.restart().readRoom(code);
assert.equal(finished.discoveryStash!.stage, 'complete');
assert.equal(finished.players[0].hand.length, 4);
const paused = JSON.parse(JSON.stringify(finished)) as Game,
record = paused.discoveryStash!;
record.stage = 'discard';
record.signature = discoveryStashSignature(record);
const sequence = paused.treacheryDiscardSequence!;
paused.resolvedTreacheryDiscardSequence = sequence - 1;
paused.pendingTreacheryDiscard = {
sequence,
batch: {
event: `discard:${paused.turn}:${paused.phase}:${sequence}`,
turn: paused.turn,
phase: paused.phase,
cause: 'discovery:stash',
entries: [{ card: discard, discardedBy: ids[0], publicFace: true }],
},
continuation: {
kind: 'discoveryStash',
event: record.event,
owner: ids[0],
card: discard.id,
},
};
store.sqlite
.prepare('UPDATE rooms SET state = ? WHERE code = ?')
.run(JSON.stringify(paused), code);
await store.restart().continueRoomAutomatic(code, clock);
const resumed = await store.restart().readRoom(code);
assert.equal(resumed.discoveryStash!.stage, 'complete');
assert.deepEqual(resumed.players, finished.players);
assert.deepEqual(resumed.deck, finished.deck);
assert.deepEqual(resumed.discard, finished.discard);
const settled = JSON.stringify(resumed);
await store.restart().continueRoomAutomatic(code, clock);
assert.equal(JSON.stringify(await store.restart().readRoom(code)), settled);
assert.deepEqual(
store.sqlite.prepare('SELECT * FROM seats ORDER BY player_id').all(),
seats,
);
} finally {
store.sqlite.close();
}
});
void test('competing saved Collection commits transfer Orgiz spice exactly once', async () => {
const store = unitStore();
try {
const made = await store.rooms.createRoom('Orgiz SQL', 'atreides', false, []);
const code = made.view.code;
const tokens = [made.token];
for (const faction of ['guild', 'fremen'] as const)
tokens.push((await store.rooms.joinRoom(code, faction, faction)).token!);
const auths = await Promise.all(tokens.map(token => store.rooms.authenticate(code, token)));
const ids = auths.map(auth => auth.playerId) as [string, string, string];
const g = discoveryFixture(false, ids);
g.code = code;
g.version = (await store.rooms.readRoom(code)).version;
const token = putDiscovery(g, 'orgiz-processing-station');
g.discoveries = revealDiscoveryToken(g.discoveries!, token.id, g.turn).state;
Object.assign(g, {
phase: 5, active: ids[2], movementRemaining: [ids[2]],
phaseOpening: null, response: null, decision: null,
storm: 18, stormPending: null,
spice: { 'hagga_basin:12': 4 },
});
for (const player of g.players)
Object.assign(player, {
forces: {}, reserves: 20, shipped: true, moved: 1,
});
g.players[0].forces = { 'orgiz-processing-station:0': 1 };
g.players[0].reserves = 19;
g.players[1].forces = { 'hagga_basin:12': 1 };
g.players[1].reserves = 19;
const beforeOwner = g.players[0].spice, beforeCollector = g.players[1].spice;
store.sqlite.prepare('UPDATE rooms SET state = ?, version = ? WHERE code = ?')
.run(JSON.stringify(g), g.version, code);
let arrivals = 0, release!: () => void;
const gate = new Promise<void>(resolve => { release = resolve; });
store.hooks.beforeWrite = async () => {
if (++arrivals === 2) release();
await gate;
};
const timer = setTimeout(release, 2000);
try {
const results = await Promise.allSettled([0, 1].map(() =>
store.restart().act(code, auths[2], g.version, { type: 'endMovement' }, clock)));
assert.equal(arrivals, 2);
assert.equal(results.filter(result => result.status === 'fulfilled').length, 1);
} finally {
clearTimeout(timer);
delete store.hooks.beforeWrite;
}
const done = await store.restart().readRoom(code);
assert.equal(done.version, g.version + 1);
assert.equal(done.players[0].spice, beforeOwner + 1);
assert.equal(done.players[1].spice, beforeCollector + 1);
assert.equal(done.spice['hagga_basin:12'], 2);
assert.equal(done.log.filter(entry => entry.automatic?.name === 'Orgiz Processing Station').length, 1);
const settled = JSON.stringify(done);
await store.restart().continueRoomAutomatic(code, clock);
assert.equal(JSON.stringify(await store.restart().readRoom(code)), settled);
} finally {
store.sqlite.close();
}
});