187 lines
6.3 KiB
JavaScript
187 lines
6.3 KiB
JavaScript
'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');
|
|
});
|