285 lines
12 KiB
JavaScript
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"
|
|
);
|
|
}
|
|
}
|