Batch of LLM-assisted fixes/feature updates

This commit is contained in:
Cam Spry committed 2026-10-30 23:02:45 -04:00
1 parent 7c6453160a
commit 05aaf394df
16 files changed
+5597 -912

No files matched your search

+186
View File
@@ -0,0 +1,186 @@
'use strict';
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
// Must be set before ../server is required.
const STORIES_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'yarn-socket-stories-'));
process.env.YARN_STORIES_DIR = STORIES_DIR;
const test = require('node:test');
const assert = require('node:assert/strict');
const { io: createClient } = require('socket.io-client');
const { server, games } = require('../server');
function waitForEvent(socket, event, { timeout = 8000 } = {}) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
socket.off(event, onEvent);
reject(new Error(`timed out waiting for "${event}"`));
}, timeout);
function onEvent(payload) {
clearTimeout(timer);
socket.off(event, onEvent);
resolve(payload);
}
socket.on(event, onEvent);
});
}
test('two humans and a bot can play a complete game end to end', async (t) => {
await new Promise((resolve) => server.listen(0, resolve));
const { port } = server.address();
const url = `http://127.0.0.1:${port}`;
const roomName = 'smoke-room';
const sockets = [];
t.after(async () => {
for (const socket of sockets) socket.disconnect();
games[roomName]?.destroy();
delete games[roomName];
await new Promise((resolve) => server.close(resolve));
});
const connect = async (uuid, name) => {
const socket = createClient(url, {
transports: ['websocket'],
forceNew: true,
reconnection: false
});
sockets.push(socket);
const joined = waitForEvent(socket, 'gameState');
socket.on('connect', () => {
socket.emit('joinRoom', { roomName, playerUUID: uuid, playerName: name });
});
await joined;
return socket;
};
const alice = await connect('alice-0001', 'Alice');
const bob = await connect('bob-0002', 'Bob');
// The first player is the host.
const lobbyState = await new Promise((resolve) => {
alice.emit('setName', { name: 'Alice' });
alice.once('gameState', resolve);
});
assert.equal(lobbyState.gameHost, 'alice-0001');
const roundStarted = waitForEvent(alice, 'newRound');
alice.emit('startGame', {
botCount: 1,
timeLimit: 30,
roundLimit: 1,
scoreLimit: 0,
startText: 'It was a dark and stormy night...'
});
const roundState = await roundStarted;
assert.equal(roundState.currentPhase, 'writing');
assert.equal(roundState.players.length, 3, 'two humans plus one bot');
const votingForAlice = waitForEvent(alice, 'votingPhase');
const votingForBob = waitForEvent(bob, 'votingPhase');
alice.emit('submitEntry', { entry: 'Alice bravely opened the door.' });
bob.emit('submitEntry', { entry: 'Bob hid behind the sofa.' });
const aliceVote = await votingForAlice;
const bobVote = await votingForBob;
assert.equal(aliceVote.entries.length, 3, 'bots submit entries too');
const aliceOwn = aliceVote.entries.find(
(entry) => entry.index === aliceVote.ownEntryIndex
);
const bobOwn = bobVote.entries.find(
(entry) => entry.index === bobVote.ownEntryIndex
);
assert.equal(aliceOwn.entry_text, 'Alice bravely opened the door.');
assert.equal(bobOwn.entry_text, 'Bob hid behind the sofa.');
const roundResults = waitForEvent(alice, 'roundResults');
const gameOver = waitForEvent(alice, 'gameOver');
// Cross-vote so no one votes for themselves and the bot cannot win alone.
alice.emit('submitVote', { entryIndex: bobVote.ownEntryIndex });
bob.emit('submitVote', { entryIndex: aliceVote.ownEntryIndex });
const round = await roundResults;
assert.equal(round.results.length, 3, 'every entry is scored');
assert.ok(round.winner);
assert.ok(round.winner.points > 0);
assert.equal(round.results.reduce((sum, entry) => sum + entry.votes, 0), 3);
const result = await gameOver;
assert.ok(result.winner);
assert.equal(typeof result.winner.score, 'number');
assert.ok(
['alice-0001', 'bob-0002'].includes(result.winner.player_id),
'a human wins when the humans vote for each other'
);
assert.ok(result.winner.score > 0);
assert.equal(result.story.length, 2, 'start text plus the winning entry');
assert.equal(result.story[0].player, 'Narrator');
// The story is persisted when the game finishes.
const saved = fs.readdirSync(STORIES_DIR).filter((name) => name.endsWith('.txt'));
assert.equal(saved.length, 1);
const savedText = fs.readFileSync(path.join(STORIES_DIR, saved[0]), 'utf8');
assert.ok(savedText.includes(round.winner.entry_text), 'saved story contains the winning line');
assert.ok(savedText.startsWith('It was a dark and stormy night...'));
// And the room is listed for the lobby browser.
const state = games[roomName].getState();
assert.equal(state.currentPhase, 'gameOver');
assert.equal(state.inProgress, false);
});
test('when the last unsubmitted player leaves, voting starts immediately', async (t) => {
await new Promise((resolve) => server.listen(0, resolve));
const { port } = server.address();
const url = `http://127.0.0.1:${port}`;
const roomName = 'disconnect-room';
const sockets = [];
t.after(async () => {
for (const socket of sockets) socket.disconnect();
games[roomName]?.destroy();
delete games[roomName];
await new Promise((resolve) => server.close(resolve));
});
const connect = async (uuid, name) => {
const socket = createClient(url, {
transports: ['websocket'],
forceNew: true,
reconnection: false
});
sockets.push(socket);
const joined = waitForEvent(socket, 'gameState');
socket.on('connect', () => {
socket.emit('joinRoom', { roomName, playerUUID: uuid, playerName: name });
});
await joined;
return socket;
};
const alice = await connect('alice-1001', 'Alice');
const bob = await connect('bob-1002', 'Bob');
const carla = await connect('carla-1003', 'Carla');
alice.emit('startGame', { botCount: 0, timeLimit: 60, roundLimit: 5, scoreLimit: 0 });
await waitForEvent(alice, 'newRound');
alice.emit('submitEntry', { entry: 'Alice wrote this.' });
bob.emit('submitEntry', { entry: 'Bob wrote this.' });
const voting = waitForEvent(alice, 'votingPhase');
carla.disconnect();
const payload = await voting;
assert.equal(payload.entries.length, 2, 'empty/absent entries are excluded');
assert.equal(games[roomName].currentPhase, 'voting');
});