A duel server: rooms, seat tokens, ledgers, and resolution when all have moved
Plain HTTP for actions and a websocket that only says "fetch again". A room is its append-only ledger replayed from the top; the engine is deterministic given the seed and the recorded inputs. Each seat sees a view filtered to what the rules let it know. Bot seats are filled at resolution time. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
95a1915cae
commit
8366c9d7ff
11 files changed
+1024
-4
No files matched your search
@@ -0,0 +1,42 @@
|
||||
// One append-only JSONL file per room. A room is its ledger replayed from the
|
||||
// top: the engine is deterministic given the seed and the recorded inputs, so
|
||||
// nothing else needs saving.
|
||||
|
||||
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import type { TurnInput, WizardId } from '../../src/lib/game/state';
|
||||
|
||||
export type LedgerLine =
|
||||
| { t: 'room'; id: string; seats: number; createdAt: number }
|
||||
| { t: 'seat'; id: WizardId; name: string; token: string; bot: boolean }
|
||||
| { t: 'start'; seed: number }
|
||||
| { t: 'turn'; at: number; inputs: Record<WizardId, TurnInput> };
|
||||
|
||||
export class Store {
|
||||
constructor(private dir: string) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
private path(roomId: string): string {
|
||||
return join(this.dir, `${roomId}.jsonl`);
|
||||
}
|
||||
|
||||
append(roomId: string, line: LedgerLine): void {
|
||||
appendFileSync(this.path(roomId), JSON.stringify(line) + '\n');
|
||||
}
|
||||
|
||||
read(roomId: string): LedgerLine[] {
|
||||
const path = this.path(roomId);
|
||||
if (!existsSync(path)) return [];
|
||||
return readFileSync(path, 'utf8')
|
||||
.split('\n')
|
||||
.filter((l) => l.trim())
|
||||
.map((l) => JSON.parse(l) as LedgerLine);
|
||||
}
|
||||
|
||||
roomIds(): string[] {
|
||||
return readdirSync(this.dir)
|
||||
.filter((f) => f.endsWith('.jsonl'))
|
||||
.map((f) => f.slice(0, -'.jsonl'.length));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user