- 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.
180 lines
7.1 KiB
JavaScript
180 lines
7.1 KiB
JavaScript
import { TestCase } from "./framework/test_case.js";
|
|
import { smallState, cityOf } from "./framework/helpers.js";
|
|
import { key } from "../shared/hex.js";
|
|
import { TRANSPORT_BY_ID } from "../shared/data/improvements.js";
|
|
|
|
const BIG_BUDGET = 10_000_000_000;
|
|
|
|
// A land tile owned by civ 0 that carries no transport improvement yet.
|
|
function bareOwnedLand(state, civ = 0) {
|
|
return state.landCells.find((coords) => {
|
|
const k = key(coords.x, coords.y);
|
|
return state.civAt(coords) === civ && !state.roads.has(k) && !state.railways.has(k);
|
|
});
|
|
}
|
|
|
|
export class ImprovementsTest extends TestCase {
|
|
test_building_a_road_costs_money_and_lays_the_tile() {
|
|
const state = smallState();
|
|
state.budgets.set(0, BIG_BUDGET);
|
|
const coords = bareOwnedLand(state);
|
|
this.assertNotNull(coords, "the map has a bare owned land tile");
|
|
const k = key(coords.x, coords.y);
|
|
const before = state.getBudget(0);
|
|
this.assertTrue(state.requestBuildImprovement(0, coords, "road"));
|
|
this.assertTrue(state.roads.has(k), "the road is on the tile");
|
|
this.assertFalse(state.railways.has(k), "it is not a railway");
|
|
this.assertApprox(before - state.getBudget(0), TRANSPORT_BY_ID.road.buildCost);
|
|
}
|
|
|
|
test_a_railway_replaces_a_road_and_moves_twice_as_fast() {
|
|
const state = smallState();
|
|
state.budgets.set(0, BIG_BUDGET);
|
|
const coords = bareOwnedLand(state);
|
|
this.assertNotNull(coords);
|
|
const k = key(coords.x, coords.y);
|
|
const unit = state._spawnUnit(coords, 0, state.protoUnits[0]);
|
|
|
|
state.roads.add(k);
|
|
const roadHours = state._tileTravelHours(unit, coords);
|
|
this.assertTrue(state.requestBuildImprovement(0, coords, "railway"));
|
|
this.assertTrue(state.railways.has(k), "the railway is on the tile");
|
|
this.assertFalse(state.roads.has(k), "the road was replaced");
|
|
const railHours = state._tileTravelHours(unit, coords);
|
|
this.assertApprox(railHours * 2, roadHours, 1e-9, "a railway is 50% faster than a road");
|
|
}
|
|
|
|
test_a_railway_moves_four_times_as_fast_as_open_grass() {
|
|
const state = smallState();
|
|
state.budgets.set(0, BIG_BUDGET);
|
|
const coords = state.landCells.find(
|
|
(c) => state.tiles[key(c.x, c.y)].terrainType === "Land" && state.civAt(c) === 0
|
|
);
|
|
this.assertNotNull(coords, "the map has owned grass");
|
|
const unit = state._spawnUnit(coords, 0, state.protoUnits[0]);
|
|
state.roads.delete(key(coords.x, coords.y));
|
|
state.railways.delete(key(coords.x, coords.y));
|
|
const open = state._tileTravelHours(unit, coords);
|
|
this.assertTrue(state.requestBuildImprovement(0, coords, "railway"));
|
|
const rail = state._tileTravelHours(unit, coords);
|
|
this.assertApprox(rail * 4, open, 1e-9, "a railway is four times grass speed");
|
|
}
|
|
|
|
test_only_a_controlled_land_tile_may_be_improved() {
|
|
const state = smallState();
|
|
state.budgets.set(0, BIG_BUDGET);
|
|
const sea = Object.keys(state.tiles)
|
|
.map((k) => state.tiles[k])
|
|
.find((tile) => tile.terrainClass === "Sea");
|
|
this.assertNotNull(sea, "the map has a sea tile");
|
|
// The exact coords of the sea tile.
|
|
const seaEntry = Object.entries(state.tiles).find(([, tile]) => tile === sea);
|
|
const [sx, sy] = seaEntry[0].split(",").map(Number);
|
|
this.assertFalse(state.requestBuildImprovement(0, { x: sx, y: sy }, "road"), "no road at sea");
|
|
|
|
// A tile owned by another civilisation is off limits.
|
|
const foreign = state.landCells.find((c) => state.civAt(c) === 1);
|
|
if (foreign) {
|
|
this.assertFalse(
|
|
state.requestBuildImprovement(0, foreign, "road"),
|
|
"cannot build on a foreign tile"
|
|
);
|
|
}
|
|
// An unknown improvement id is refused.
|
|
const owned = bareOwnedLand(state);
|
|
this.assertFalse(state.requestBuildImprovement(0, owned, "monorail"));
|
|
}
|
|
|
|
test_the_same_improvement_cannot_be_built_twice() {
|
|
const state = smallState();
|
|
state.budgets.set(0, BIG_BUDGET);
|
|
const coords = bareOwnedLand(state);
|
|
this.assertTrue(state.requestBuildImprovement(0, coords, "road"));
|
|
const after = state.getBudget(0);
|
|
this.assertFalse(state.requestBuildImprovement(0, coords, "road"), "already a road");
|
|
this.assertApprox(state.getBudget(0), after, 1e-9, "nothing was charged");
|
|
}
|
|
|
|
test_poor_nations_cannot_build() {
|
|
const state = smallState();
|
|
state.budgets.set(0, 0);
|
|
const coords = bareOwnedLand(state);
|
|
this.assertFalse(state.requestBuildImprovement(0, coords, "road"));
|
|
this.assertFalse(state.roads.has(key(coords.x, coords.y)));
|
|
}
|
|
|
|
test_maintenance_counts_roads_and_railways_the_nation_holds() {
|
|
const state = smallState();
|
|
state.budgets.set(0, BIG_BUDGET);
|
|
const coords = bareOwnedLand(state);
|
|
const k = key(coords.x, coords.y);
|
|
const before = state.getTransportUpkeep(0);
|
|
this.assertTrue(state.requestBuildImprovement(0, coords, "road"));
|
|
this.assertApprox(
|
|
state.getTransportUpkeep(0) - before,
|
|
TRANSPORT_BY_ID.road.maintenance,
|
|
1e-9,
|
|
"a road adds its hourly maintenance"
|
|
);
|
|
this.assertTrue(state.requestBuildImprovement(0, coords, "railway"));
|
|
this.assertApprox(
|
|
state.getTransportUpkeep(0) - before,
|
|
TRANSPORT_BY_ID.railway.maintenance,
|
|
1e-9,
|
|
"the railway replaces the road's maintenance"
|
|
);
|
|
}
|
|
|
|
test_the_snapshot_ships_railways() {
|
|
const state = smallState();
|
|
state.budgets.set(0, BIG_BUDGET);
|
|
const coords = bareOwnedLand(state);
|
|
this.assertTrue(state.requestBuildImprovement(0, coords, "railway"));
|
|
const snapshot = state.snapshot(0);
|
|
this.assertTrue(
|
|
snapshot.railways.some(([x, y]) => x === coords.x && y === coords.y),
|
|
"the built railway is in the snapshot"
|
|
);
|
|
this.assertEqual(
|
|
snapshot.versions.improvements,
|
|
state._improvementVersion,
|
|
"the improvement version is shipped"
|
|
);
|
|
}
|
|
|
|
test_an_improvement_can_be_removed_for_free() {
|
|
const state = smallState();
|
|
state.budgets.set(0, BIG_BUDGET);
|
|
const coords = bareOwnedLand(state);
|
|
const k = key(coords.x, coords.y);
|
|
this.assertTrue(state.requestBuildImprovement(0, coords, "railway"));
|
|
const before = state.getBudget(0);
|
|
this.assertTrue(state.requestRemoveImprovement(0, coords), "the railway is removed");
|
|
this.assertFalse(state.railways.has(k), "the tile is bare again");
|
|
this.assertApprox(state.getBudget(0), before, 1e-9, "removal is free");
|
|
this.assertFalse(state.requestRemoveImprovement(0, coords), "nothing left to remove");
|
|
}
|
|
|
|
test_only_the_controller_may_remove_an_improvement() {
|
|
const state = smallState();
|
|
state.budgets.set(0, BIG_BUDGET);
|
|
const coords = bareOwnedLand(state);
|
|
this.assertTrue(state.requestBuildImprovement(0, coords, "road"));
|
|
this.assertFalse(
|
|
state.requestRemoveImprovement(1, coords),
|
|
"another nation cannot tear up the road"
|
|
);
|
|
this.assertTrue(state.roads.has(key(coords.x, coords.y)));
|
|
}
|
|
|
|
test_a_city_is_automatically_improved_with_a_railway() {
|
|
const state = smallState();
|
|
state.budgets.set(0, BIG_BUDGET);
|
|
const city = cityOf(state, 0);
|
|
const coords = { x: city.coords.x, y: city.coords.y };
|
|
const k = key(coords.x, coords.y);
|
|
this.assertTrue(state.railways.has(k), "a city carries a railway");
|
|
this.assertFalse(state.roads.has(k), "not a road");
|
|
}
|
|
}
|