Files
Battle-for-Tismo/tests/improvements_test.js

285 lines
12 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, TILE_IMPROVEMENT_BY_ID } from "../shared/data/improvements.js";
import { RESOURCE_RULES } from "../shared/data/resources.js";
import { tileImprovementResourceCost } from "../shared/resources.js";
const BIG_BUDGET = 10_000_000_000;
// A land tile owned by civ 0 that carries no transport improvement yet and is
// not already the site of a work.
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) &&
!state.constructionSiteAt(coords);
});
}
// Stocks every owned city so the material bill can be drawn from the stores.
function stockMaterials(state, civ = 0) {
for (const city of state.cities) {
if (city.civ !== civ) continue;
const stock = state.getCityResourceStock(city);
stock.steel = 1e9;
stock.hightech = 1e6;
stock.energy = 1e9;
}
}
// Runs the clock until every construction site has resolved (or the bound is
// hit, so a stalled site fails the caller's assertion rather than hanging).
function completeSites(state, maxHours = 50000) {
for (let i = 0; i < maxHours && state.constructionSites.size > 0; i++) {
state.advanceHour();
}
}
export class ImprovementsTest extends TestCase {
test_building_a_road_opens_a_site_then_lays_the_tile() {
const state = smallState();
state.budgets.set(0, BIG_BUDGET);
// Stock enough steel that the whole material bill is drawn from the stores;
// the treasury pays the build cost plus the steel it buys from the region.
stockMaterials(state);
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.assertNotNull(state.constructionSiteAt(coords), "a site is opened");
this.assertFalse(state.roads.has(k), "nothing is raised until the work is done");
// The forecast is handed to the site at once: the build cost plus the steel.
const materials = tileImprovementResourceCost(TRANSPORT_BY_ID.road);
const steelValue = materials.steel * state.getResourcePrice("steel");
this.assertApprox(
before - state.getBudget(0),
TRANSPORT_BY_ID.road.buildCost + steelValue,
1e-3,
"the forecast covers the build cost plus the steel the region supplied"
);
completeSites(state);
this.assertNull(state.constructionSiteAt(coords), "the work completed");
this.assertTrue(state.roads.has(k), "the road is on the tile");
this.assertFalse(state.railways.has(k), "it is not a railway");
}
test_a_railway_replaces_a_road_and_moves_twice_as_fast() {
const state = smallState();
state.budgets.set(0, BIG_BUDGET);
stockMaterials(state);
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"));
completeSites(state);
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);
stockMaterials(state);
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"));
completeSites(state);
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);
stockMaterials(state);
const coords = bareOwnedLand(state);
this.assertTrue(state.requestBuildImprovement(0, coords, "road"));
this.assertFalse(
state.requestBuildImprovement(0, coords, "road"),
"a work is already under way"
);
completeSites(state);
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_a_road_can_be_built_on_an_empty_treasury() {
const state = smallState();
state.budgets.set(0, 0);
stockMaterials(state);
const coords = bareOwnedLand(state);
// The government can always pay, so the work goes ahead on borrowed money.
this.assertTrue(state.requestBuildImprovement(0, coords, "road"));
this.assertLess(state.getBudget(0), 0, "the forecast became central-bank debt");
completeSites(state);
this.assertTrue(state.roads.has(key(coords.x, coords.y)));
}
test_a_negative_treasury_never_refuses_a_work() {
const state = smallState();
state.budgets.set(0, -1_000_000);
stockMaterials(state);
const coords = bareOwnedLand(state);
// A treasury already in the red borrows still more from the central bank;
// the order is never refused for want of money.
this.assertTrue(state.requestBuildImprovement(0, coords, "road"), "the work is still ordered");
this.assertLess(state.getBudget(0), -1_000_000, "the shortfall deepened the debt");
completeSites(state);
this.assertTrue(state.roads.has(key(coords.x, coords.y)), "the road was built");
}
test_maintenance_counts_roads_and_railways_the_nation_holds() {
const state = smallState();
state.budgets.set(0, BIG_BUDGET);
stockMaterials(state);
const coords = bareOwnedLand(state);
const before = state.getTransportUpkeep(0);
this.assertTrue(state.requestBuildImprovement(0, coords, "road"));
completeSites(state);
this.assertApprox(
state.getTransportUpkeep(0) - before,
RESOURCE_RULES.transportUpkeepSteel.road,
1e-9,
"a road adds its daily steel wear"
);
this.assertTrue(state.requestBuildImprovement(0, coords, "railway"));
completeSites(state);
this.assertApprox(
state.getTransportUpkeep(0) - before,
RESOURCE_RULES.transportUpkeepSteel.railway,
1e-9,
"the railway replaces the road's wear"
);
}
test_the_snapshot_ships_railways() {
const state = smallState();
state.budgets.set(0, BIG_BUDGET);
stockMaterials(state);
const coords = bareOwnedLand(state);
this.assertTrue(state.requestBuildImprovement(0, coords, "railway"));
completeSites(state);
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);
stockMaterials(state);
const coords = bareOwnedLand(state);
const k = key(coords.x, coords.y);
this.assertTrue(state.requestBuildImprovement(0, coords, "railway"));
completeSites(state);
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);
stockMaterials(state);
const coords = bareOwnedLand(state);
this.assertTrue(state.requestBuildImprovement(0, coords, "road"));
completeSites(state);
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");
}
test_a_tile_work_costs_steel_energy_and_high_tech() {
const state = smallState();
state.budgets.set(0, 1.0e15);
// Enough stores that the whole material bill is drawn from them; the
// treasury pays the build cost plus the steel and high-tech it buys from the
// region. Construction electricity is drawn from the grid as it runs.
stockMaterials(state);
const coords = state.landCells.find((cell) => {
const k = key(cell.x, cell.y);
return state.civAt(cell) === 0 && !state.cityAt(cell) && !state.tileImprovements.has(k);
});
this.assertNotNull(coords, "the player owns a bare land tile");
// A radar tower is advanced, so it also spends high-tech.
const radar = TILE_IMPROVEMENT_BY_ID.radar_tower;
const materials = tileImprovementResourceCost(radar);
this.assertGreater(materials.steel, 0, "steel is spent");
this.assertGreater(materials.energy, 0, "energy is drawn");
this.assertGreater(materials.hightech, 0, "high-tech is spent");
const before = state.getBudget(0);
this.assertTrue(state.requestBuildTileImprovement(0, coords, "radar_tower"));
const steelValue = materials.steel * state.getResourcePrice("steel");
const techValue = materials.hightech * state.getResourcePrice("hightech");
this.assertApprox(
before - state.getBudget(0),
radar.buildCost + steelValue + techValue,
1e-3,
"the build cost plus the materials the region supplied"
);
}
}