v0.7.0 — four game types, a lobby you can read and leave, and a multiplayer game that makes a sound
The multiplayer set-up, the lobby, the start of a game, and four signals a remote client had never been sent. Reasoning, the preset table and what was verified how: CHANGELOG.md. - Co-op, Competitive, Cutthroat, Solitaire and Custom, on both screens, from one shared block — they had drifted, and each was missing a question the other asked. - A player reads the whole rule set before taking a seat, may leave a lobby or a running game, and keeps a seat across a reload. The host may clear a chair. The browser remembers every game it is in, not just the last one. - The start of a game is drawn: a handoff beat, an announcement, the code and type in the header. - Sound, the timetable flash, announcements and the just-drawn badge now reach a remote client; justDrawn goes to the seat that drew it and nobody else. - Played on StartOS, which found the rest: an Extra belongs to the player who played it, the board never named the Superintendent, bot seats were reported as absent players, and rule section numbers are out of every string a player reads. Also carries the previous session's Heavy Grade documentation work — asked again, answer unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016JczK5i33ZNSf2PtzZqdhS
This commit is contained in:
co-authored by
Claude Opus 5
parent
42adfda390
commit
06db36e5b5
@@ -10,6 +10,7 @@ import {
|
||||
createLobby,
|
||||
freshGameCode,
|
||||
joinLobby,
|
||||
leaveLobby,
|
||||
playerCountAllowed,
|
||||
reassignHost,
|
||||
setBotSeat,
|
||||
@@ -250,3 +251,96 @@ describe('game codes', () => {
|
||||
assert.match(code, /^[A-Z]+-\d{4}$/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('a name nobody else at the table is using', () => {
|
||||
/**
|
||||
* The display name is not decoration: it labels the district on the Division map, it is what the
|
||||
* turn chart means by "waiting on Jesse", and `record()` puts it in front of every line that
|
||||
* player causes. Two identical names make all three ambiguous, and the names lock at
|
||||
* `Lobby.Start` — so the refusal has to happen at the door.
|
||||
*/
|
||||
it('refuses a second player using a name already at the table, whatever the case or spacing', () => {
|
||||
const { lobby } = createLobby(competitive, 'Alice', 'RAIL-0001', 4);
|
||||
for (const attempt of ['Alice', 'alice', ' ALICE ']) {
|
||||
const result = joinLobby(lobby, attempt);
|
||||
assert.equal(result.ok, false, `"${attempt}" was allowed alongside Alice`);
|
||||
if (!result.ok) assert.equal(result.code, 'NAME_TAKEN');
|
||||
}
|
||||
});
|
||||
|
||||
it('frees the name again when that player leaves', () => {
|
||||
const { lobby, session } = createLobby(competitive, 'Alice', 'RAIL-0001', 4);
|
||||
const joined = joinLobby(lobby, 'Bob');
|
||||
assert.ok(joined.ok);
|
||||
if (!joined.ok) return;
|
||||
const after = leaveLobby(joined.lobby, joined.session.token);
|
||||
const again = joinLobby(after.lobby, 'Bob');
|
||||
assert.equal(again.ok, true, 'a departed player’s name was still held against the table');
|
||||
assert.equal(after.empty, false, 'the host is still seated');
|
||||
assert.equal(after.lobby.hostToken, session.token, 'a non-host leaving moved the host chair');
|
||||
});
|
||||
});
|
||||
|
||||
describe('leaving a lobby', () => {
|
||||
/**
|
||||
* There was no way out at all before 2026-08-23. Start needs every chair filled and `setBotSeat`
|
||||
* refuses to touch an occupied human seat, so a mis-join or a player who wandered off wedged the
|
||||
* whole table.
|
||||
*/
|
||||
const table = () => {
|
||||
const { lobby, session } = createLobby(competitive, 'Alice', 'RAIL-0001', 3);
|
||||
const bob = joinLobby(lobby, 'Bob');
|
||||
assert.ok(bob.ok);
|
||||
if (!bob.ok) throw new Error('Bob could not sit down');
|
||||
const carol = joinLobby(bob.lobby, 'Carol');
|
||||
assert.ok(carol.ok);
|
||||
if (!carol.ok) throw new Error('Carol could not sit down');
|
||||
return { lobby: carol.lobby, alice: session, bob: bob.session, carol: carol.session };
|
||||
};
|
||||
|
||||
it('empties the chair and forgets the token, leaving the rest of the table alone', () => {
|
||||
const { lobby, bob } = table();
|
||||
const after = leaveLobby(lobby, bob.token);
|
||||
assert.equal(after.lobby.seats[1], null, 'the seat was not freed');
|
||||
assert.equal(after.lobby.seats.length, 3, 'the table changed size');
|
||||
assert.ok(!after.lobby.joinOrder.includes(bob.token), 'the departed token is still in the join order');
|
||||
assert.equal((after.lobby.seats[0] as { displayName: string }).displayName, 'Alice');
|
||||
assert.equal((after.lobby.seats[2] as { displayName: string }).displayName, 'Carol');
|
||||
});
|
||||
|
||||
it('names a seat to clear somebody else — what the host’s "remove" does', () => {
|
||||
const { lobby, alice, carol } = table();
|
||||
const after = leaveLobby(lobby, alice.token, 2);
|
||||
assert.equal(after.lobby.seats[2], null, 'the named seat was not cleared');
|
||||
assert.ok(!after.lobby.joinOrder.includes(carol.token));
|
||||
// Seat index IS player index (`lobby.ts`), so nothing may shuffle up to fill the hole.
|
||||
assert.equal((after.lobby.seats[0] as { displayName: string }).displayName, 'Alice');
|
||||
assert.equal((after.lobby.seats[1] as { displayName: string }).displayName, 'Bob');
|
||||
});
|
||||
|
||||
it('passes host rights on when the host is the one who leaves', () => {
|
||||
const { lobby, alice, bob } = table();
|
||||
const after = leaveLobby(lobby, alice.token);
|
||||
assert.equal(after.lobby.hostToken, bob.token, 'the earliest-joined remaining player did not become host');
|
||||
assert.equal(after.empty, false);
|
||||
});
|
||||
|
||||
it('reports the table empty when the last human goes, so the caller can drop it', () => {
|
||||
const { lobby, alice, bob, carol } = table();
|
||||
const one = leaveLobby(lobby, bob.token);
|
||||
const two = leaveLobby(one.lobby, carol.token);
|
||||
assert.equal(two.empty, false, 'Alice is still sitting there');
|
||||
const three = leaveLobby(two.lobby, alice.token);
|
||||
assert.equal(three.empty, true, 'a lobby with nobody human in it did not report itself empty');
|
||||
});
|
||||
|
||||
it('leaves a bot seat to the bot controls, and ignores a token holding no seat', () => {
|
||||
const { lobby, alice } = table();
|
||||
const withBot = setBotSeat(leaveLobby(lobby, alice.token, 1).lobby, 1, true);
|
||||
// `leaveLobby` is about people. A bot is removed with the button that put it there.
|
||||
const after = leaveLobby(withBot, alice.token, 1);
|
||||
assert.deepEqual(after.lobby.seats[1], { kind: 'bot' }, 'leaving cleared a bot seat');
|
||||
const stranger = leaveLobby(withBot, 'not-a-token');
|
||||
assert.equal(stranger.lobby, withBot, 'an unknown token changed the lobby');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user