- 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.
112 lines
5.2 KiB
JavaScript
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");
|
|
}
|
|
}
|