Files
Battle-for-Tismo/tests/game_server_test.js
T
2026-09-17 00:56:59 +02:00

334 lines
12 KiB
JavaScript

import { TestCase } from "./framework/test_case.js";
import { GameServer } from "../server/game_server.js";
import { CIVILISATIONS } from "../shared/data.js";
import { smallConfig, adjacentLand, SEED } from "./framework/helpers.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 };
}
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_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_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_order_for_own_city_is_accepted_once() {
const { server } = makeServer();
server.peerCiv.set(1, 0);
const city = server.state.cities[0];
const order = { type: "train", city: city.id, proto: 0 };
this.assertTrue(server.handleOrder(1, order));
this.assertFalse(server.handleOrder(1, order), "city is already training");
}
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 }));
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_technology_order() {
const { server } = makeServer();
server.peerCiv.set(1, 0);
server.state.research.set(0, 1.0e6);
this.assertTrue(server.handleOrder(1, { type: "technology", technology: 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_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, "britain");
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");
}
}
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[0], "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);
}
}