Files
Battle-for-Tismo/tests/roads_test.js
T
adrien 3acd2487df Added roads, regional taxation and a reworked budget view
Roads are pre-generated between cities with a greedy geometric spanner and flatten unit movement cost. City improvements now only affect the tiles their region controls, with per-region tax collection and a Cities tab. The budget is a grouped, scrollable table with 25%/year tax and a 10%-of-GDP opening treasury. Also merged the tile info into the city panel, kept conflict and budget expansion state across snapshots, added join notifications, filled border corners, drew battle sides as stacks and made tab overflow scroll.
2026-09-18 07:11:59 +02:00

96 lines
4.4 KiB
JavaScript

import { TestCase } from "./framework/test_case.js";
import { smallState } from "./framework/helpers.js";
import { buildRoadNetwork, greedySpanner, tileRoadCost } from "../shared/roads.js";
import { ROADS, TERRAIN_TILES } from "../shared/data.js";
import { key, parseKey } from "../shared/hex.js";
export class RoadsTest extends TestCase {
test_road_cost_grows_exponentially_with_terrain() {
const plains = TERRAIN_TILES.find((t) => t.terrainType === "Land");
const hills = TERRAIN_TILES.find((t) => t.terrainType === "Hills");
const mountain = TERRAIN_TILES.find((t) => t.terrainType === "Mountain");
const sea = TERRAIN_TILES.find((t) => t.terrainType === "Sea");
this.assertApprox(tileRoadCost(plains), Math.pow(ROADS.costBase, 1));
this.assertApprox(tileRoadCost(hills), Math.pow(ROADS.costBase, 1.8));
this.assertApprox(tileRoadCost(mountain), Math.pow(ROADS.costBase, 5));
this.assertGreater(tileRoadCost(mountain), tileRoadCost(hills) * 8, "mountains dwarf hills");
this.assertGreater(tileRoadCost(hills), tileRoadCost(plains), "hills cost more than plains");
this.assertFalse(Number.isFinite(tileRoadCost(sea)), "the sea cannot carry a road");
this.assertEqual(tileRoadCost(sea, true), 0, "a city tile is free");
}
test_cities_start_with_a_road_and_are_connected_by_the_network() {
const state = smallState(["france", "britain", "poland"], 12345, { citiesPerCiv: 3 });
const cityKeys = state.cities.map((city) => key(city.coords.x, city.coords.y));
for (const city of state.cities) {
this.assertTrue(
city.improvements.includes(ROADS.improvementName),
"a city is founded with a road"
);
}
for (const cityKey of cityKeys) this.assertTrue(state.roads.has(cityKey));
this.assertGreater(state.roads.size, cityKeys.length, "roads also cross the land");
// Following roads from the first city must reach every other city.
const seen = new Set([cityKeys[0]]);
const queue = [parseKey(cityKeys[0])];
while (queue.length > 0) {
const coords = queue.pop();
for (const neighbour of state.topology.neighbours(coords.x, coords.y)) {
const nk = key(neighbour.x, neighbour.y);
if (!state.roads.has(nk) || seen.has(nk)) continue;
seen.add(nk);
queue.push(neighbour);
}
}
for (const cityKey of cityKeys) {
this.assertTrue(seen.has(cityKey), `city ${cityKey} is reachable by road`);
}
}
test_roads_level_the_movement_cost_across_terrain() {
const state = smallState();
const hill = state.landCells.find(
(coords) => state.tiles[key(coords.x, coords.y)].terrainType === "Hills"
);
this.assertNotNull(hill, "the map has a hill");
const unit = state._spawnUnit(hill, 0, state.protoUnits[0]);
const before = state._tileTravelHours(unit, hill);
state.roads.add(key(hill.x, hill.y));
const after = state._tileTravelHours(unit, hill);
this.assertLess(after, before, "a road makes the hill as cheap as plains");
this.assertApprox(
after,
ROADS.movementCostMultiplier / state._effectiveSpeedAt(unit, hill),
1e-9
);
}
test_greedy_spanner_drops_links_the_others_already_cover() {
// Four collinear points. The direct long links are covered by the chain of
// unit links, so only the three short ones survive.
const edges = [];
for (let a = 0; a < 4; a++) {
for (let b = a + 1; b < 4; b++) {
edges.push({ a, b, cost: b - a, path: [] });
}
}
edges.sort((x, y) => x.cost - y.cost);
const chosen = greedySpanner(edges, 2);
this.assertSize(chosen, 3, "only the short links are kept");
for (const edge of chosen) this.assertEqual(edge.cost, 1);
}
test_the_network_is_sparse() {
const state = smallState(["france", "britain", "poland", "slovenia"], 12345, { citiesPerCiv: 4 });
const network = buildRoadNetwork(state.cities, state.topology, state.tiles);
const cityKeys = state.cities.map((city) => key(city.coords.x, city.coords.y));
const allPairs = (state.cities.length * (state.cities.length - 1)) / 2;
this.assertGreater(allPairs, 6, "several candidate links exist");
this.assertGreater(network.size, cityKeys.length, "roads cross the land");
// 16 cities would need 120 links if every pair were connected; the spanner
// stays far below that even counting the tiles between cities.
this.assertLess(network.size, allPairs * 20, "the network stays sparse");
}
}