Files
Battle-for-Tismo/tests/diplomacy_test.js
T

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);
}
}