Add an offline Ix initializer through the real setup pipeline, supporting Basic and Advanced play, base factions, Ixians and Tleilaxu with the full 47-card deck and 22-card spice deck. Preserve all normal start and publication gates. Add a reusable local prototype starter: back up the named fresh ready lobby, initialize it once by exact version/state comparison and preserve seat/recovery tables and other rooms. Update the existing checklist with a separate prototype stage and document functional scope, source interpretations and later refinement. Six new tests cover all-profile private setup, exact typed starting forces, Face Dancer/treachery custody, JSON continuation, two fixed-seed complete games and local SQLite write/rejection boundaries. Independent review caught a missing JSON version update, now fixed and covered; focused checking fixed one test typing error before the stable broad check. No new rule interpretation was introduced. Validation: types, lint and 4,211 offline tests passed (123.237s); production build passed (6.543s); all 40 HTTP tests passed (23.578s). Source-bound report: 20260913-ix-prototype-final/report.json, fingerprint 3b8f295593bcb314bb72308d81739006b11d949d1e5906dbf6ce0704b15ebd72. Browser W5LE6VFB: human Ixian + Medium Tleilaxu + Hard Atreides from genuine setup, private starting-card refresh, Weather Control, mobile stronghold placement, then owned Brutal autopilot. Atreides won on turn 1; this short complete journey is not proof of every card or faction interaction. Finished private hand restored at phone width; expanded Poison Tooth was readable. Fixed-seed offline games 2026091301 (Basic) and 2026091302 (Advanced) both completed with both Ix factions. All 64 opening room versions/hashes are unchanged. One browser room and 30 HTTP rooms were added, totaling 95. Reused the running server; no restart or automation. Aggregate goal counters: 59,524,858 to 59,976,567 at completed verification, 451,709 tokens before final Git overhead. Two bounded workhorse helpers handled inventory/tests and independent review; one stable full check/build/HTTP run. Full game scope remains active and unfinished; future work prioritizes missing functional prototypes before exhaustive refinement and final acceptance.
99 lines
3.1 KiB
TypeScript
99 lines
3.1 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { DatabaseSync } from 'node:sqlite';
|
|
import { createGame, joinGame, newPlayer } from '../game/engine';
|
|
import { startIxPrototypeRoom } 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('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,
|
|
);
|
|
}
|
|
});
|