249 lines
9.2 KiB
JavaScript
249 lines
9.2 KiB
JavaScript
import { TestCase } from "./framework/test_case.js";
|
|
import { GameServer } from "../server/game_server.js";
|
|
import { smallState, SEED } from "./framework/helpers.js";
|
|
|
|
function parseKey(k) {
|
|
const i = k.indexOf(",");
|
|
return { x: Number(k.slice(0, i)), y: Number(k.slice(i + 1)) };
|
|
}
|
|
|
|
// A tile owned by a civilisation other than `mover`, plus an adjacent land tile
|
|
// the mover can stage a unit on. Both tiles are free of units and cities.
|
|
function foreignTerritoryWithStaging(state, mover) {
|
|
for (const [k, owner] of state.territory) {
|
|
if (owner === mover) continue;
|
|
const target = parseKey(k);
|
|
if (state.unitAt(target) || state.cityAt(target)) continue;
|
|
for (const neighbour of state.topology.neighbours(target.x, target.y)) {
|
|
if (!state._isLand(neighbour)) continue;
|
|
if (state.unitAt(neighbour) || state.cityAt(neighbour)) continue;
|
|
if (state.civAt(neighbour) === owner) continue;
|
|
return { staging: neighbour, target };
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function ownLandWithStaging(state, civ) {
|
|
for (const cell of state.landCells) {
|
|
if (state.civAt(cell) !== civ) continue;
|
|
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.civAt(neighbour) !== civ) continue;
|
|
if (state.unitAt(neighbour) || state.cityAt(neighbour)) continue;
|
|
return { staging: { x: cell.x, y: cell.y }, target: { x: neighbour.x, y: neighbour.y } };
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
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 DiplomacyStateTest extends TestCase {
|
|
test_everyone_starts_at_peace() {
|
|
const state = smallState(["france", "britain", "poland"]);
|
|
this.assertFalse(state.isAtWar(0, 1));
|
|
this.assertFalse(state.isAtWar(1, 2));
|
|
this.assertEqual(state.getRelation(0, 1), "peace");
|
|
this.assertSize(state.wars, 0);
|
|
this.assertEmpty(state.news);
|
|
}
|
|
|
|
test_declaring_war_sets_the_relation() {
|
|
const state = smallState();
|
|
this.assertTrue(state.requestDeclareWar(0, 1));
|
|
this.assertTrue(state.isAtWar(0, 1));
|
|
this.assertTrue(state.isAtWar(1, 0), "the relation is symmetric");
|
|
this.assertEqual(state.getRelation(1, 0), "war");
|
|
}
|
|
|
|
test_declaring_war_twice_is_rejected() {
|
|
const state = smallState();
|
|
this.assertTrue(state.requestDeclareWar(0, 1));
|
|
this.assertFalse(state.requestDeclareWar(0, 1), "already at war");
|
|
this.assertFalse(state.requestDeclareWar(1, 0), "the other direction is the same war");
|
|
}
|
|
|
|
test_declaring_war_rejects_invalid_targets() {
|
|
const state = smallState();
|
|
this.assertFalse(state.requestDeclareWar(0, 0), "cannot fight yourself");
|
|
this.assertFalse(state.requestDeclareWar(0, 99), "unknown target");
|
|
this.assertFalse(state.requestDeclareWar(-1, 1), "unknown attacker");
|
|
}
|
|
|
|
test_declaration_is_newsworthy() {
|
|
const state = smallState();
|
|
for (let i = 0; i < 5; i++) state.advanceHour();
|
|
state.requestDeclareWar(0, 1);
|
|
this.assertSize(state.news, 1);
|
|
const entry = state.news[0];
|
|
this.assertEqual(entry.type, "war");
|
|
this.assertEqual(entry.actor, 0);
|
|
this.assertEqual(entry.target, 1);
|
|
this.assertEqual(entry.totalHours, 5);
|
|
this.assertTrue(entry.date.includes("05:00"), `date has the hour: ${entry.date}`);
|
|
}
|
|
|
|
test_the_news_feed_is_capped() {
|
|
const state = smallState();
|
|
for (let i = 0; i < 250; i++) state._addNews({ type: "war", actor: 0, target: 1 });
|
|
this.assertEqual(state.news.length, 100);
|
|
this.assertEqual(state.news[0].id, 151);
|
|
this.assertEqual(state.news[state.news.length - 1].id, 250);
|
|
}
|
|
|
|
test_moving_a_military_unit_into_peaceful_territory_declares_war() {
|
|
const state = smallState();
|
|
const site = foreignTerritoryWithStaging(state, 0);
|
|
this.assertNotNull(site, "found foreign territory with a staging tile");
|
|
const unit = state._spawnUnit(site.staging, 0, state.protoUnits[0]);
|
|
this.assertTrue(state.requestMove(unit.id, site.target));
|
|
this.assertTrue(state.isAtWar(0, 1), "the march declared war");
|
|
this.assertSize(state.news.filter((e) => e.type === "war"), 1);
|
|
}
|
|
|
|
test_peaceful_movement_adds_no_news_when_already_at_war() {
|
|
const state = smallState();
|
|
state.requestDeclareWar(0, 1);
|
|
const site = foreignTerritoryWithStaging(state, 0);
|
|
const unit = state._spawnUnit(site.staging, 0, state.protoUnits[0]);
|
|
this.assertTrue(state.requestMove(unit.id, site.target));
|
|
this.assertSize(state.news, 1, "only the original declaration is news");
|
|
this.assertSize(state.news.filter((e) => e.type === "war"), 1);
|
|
}
|
|
|
|
test_civilian_movement_does_not_declare_war() {
|
|
const state = smallState();
|
|
const site = foreignTerritoryWithStaging(state, 0);
|
|
const worker = state._spawnUnit(site.staging, 0, state.protoUnits[1]);
|
|
this.assertFalse(state.isMilitary(worker), "the unit is a civilian");
|
|
this.assertTrue(state.requestMove(worker.id, site.target));
|
|
this.assertFalse(state.isAtWar(0, 1), "civilians cross without a war");
|
|
this.assertEmpty(state.news);
|
|
}
|
|
|
|
test_capturing_a_city_is_newsworthy() {
|
|
const state = smallState();
|
|
const city = state.cities.find((c) => c.civ === 1);
|
|
this.assertTrue(state._captureCity(city, 0));
|
|
const capture = state.news.find((e) => e.type === "capture");
|
|
this.assertNotNull(capture);
|
|
this.assertEqual(capture.city, city.name);
|
|
this.assertEqual(capture.from, 1);
|
|
this.assertEqual(capture.to, 0);
|
|
this.assertHas(capture, "date");
|
|
}
|
|
|
|
test_snapshot_carries_diplomacy_and_news() {
|
|
const state = smallState();
|
|
state.requestDeclareWar(0, 1);
|
|
const snapshot = state.snapshot(0);
|
|
this.assertHas(snapshot, "diplomacy");
|
|
this.assertHas(snapshot, "news");
|
|
const relation = snapshot.diplomacy.find((e) => e[0] === 0 && e[1] === 1);
|
|
this.assertEqual(relation[2], "war");
|
|
this.assertSize(snapshot.news, 1);
|
|
}
|
|
|
|
test_diplomacy_and_news_are_shared_by_every_viewer() {
|
|
const state = smallState();
|
|
state.requestDeclareWar(0, 1);
|
|
const first = state.snapshot(0);
|
|
const second = state.snapshot(1);
|
|
this.assertEqual(first.diplomacy, second.diplomacy);
|
|
this.assertEqual(first.news, second.news);
|
|
}
|
|
|
|
test_snapshot_carries_every_nations_economy() {
|
|
const state = smallState(["france", "britain", "poland"]);
|
|
const stats = state.snapshot(0).civStats;
|
|
this.assertSize(stats, 3, "one entry per nation");
|
|
for (let civ = 0; civ < 3; civ++) {
|
|
this.assertEqual(stats[civ].population, state.getPlayerPopulation(civ));
|
|
this.assertApprox(stats[civ].gdp, state.getPlayerGdp(civ), 1e-6);
|
|
const expected = stats[civ].population > 0
|
|
? stats[civ].gdp / stats[civ].population
|
|
: 0;
|
|
this.assertApprox(stats[civ].gdpPerCapita, expected, 1e-9);
|
|
}
|
|
this.assertEqual(state.snapshot(0).civStats, state.snapshot(1).civStats, "shared by every viewer");
|
|
}
|
|
|
|
test_declaration_notifies_listeners() {
|
|
const state = smallState();
|
|
let changes = 0;
|
|
state.onChanged(() => { changes += 1; });
|
|
state.requestDeclareWar(0, 1);
|
|
this.assertGreater(changes, 0, "the declaration is published");
|
|
}
|
|
}
|
|
|
|
export class DiplomacyServerTest extends TestCase {
|
|
test_declare_war_order_is_accepted() {
|
|
const { server } = makeServer();
|
|
server.peerCiv.set(1, 0);
|
|
this.assertTrue(server.handleOrder(1, { type: "declare_war", target: 1 }));
|
|
this.assertTrue(server.state.isAtWar(0, 1));
|
|
this.assertSize(server.state.news, 1);
|
|
}
|
|
|
|
test_declare_war_order_rejects_bad_targets() {
|
|
const { server } = makeServer();
|
|
server.peerCiv.set(1, 0);
|
|
this.assertFalse(server.handleOrder(1, { type: "declare_war", target: 0 }));
|
|
this.assertFalse(server.handleOrder(1, { type: "declare_war", target: 99 }));
|
|
}
|
|
|
|
test_declare_war_order_is_rejected_for_spectators() {
|
|
const { server } = makeServer();
|
|
this.assertFalse(server.handleOrder(7, { type: "declare_war", target: 1 }));
|
|
}
|
|
|
|
test_move_order_into_peaceful_territory_declares_war() {
|
|
const { server } = makeServer();
|
|
server.peerCiv.set(1, 0);
|
|
const site = foreignTerritoryWithStaging(server.state, 0);
|
|
const unit = server.state._spawnUnit(site.staging, 0, server.state.protoUnits[0]);
|
|
this.assertTrue(server.handleOrder(1, {
|
|
type: "move",
|
|
unit: unit.id,
|
|
coords: [site.target.x, site.target.y],
|
|
}));
|
|
this.assertTrue(server.state.isAtWar(0, 1));
|
|
}
|
|
|
|
test_move_order_into_own_territory_is_not_news() {
|
|
const { server } = makeServer();
|
|
server.peerCiv.set(1, 0);
|
|
const site = ownLandWithStaging(server.state, 0);
|
|
this.assertNotNull(site, "found two adjacent friendly land tiles");
|
|
const unit = server.state._spawnUnit(site.staging, 0, server.state.protoUnits[0]);
|
|
this.assertTrue(server.handleOrder(1, {
|
|
type: "move",
|
|
unit: unit.id,
|
|
coords: [site.target.x, site.target.y],
|
|
}));
|
|
this.assertFalse(server.state.isAtWar(0, 1));
|
|
this.assertEmpty(server.state.news);
|
|
}
|
|
}
|