Files
Battle-for-Tismo/tests/game_server_test.js
T
adrien c2f131da5c Turned tile improvements into construction sites with their own budgets
Every road, railway and tile work now rises as an economic construction site instead of a queue entry. A site gathers its materials over days from reachable regions, refusing any supplier quoting above the price it was budgeted at (a 1% tolerance); a day that buys nothing stalls the site, raising a red mark and letting the nation raise the budget to current prices or cancel it. The upfront budget covers the build cost plus the market value of its steel and high-tech, energy stays a grid draw, and any money left over stays with the finished work as a maintenance reserve.

That reserve now pays the work's own daily steel and high-tech wear before the treasury is billed, so a well funded work spares the state until it runs dry; the spent money is paid into the nation's regions. Building upgrades run through the same gather-build-maintain machinery, sharing the site's kind flag and target level. The legacy tile-work queue and the market building are gone.

Bundles the in-progress chart sizing for the central bank panel.
2026-09-22 18:08:31 +02:00

616 lines
24 KiB
JavaScript

import { TestCase } from "./framework/test_case.js";
import { GameServer } from "../server/game_server.js";
import { CIVILISATIONS } from "../shared/data.js";
import { adjacentLand, SEED, grantBuilding } from "./framework/helpers.js";
import { key } from "../shared/hex.js";
function fakeNetwork() {
return {
peers: new Set(),
sent: [],
getPeerIds() {
return Array.from(this.peers);
},
send(peerId, message) {
this.sent.push({ peerId, message });
},
};
}
function makeServer(civIds = ["france", "britain"]) {
const network = fakeNetwork();
const server = new GameServer(network);
server.configureGame({ seed: SEED, player_civ: 0, civilisations: civIds });
return { server, network };
}
function emptyLandPair(state) {
for (const cell of state.landCells) {
if (state.unitAt(cell) || state.cityAt(cell)) continue;
for (const neighbour of state.topology.neighbours(cell.x, cell.y)) {
if (!state._isLand(neighbour)) continue;
if (state.unitAt(neighbour) || state.cityAt(neighbour)) continue;
return { a: { x: cell.x, y: cell.y }, b: { x: neighbour.x, y: neighbour.y } };
}
}
throw new Error("no empty land pair");
}
export class GameServerTest extends TestCase {
test_configure_game_builds_a_state() {
const { server } = makeServer();
this.assertTrue(server.isConfigured());
this.assertNotNull(server.state);
this.assertEqual(server.hostCiv, 0);
this.assertSize(server.state.units, 2);
this.assertSize(server.state.civilisations, 2);
}
test_configure_game_ignores_a_second_call() {
const { server } = makeServer();
const firstSeed = server.state.seed;
server.configureGame({ seed: 999, player_civ: 1, civilisations: [] });
this.assertEqual(server.state.seed, firstSeed);
}
test_snapshots_are_delta_encoded_after_the_first() {
const { server, network } = makeServer();
network.peers.add(1);
server.peerCiv.set(1, 0);
server._broadcastState();
const full = network.sent[network.sent.length - 1].message.state;
this.assertFalse(full.delta, "the first snapshot to a peer is full");
this.assertNotNull(full.territory, "it carries the whole territory");
this.assertTrue(Array.isArray(full.explored), "and the explored list");
server._broadcastState();
const delta = network.sent[network.sent.length - 1].message.state;
this.assertTrue(delta.delta, "later snapshots are deltas");
this.assertNull(delta.territory, "unchanged territory is left out");
this.assertNull(delta.regions, "unchanged regions are left out");
this.assertNull(delta.population, "unchanged population is left out");
this.assertNull(delta.gdpBaseline, "the static growth baseline is left out");
this.assertNull(delta.tileEthnicity, "unchanged ethnicity is left out");
this.assertNull(delta.cityStats, "unchanged city stats are left out");
this.assertNull(delta.visible, "unchanged visibility is left out");
this.assertNull(delta.explored, "no explored tiles are re-sent");
this.assertNull(delta.exploredAdded, "and none were added");
this.assertNotNull(delta.units, "the always-changing fields still ship");
// A real territory change must ship the array again.
server.state._reindexTerritory();
server._broadcastState();
const after = network.sent[network.sent.length - 1].message.state;
this.assertNotNull(after.territory, "changed territory is sent");
// A visibility refresh ships the visible set again.
server.state._refreshVisibility();
server._broadcastState();
const vision = network.sent[network.sent.length - 1].message.state;
this.assertNotNull(vision.visible, "changed visibility is sent");
// An economy tick ships the city stats again.
server.state._tickPopulation();
server._broadcastState();
const economy = network.sent[network.sent.length - 1].message.state;
this.assertNotNull(economy.cityStats, "changed city stats are sent");
// Discovering a new tile ships only the tail of the explored list. Reset
// the viewer's exploration so there is a fresh tile to reveal.
server.state.explored.set(0, new Set());
server.state._serializedExplored.delete(0);
server._peerExploredSent.set(1, 0);
const [fx, fy] = Object.keys(server.state.tiles)[0].split(",").map(Number);
server.state._reveal(0, { x: fx, y: fy }, 0);
server._broadcastState();
const grown = network.sent[network.sent.length - 1].message.state;
this.assertNull(grown.explored, "the full explored list is not re-sent");
this.assertNotNull(grown.exploredAdded, "only the newly explored tiles are");
this.assertGreaterOrEqual(grown.exploredAdded.length, 1, "the tail holds the new tile");
}
test_city_economy_memo_is_scoped_to_one_broadcast() {
const { server } = makeServer();
const state = server.state;
const city = state.cities[0];
state.beginSnapshotCache();
const within = state.getCityEconomy(city);
// While the snapshot is being built the same figure is handed back...
this.assert(state.getCityEconomy(city) === within, "the memo is reused in scope");
state.tilePopulation.set(key(city.coords.x, city.coords.y), 999999);
this.assert(state.getCityEconomy(city) === within, "and is stable even as data shifts");
state.endSnapshotCache();
// ...but a simulation read afterwards always derives a fresh figure.
this.assertNotEqual(state.getCityEconomy(city), within, "ticks never read a stale figure");
}
test_a_civ_change_forces_a_full_snapshot() {
const { server, network } = makeServer();
network.peers.add(1);
server.peerCiv.set(1, 0);
server._broadcastState();
server._broadcastState();
const delta = network.sent[network.sent.length - 1].message.state;
this.assertTrue(delta.delta, "steady state is a delta");
this.assertNull(delta.visible, "the visible set is omitted while unchanged");
// The visible set is per-viewer, so switching nation must resend it.
server.peerCiv.set(1, 1);
server._broadcastState();
const full = network.sent[network.sent.length - 1].message.state;
this.assertFalse(full.delta, "a civ change resends the whole state");
this.assertNotNull(full.visible, "including the per-viewer visible set");
}
test_move_order_for_own_unit_is_accepted() {
const { server } = makeServer();
server.peerCiv.set(1, 0);
const unit = server.state.units[0];
const goal = adjacentLand(server.state, unit);
this.assertTrue(server.handleOrder(1, { type: "move", unit: unit.id, coords: [goal.x, goal.y] }));
}
test_server_advances_movement_between_hourly_ticks() {
const { server } = makeServer();
server.peerCiv.set(1, 0);
const unit = server.state.units[0];
const goal = adjacentLand(server.state, unit);
this.assertTrue(server.handleOrder(1, { type: "move", unit: unit.id, coords: [goal.x, goal.y] }));
for (let i = 0; i < 30; i++) server.advanceMovement(1 / 60);
this.assertGreater(unit.progressHours, 0, "movement advanced between hourly ticks");
this.assertEqual(server.state.totalHours, 0, "does not tick the clock");
}
test_state_changes_are_flushed_in_one_broadcast() {
const { server, network } = makeServer();
server.peerCiv.set(1, 0);
network.peers.add(1);
const unit = server.state.units[0];
const goal = adjacentLand(server.state, unit);
server.handleOrder(1, { type: "move", unit: unit.id, coords: [goal.x, goal.y] });
const before = network.sent.length;
server.flushIfDirty();
this.assertEqual(network.sent.length, before + 1, "one snapshot was sent");
this.assertEqual(network.sent[network.sent.length - 1].message.t, "game_state");
server.flushIfDirty();
this.assertEqual(network.sent.length, before + 1, "second flush is a no-op");
}
test_queued_orders_wait_for_the_simulation_tick() {
const { server } = makeServer();
server.peerCiv.set(1, 0);
const unit = server.state.units[0];
const goal = adjacentLand(server.state, unit);
server.enqueueOrder(1, { type: "move", unit: unit.id, coords: [goal.x, goal.y] });
this.assertEqual(unit.path.length, 0, "the socket callback did not run the order");
this.assertEqual(server.processOrders(), 1, "the tick drains the queue");
this.assertGreaterOrEqual(unit.path.length, 2, "the queued route was computed");
}
test_order_queue_drains_within_the_time_budget() {
const { server } = makeServer();
server.peerCiv.set(1, 0);
const unit = server.state.units[0];
const goal = adjacentLand(server.state, unit);
for (let i = 0; i < 5; i++) {
server.enqueueOrder(1, { type: "move", unit: unit.id, coords: [goal.x, goal.y] });
}
// A zero budget still runs one order and leaves the rest for later ticks,
// so a burst can never monopolise the loop.
this.assertEqual(server.processOrders(0), 1);
this.assertEqual(server.processOrders(0), 1);
this.assertEqual(server.processOrders(0), 1);
}
test_departing_players_orders_are_dropped() {
const { server } = makeServer();
server.peerCiv.set(1, 0);
const unit = server.state.units[0];
const goal = adjacentLand(server.state, unit);
server.enqueueOrder(1, { type: "move", unit: unit.id, coords: [goal.x, goal.y] });
server.onPeerLeft(1);
this.assertEqual(server.processOrders(0), 0, "the departed player's order is gone");
this.assertEqual(unit.path.length, 0, "and it never ran");
}
test_large_wire_move_orders_are_split_across_ticks() {
const { server } = makeServer();
server.peerCiv.set(1, 0);
const state = server.state;
const sample = state.units.find((u) => u.civ === 0);
const proto = state.unitProto(sample);
const ids = [];
for (const cell of state.landCells) {
if (ids.length >= 8) break;
if (state.unitAt(cell)) continue;
ids.push(state._spawnUnit(cell, 0, proto).id);
}
const goal = adjacentLand(state, sample);
server.enqueueOrder(1, { type: "move", units: ids, coords: [goal.x, goal.y] });
this.assertEqual(server._orderQueue.length, 2, "eight units split into two chunks");
const [first, second] = server._orderQueue.map((entry) => entry.order);
this.assertSize(first.units, 4);
this.assertSize(second.units, 4);
this.assertEqual(first.speed, second.speed, "the chunks share one formation pace");
this.assertEqual(first.speed, state._unitSpeed(sample), "the pace is the slowest member's");
}
test_move_order_for_enemy_unit_is_rejected() {
const { server } = makeServer();
server.peerCiv.set(1, 0);
const enemy = server.state.units.find((u) => u.civ !== 0);
const goal = adjacentLand(server.state, enemy);
this.assertFalse(server.handleOrder(1, { type: "move", unit: enemy.id, coords: [goal.x, goal.y] }));
}
test_train_orders_queue_for_own_city() {
const { server } = makeServer();
server.peerCiv.set(1, 0);
const city = server.state.cities.find((c) => c.civ === 0);
grantBuilding(server.state, city, "barracks");
const order = { type: "train", city: city.id, proto: 0 };
this.assertTrue(server.handleOrder(1, order));
this.assertTrue(server.handleOrder(1, order), "a second unit joins the queue");
this.assertSize(server.state.getTraining(city.id), 2);
}
test_cancel_train_order_for_own_city_is_accepted() {
const { server } = makeServer();
server.peerCiv.set(1, 0);
const city = server.state.cities.find((c) => c.civ === 0);
grantBuilding(server.state, city, "barracks");
server.handleOrder(1, { type: "train", city: city.id, proto: 0 });
server.handleOrder(1, { type: "train", city: city.id, proto: 0 });
this.assertTrue(server.handleOrder(1, { type: "cancel_train", city: city.id, index: 0 }));
this.assertSize(server.state.getTraining(city.id), 1);
}
test_train_order_for_enemy_city_is_rejected() {
const { server } = makeServer();
server.peerCiv.set(1, 0);
const enemy = server.state.cities.find((c) => c.civ !== 0);
this.assertFalse(server.handleOrder(1, { type: "train", city: enemy.id, proto: 0 }));
}
test_orders_from_unknown_peers_are_rejected() {
const { server } = makeServer();
this.assertFalse(server.handleOrder(42, { type: "move", unit: 1, coords: [0, 0] }));
}
test_unknown_order_types_are_ignored() {
const { server } = makeServer();
server.peerCiv.set(1, 0);
this.assertFalse(server.handleOrder(1, { type: "dance" }));
}
test_orders_before_configuration_are_ignored() {
const server = new GameServer(fakeNetwork());
this.assertFalse(server.handleOrder(1, { type: "move", unit: 1, coords: [0, 0] }));
}
test_empty_civilisation_list_falls_back_to_the_project() {
const server = new GameServer(fakeNetwork());
server.configureGame({ seed: SEED, player_civ: 0, civilisations: [] });
this.assertEqual(server.state.civilisations.length, CIVILISATIONS.length);
}
}
export class GameServerOrdersTest extends TestCase {
test_build_and_demolish_orders() {
const { server } = makeServer();
server.peerCiv.set(1, 0);
server.state.budgets.set(0, 1.0e15);
const city = server.state.cities.find((c) => c.civ === 0);
this.assertTrue(server.handleOrder(1, { type: "build", city: city.id, building: 0 }));
for (let i = 0; i < 100000 && server.state.training.size > 0; i++) server.state.advanceHour();
this.assertTrue(server.handleOrder(1, { type: "demolish", city: city.id, building: 0 }));
}
test_build_order_for_enemy_city_is_rejected() {
const { server } = makeServer();
server.peerCiv.set(1, 0);
server.state.budgets.set(0, 1.0e15);
const enemy = server.state.cities.find((c) => c.civ !== 0);
this.assertFalse(server.handleOrder(1, { type: "build", city: enemy.id, building: 0 }));
}
test_government_order() {
const { server } = makeServer();
server.peerCiv.set(1, 0);
server.state.budgets.set(0, 1.0e15);
this.assertTrue(server.handleOrder(1, { type: "government", government: 1 }));
this.assertEqual(server.state.getGovernment(0), 1);
}
test_focus_technology_order() {
const { server } = makeServer();
server.peerCiv.set(1, 0);
this.assertTrue(server.handleOrder(1, { type: "focus_technology", technology: 0 }));
this.assertEqual(server.state.getFocusedTechnology(0), 0);
}
test_move_order_with_bad_coords_is_rejected_cleanly() {
const { server } = makeServer();
server.peerCiv.set(1, 0);
const result = server.handleOrder(1, { type: "move", unit: server.state.units[0].id, coords: null });
this.assertEqual(typeof result, "boolean");
}
test_attack_order_targets_an_adjacent_enemy() {
const { server } = makeServer();
server.peerCiv.set(1, 0);
const pair = emptyLandPair(server.state);
const attacker = server.state._spawnUnit(pair.a, 0, server.state.protoUnits[0]);
server.state._spawnUnit(pair.b, 1, server.state.protoUnits[0]);
this.assertTrue(
server.handleOrder(1, { type: "attack", unit: attacker.id, coords: [pair.b.x, pair.b.y] })
);
}
test_ranged_attack_order_bombs_without_closing_in() {
const { server } = makeServer();
server.peerCiv.set(1, 0);
const state = server.state;
const pair = emptyLandPair(state);
const artillery = state.protoUnits.find((u) => u.id === "artillery");
const attacker = state._spawnUnit(pair.a, 0, artillery);
const defender = state._spawnUnit(pair.b, 1, state.protoUnits[0]);
this.assertTrue(
server.handleOrder(1, { type: "attack", unit: attacker.id, coords: [pair.b.x, pair.b.y] }),
"the ranged attack is accepted"
);
this.assertEqual(attacker.coords, pair.a, "the artillery held its ground");
this.assertEqual(defender.hp, defender.maxHp, "the first volley waits for the hour");
state.tickHour();
this.assertLess(defender.hp, defender.maxHp, "the enemy stack was bombed");
}
test_attack_order_without_a_target_is_rejected() {
const { server } = makeServer();
server.peerCiv.set(1, 0);
const pair = emptyLandPair(server.state);
const attacker = server.state._spawnUnit(pair.a, 0, server.state.protoUnits[0]);
this.assertFalse(
server.handleOrder(1, { type: "attack", unit: attacker.id, coords: [pair.b.x, pair.b.y] })
);
}
test_schedule_order_queues_destinations() {
const { server } = makeServer();
server.peerCiv.set(1, 0);
const unit = server.state.units.find((u) => u.civ === 0);
const goal = adjacentLand(server.state, unit);
const payload = { type: "schedule", unit: unit.id, goals: [[goal.x, goal.y]], append: true };
this.assertTrue(server.handleOrder(1, payload));
this.assertTrue(server.handleOrder(1, payload));
this.assertSize(unit.waypoints, 1, "the appended destination is queued");
}
test_extra_players_become_spectators() {
const { server, network } = makeServer();
server.authenticate(1, "Host", "pw");
server.onPeerJoined(1, "Host");
this.assertTrue(server.claimCiv(1, 0));
server.authenticate(2, "Bob", "pw");
server.onPeerJoined(2, "Bob");
this.assertTrue(server.claimCiv(2, 1));
server.authenticate(3, "Cat", "pw");
server.onPeerJoined(3, "Cat");
this.assertEqual(server.peerCiv.get(1), 0);
this.assertEqual(server.peerCiv.get(2), 1);
this.assertEqual(server.peerCiv.get(3), -1, "a third player has no nation left");
this.assertEmpty(server.availableCivs());
}
test_joining_player_is_offered_the_unclaimed_civilisations() {
const { server, network } = makeServer();
server.authenticate(1, "Host", "pw");
server.onPeerJoined(1, "Host");
server.claimCiv(1, 0);
network.sent.length = 0;
server.authenticate(2, "Bob", "pw");
server.onPeerJoined(2, "Bob");
const choose = network.sent.find((m) => m.peerId === 2 && m.message.t === "choose_civ");
this.assertNotNull(choose, "the joiner is offered a choice");
this.assertEqual(choose.message.civs.map((c) => c.index), [1]);
this.assertEqual(choose.message.civs[0].id, server.state.civilisations[1].id);
this.assertEqual(server.peerCiv.get(2), -1, "no nation until the player picks");
}
test_claiming_a_taken_or_unknown_civilisation_is_rejected() {
const { server } = makeServer();
server.authenticate(1, "Host", "pw");
server.onPeerJoined(1, "Host");
this.assertTrue(server.claimCiv(1, 0));
server.authenticate(2, "Bob", "pw");
server.onPeerJoined(2, "Bob");
this.assertFalse(server.claimCiv(2, 0), "already claimed");
this.assertFalse(server.claimCiv(2, 99), "out of range");
this.assertFalse(server.claimCiv(2, -1), "unknown index");
this.assertEqual(server.peerCiv.get(2), -1);
}
test_lifecycle_calls_are_safe_before_configuration() {
const network = fakeNetwork();
const server = new GameServer(network);
server.tickHour();
server.advanceMovement(1);
server.onPeerJoined(1, "Alice");
this.assertFalse(server.isConfigured());
this.assertEmpty(network.sent, "no snapshot is sent before configuration");
}
test_tick_hour_advances_the_state() {
const { server } = makeServer();
server.tickHour();
this.assertEqual(server.state.totalHours, 1);
}
}
export class GameServerLoginTest extends TestCase {
test_first_player_becomes_admin() {
const { server } = makeServer();
const result = server.authenticate(1, "Alice", "pw");
this.assertTrue(result.ok);
this.assertTrue(result.admin);
this.assertEqual(server.adminName, "Alice");
}
test_later_players_are_not_admin() {
const { server } = makeServer();
server.authenticate(1, "Alice", "pw");
const result = server.authenticate(2, "Bob", "pw");
this.assertTrue(result.ok);
this.assertFalse(result.admin);
}
test_missing_credentials_are_rejected() {
const { server } = makeServer();
this.assertFalse(server.authenticate(1, "", "pw").ok);
this.assertFalse(server.authenticate(1, "Alice", "").ok);
}
test_duplicate_online_name_is_rejected() {
const { server } = makeServer();
server.authenticate(1, "Alice", "pw");
const result = server.authenticate(2, "Alice", "pw");
this.assertFalse(result.ok);
}
test_wrong_password_is_rejected() {
const { server } = makeServer();
server.authenticate(1, "Alice", "correct");
server.onPeerLeft(1);
const result = server.authenticate(2, "Alice", "wrong");
this.assertFalse(result.ok);
this.assertFalse(server.peerNames.has(2));
}
test_new_name_sets_its_own_password() {
const { server } = makeServer();
this.assertTrue(server.authenticate(1, "Alice", "anything").ok);
}
test_admin_identity_survives_reconnect() {
const { server } = makeServer();
server.authenticate(1, "Alice", "pw");
server.onPeerLeft(1);
server.authenticate(2, "Bob", "pw");
const result = server.authenticate(3, "Alice", "pw");
this.assertTrue(result.ok);
this.assertTrue(result.admin, "the founder is still admin after rejoining");
}
test_returning_player_reclaims_their_civilisation() {
const { server } = makeServer();
server.authenticate(1, "Alice", "pw");
server.onPeerJoined(1, "Alice");
this.assertTrue(server.claimCiv(1, 0));
const firstCiv = server.peerCiv.get(1);
server.onPeerLeft(1);
this.assertEmpty(server.peerCiv);
this.assertTrue(server.authenticate(2, "Alice", "pw").ok);
server.onPeerJoined(2, "Alice");
this.assertEqual(server.peerCiv.get(2), firstCiv);
}
test_simulation_continues_with_no_players() {
const { server } = makeServer();
server.authenticate(1, "Alice", "pw");
server.onPeerJoined(1, "Alice");
server.claimCiv(1, 0);
const unit = server.state.units[0];
const goal = adjacentLand(server.state, unit);
this.assertTrue(server.handleOrder(1, { type: "move", unit: unit.id, coords: [goal.x, goal.y] }));
server.onPeerLeft(1);
for (let i = 0; i < 30; i++) server.advanceMovement(1 / 60);
this.assertGreater(unit.progressHours, 0, "the server advanced with nobody connected");
}
test_build_improvement_order_lays_a_road_on_controlled_land() {
const { server } = makeServer();
server.peerCiv.set(1, 0);
server.state.budgets.set(0, 10_000_000_000);
for (const city of server.state.cities) {
if (city.civ !== 0) continue;
const stock = server.state.getCityResourceStock(city);
stock.steel = 1e9;
stock.hightech = 1e6;
stock.energy = 1e9;
}
// A land tile France owns but where the pre-generated network left no road.
const coords = server.state.landCells.find((cell) => {
const k = `${cell.x},${cell.y}`;
return server.state.civAt(cell) === 0 && !server.state.roads.has(k);
});
this.assertNotNull(coords, "the map has a bare owned tile");
this.assertTrue(
server.handleOrder(1, {
type: "build_improvement",
coords: [coords.x, coords.y],
improvement: "road",
})
);
const k = `${coords.x},${coords.y}`;
this.assertNotNull(server.state.constructionSiteAt(coords), "the work is queued");
for (let i = 0; i < 50000 && server.state.constructionSites.size > 0; i++) {
server.state.advanceHour();
}
this.assertTrue(server.state.roads.has(k), "the road is laid");
// The owner may replace it with a railway.
this.assertTrue(
server.handleOrder(1, {
type: "build_improvement",
coords: [coords.x, coords.y],
improvement: "railway",
})
);
for (let i = 0; i < 50000 && server.state.constructionSites.size > 0; i++) {
server.state.advanceHour();
}
this.assertTrue(server.state.railways.has(k));
this.assertFalse(server.state.roads.has(k), "the road was replaced");
// A player who owns nothing cannot build there.
server.peerCiv.set(2, 1);
this.assertFalse(
server.handleOrder(2, {
type: "build_improvement",
coords: [coords.x, coords.y],
improvement: "road",
})
);
}
}
export class GameServerLobbyTest extends TestCase {
test_unconfigured_server_lists_no_games() {
const server = new GameServer(fakeNetwork());
this.assertEmpty(server.gameList());
}
test_configured_server_lists_one_game() {
const { server } = makeServer();
const games = server.gameList();
this.assertSize(games, 1);
const game = games[0];
this.assertEqual(game.seed, SEED);
this.assertEqual(game.id, SEED);
this.assertSize(game.civs, 2);
this.assertEqual(game.civs.slice().sort(), ["Britain", "France"]);
this.assertEqual(game.players, 0);
this.assertTrue(game.date.includes("2000"));
}
test_player_count_follows_logins() {
const { server } = makeServer();
server.authenticate(1, "Alice", "pw");
server.authenticate(2, "Bob", "pw");
this.assertEqual(server.gameList()[0].players, 2);
server.onPeerLeft(1);
this.assertEqual(server.gameList()[0].players, 1);
}
}