1056 lines
59 KiB
TypeScript
1056 lines
59 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { DatabaseSync } from 'node:sqlite';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { mkdtempSync, rmSync, statSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import {
|
|
applyAction,
|
|
createGame,
|
|
joinGame,
|
|
newPlayer,
|
|
viewGame,
|
|
type Game,
|
|
} from '../game/engine';
|
|
import {
|
|
startIxPrototypeRoom,
|
|
startPrototypeRoom,
|
|
} from '../tools/prototype-room';
|
|
import { NEXUS_FACTIONS } from '../game/nexus-cards';
|
|
import { createLeaderSkills } from '../game/leader-skills';
|
|
import { createStrongholdCards } from '../game/stronghold-cards';
|
|
import { treacheryDeck } from '../game/cards';
|
|
import type { FactionId } from '../game/catalog';
|
|
import { richeseCards } from '../game/richese-cards';
|
|
|
|
function fixture(t: test.TestContext) {
|
|
const db = new DatabaseSync(':memory:');
|
|
t.after(() => db.close());
|
|
db.exec(
|
|
'CREATE TABLE rooms(code TEXT PRIMARY KEY, state TEXT, version INTEGER, updated_at INTEGER); CREATE TABLE seats(room_code TEXT, player_id TEXT, token_hash TEXT);',
|
|
);
|
|
const game = createGame(
|
|
'PROTOTYP',
|
|
newPlayer('i', 'Ix player', 'ixians'),
|
|
true,
|
|
);
|
|
game.expansions = ['ix'];
|
|
joinGame(game, newPlayer('t', 'Tleilaxu player', 'tleilaxu'));
|
|
game.players.forEach((player) => {
|
|
player.ready = true;
|
|
});
|
|
db.prepare('INSERT INTO rooms VALUES(?,?,?,?)').run(
|
|
game.code,
|
|
JSON.stringify(game),
|
|
7,
|
|
100,
|
|
);
|
|
db.prepare('INSERT INTO rooms VALUES(?,?,?,?)').run(
|
|
'KEEPME00',
|
|
'{"unchanged":true}',
|
|
3,
|
|
50,
|
|
);
|
|
db.prepare('INSERT INTO seats VALUES(?,?,?)').run(
|
|
game.code,
|
|
'i',
|
|
'unchanged-private-session',
|
|
);
|
|
return { db, game };
|
|
}
|
|
|
|
void test('Ecaz Treachery preview saves the complete independent inventory before genuine setup once', (t) => {
|
|
const { db } = fixture(t);
|
|
const beforeSeats = db.prepare('SELECT * FROM seats').all();
|
|
const other = db.prepare('SELECT * FROM rooms WHERE code=?').get('KEEPME00');
|
|
const result = startPrototypeRoom(db, 'PROTOTYP', 7, 'ecaz-treachery');
|
|
const row = db.prepare('SELECT state FROM rooms WHERE code=?').get('PROTOTYP')!;
|
|
const g = JSON.parse(row.state as string) as Game;
|
|
assert.equal(result.version, 8);
|
|
assert.equal(g.ecazTreachery, true);
|
|
const cards = [...g.deck, ...g.players.flatMap((p) => p.hand), ...(g.ixSetupCards ?? [])];
|
|
assert.deepEqual(cards.filter((c) => c.id.startsWith('ecaz-')).map((c) => c.id).sort(),
|
|
['ecaz-harass-withdraw', 'ecaz-recruits', 'ecaz-reinforcements']);
|
|
assert.deepEqual(db.prepare('SELECT * FROM seats').all(), beforeSeats);
|
|
assert.deepEqual(db.prepare('SELECT * FROM rooms WHERE code=?').get('KEEPME00'), other);
|
|
const rows = db.prepare('SELECT * FROM rooms').all();
|
|
assert.throws(() => startPrototypeRoom(db, 'PROTOTYP', 8, 'ecaz-treachery'), /cannot redeal/);
|
|
assert.deepEqual(db.prepare('SELECT * FROM rooms').all(), rows);
|
|
});
|
|
|
|
void test('local prototype starts the named ready lobby once, retaining every seat and other room', (t) => {
|
|
const { db } = fixture(t);
|
|
const seats = db.prepare('SELECT * FROM seats').all();
|
|
const other = db
|
|
.prepare('SELECT * FROM rooms WHERE code = ?')
|
|
.get('KEEPME00');
|
|
const result = startIxPrototypeRoom(db, 'PROTOTYP', 7);
|
|
assert.equal(result.version, 8);
|
|
assert.equal(result.status, 'setup');
|
|
const saved = db
|
|
.prepare('SELECT state, version FROM rooms WHERE code = ?')
|
|
.get('PROTOTYP')!;
|
|
assert.equal(JSON.parse(saved.state as string).version, saved.version);
|
|
assert.deepEqual(db.prepare('SELECT * FROM seats').all(), seats);
|
|
assert.deepEqual(
|
|
db.prepare('SELECT * FROM rooms WHERE code = ?').get('KEEPME00'),
|
|
other,
|
|
);
|
|
const rows = db.prepare('SELECT * FROM rooms ORDER BY code').all();
|
|
assert.throws(() => startIxPrototypeRoom(db, 'PROTOTYP', 7), /changed/);
|
|
assert.throws(() => startIxPrototypeRoom(db, 'PROTOTYP', 8), /fresh lobby/);
|
|
assert.deepEqual(db.prepare('SELECT * FROM rooms ORDER BY code').all(), rows);
|
|
});
|
|
|
|
void test('unready, wrong identity and incompatible lobbies fail without writing any room or seat', (t) => {
|
|
const { db, game } = fixture(t);
|
|
for (const mutate of [
|
|
() => {
|
|
game.players[0].ready = false;
|
|
},
|
|
() => {
|
|
game.players[0].ready = true;
|
|
game.code = 'NOTMATCH';
|
|
},
|
|
() => {
|
|
game.code = 'PROTOTYP';
|
|
game.expansions = [];
|
|
},
|
|
]) {
|
|
mutate();
|
|
db.prepare('UPDATE rooms SET state = ? WHERE code = ?').run(
|
|
JSON.stringify(game),
|
|
'PROTOTYP',
|
|
);
|
|
const before = db.prepare('SELECT * FROM rooms ORDER BY code').all();
|
|
assert.throws(() => startIxPrototypeRoom(db, 'PROTOTYP', 7));
|
|
assert.deepEqual(
|
|
db.prepare('SELECT * FROM rooms ORDER BY code').all(),
|
|
before,
|
|
);
|
|
assert.equal(
|
|
db.prepare('SELECT count(*) AS count FROM seats').get()!.count,
|
|
1,
|
|
);
|
|
}
|
|
});
|
|
|
|
void test('Leader Skills prototype saves the real private setup once without changing other rooms or seats', (t) => {
|
|
const { db } = fixture(t);
|
|
const game = createGame(
|
|
'PROTOTYP',
|
|
newPlayer('i', 'Atreides', 'atreides'),
|
|
true,
|
|
);
|
|
joinGame(game, newPlayer('h', 'Harkonnen', 'harkonnen'));
|
|
game.players.forEach((p) => {
|
|
p.ready = true;
|
|
});
|
|
db.prepare('UPDATE rooms SET state = ? WHERE code = ?').run(
|
|
JSON.stringify(game),
|
|
game.code,
|
|
);
|
|
const seats = db.prepare('SELECT * FROM seats').all();
|
|
const other = db
|
|
.prepare('SELECT * FROM rooms WHERE code = ?')
|
|
.get('KEEPME00');
|
|
const result = startPrototypeRoom(db, game.code, 7, 'leader-skills');
|
|
assert.equal(result.version, 8);
|
|
const saved = JSON.parse(
|
|
db.prepare('SELECT state FROM rooms WHERE code = ?').get(game.code)!
|
|
.state as string,
|
|
);
|
|
assert.equal(saved.version, 8);
|
|
assert.equal(saved.setupStage, 'leaderSkills');
|
|
assert.equal(saved.leaderSkills.offers.i.cards.length, 2);
|
|
assert.equal(saved.leaderSkills.offers.h.cards.length, 2);
|
|
assert.equal(saved.players[0].hand.length, 1);
|
|
assert.equal(saved.players[1].hand.length, 2);
|
|
const rooms = db.prepare('SELECT * FROM rooms ORDER BY code').all();
|
|
assert.throws(
|
|
() => startPrototypeRoom(db, game.code, 8, 'leader-skills'),
|
|
/redeal/,
|
|
);
|
|
assert.deepEqual(
|
|
db.prepare('SELECT * FROM rooms ORDER BY code').all(),
|
|
rooms,
|
|
);
|
|
assert.deepEqual(db.prepare('SELECT * FROM seats').all(), seats);
|
|
assert.deepEqual(
|
|
db.prepare('SELECT * FROM rooms WHERE code = ?').get('KEEPME00'),
|
|
other,
|
|
);
|
|
});
|
|
|
|
void test('Nexus prototype initializes all twelve hidden cards once and preserves unrelated saved state', (t) => {
|
|
const { db } = fixture(t);
|
|
const game = createGame(
|
|
'PROTOTYP',
|
|
newPlayer('i', 'Atreides', 'atreides'),
|
|
true,
|
|
);
|
|
joinGame(game, newPlayer('h', 'Harkonnen', 'harkonnen'));
|
|
game.players.forEach((player) => {
|
|
player.ready = true;
|
|
});
|
|
db.prepare('UPDATE rooms SET state = ? WHERE code = ?').run(
|
|
JSON.stringify(game),
|
|
game.code,
|
|
);
|
|
const seats = db.prepare('SELECT * FROM seats').all();
|
|
const other = db
|
|
.prepare('SELECT * FROM rooms WHERE code = ?')
|
|
.get('KEEPME00');
|
|
startPrototypeRoom(db, game.code, 7, 'nexus');
|
|
const saved = JSON.parse(
|
|
db.prepare('SELECT state FROM rooms WHERE code = ?').get(game.code)!
|
|
.state as string,
|
|
) as Game;
|
|
assert.equal(saved.version, 8);
|
|
assert.equal(saved.status, 'setup');
|
|
assert.equal(saved.nexusCards!.cards!.deck.length, 12);
|
|
assert.equal(new Set(saved.nexusCards!.cards!.deck).size, 12);
|
|
for (const player of saved.players) {
|
|
const view = viewGame(saved, player.id);
|
|
assert.equal(view.nexusCards!.card, null);
|
|
assert.equal(view.nexusCards!.deckCount, 12);
|
|
assert.equal('deck' in view.nexusCards!, false);
|
|
}
|
|
const rows = db.prepare('SELECT * FROM rooms ORDER BY code').all();
|
|
assert.throws(() => startPrototypeRoom(db, game.code, 7, 'nexus'), /changed/);
|
|
assert.throws(() => startPrototypeRoom(db, game.code, 8, 'nexus'));
|
|
assert.deepEqual(db.prepare('SELECT * FROM rooms ORDER BY code').all(), rows);
|
|
assert.deepEqual(db.prepare('SELECT * FROM seats').all(), seats);
|
|
assert.deepEqual(
|
|
db.prepare('SELECT * FROM rooms WHERE code = ?').get('KEEPME00'),
|
|
other,
|
|
);
|
|
});
|
|
|
|
void test('Moritani assassination preview starts only the fresh opted-in Advanced profile and preserves seats', (t) => {
|
|
const {db}=fixture(t);
|
|
const game=createGame('PROTOTYP',newPlayer('i','Moritani','moritani'),true,['ecaz']);
|
|
joinGame(game,newPlayer('g','Guild','guild'));for(const p of game.players)p.ready=true;
|
|
db.prepare('UPDATE rooms SET state=? WHERE code=?').run(JSON.stringify(game),game.code);
|
|
const seats=db.prepare('SELECT * FROM seats').all(),other=db.prepare('SELECT * FROM rooms WHERE code=?').get('KEEPME00');
|
|
startPrototypeRoom(db,game.code,7,'moritani-assassinate');
|
|
const saved=JSON.parse(db.prepare('SELECT state FROM rooms WHERE code=?').get(game.code)!.state as string) as Game;
|
|
assert.equal(saved.status,'setup');assert.equal(saved.version,8);assert.equal(saved.advanced,true);
|
|
assert.equal(saved.moritaniAssassinatePreview,true);assert.equal(saved.moritaniAssassinate!.owner,'i');
|
|
assert.deepEqual(saved.moritaniAssassinate!.opportunities,[]);assert.deepEqual(saved.moritaniAssassinateCallEvents,[]);
|
|
for(const p of saved.players)assert.equal('moritaniAssassinatePreview' in viewGame(saved,p.id),false);
|
|
const rows=db.prepare('SELECT * FROM rooms ORDER BY code').all();
|
|
assert.throws(()=>startPrototypeRoom(db,game.code,7,'moritani-assassinate'));
|
|
assert.throws(()=>startPrototypeRoom(db,game.code,8,'moritani-assassinate'));
|
|
assert.deepEqual(db.prepare('SELECT * FROM rooms ORDER BY code').all(),rows);
|
|
assert.deepEqual(db.prepare('SELECT * FROM seats').all(),seats);assert.deepEqual(db.prepare('SELECT * FROM rooms WHERE code=?').get('KEEPME00'),other);
|
|
});
|
|
|
|
void test('Semuta development profile begins only a fresh Richese lobby without changing saved seats', (t) => {
|
|
const { db } = fixture(t);
|
|
const game = createGame('PROTOTYP', newPlayer('r', 'Richese', 'richese'), false, ['choam']);
|
|
joinGame(game, newPlayer('a', 'Atreides', 'atreides'));
|
|
for (const p of game.players) p.ready = true;
|
|
db.prepare('UPDATE rooms SET state=? WHERE code=?').run(JSON.stringify(game), game.code);
|
|
const seats = db.prepare('SELECT * FROM seats').all();
|
|
const other = db.prepare('SELECT * FROM rooms WHERE code=?').get('KEEPME00');
|
|
const result = startPrototypeRoom(db, game.code, 7, 'semuta');
|
|
const saved = JSON.parse(db.prepare('SELECT state FROM rooms WHERE code=?').get(game.code)!.state as string) as Game;
|
|
assert.equal(result.status, 'setup');
|
|
assert.equal(saved.semutaPreview, true);
|
|
assert.ok(saved.richeseCache?.some(card => card.id === 'richese-semuta-drug'));
|
|
const rows = db.prepare('SELECT * FROM rooms ORDER BY code').all();
|
|
assert.throws(() => startPrototypeRoom(db, game.code, 7, 'semuta'));
|
|
assert.throws(() => startPrototypeRoom(db, game.code, 8, 'semuta'));
|
|
assert.deepEqual(db.prepare('SELECT * FROM rooms ORDER BY code').all(), rows);
|
|
assert.deepEqual(db.prepare('SELECT * FROM seats').all(), seats);
|
|
assert.deepEqual(db.prepare('SELECT * FROM rooms WHERE code=?').get('KEEPME00'), other);
|
|
});
|
|
|
|
void test('Kull preview starts a fresh ready CHOAM lobby once and preserves other rooms and seats', (t) => {
|
|
const { db } = fixture(t);
|
|
const game = createGame('PROTOTYP', newPlayer('c', 'CHOAM', 'choam'), false, ['choam', 'ix']);
|
|
joinGame(game, newPlayer('a', 'Atreides', 'atreides'));
|
|
for (const p of game.players) p.ready = true;
|
|
db.prepare('UPDATE rooms SET state=? WHERE code=?').run(JSON.stringify(game), game.code);
|
|
const seats = db.prepare('SELECT * FROM seats').all();
|
|
const other = db.prepare('SELECT * FROM rooms WHERE code=?').get('KEEPME00');
|
|
const result = startPrototypeRoom(db, game.code, 7, 'kull');
|
|
const saved = JSON.parse(db.prepare('SELECT state FROM rooms WHERE code=?').get(game.code)!.state as string) as Game;
|
|
assert.equal(result.status, 'setup');
|
|
assert.equal(saved.kullPreview, true);
|
|
const cards = [...saved.deck, ...saved.players.flatMap(p => p.hand), ...(saved.ixSetupCards ?? [])];
|
|
assert.equal(cards.filter(card => card.id === 'ix-kull-wahad').length, 1);
|
|
const rows = db.prepare('SELECT * FROM rooms ORDER BY code').all();
|
|
assert.throws(() => startPrototypeRoom(db, game.code, 7, 'kull'));
|
|
assert.throws(() => startPrototypeRoom(db, game.code, 8, 'kull'));
|
|
assert.deepEqual(db.prepare('SELECT * FROM rooms ORDER BY code').all(), rows);
|
|
assert.deepEqual(db.prepare('SELECT * FROM seats').all(), seats);
|
|
assert.deepEqual(db.prepare('SELECT * FROM rooms WHERE code=?').get('KEEPME00'), other);
|
|
});
|
|
|
|
void test('Richese Betrayal profile initializes one fresh paired Nexus game and preserves seats and other saves', (t) => {
|
|
const { db } = fixture(t);
|
|
const game = createGame('PROTOTYP', newPlayer('r', 'Richese', 'richese'), false, ['choam']);
|
|
joinGame(game, newPlayer('c', 'CHOAM', 'choam'));
|
|
for (const p of game.players) p.ready = true;
|
|
db.prepare('UPDATE rooms SET state=? WHERE code=?').run(JSON.stringify(game), game.code);
|
|
const seats = db.prepare('SELECT * FROM seats').all();
|
|
const other = db.prepare('SELECT * FROM rooms WHERE code=?').get('KEEPME00');
|
|
const result = startPrototypeRoom(db, game.code, 7, 'richese-betrayal');
|
|
const saved = JSON.parse(db.prepare('SELECT state FROM rooms WHERE code=?').get(game.code)!.state as string) as Game;
|
|
assert.equal(result.status, 'setup');
|
|
assert.equal(saved.richeseBetrayalPreview, true);
|
|
assert.equal(saved.nexusCards!.cards!.deck.length, 12);
|
|
assert.equal(new Set(saved.nexusCards!.cards!.deck).size, 12);
|
|
assert.equal(saved.richeseCache?.length, 10);
|
|
const rows = db.prepare('SELECT * FROM rooms ORDER BY code').all();
|
|
assert.throws(() => startPrototypeRoom(db, game.code, 7, 'richese-betrayal'));
|
|
assert.throws(() => startPrototypeRoom(db, game.code, 8, 'richese-betrayal'));
|
|
assert.deepEqual(db.prepare('SELECT * FROM rooms ORDER BY code').all(), rows);
|
|
assert.deepEqual(db.prepare('SELECT * FROM seats').all(), seats);
|
|
assert.deepEqual(db.prepare('SELECT * FROM rooms WHERE code=?').get('KEEPME00'), other);
|
|
});
|
|
|
|
for (const advanced of [false, true]) {
|
|
void test(`Nexus Kull ${advanced ? 'Advanced' : 'Basic'} entry preserves canonical inventories and cannot redeal a saved game`, (t) => {
|
|
const { db } = fixture(t);
|
|
const game = createGame('PROTOTYP', newPlayer('c', 'CHOAM', 'choam'), advanced, ['choam', 'ix']);
|
|
joinGame(game, newPlayer('a', 'Atreides', 'atreides'));
|
|
for (const p of game.players) p.ready = true;
|
|
db.prepare('UPDATE rooms SET state=? WHERE code=?').run(JSON.stringify(game), game.code);
|
|
const seats = db.prepare('SELECT * FROM seats').all();
|
|
const other = db.prepare('SELECT * FROM rooms WHERE code=?').get('KEEPME00');
|
|
startPrototypeRoom(db, game.code, 7, 'nexus-kull');
|
|
const saved = JSON.parse(db.prepare('SELECT state FROM rooms WHERE code=?').get(game.code)!.state as string) as Game;
|
|
assert.equal(saved.status, 'setup');
|
|
assert.equal(saved.nexusKullPreview, true);
|
|
assert.equal(saved.kullPreview, true);
|
|
assert.deepEqual([...saved.nexusCards!.cards!.deck].sort((a,b) => a.localeCompare(b)),
|
|
[...NEXUS_FACTIONS].sort((a,b) => a.localeCompare(b)));
|
|
const cards = [...saved.deck, ...saved.players.flatMap(p => p.hand), ...(saved.ixSetupCards ?? [])];
|
|
assert.equal(cards.filter(card => card.id === 'ix-kull-wahad').length, 1);
|
|
const rows = db.prepare('SELECT * FROM rooms ORDER BY code').all();
|
|
assert.throws(() => startPrototypeRoom(db, game.code, 7, 'nexus-kull'));
|
|
assert.throws(() => startPrototypeRoom(db, game.code, 8, 'nexus-kull'));
|
|
assert.deepEqual(db.prepare('SELECT * FROM rooms ORDER BY code').all(), rows);
|
|
assert.deepEqual(db.prepare('SELECT * FROM seats').all(), seats);
|
|
assert.deepEqual(db.prepare('SELECT * FROM rooms WHERE code=?').get('KEEPME00'), other);
|
|
});
|
|
}
|
|
|
|
for (const advanced of [false, true]) {
|
|
for (const homeworlds of [false, true]) {
|
|
void test(`Guild Betrayal ${advanced ? 'Advanced' : 'Basic'} ${homeworlds ? 'Homeworld' : 'base'} entry preserves explicit modules and rejects redealing without touching other saves`, (t) => {
|
|
const { db } = fixture(t);
|
|
let game = createGame('PROTOTYP', newPlayer('g', 'Guild', 'guild'), advanced);
|
|
joinGame(game, newPlayer('e', 'Emperor', 'emperor'));
|
|
if (homeworlds)
|
|
game = applyAction(game, game.host, { type: 'homeworlds', enabled: true });
|
|
for (const p of game.players) p.ready = true;
|
|
db.prepare('UPDATE rooms SET state=? WHERE code=?').run(JSON.stringify(game), game.code);
|
|
db.prepare('UPDATE seats SET player_id=? WHERE room_code=?').run('g', game.code);
|
|
db.prepare('INSERT INTO seats VALUES(?,?,?)').run(game.code, 'e', 'unchanged-emperor-session');
|
|
const seats = db.prepare('SELECT * FROM seats').all();
|
|
const other = db.prepare('SELECT * FROM rooms WHERE code=?').get('KEEPME00');
|
|
const result = startPrototypeRoom(db, game.code, 7, 'guild-betrayal');
|
|
const saved = JSON.parse(db.prepare('SELECT state FROM rooms WHERE code=?').get(game.code)!.state as string) as Game;
|
|
assert.equal(result.version, 8);
|
|
assert.equal(saved.status, 'setup');
|
|
assert.equal(saved.guildBetrayalPreview, true);
|
|
assert.equal(!!saved.homeworlds, homeworlds);
|
|
assert.deepEqual([...saved.nexusCards!.cards!.deck].sort((a,b) => a.localeCompare(b)),
|
|
[...NEXUS_FACTIONS].sort((a,b) => a.localeCompare(b)));
|
|
assert.deepEqual(saved.expansions, []);
|
|
for (const p of game.players)
|
|
assert.equal(saved.players.find(seat => seat.id === p.id)!.name, p.name);
|
|
const rows = db.prepare('SELECT * FROM rooms ORDER BY code').all();
|
|
assert.throws(() => startPrototypeRoom(db, game.code, 7, 'guild-betrayal'));
|
|
assert.throws(() => startPrototypeRoom(db, game.code, 8, 'guild-betrayal'));
|
|
assert.deepEqual(db.prepare('SELECT * FROM rooms ORDER BY code').all(), rows);
|
|
assert.deepEqual(db.prepare('SELECT * FROM seats').all(), seats);
|
|
assert.deepEqual(db.prepare('SELECT * FROM rooms WHERE code=?').get('KEEPME00'), other);
|
|
});
|
|
}
|
|
}
|
|
|
|
for (const advanced of [false, true]) {
|
|
void test(`Ixian Nexus replacement ${advanced ? 'Advanced' : 'Basic'} starts only the exact fresh classic lobby without changing seats or native options`, t => {
|
|
const { db } = fixture(t);
|
|
const game = createGame('PROTOTYP', newPlayer('i', 'Atreides', 'atreides'), advanced);
|
|
joinGame(game, newPlayer('e', 'Emperor', 'emperor'));
|
|
for (const p of game.players) p.ready = true;
|
|
db.prepare('UPDATE rooms SET state=? WHERE code=?').run(JSON.stringify(game), game.code);
|
|
const seats = db.prepare('SELECT * FROM seats').all();
|
|
const other = db.prepare('SELECT * FROM rooms WHERE code=?').get('KEEPME00');
|
|
const lobbyRows = db.prepare('SELECT * FROM rooms ORDER BY code').all();
|
|
assert.throws(() => startPrototypeRoom(db, game.code, 6, 'ixian-replacement'));
|
|
assert.deepEqual(db.prepare('SELECT * FROM rooms ORDER BY code').all(), lobbyRows);
|
|
const result = startPrototypeRoom(db, game.code, 7, 'ixian-replacement');
|
|
const saved = JSON.parse(db.prepare('SELECT state FROM rooms WHERE code=?').get(game.code)!.state as string) as Game;
|
|
assert.equal(result.version, 8);
|
|
assert.equal(saved.status, 'setup');
|
|
assert.equal(saved.code, game.code);
|
|
assert.equal(saved.host, game.host);
|
|
assert.equal(saved.advanced, advanced);
|
|
assert.deepEqual(saved.expansions, game.expansions);
|
|
assert.deepEqual(saved.playerPositions, game.playerPositions);
|
|
assert.deepEqual(saved.players.map(p => [p.id, p.name, p.faction]),
|
|
game.players.map(p => [p.id, p.name, p.faction]));
|
|
assert.equal(saved.nexusIxianReplacementPreview, true);
|
|
assert.deepEqual([...saved.nexusCards!.cards!.deck].sort(), [...NEXUS_FACTIONS].sort());
|
|
for (const p of saved.players) {
|
|
const view = viewGame(saved, p.id);
|
|
assert.equal(view.nexusIxianReplacementPreview, true);
|
|
assert.equal(view.nexusCards!.card, null);
|
|
assert.equal(Object.hasOwn(view.nexusCards!, 'deck'), false);
|
|
}
|
|
const startedRows = db.prepare('SELECT * FROM rooms ORDER BY code').all();
|
|
assert.throws(() => startPrototypeRoom(db, game.code, 8, 'ixian-replacement'));
|
|
assert.deepEqual(db.prepare('SELECT * FROM rooms ORDER BY code').all(), startedRows);
|
|
assert.deepEqual(db.prepare('SELECT * FROM seats').all(), seats);
|
|
assert.deepEqual(db.prepare('SELECT * FROM rooms WHERE code=?').get('KEEPME00'), other);
|
|
});
|
|
}
|
|
|
|
void test('Ixian Nexus replacement rejects unready, expansion, native Ixian and Homeworld lobbies without reconfiguring them', t => {
|
|
const { db } = fixture(t);
|
|
const classic = createGame('PROTOTYP', newPlayer('i', 'Atreides', 'atreides'), true);
|
|
joinGame(classic, newPlayer('e', 'Emperor', 'emperor'));
|
|
for (const p of classic.players) p.ready = true;
|
|
const unready = structuredClone(classic);
|
|
unready.players[0].ready = false;
|
|
const expansion = structuredClone(classic);
|
|
expansion.expansions = ['ix'];
|
|
const native = createGame('PROTOTYP', newPlayer('i', 'Ixians', 'ixians'), true, ['ix']);
|
|
joinGame(native, newPlayer('e', 'Emperor', 'emperor'));
|
|
for (const p of native.players) p.ready = true;
|
|
const homeworld = applyAction(classic, classic.host, { type: 'homeworlds', enabled: true });
|
|
for (const p of homeworld.players) p.ready = true;
|
|
for (const game of [unready, expansion, native, homeworld]) {
|
|
db.prepare('UPDATE rooms SET state=? WHERE code=?').run(JSON.stringify(game), game.code);
|
|
const rooms = db.prepare('SELECT * FROM rooms ORDER BY code').all();
|
|
const seats = db.prepare('SELECT * FROM seats').all();
|
|
assert.throws(() => startPrototypeRoom(db, game.code, 7, 'ixian-replacement'));
|
|
assert.deepEqual(db.prepare('SELECT * FROM rooms ORDER BY code').all(), rooms);
|
|
assert.deepEqual(db.prepare('SELECT * FROM seats').all(), seats);
|
|
}
|
|
});
|
|
|
|
void test('Ixian replacement CLI takes a private exact-version backup before starting and cannot overwrite a started game or prior proof', t => {
|
|
const directory = mkdtempSync(join(tmpdir(), 'dune-ixian-replacement-'));
|
|
t.after(() => rmSync(directory, { recursive: true, force: true }));
|
|
const database = join(directory, 'rooms.sqlite');
|
|
const db = new DatabaseSync(database);
|
|
db.exec('CREATE TABLE rooms(code TEXT PRIMARY KEY,state TEXT,version INTEGER,updated_at INTEGER); CREATE TABLE seats(room_code TEXT,player_id TEXT,token_hash TEXT);');
|
|
const game = createGame('PROTOTYP', newPlayer('a', 'Atreides', 'atreides'), true);
|
|
joinGame(game, newPlayer('e', 'Emperor', 'emperor'));
|
|
for (const p of game.players) p.ready = true;
|
|
db.prepare('INSERT INTO rooms VALUES(?,?,?,?)').run(game.code, JSON.stringify(game), 7, 100);
|
|
const other = structuredClone(game);
|
|
other.code = 'KEEPGAME';
|
|
db.prepare('INSERT INTO rooms VALUES(?,?,?,?)').run(other.code, JSON.stringify(other), 9, 50);
|
|
startPrototypeRoom(db, other.code, 9, 'ixian-replacement');
|
|
for (const p of game.players)
|
|
db.prepare('INSERT INTO seats VALUES(?,?,?)').run(game.code, p.id, 'isolated-test-' + p.id);
|
|
const before = db.prepare('SELECT * FROM rooms ORDER BY code').all();
|
|
const seats = db.prepare('SELECT * FROM seats ORDER BY player_id').all();
|
|
db.close();
|
|
const output = join(directory, 'private-proof');
|
|
const invoke = (version: number, out: string) => spawnSync(process.execPath, [
|
|
'--import', 'tsx', fileURLToPath(new URL('../tools/start-prototype.ts', import.meta.url)),
|
|
'--profile', 'ixian-replacement', '--db', database, '--room', game.code,
|
|
'--version', String(version), '--out', out,
|
|
], { encoding: 'utf8', timeout: 120000 });
|
|
const started = invoke(7, output);
|
|
assert.equal(started.status, 0, started.stderr);
|
|
assert.equal(statSync(output).mode & 0o777, 0o700);
|
|
for (const name of ['games.sqlite', 'snapshot.json', 'prototype.json'])
|
|
assert.equal(statSync(join(output, name)).mode & 0o777, 0o600);
|
|
const backup = new DatabaseSync(join(output, 'games.sqlite'), { readOnly: true });
|
|
assert.deepEqual(backup.prepare('SELECT * FROM rooms ORDER BY code').all(), before);
|
|
assert.deepEqual(backup.prepare('SELECT * FROM seats ORDER BY player_id').all(), seats);
|
|
backup.close();
|
|
const current = new DatabaseSync(database, { readOnly: true });
|
|
t.after(() => current.close());
|
|
const after = current.prepare('SELECT * FROM rooms ORDER BY code').all();
|
|
assert.deepEqual(current.prepare('SELECT * FROM rooms WHERE code=?').get(other.code), before.find(row => row.code === other.code));
|
|
assert.deepEqual(current.prepare('SELECT * FROM seats ORDER BY player_id').all(), seats);
|
|
const savedRow = current.prepare('SELECT state,version FROM rooms WHERE code=?').get(game.code)!;
|
|
assert.equal(savedRow.version, 8);
|
|
const saved = JSON.parse(savedRow.state as string) as Game;
|
|
assert.equal(saved.nexusIxianReplacementPreview, true);
|
|
assert.equal(saved.status, 'setup');
|
|
assert.equal(saved.advanced, game.advanced);
|
|
assert.deepEqual(saved.playerPositions, game.playerPositions);
|
|
assert.deepEqual(saved.players.map(p => [p.id,p.name,p.faction]), game.players.map(p => [p.id,p.name,p.faction]));
|
|
assert.notEqual(invoke(8, output).status, 0, 'existing proof directory cannot be reused');
|
|
assert.notEqual(invoke(7, join(directory, 'stale-proof')).status, 0, 'stale backup version cannot be admitted');
|
|
assert.notEqual(invoke(8, join(directory, 'redeal-proof')).status, 0, 'current version cannot redeal a started game');
|
|
assert.deepEqual(current.prepare('SELECT * FROM rooms ORDER BY code').all(), after);
|
|
assert.deepEqual(current.prepare('SELECT * FROM seats ORDER BY player_id').all(), seats);
|
|
});
|
|
|
|
for (const advanced of [false, true]) {
|
|
for (const seats of [2, 3, 4, 6]) {
|
|
void test(`Ixian Betrayal fresh ${advanced ? 'Advanced' : 'Basic'} ${seats}-seat entry keeps original identity and undealt Nexus`, t => {
|
|
const { db } = fixture(t);
|
|
const game = createGame('PROTOTYP', newPlayer('i', 'Native Ixians', 'ixians'), advanced, ['ix']);
|
|
const factions = ['atreides', 'emperor', 'guild', 'tleilaxu', 'fremen'] as const;
|
|
for (let index = 0; index < seats - 1; index++)
|
|
joinGame(game, newPlayer(`p${index}`, `Original ${index}`, factions[index]));
|
|
for (const p of game.players) p.ready = true;
|
|
db.prepare('UPDATE rooms SET state=? WHERE code=?').run(JSON.stringify(game), game.code);
|
|
const credentials = db.prepare('SELECT * FROM seats').all();
|
|
const other = db.prepare('SELECT * FROM rooms WHERE code=?').get('KEEPME00');
|
|
const before = db.prepare('SELECT * FROM rooms ORDER BY code').all();
|
|
assert.throws(() => startPrototypeRoom(db, game.code, 6, 'ixian-betrayal'));
|
|
assert.deepEqual(db.prepare('SELECT * FROM rooms ORDER BY code').all(), before);
|
|
const started = startPrototypeRoom(db, game.code, 7, 'ixian-betrayal');
|
|
const saved = JSON.parse(db.prepare('SELECT state FROM rooms WHERE code=?').get(game.code)!.state as string) as Game;
|
|
assert.equal(started.version, 8);
|
|
assert.equal(saved.version, 8);
|
|
assert.equal(saved.status, 'setup');
|
|
assert.equal(saved.code, game.code);
|
|
assert.equal(saved.host, game.host);
|
|
assert.equal(saved.advanced, game.advanced);
|
|
assert.deepEqual(saved.expansions, game.expansions);
|
|
assert.deepEqual(saved.playerPositions, game.playerPositions);
|
|
assert.deepEqual(saved.players.map(p => [p.id, p.name, p.faction]), game.players.map(p => [p.id, p.name, p.faction]));
|
|
assert.equal(saved.nexusIxianBetrayalPreview, true);
|
|
const physical = [...saved.deck, ...saved.discard, ...saved.players.flatMap(p => p.hand), ...(saved.ixSetupCards ?? [])];
|
|
assert.equal(physical.length, 47);
|
|
assert.equal(new Set(physical.map(card => card.id)).size, 47);
|
|
assert.deepEqual([...saved.nexusCards!.cards!.deck].sort(), [...NEXUS_FACTIONS].sort());
|
|
assert.deepEqual(saved.nexusCards!.cards!.discard, []);
|
|
for (const p of saved.players) {
|
|
assert.equal(saved.nexusCards!.cards!.hands[p.id], null);
|
|
const view = viewGame(saved, p.id);
|
|
assert.equal(view.nexusIxianBetrayalPreview, true);
|
|
assert.equal(view.nexusIxianBetrayalReaction, null);
|
|
assert.equal(view.nexusCards!.card, null);
|
|
assert.equal(Object.hasOwn(view.nexusCards!, 'deck'), false);
|
|
}
|
|
const after = db.prepare('SELECT * FROM rooms ORDER BY code').all();
|
|
assert.throws(() => startPrototypeRoom(db, game.code, 8, 'ixian-betrayal'));
|
|
assert.throws(() => startPrototypeRoom(db, game.code, 8, 'ix'));
|
|
assert.deepEqual(db.prepare('SELECT * FROM rooms ORDER BY code').all(), after);
|
|
assert.deepEqual(db.prepare('SELECT * FROM seats').all(), credentials);
|
|
assert.deepEqual(db.prepare('SELECT * FROM rooms WHERE code=?').get('KEEPME00'), other);
|
|
});
|
|
}
|
|
}
|
|
|
|
void test('Ixian Betrayal rejects incompatible original lobbies and public start without durable changes', t => {
|
|
const { db } = fixture(t);
|
|
const native = createGame('PROTOTYP', newPlayer('i', 'Ixians', 'ixians'), true, ['ix']);
|
|
joinGame(native, newPlayer('a', 'Atreides', 'atreides'));
|
|
for (const p of native.players) p.ready = true;
|
|
const alterations: ((g: Game) => void)[] = [
|
|
g => { g.players[0].ready = false; },
|
|
g => { g.code = 'NOTMATCH'; },
|
|
g => { g.players[0].faction = 'emperor'; },
|
|
g => { g.players[1].faction = 'ixians'; },
|
|
g => { g.players[1].faction = 'choam'; },
|
|
g => { g.players[1].faction = 'richese'; },
|
|
g => { g.players[1].faction = 'ecaz'; },
|
|
g => { g.players[1].faction = 'moritani'; },
|
|
g => { g.expansions = []; },
|
|
g => { g.expansions = ['ix', 'choam']; },
|
|
g => { g.expansions = ['ix', 'ecaz']; },
|
|
g => { g.discoveryEnabled = true; },
|
|
g => { g.ecazTreachery = true; },
|
|
g => { g.semutaPreview = true; },
|
|
g => { g.nexusIxianReplacementPreview = true; },
|
|
g => { g.nexusIxianBetrayalPreview = true; },
|
|
g => { g.kullPreview = true; },
|
|
g => { g.nexusKullPreview = true; },
|
|
g => { g.guildBetrayalPreview = true; },
|
|
g => { g.richeseBetrayalPreview = true; },
|
|
g => { g.moritaniAssassinatePreview = true; },
|
|
g => { g.leaderSkills = createLeaderSkills(() => 0.5); },
|
|
g => { g.players.pop(); },
|
|
g => { g.players.push(...Array.from({ length: 5 }, (_, index) => newPlayer(`extra${index}`, `Extra ${index}`, 'guild'))); },
|
|
];
|
|
const modules = ['homeworlds', 'techTokens', 'strongholdCards'].map(type => {
|
|
const game = applyAction(native, native.host, { type, enabled: true });
|
|
for (const p of game.players) p.ready = true;
|
|
return game;
|
|
});
|
|
const candidates = alterations.map(change => {
|
|
const game = structuredClone(native);
|
|
change(game);
|
|
return game;
|
|
}).concat(modules);
|
|
for (const game of candidates) {
|
|
db.prepare('UPDATE rooms SET state=? WHERE code=?').run(JSON.stringify(game), native.code);
|
|
const before = db.prepare('SELECT * FROM rooms ORDER BY code').all();
|
|
const credentials = db.prepare('SELECT * FROM seats').all();
|
|
const changes = db.prepare('SELECT total_changes() AS changes').get()!.changes;
|
|
assert.throws(() => startPrototypeRoom(db, native.code, 7, 'ixian-betrayal'));
|
|
assert.deepEqual(db.prepare('SELECT * FROM rooms ORDER BY code').all(), before);
|
|
assert.deepEqual(db.prepare('SELECT * FROM seats').all(), credentials);
|
|
assert.equal(db.prepare('SELECT total_changes() AS changes').get()!.changes, changes);
|
|
}
|
|
const original = structuredClone(native);
|
|
for (const action of [{ type: 'start' }, { type: 'start', advancedPreview: true }])
|
|
assert.throws(() => applyAction(native, native.host, action));
|
|
assert.deepEqual(native, original);
|
|
});
|
|
|
|
void test('Ixian Betrayal CLI preserves a private consistent exact-version backup and never overwrites proof or resets a started room', t => {
|
|
const directory = mkdtempSync(join(tmpdir(), 'dune-ixian-betrayal-'));
|
|
t.after(() => rmSync(directory, { recursive: true, force: true }));
|
|
const database = join(directory, 'rooms.sqlite');
|
|
const db = new DatabaseSync(database);
|
|
db.exec('CREATE TABLE rooms(code TEXT PRIMARY KEY,state TEXT,version INTEGER,updated_at INTEGER); CREATE TABLE seats(room_code TEXT,player_id TEXT,token_hash TEXT);');
|
|
const game = createGame('PROTOTYP', newPlayer('i', 'Native Ixians', 'ixians'), true, ['ix']);
|
|
joinGame(game, newPlayer('a', 'Atreides', 'atreides'));
|
|
joinGame(game, newPlayer('e', 'Emperor', 'emperor'));
|
|
for (const p of game.players) p.ready = true;
|
|
db.prepare('INSERT INTO rooms VALUES(?,?,?,?)').run(game.code, JSON.stringify(game), 7, 100);
|
|
const other = createGame('KEEPGAME', newPlayer('a', 'Atreides', 'atreides'), true);
|
|
joinGame(other, newPlayer('e', 'Emperor', 'emperor'));
|
|
for (const p of other.players) p.ready = true;
|
|
db.prepare('INSERT INTO rooms VALUES(?,?,?,?)').run(other.code, JSON.stringify(other), 9, 50);
|
|
startPrototypeRoom(db, other.code, 9, 'ixian-replacement');
|
|
for (const p of game.players)
|
|
db.prepare('INSERT INTO seats VALUES(?,?,?)').run(game.code, p.id, 'isolated-test-' + p.id);
|
|
const before = db.prepare('SELECT * FROM rooms ORDER BY code').all();
|
|
const credentials = db.prepare('SELECT * FROM seats ORDER BY player_id').all();
|
|
db.close();
|
|
const output = join(directory, 'private-proof');
|
|
const invoke = (version: number, out: string) => spawnSync(process.execPath, [
|
|
'--import', 'tsx', fileURLToPath(new URL('../tools/start-prototype.ts', import.meta.url)),
|
|
'--profile', 'ixian-betrayal', '--db', database, '--room', game.code,
|
|
'--version', String(version), '--out', out,
|
|
], { encoding: 'utf8', timeout: 120000 });
|
|
const started = invoke(7, output);
|
|
assert.equal(started.status, 0, started.stderr);
|
|
assert.equal(statSync(output).mode & 0o777, 0o700);
|
|
for (const name of ['games.sqlite', 'snapshot.json', 'prototype.json'])
|
|
assert.equal(statSync(join(output, name)).mode & 0o777, 0o600);
|
|
const backup = new DatabaseSync(join(output, 'games.sqlite'), { readOnly: true });
|
|
assert.deepEqual(backup.prepare('SELECT * FROM rooms ORDER BY code').all(), before);
|
|
assert.deepEqual(backup.prepare('SELECT * FROM seats ORDER BY player_id').all(), credentials);
|
|
backup.close();
|
|
const current = new DatabaseSync(database, { readOnly: true });
|
|
t.after(() => current.close());
|
|
const after = current.prepare('SELECT * FROM rooms ORDER BY code').all();
|
|
assert.deepEqual(current.prepare('SELECT * FROM rooms WHERE code=?').get(other.code), before.find(row => row.code === other.code));
|
|
assert.deepEqual(current.prepare('SELECT * FROM seats ORDER BY player_id').all(), credentials);
|
|
const savedRow = current.prepare('SELECT state,version FROM rooms WHERE code=?').get(game.code)!;
|
|
assert.equal(savedRow.version, 8);
|
|
const saved = JSON.parse(savedRow.state as string) as Game;
|
|
assert.equal(saved.nexusIxianBetrayalPreview, true);
|
|
assert.equal(saved.status, 'setup');
|
|
assert.equal(saved.advanced, game.advanced);
|
|
assert.deepEqual(saved.expansions, game.expansions);
|
|
assert.deepEqual(saved.playerPositions, game.playerPositions);
|
|
assert.deepEqual(saved.players.map(p => [p.id, p.name, p.faction]), game.players.map(p => [p.id, p.name, p.faction]));
|
|
assert.notEqual(invoke(8, output).status, 0);
|
|
assert.notEqual(invoke(7, join(directory, 'stale-proof')).status, 0);
|
|
assert.notEqual(invoke(8, join(directory, 'redeal-proof')).status, 0);
|
|
assert.deepEqual(current.prepare('SELECT * FROM rooms ORDER BY code').all(), after);
|
|
assert.deepEqual(current.prepare('SELECT * FROM seats ORDER BY player_id').all(), credentials);
|
|
});
|
|
|
|
function harkonnenLobby(advanced = true, seats = 3) {
|
|
const game = createGame('PROTOTYP', newPlayer('h', 'Native Harkonnen', 'harkonnen'), advanced);
|
|
const factions = ['atreides', 'emperor', 'guild', 'fremen', 'beneGesserit'] as const;
|
|
for (let index = 0; index < seats - 1; index++)
|
|
joinGame(game, newPlayer(`p${index}`, `Original ${index}`, factions[index]));
|
|
for (const player of game.players) player.ready = true;
|
|
return game;
|
|
}
|
|
|
|
for (const advanced of [false, true]) {
|
|
for (const seats of [2, 3, 4, 5, 6]) {
|
|
void test(`Harkonnen Betrayal ${advanced ? 'Advanced' : 'Basic'} ${seats}-seat entry preserves seats and leaves all Nexus physically undealt`, t => {
|
|
const { db } = fixture(t);
|
|
const lobby = harkonnenLobby(advanced, seats);
|
|
db.prepare('UPDATE rooms SET state=? WHERE code=?').run(JSON.stringify(lobby), lobby.code);
|
|
const credentials = db.prepare('SELECT * FROM seats').all();
|
|
const other = db.prepare('SELECT * FROM rooms WHERE code=?').get('KEEPME00');
|
|
const before = db.prepare('SELECT * FROM rooms ORDER BY code').all();
|
|
assert.throws(() => startPrototypeRoom(db, lobby.code, 6, 'harkonnen-betrayal'));
|
|
assert.deepEqual(db.prepare('SELECT * FROM rooms ORDER BY code').all(), before);
|
|
const result = startPrototypeRoom(db, lobby.code, 7, 'harkonnen-betrayal');
|
|
const saved = JSON.parse(db.prepare('SELECT state FROM rooms WHERE code=?').get(lobby.code)!.state as string) as Game;
|
|
assert.equal(result.version, 8);
|
|
assert.equal(saved.version, 8);
|
|
assert.equal(saved.status, 'setup');
|
|
assert.equal(saved.nexusHarkonnenBetrayalPreview, true);
|
|
assert.equal(saved.advanced, advanced);
|
|
assert.equal(saved.host, lobby.host);
|
|
assert.deepEqual(saved.expansions, []);
|
|
assert.deepEqual(saved.playerPositions, lobby.playerPositions);
|
|
assert.deepEqual(saved.players.map(p => [p.id, p.name, p.faction]), lobby.players.map(p => [p.id, p.name, p.faction]));
|
|
const stock = [...saved.deck, ...saved.discard, ...saved.players.flatMap(p => p.hand)];
|
|
assert.equal(stock.length, 33);
|
|
assert.equal(new Set(stock.map(card => card.id)).size, 33);
|
|
assert.deepEqual([...saved.nexusCards!.cards!.deck].sort(), [...NEXUS_FACTIONS].sort());
|
|
assert.deepEqual(saved.nexusCards!.cards!.discard, []);
|
|
for (const player of saved.players) {
|
|
assert.equal(saved.nexusCards!.cards!.hands[player.id], null);
|
|
const view = viewGame(saved, player.id);
|
|
assert.equal(view.nexusHarkonnenBetrayalReaction, null);
|
|
assert.equal(view.nexusCards!.card, null);
|
|
assert.equal(Object.hasOwn(view.nexusCards!, 'deck'), false);
|
|
}
|
|
const started = db.prepare('SELECT * FROM rooms ORDER BY code').all();
|
|
assert.throws(() => startPrototypeRoom(db, lobby.code, 8, 'harkonnen-betrayal'));
|
|
assert.throws(() => startPrototypeRoom(db, lobby.code, 8, 'nexus'));
|
|
assert.deepEqual(db.prepare('SELECT * FROM rooms ORDER BY code').all(), started);
|
|
assert.deepEqual(db.prepare('SELECT * FROM seats').all(), credentials);
|
|
assert.deepEqual(db.prepare('SELECT * FROM rooms WHERE code=?').get('KEEPME00'), other);
|
|
});
|
|
}
|
|
}
|
|
|
|
void test('Harkonnen Betrayal rejects incompatible lobbies, overlays and ordinary public starts without changing durable records', t => {
|
|
const { db } = fixture(t);
|
|
const native = harkonnenLobby();
|
|
const changes: ((game: Game) => void)[] = [
|
|
game => { game.players[0].ready = false; },
|
|
game => { game.code = 'NOTMATCH'; },
|
|
game => { game.players[0].faction = 'fremen'; },
|
|
game => { game.players[1].faction = 'harkonnen'; },
|
|
...(['ixians', 'tleilaxu', 'choam', 'richese', 'ecaz', 'moritani'] as const).map(faction =>
|
|
(game: Game) => { game.players[1].faction = faction; }),
|
|
...(['ix', 'choam', 'ecaz'] as const).map(expansion =>
|
|
(game: Game) => { game.expansions = [expansion]; }),
|
|
game => { game.discoveryEnabled = true; },
|
|
game => { game.ecazTreachery = true; },
|
|
game => { game.semutaPreview = true; },
|
|
game => { game.nexusIxianReplacementPreview = true; },
|
|
game => { game.nexusIxianBetrayalPreview = true; },
|
|
game => { game.nexusHarkonnenBetrayalPreview = true; },
|
|
game => { game.kullPreview = true; },
|
|
game => { game.nexusKullPreview = true; },
|
|
game => { game.guildBetrayalPreview = true; },
|
|
game => { game.richeseBetrayalPreview = true; },
|
|
game => { game.moritaniAssassinatePreview = true; },
|
|
game => { game.leaderSkills = createLeaderSkills(() => 0.5); },
|
|
game => { game.players.splice(1); },
|
|
game => { game.players.push(...Array.from({ length: 4 }, (_, index) => newPlayer(`extra${index}`, `Extra ${index}`, 'guild'))); },
|
|
];
|
|
const modules = ['homeworlds', 'techTokens', 'strongholdCards'].map(type => {
|
|
const game = applyAction(native, native.host, { type, enabled: true });
|
|
for (const player of game.players) player.ready = true;
|
|
return game;
|
|
});
|
|
for (const game of [...changes.map(change => {
|
|
const game = structuredClone(native);
|
|
change(game);
|
|
return game;
|
|
}), ...modules]) {
|
|
db.prepare('UPDATE rooms SET state=? WHERE code=?').run(JSON.stringify(game), native.code);
|
|
const rooms = db.prepare('SELECT * FROM rooms ORDER BY code').all();
|
|
const seats = db.prepare('SELECT * FROM seats').all();
|
|
assert.throws(() => startPrototypeRoom(db, native.code, 7, 'harkonnen-betrayal'));
|
|
assert.deepEqual(db.prepare('SELECT * FROM rooms ORDER BY code').all(), rooms);
|
|
assert.deepEqual(db.prepare('SELECT * FROM seats').all(), seats);
|
|
}
|
|
const original = structuredClone(native);
|
|
for (const action of [{ type: 'start', profile: 'harkonnen-betrayal' }, { type: 'initializeHarkonnenNexusBetrayalGameForAudit' }])
|
|
assert.throws(() => applyAction(native, native.host, action));
|
|
assert.deepEqual(native, original);
|
|
});
|
|
|
|
void test('Harkonnen Betrayal CLI backs up every room and seat privately before the exact fresh-version start and cannot redeal or overwrite proof', t => {
|
|
const directory = mkdtempSync(join(tmpdir(), 'dune-harkonnen-betrayal-'));
|
|
t.after(() => rmSync(directory, { recursive: true, force: true }));
|
|
const database = join(directory, 'rooms.sqlite');
|
|
const db = new DatabaseSync(database);
|
|
db.exec('CREATE TABLE rooms(code TEXT PRIMARY KEY,state TEXT,version INTEGER,updated_at INTEGER); CREATE TABLE seats(room_code TEXT,player_id TEXT,token_hash TEXT);');
|
|
const lobby = harkonnenLobby();
|
|
db.prepare('INSERT INTO rooms VALUES(?,?,?,?)').run(lobby.code, JSON.stringify(lobby), 7, 100);
|
|
const other = harkonnenLobby(false, 2);
|
|
other.code = 'KEEPGAME';
|
|
db.prepare('INSERT INTO rooms VALUES(?,?,?,?)').run(other.code, JSON.stringify(other), 9, 50);
|
|
startPrototypeRoom(db, other.code, 9, 'harkonnen-betrayal');
|
|
for (const player of lobby.players)
|
|
db.prepare('INSERT INTO seats VALUES(?,?,?)').run(lobby.code, player.id, 'isolated-test-' + player.id);
|
|
const before = db.prepare('SELECT * FROM rooms ORDER BY code').all();
|
|
const seats = db.prepare('SELECT * FROM seats ORDER BY player_id').all();
|
|
db.close();
|
|
const output = join(directory, 'private-proof');
|
|
const invoke = (version: number, out: string) => spawnSync(process.execPath, [
|
|
'--import', 'tsx', fileURLToPath(new URL('../tools/start-prototype.ts', import.meta.url)),
|
|
'--profile', 'harkonnen-betrayal', '--db', database, '--room', lobby.code,
|
|
'--version', String(version), '--out', out,
|
|
], { encoding: 'utf8', timeout: 120000 });
|
|
const result = invoke(7, output);
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.equal(statSync(output).mode & 0o777, 0o700);
|
|
for (const name of ['games.sqlite', 'snapshot.json', 'prototype.json'])
|
|
assert.equal(statSync(join(output, name)).mode & 0o777, 0o600);
|
|
const backup = new DatabaseSync(join(output, 'games.sqlite'), { readOnly: true });
|
|
assert.deepEqual(backup.prepare('SELECT * FROM rooms ORDER BY code').all(), before);
|
|
assert.deepEqual(backup.prepare('SELECT * FROM seats ORDER BY player_id').all(), seats);
|
|
backup.close();
|
|
const current = new DatabaseSync(database, { readOnly: true });
|
|
t.after(() => current.close());
|
|
const after = current.prepare('SELECT * FROM rooms ORDER BY code').all();
|
|
assert.deepEqual(current.prepare('SELECT * FROM rooms WHERE code=?').get(other.code), before.find(row => row.code === other.code));
|
|
assert.deepEqual(current.prepare('SELECT * FROM seats ORDER BY player_id').all(), seats);
|
|
const row = current.prepare('SELECT state,version FROM rooms WHERE code=?').get(lobby.code)!;
|
|
const saved = JSON.parse(row.state as string) as Game;
|
|
assert.equal(row.version, 8);
|
|
assert.equal(saved.nexusHarkonnenBetrayalPreview, true);
|
|
assert.equal(saved.status, 'setup');
|
|
assert.deepEqual(saved.players.map(p => [p.id, p.name, p.faction]), lobby.players.map(p => [p.id, p.name, p.faction]));
|
|
assert.notEqual(invoke(8, output).status, 0);
|
|
assert.notEqual(invoke(7, join(directory, 'stale-proof')).status, 0);
|
|
assert.notEqual(invoke(8, join(directory, 'redeal-proof')).status, 0);
|
|
assert.deepEqual(current.prepare('SELECT * FROM rooms ORDER BY code').all(), after);
|
|
assert.deepEqual(current.prepare('SELECT * FROM seats ORDER BY player_id').all(), seats);
|
|
});
|
|
|
|
void test('Leader Skills rejects invalid or already-dealt lobbies and forged public activation without changing saved rooms or credentials', t => {
|
|
const { db } = fixture(t);
|
|
const native = harkonnenLobby();
|
|
const changes: ((game: Game) => void)[] = [
|
|
game => { game.players[0].ready = false; },
|
|
game => { game.code = 'NOTMATCH'; },
|
|
game => { game.players[1].faction = game.players[0].faction; },
|
|
game => { game.leaderSkills = createLeaderSkills(() => 0.5); },
|
|
game => { game.players.splice(1); },
|
|
game => { game.players.push(...Array.from({ length: 4 }, (_, index) => newPlayer(`extra${index}`, `Extra ${index}`, 'guild'))); },
|
|
];
|
|
for (const game of changes.map(change => {
|
|
const game = structuredClone(native);
|
|
change(game);
|
|
return game;
|
|
})) {
|
|
db.prepare('UPDATE rooms SET state=? WHERE code=?').run(JSON.stringify(game), native.code);
|
|
const rooms = db.prepare('SELECT * FROM rooms ORDER BY code').all();
|
|
const credentials = db.prepare('SELECT * FROM seats').all();
|
|
assert.throws(() => startPrototypeRoom(db, native.code, 7, 'leader-skills'));
|
|
assert.deepEqual(db.prepare('SELECT * FROM rooms ORDER BY code').all(), rooms);
|
|
assert.deepEqual(db.prepare('SELECT * FROM seats').all(), credentials);
|
|
}
|
|
const original = structuredClone(native);
|
|
for (const action of [{ type: 'start', profile: 'banker-income' }, { type: 'initializeSpiceBankerIncomeGameForAudit' },
|
|
{ type: 'spiceBankerIncome', enabled: true }])
|
|
assert.throws(() => applyAction(native, native.host, action));
|
|
assert.deepEqual(native, original);
|
|
});
|
|
|
|
void test('Leader Skills CLI requires a fresh exact-version private backup and preserves native setup, old rooms and credentials', t => {
|
|
const directory = mkdtempSync(join(tmpdir(), 'dune-leader-skills-'));
|
|
t.after(() => rmSync(directory, { recursive: true, force: true }));
|
|
const database = join(directory, 'rooms.sqlite');
|
|
const db = new DatabaseSync(database);
|
|
db.exec('CREATE TABLE rooms(code TEXT PRIMARY KEY,state TEXT,version INTEGER,updated_at INTEGER); CREATE TABLE seats(room_code TEXT,player_id TEXT,token_hash TEXT);');
|
|
const lobby = harkonnenLobby(true, 6);
|
|
db.prepare('INSERT INTO rooms VALUES(?,?,?,?)').run(lobby.code, JSON.stringify(lobby), 7, 100);
|
|
const other = harkonnenLobby(false, 2);
|
|
other.code = 'KEEPGAME';
|
|
db.prepare('INSERT INTO rooms VALUES(?,?,?,?)').run(other.code, JSON.stringify(other), 9, 50);
|
|
startPrototypeRoom(db, other.code, 9, 'leader-skills');
|
|
for (const player of lobby.players)
|
|
db.prepare('INSERT INTO seats VALUES(?,?,?)').run(lobby.code, player.id, 'isolated-test-' + player.id);
|
|
const before = db.prepare('SELECT * FROM rooms ORDER BY code').all();
|
|
const credentials = db.prepare('SELECT * FROM seats ORDER BY player_id').all();
|
|
db.close();
|
|
const output = join(directory, 'private-proof');
|
|
const invoke = (version: number, out: string) => spawnSync(process.execPath, [
|
|
'--import', 'tsx', fileURLToPath(new URL('../tools/start-prototype.ts', import.meta.url)),
|
|
'--profile', 'leader-skills', '--db', database, '--room', lobby.code,
|
|
'--version', String(version), '--out', out,
|
|
], { encoding: 'utf8', timeout: 120000 });
|
|
const result = invoke(7, output);
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.equal(statSync(output).mode & 0o777, 0o700);
|
|
for (const name of ['games.sqlite', 'snapshot.json', 'prototype.json'])
|
|
assert.equal(statSync(join(output, name)).mode & 0o777, 0o600);
|
|
const backup = new DatabaseSync(join(output, 'games.sqlite'), { readOnly: true });
|
|
assert.deepEqual(backup.prepare('SELECT * FROM rooms ORDER BY code').all(), before);
|
|
assert.deepEqual(backup.prepare('SELECT * FROM seats ORDER BY player_id').all(), credentials);
|
|
backup.close();
|
|
const current = new DatabaseSync(database, { readOnly: true });
|
|
t.after(() => current.close());
|
|
const after = current.prepare('SELECT * FROM rooms ORDER BY code').all();
|
|
assert.deepEqual(current.prepare('SELECT * FROM rooms WHERE code=?').get(other.code), before.find(row => row.code === other.code));
|
|
assert.deepEqual(current.prepare('SELECT * FROM seats ORDER BY player_id').all(), credentials);
|
|
const row = current.prepare('SELECT state,version FROM rooms WHERE code=?').get(lobby.code)!;
|
|
const saved = JSON.parse(row.state as string) as Game;
|
|
assert.equal(row.version, 8);
|
|
assert.equal(saved.spiceBankerIncomePreview, true);
|
|
assert.equal(saved.status, 'setup');
|
|
assert.equal(saved.setupStage, 'prediction');
|
|
assert.deepEqual(saved.players.map(p => [p.id, p.name, p.faction]), lobby.players.map(p => [p.id, p.name, p.faction]));
|
|
assert.deepEqual(saved.players.map(p => p.hand), lobby.players.map(p => p.hand));
|
|
assert.deepEqual(saved.leaderSkills!.offers, {});
|
|
assert.notEqual(invoke(8, output).status, 0);
|
|
assert.notEqual(invoke(7, join(directory, 'stale-proof')).status, 0);
|
|
assert.notEqual(invoke(8, join(directory, 'redeal-proof')).status, 0);
|
|
assert.deepEqual(current.prepare('SELECT * FROM rooms ORDER BY code').all(), after);
|
|
assert.deepEqual(current.prepare('SELECT * FROM seats ORDER BY player_id').all(), credentials);
|
|
});
|
|
|
|
function strongholdFactionsLobby(
|
|
count = 3,
|
|
roster: readonly FactionId[] = ['ixians', 'choam', 'atreides', 'harkonnen', 'emperor', 'guild'],
|
|
expansions = ['ix', 'choam'],
|
|
) {
|
|
const game = createGame('PROTOTYP', newPlayer('i', roster[0], roster[0]), true, expansions);
|
|
for (let index = 1; index < count; index++)
|
|
joinGame(game, newPlayer('seat-' + index, roster[index], roster[index]));
|
|
game.players.forEach(player => { player.ready = true; });
|
|
return game;
|
|
}
|
|
|
|
void test('Stronghold factions admits only fresh exact-version native Advanced lobbies without changing seats or other rooms', t => {
|
|
const { db } = fixture(t);
|
|
const families: { roster: FactionId[]; expansions: string[]; minimum?: number }[] = [
|
|
{ roster: ['ixians', 'choam', 'atreides', 'harkonnen', 'emperor', 'guild'], expansions: ['ix', 'choam'] },
|
|
{ roster: ['ixians', 'tleilaxu', 'atreides', 'harkonnen', 'emperor', 'guild'], expansions: ['ix'] },
|
|
{ roster: ['choam', 'richese', 'atreides', 'harkonnen', 'emperor', 'guild'], expansions: ['choam'] },
|
|
{ roster: ['ixians', 'tleilaxu', 'choam', 'richese', 'emperor', 'guild'], expansions: ['ix', 'choam'], minimum: 4 },
|
|
...(['ixians', 'tleilaxu', 'choam', 'richese'] as const).map(faction => ({
|
|
roster: [faction, 'guild', 'atreides', 'harkonnen', 'emperor', 'fremen'] as FactionId[],
|
|
expansions: [faction === 'ixians' || faction === 'tleilaxu' ? 'ix' : 'choam'],
|
|
})),
|
|
];
|
|
for (const { family, count } of families.flatMap(family =>
|
|
[2, 3, 4, 5, 6].filter(count => count >= (family.minimum ?? 2)).map(count => ({ family, count })))) {
|
|
const lobby = strongholdFactionsLobby(count, family.roster, family.expansions);
|
|
const original = structuredClone(lobby);
|
|
db.prepare('UPDATE rooms SET state=?,version=7 WHERE code=?').run(JSON.stringify(lobby), lobby.code);
|
|
const seats = db.prepare('SELECT * FROM seats').all();
|
|
const other = db.prepare('SELECT * FROM rooms WHERE code=?').get('KEEPME00');
|
|
const before = db.prepare('SELECT * FROM rooms ORDER BY code').all();
|
|
assert.throws(() => startPrototypeRoom(db, lobby.code, 6, 'stronghold-factions'));
|
|
assert.deepEqual(db.prepare('SELECT * FROM rooms ORDER BY code').all(), before);
|
|
startPrototypeRoom(db, lobby.code, 7, 'stronghold-factions');
|
|
const row = db.prepare('SELECT state,version FROM rooms WHERE code=?').get(lobby.code)!;
|
|
const saved = JSON.parse(row.state as string) as Game;
|
|
assert.equal(saved.version, 8);
|
|
assert.equal(row.version, 8);
|
|
assert.equal(saved.status, 'setup');
|
|
assert.equal(saved.advanced, true);
|
|
assert.deepEqual(saved.expansions, family.expansions);
|
|
assert.deepEqual(saved.players.map(p => [p.id, p.name, p.faction]),
|
|
original.players.map(p => [p.id, p.name, p.faction]));
|
|
assert.deepEqual(saved.playerPositions, original.playerPositions);
|
|
const cards = [...saved.deck, ...saved.discard, ...(saved.ixSetupCards ?? []), ...saved.players.flatMap(p => p.hand)];
|
|
assert.deepEqual(cards.map(card => card.id).sort(), treacheryDeck(family.expansions).map(card => card.id).sort());
|
|
assert.equal(new Set(cards.map(card => card.id)).size, family.expansions.includes('ix') ? 47 : 35);
|
|
const cache = family.roster.includes('richese') ? richeseCards().map(card => card.id).sort() : [];
|
|
assert.deepEqual((saved.richeseCache ?? []).map(card => card.id).sort(), cache);
|
|
assert.deepEqual(saved.strongholdCards, createStrongholdCards());
|
|
const ix = saved.players.find(p => p.faction === 'ixians');
|
|
if (ix) assert.equal((ix.elites?.reserves ?? 0) + (ix.elites?.tanks ?? 0) +
|
|
Object.values(ix.elites?.forces ?? {}).reduce((sum, amount) => sum + amount, 0), 7);
|
|
const choam = saved.players.find(p => p.faction === 'choam');
|
|
if (choam) assert.equal(choam.leaders.filter(leader => leader.id === 'choam-auditor').length, 1);
|
|
for (const p of saved.players) {
|
|
const view = viewGame(saved, p.id);
|
|
assert.deepEqual(view.strongholdCards, saved.strongholdCards);
|
|
assert.equal('deck' in view, false);
|
|
for (const otherPlayer of view.players)
|
|
if (otherPlayer.id !== p.id) assert.equal(otherPlayer.hand, undefined);
|
|
}
|
|
assert.deepEqual(db.prepare('SELECT * FROM seats').all(), seats);
|
|
assert.deepEqual(db.prepare('SELECT * FROM rooms WHERE code=?').get('KEEPME00'), other);
|
|
const started = db.prepare('SELECT * FROM rooms ORDER BY code').all();
|
|
assert.throws(() => startPrototypeRoom(db, lobby.code, 8, 'stronghold-factions'));
|
|
assert.deepEqual(db.prepare('SELECT * FROM rooms ORDER BY code').all(), started);
|
|
}
|
|
});
|
|
|
|
void test('Stronghold faction admission rejects incompatible modules, history, ownership and public retrofit immutably', t => {
|
|
const { db } = fixture(t);
|
|
const native = strongholdFactionsLobby();
|
|
const variants: ((game: Game) => void)[] = [
|
|
game => { game.advanced = false; },
|
|
game => { game.expansions = ['ix']; },
|
|
game => { game.expansions = ['ix', 'choam', 'ecaz']; },
|
|
game => { game.expansions = ['choam']; },
|
|
game => { game.expansions = ['ix', 'choam', 'ix']; },
|
|
game => { game.expansions = []; },
|
|
game => { game.players[2].faction = 'ecaz'; },
|
|
game => { game.players[0].faction = 'guild'; game.players[1].faction = 'fremen'; },
|
|
game => { game.players[0].ready = false; },
|
|
game => { game.turn = 2; },
|
|
game => { game.players[0].battleLosses = 1; },
|
|
game => { game.strongholdCards = createStrongholdCards(); game.strongholdCards.owners.arrakeen = game.host; },
|
|
game => { game.strongholdCards = createStrongholdCards(); game.strongholdCards.claimedTurn = 1; },
|
|
game => { game.nexusCards = { cards: null, phase: null }; },
|
|
game => { game.leaderSkills = createLeaderSkills(() => 0); },
|
|
game => { game.homeworlds = { custody: null }; },
|
|
];
|
|
for (const mutate of variants) {
|
|
const game = structuredClone(native);
|
|
mutate(game);
|
|
db.prepare('UPDATE rooms SET state=?,version=7 WHERE code=?').run(JSON.stringify(game), native.code);
|
|
const before = db.prepare('SELECT * FROM rooms ORDER BY code').all();
|
|
const seats = db.prepare('SELECT * FROM seats').all();
|
|
assert.throws(() => startPrototypeRoom(db, native.code, 7, 'stronghold-factions'));
|
|
assert.deepEqual(db.prepare('SELECT * FROM rooms ORDER BY code').all(), before);
|
|
assert.deepEqual(db.prepare('SELECT * FROM seats').all(), seats);
|
|
}
|
|
const original = structuredClone(native);
|
|
for (const action of [{ type: 'start', profile: 'stronghold-factions' },
|
|
{ type: 'initializeStrongholdFactionsGameForAudit' }, { type: 'strongholdFactions', enabled: true }])
|
|
assert.throws(() => applyAction(native, native.host, action));
|
|
assert.deepEqual(native, original);
|
|
});
|
|
|
|
void test('Stronghold factions CLI privately backs up the exact original native lobby and cannot reset or overwrite proof', t => {
|
|
const directory = mkdtempSync(join(tmpdir(), 'dune-stronghold-factions-'));
|
|
t.after(() => rmSync(directory, { recursive: true, force: true }));
|
|
const database = join(directory, 'rooms.sqlite');
|
|
const db = new DatabaseSync(database);
|
|
db.exec('CREATE TABLE rooms(code TEXT PRIMARY KEY,state TEXT,version INTEGER,updated_at INTEGER); CREATE TABLE seats(room_code TEXT,player_id TEXT,token_hash TEXT);');
|
|
const lobby = strongholdFactionsLobby(6);
|
|
db.prepare('INSERT INTO rooms VALUES(?,?,?,?)').run(lobby.code, JSON.stringify(lobby), 7, 100);
|
|
db.prepare('INSERT INTO rooms VALUES(?,?,?,?)').run('KEEPGAME', JSON.stringify(harkonnenLobby(false, 2)), 9, 50);
|
|
for (const p of lobby.players)
|
|
db.prepare('INSERT INTO seats VALUES(?,?,?)').run(lobby.code, p.id, 'private-native-session-' + p.id);
|
|
const before = db.prepare('SELECT * FROM rooms ORDER BY code').all();
|
|
const credentials = db.prepare('SELECT * FROM seats ORDER BY player_id').all();
|
|
db.close();
|
|
const output = join(directory, 'private-proof');
|
|
const invoke = (version: number, out: string) => spawnSync(process.execPath, [
|
|
'--import', 'tsx', fileURLToPath(new URL('../tools/start-prototype.ts', import.meta.url)),
|
|
'--profile', 'stronghold-factions', '--db', database, '--room', lobby.code,
|
|
'--version', String(version), '--out', out,
|
|
], { encoding: 'utf8', timeout: 120000 });
|
|
const result = invoke(7, output);
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.equal(statSync(output).mode & 0o777, 0o700);
|
|
for (const name of ['games.sqlite', 'snapshot.json', 'prototype.json'])
|
|
assert.equal(statSync(join(output, name)).mode & 0o777, 0o600);
|
|
const backup = new DatabaseSync(join(output, 'games.sqlite'), { readOnly: true });
|
|
assert.deepEqual(backup.prepare('SELECT * FROM rooms ORDER BY code').all(), before);
|
|
assert.deepEqual(backup.prepare('SELECT * FROM seats ORDER BY player_id').all(), credentials);
|
|
backup.close();
|
|
const current = new DatabaseSync(database, { readOnly: true });
|
|
t.after(() => current.close());
|
|
const after = current.prepare('SELECT * FROM rooms ORDER BY code').all();
|
|
assert.deepEqual(current.prepare('SELECT * FROM rooms WHERE code=?').get('KEEPGAME'), before.find(row => row.code === 'KEEPGAME'));
|
|
assert.deepEqual(current.prepare('SELECT * FROM seats ORDER BY player_id').all(), credentials);
|
|
const row = current.prepare('SELECT state,version FROM rooms WHERE code=?').get(lobby.code)!;
|
|
const saved = JSON.parse(row.state as string) as Game;
|
|
assert.equal(row.version, 8);
|
|
assert.equal(saved.version, 8);
|
|
assert.equal(saved.status, 'setup');
|
|
assert.deepEqual(saved.players.map(p => [p.id, p.name, p.faction]),
|
|
lobby.players.map(p => [p.id, p.name, p.faction]));
|
|
assert.deepEqual(saved.playerPositions, lobby.playerPositions);
|
|
assert.deepEqual(saved.strongholdCards, createStrongholdCards());
|
|
assert.deepEqual([...saved.deck, ...(saved.ixSetupCards ?? []), ...saved.players.flatMap(p => p.hand)].map(card => card.id).sort(),
|
|
treacheryDeck(['ix', 'choam']).map(card => card.id).sort());
|
|
assert.notEqual(invoke(8, output).status, 0);
|
|
assert.notEqual(invoke(7, join(directory, 'stale-proof')).status, 0);
|
|
assert.notEqual(invoke(8, join(directory, 'redeal-proof')).status, 0);
|
|
assert.deepEqual(current.prepare('SELECT * FROM rooms ORDER BY code').all(), after);
|
|
assert.deepEqual(current.prepare('SELECT * FROM seats ORDER BY player_id').all(), credentials);
|
|
});
|