The template's room server guards a stale room once, by name; the test's tick helper is named for what it waits on; the port list and the hall's tally lose a set that was always empty
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CCLa2PaEeCj8njQZSWGqHx
This commit is contained in:
1 parent
62cb00be33
commit
75428b5a9b
4 files changed
+15
-13
No files matched your search
@@ -18,7 +18,7 @@ service user, /opt/<slug> and /var/lib/<slug> trees, access log
|
|||||||
small Node process capped at 300 MB by its unit file, and a room is a few
|
small Node process capped at 300 MB by its unit file, and a room is a few
|
||||||
kilobytes of ledger, so the host carries several. Each game needs a port
|
kilobytes of ledger, so the host carries several. Each game needs a port
|
||||||
of its own on the host: wizwar 8787, waving-hands 8788, hnefatafl 8789,
|
of its own on the host: wizwar 8787, waving-hands 8788, hnefatafl 8789,
|
||||||
dip 8790.
|
dip 8790, wyrm 8791.
|
||||||
|
|
||||||
## Current production
|
## Current production
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ afterEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/** The bots move once the request that made their move due has been answered: after this tick. */
|
/** The bots move once the request that made their move due has been answered: after this tick. */
|
||||||
const settled = () => new Promise<void>((r) => setImmediate(r));
|
const answered = () => new Promise<void>((r) => setImmediate(r));
|
||||||
|
|
||||||
/** A table of the demo game: Ann, who opened it, Bo, and Cy when asked for, the game begun. */
|
/** A table of the demo game: Ann, who opened it, Bo, and Cy when asked for, the game begun. */
|
||||||
function table(spec: GameSpec<State, Input> = game, withCy = false) {
|
function table(spec: GameSpec<State, Input> = game, withCy = false) {
|
||||||
@@ -182,7 +182,7 @@ describe('rooms', () => {
|
|||||||
const cy = rooms.join(host.room, 'Cy');
|
const cy = rooms.join(host.room, 'Cy');
|
||||||
const room = host.room;
|
const room = host.room;
|
||||||
rooms.begin(room, host.token);
|
rooms.begin(room, host.token);
|
||||||
await settled();
|
await answered();
|
||||||
rooms.say(room, host.token, 'Psst.', ['B']);
|
rooms.say(room, host.token, 'Psst.', ['B']);
|
||||||
rooms.say(room, cy.token, 'Hello, all.');
|
rooms.say(room, cy.token, 'Hello, all.');
|
||||||
const annSees = rooms.view(room, 'A').chat;
|
const annSees = rooms.view(room, 'A').chat;
|
||||||
@@ -192,7 +192,7 @@ describe('rooms', () => {
|
|||||||
expect(rooms.view(room, '').chat.map((l) => l.text)).toEqual(['Hello, all.']);
|
expect(rooms.view(room, '').chat.map((l) => l.text)).toEqual(['Hello, all.']);
|
||||||
rooms.submit(room, host.token, { pick: 1 }, 0);
|
rooms.submit(room, host.token, { pick: 1 }, 0);
|
||||||
rooms.submit(room, cy.token, { pick: 2 }, 0);
|
rooms.submit(room, cy.token, { pick: 2 }, 0);
|
||||||
await settled();
|
await answered();
|
||||||
expect(heard[0]).toEqual(['Round 1.', 'Psst.', 'I hear you.', 'Hello, all.', 'I hear you.']);
|
expect(heard[0]).toEqual(['Round 1.', 'Psst.', 'I hear you.', 'Hello, all.', 'I hear you.']);
|
||||||
expect(room.chat.filter((l) => l.note === 'secret').map((l) => l.text)).toEqual(['Round 1.', 'Round 2.']);
|
expect(room.chat.filter((l) => l.note === 'secret').map((l) => l.text)).toEqual(['Round 1.', 'Round 2.']);
|
||||||
expect(rooms.view(room, 'A').parting).toEqual([]);
|
expect(rooms.view(room, 'A').parting).toEqual([]);
|
||||||
@@ -254,13 +254,13 @@ describe('the host and the other seats', () => {
|
|||||||
rooms.submit(room, ann, { pick: 5 }, 0);
|
rooms.submit(room, ann, { pick: 5 }, 0);
|
||||||
rooms.standIn(room, ann, 'B');
|
rooms.standIn(room, ann, 'B');
|
||||||
// The round waited on Bo alone; the bot plays it as soon as the host has been answered.
|
// The round waited on Bo alone; the bot plays it as soon as the host has been answered.
|
||||||
await settled();
|
await answered();
|
||||||
expect(room.resolved).toBe(1);
|
expect(room.resolved).toBe(1);
|
||||||
expect(rooms.waitingOn(room)).toEqual(['A']);
|
expect(rooms.waitingOn(room)).toEqual(['A']);
|
||||||
expect(() => rooms.seatOf(room, bo)).toThrow(/gave your seat to a bot/);
|
expect(() => rooms.seatOf(room, bo)).toThrow(/gave your seat to a bot/);
|
||||||
expect(rooms.view(room, 'A').seats[1]).toEqual({ id: 'B', name: 'Bo', bot: true, standIn: true });
|
expect(rooms.view(room, 'A').seats[1]).toEqual({ id: 'B', name: 'Bo', bot: true, standIn: true });
|
||||||
rooms.submit(room, ann, { pick: 3 }, 1);
|
rooms.submit(room, ann, { pick: 3 }, 1);
|
||||||
await settled();
|
await answered();
|
||||||
expect(room.resolved).toBe(2);
|
expect(room.resolved).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -320,7 +320,7 @@ describe('the host and the other seats', () => {
|
|||||||
expect(() => rooms.claim(phrase)).toThrow(/unknown or has expired/);
|
expect(() => rooms.claim(phrase)).toThrow(/unknown or has expired/);
|
||||||
while (!game.over(room.state!)) {
|
while (!game.over(room.state!)) {
|
||||||
rooms.submit(room, ann, { pick: 5 }, room.resolved);
|
rooms.submit(room, ann, { pick: 5 }, room.resolved);
|
||||||
await settled();
|
await answered();
|
||||||
}
|
}
|
||||||
const next = rooms.get(rooms.rematch(room, ann).roomId);
|
const next = rooms.get(rooms.rematch(room, ann).roomId);
|
||||||
expect(next.expected).toEqual(['Bo']);
|
expect(next.expected).toEqual(['Bo']);
|
||||||
|
|||||||
@@ -361,10 +361,14 @@ export class Rooms<State, Input> {
|
|||||||
* to hang and the bot's to come at once. The table is told as each move
|
* to hang and the bot's to come at once. The table is told as each move
|
||||||
* lands, the same as any other.
|
* lands, the same as any other.
|
||||||
*/
|
*/
|
||||||
|
/** A room forgotten and loaded again drives itself; a timer or tick that wakes with the old one must do nothing. */
|
||||||
|
private isTable(room: Room<State>): boolean {
|
||||||
|
return this.rooms.get(room.id) === room;
|
||||||
|
}
|
||||||
|
|
||||||
private botsAfterAnswering(room: Room<State>): void {
|
private botsAfterAnswering(room: Room<State>): void {
|
||||||
setImmediate(() => {
|
setImmediate(() => {
|
||||||
// A room forgotten and loaded again drives itself; this one is no longer the table.
|
if (!this.isTable(room)) return;
|
||||||
if (this.rooms.get(room.id) !== room) return;
|
|
||||||
try {
|
try {
|
||||||
this.driveBots(room);
|
this.driveBots(room);
|
||||||
this.botsSpeak(room, 'phase');
|
this.botsSpeak(room, 'phase');
|
||||||
@@ -403,8 +407,7 @@ export class Rooms<State, Input> {
|
|||||||
const hurried = this.hurried.has(room.id);
|
const hurried = this.hurried.has(room.id);
|
||||||
const next = setTimeout(() => {
|
const next = setTimeout(() => {
|
||||||
this.pacing.delete(room.id);
|
this.pacing.delete(room.id);
|
||||||
// A room forgotten and loaded again drives itself; this one is no longer the table.
|
if (!this.isTable(room)) return;
|
||||||
if (this.rooms.get(room.id) !== room) return;
|
|
||||||
this.driveBots(room, hurried ? HURRIED_STEP : 1);
|
this.driveBots(room, hurried ? HURRIED_STEP : 1);
|
||||||
// Played out: the bots have their say once, as after any round, which ends a game with parting words.
|
// Played out: the bots have their say once, as after any round, which ends a game with parting words.
|
||||||
if (!this.pacing.has(room.id)) this.botsSpeak(room, 'phase');
|
if (!this.pacing.has(room.id)) this.botsSpeak(room, 'phase');
|
||||||
|
|||||||
@@ -45,14 +45,13 @@
|
|||||||
{ n: t.turnsPlayed, label: 'turns recorded' },
|
{ n: t.turnsPlayed, label: 'turns recorded' },
|
||||||
{ n: hours(t.minutesAtTable), label: 'at the table' }
|
{ n: hours(t.minutesAtTable), label: 'at the table' }
|
||||||
];
|
];
|
||||||
const onCards = new Set<string>([]);
|
|
||||||
const details = [
|
const details = [
|
||||||
{ n: t.gamesBegun, label: `games begun, at ${t.tablesOpened} ${t.tablesOpened === 1 ? 'table' : 'tables'}` },
|
{ n: t.gamesBegun, label: `games begun, at ${t.tablesOpened} ${t.tablesOpened === 1 ? 'table' : 'tables'}` },
|
||||||
{ n: t.namesSeated, label: t.namesSeated === 1 ? 'name has taken a seat' : 'names have taken a seat' },
|
{ n: t.namesSeated, label: t.namesSeated === 1 ? 'name has taken a seat' : 'names have taken a seat' },
|
||||||
{ n: t.turnsPlayed, label: `turns recorded; ${t.longestGameTurns} in the longest game` },
|
{ n: t.turnsPlayed, label: `turns recorded; ${t.longestGameTurns} in the longest game` },
|
||||||
{ n: t.gamesWithBot, label: t.gamesWithBot === 1 ? 'game with a bot seated' : 'games with a bot seated' },
|
{ n: t.gamesWithBot, label: t.gamesWithBot === 1 ? 'game with a bot seated' : 'games with a bot seated' },
|
||||||
...(t.talkLines ? [{ n: t.talkLines, label: t.talkLines === 1 ? 'line of table talk' : 'lines of table talk' }] : []),
|
...(t.talkLines ? [{ n: t.talkLines, label: t.talkLines === 1 ? 'line of table talk' : 'lines of table talk' }] : []),
|
||||||
...Object.entries(t.byGame).filter(([label]) => !onCards.has(label)).map(([label, n]) => ({ n, label }))
|
...Object.entries(t.byGame).map(([label, n]) => ({ n, label }))
|
||||||
];
|
];
|
||||||
const first = t.firstGameAt ? `The first game began ${new Date(t.firstGameAt).toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })}. ` : '';
|
const first = t.firstGameAt ? `The first game began ${new Date(t.firstGameAt).toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' })}. ` : '';
|
||||||
return { headline, details, note: `${first}Names are counted, not people: one player under two names is two here. Table time counts only the minutes between moves, so a game left open overnight adds nothing for the night.` };
|
return { headline, details, note: `${first}Names are counted, not people: one player under two names is two here. Table time counts only the minutes between moves, so a game left open overnight adds nothing for the night.` };
|
||||||
|
|||||||
Reference in new issue
Block a user