Files
Battle-for-Tismo/tests/roads_test.js
adrien ede258d39f Added warfare depth, focused research and a reworked HUD
- Tile improvements (fortifications, radar towers, coastal cannons), trenches,
  zone of control, support-unit capture and launch transports, with parabolic
  barrage arcs drawn between attacker and target.
- Focused research that pours points straight into the chosen technology, with
  per-technology progress kept when the focus changes, plus repeatable levels.
- Popularity reworked to integer points, war and pillage effects, minority
  policies and month-over-month arrows on the nation summary.
- City progress bars, square improvement buttons, a technology tab of wide
  short cards with progress bars, transport cargo readout and double-click to
  open a city panel.
2026-09-21 01:07:36 +02:00

112 lines
5.2 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, transportImprovement } 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_are_founded_on_a_railway_and_connected_by_the_network() {
const state = smallState(["france", "britain", "poland"], 12345, { citiesPerCiv: 3 });
const railwayName = transportImprovement("railway").name;
const cityKeys = state.cities.map((city) => key(city.coords.x, city.coords.y));
for (const city of state.cities) {
this.assertTrue(
city.improvements.includes(railwayName),
"a city is automatically improved with a railway"
);
}
for (const cityKey of cityKeys) this.assertTrue(state.railways.has(cityKey));
this.assertGreater(state.roads.size, 0, "roads also cross the land");
// Following the network from the first city must reach every other city.
const passable = (k) => state.roads.has(k) || state.railways.has(k);
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 (!passable(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 levels the hill below open ground");
this.assertApprox(
after,
ROADS.movementCostMultiplier / state._effectiveSpeedAt(unit, hill),
1e-9
);
}
test_roads_move_twice_as_fast_as_open_grass() {
const state = smallState();
const grass = state.landCells.find(
(coords) => state.tiles[key(coords.x, coords.y)].terrainType === "Land"
);
this.assertNotNull(grass, "the map has open grass");
const unit = state._spawnUnit(grass, 0, state.protoUnits[0]);
const open = state._tileTravelHours(unit, grass);
state.roads.add(key(grass.x, grass.y));
const road = state._tileTravelHours(unit, grass);
this.assertApprox(road * 2, open, 1e-9, "a road halves the travel time");
this.assertApprox(ROADS.movementCostMultiplier, 0.5, 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");
}
}