Batch of LLM-assisted fixes/feature updates
This commit is contained in:
1 parent
7c6453160a
commit
05aaf394df
16 files changed
+5597
-912
No files matched your search
@@ -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');
|
||||
});
|
||||
Reference in new issue
Block a user