Files
Dune/tests/prototype-room.test.ts
T

304 lines
13 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { DatabaseSync } from 'node:sqlite';
import {
createGame,
joinGame,
newPlayer,
viewGame,
type Game,
} from '../game/engine';
import {
startIxPrototypeRoom,
startPrototypeRoom,
} from '../tools/prototype-room';
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);
});