World generation now aims at a fixed share of the ice-free tiles (60%) instead of a minimum continent count, bisecting the water level for it rather than sweeping every step, and still joins the ocean into one body. Ice and tundra are the outermost rows only, scaled down on small maps so the fixtures keep usable land. Cities score their site, coastal plains best and landlocked worst, avoiding tundra and desert. Every nation that holds a coast gets a port city on each island it owns, and any landmass left empty is settled by the nearest nation, so no land starts unowned. A coastal city opens with a port already built. Bundles the in-progress work in the tree: the commodity monthly price series and inflation the central bank's chart reads, and the rolling city-to-city migration graph the population map draws as flow arrows.
164 lines
5.3 KiB
JavaScript
164 lines
5.3 KiB
JavaScript
// Shared fixtures for the test suite. A small deterministic map is used
|
|
// throughout so tests are fast and their results stable.
|
|
|
|
import { GameState } from "../../shared/game_state.js";
|
|
import { parseKey } from "../../shared/hex.js";
|
|
import { MAP_CONFIG, CIVILISATIONS } from "../../shared/data.js";
|
|
import { startServer } from "../../server/server.js";
|
|
|
|
// Boots the real server on an ephemeral port for an integration test, then
|
|
// guarantees it is closed afterwards. The server configures its game at
|
|
// startup, so a small deterministic roster keeps the fixtures fast and stable.
|
|
export async function withServer(fn, options = {}) {
|
|
const server = startServer({
|
|
port: 0,
|
|
bind: "127.0.0.1",
|
|
log: () => {},
|
|
seed: SEED,
|
|
civilisations: ["france", "britain"],
|
|
...options,
|
|
});
|
|
const address = await server.listen;
|
|
try {
|
|
await fn(address.port, server);
|
|
} finally {
|
|
server.close();
|
|
}
|
|
}
|
|
|
|
export const SEED = 12345;
|
|
export const SEA = "Sea";
|
|
|
|
export function smallConfig(overrides = {}) {
|
|
return {
|
|
...MAP_CONFIG,
|
|
mapSize: { x: 24, y: 16 },
|
|
minCapitalLandmass: 1,
|
|
minCapitalDistance: 2,
|
|
citiesPerCiv: 2,
|
|
citySpacing: 2,
|
|
// The fixtures stay one continent: splitting them changes where capitals
|
|
// and cities fall, which the tests pin down.
|
|
minContinents: 1,
|
|
// The tiny fixture has no room to run the chip foundries' grids, so its
|
|
// nations open without one; tests that want them opt back in.
|
|
startingChipFabs: 0,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
export function smallState(civIds = ["france", "britain"], seed = SEED, overrides = {}) {
|
|
const state = new GameState();
|
|
state.mapConfig = smallConfig(overrides);
|
|
const civs = civIds.map((id) => CIVILISATIONS.find((c) => c.id === id));
|
|
state.configure(civs, seed);
|
|
return state;
|
|
}
|
|
|
|
// A state on the full default map, so its coordinates line up with the terrain
|
|
// the browser view regenerates from MAP_CONFIG.
|
|
export function defaultState(civIds = ["france", "britain"], seed = SEED) {
|
|
const state = new GameState();
|
|
const civs = civIds.map((id) => CIVILISATIONS.find((c) => c.id === id));
|
|
state.configure(civs, seed);
|
|
return state;
|
|
}
|
|
|
|
// The six render layers of the world map, as jQuery objects.
|
|
export function mapLayers($) {
|
|
return {
|
|
terrain: $("#layer-terrain"),
|
|
roads: $("#layer-roads"),
|
|
borders: $("#layer-borders"),
|
|
fog: $("#layer-fog"),
|
|
highlight: $("#layer-highlight"),
|
|
paths: $("#layer-paths"),
|
|
targets: $("#layer-targets"),
|
|
resources: $("#layer-resources"),
|
|
entities: $("#layer-entities"),
|
|
labels: $("#layer-labels"),
|
|
};
|
|
}
|
|
|
|
export function adjacentLand(state, unit) {
|
|
for (const neighbour of state.topology.neighbours(unit.coords.x, unit.coords.y)) {
|
|
if (state._isLand(neighbour)) return neighbour;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function seaTile(state) {
|
|
for (const k in state.tiles) {
|
|
if (state.tiles[k].terrainClass === SEA) return parseKey(k);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function cityOf(state, civ) {
|
|
return state.cities.find((city) => city.civ === civ) || null;
|
|
}
|
|
|
|
// A city on an inland tile, for the tests that need one to check the coastal
|
|
// placement rules. The generator very strongly prefers the coast, so a small
|
|
// fixture may well have none; this settles a spare inland tile if it has to.
|
|
export function settleInlandCity(state, civ = 0, accept = null) {
|
|
const existing = state.cities.find(
|
|
(city) => city.civ === civ && !state.isCoastalCity(city) && (!accept || accept(city.coords))
|
|
);
|
|
if (existing) return existing;
|
|
for (const coords of state.landCells) {
|
|
if (state.cityAt(coords) || state.unitAt(coords)) continue;
|
|
if (state.isCoastalCoords(coords)) continue;
|
|
if (accept && !accept(coords)) continue;
|
|
const city = state._spawnCity({ civIndex: civ, coords, isCapital: false });
|
|
state._rebuildCityIndex();
|
|
state._recomputeTerritory();
|
|
return city;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Gives a city a level of a building directly, so a test that trains a unit
|
|
// gated behind a building does not have to walk the construction queue first.
|
|
export function grantBuilding(state, city, id, level = 1) {
|
|
const index = state.protoBuildings.findIndex((building) => building.id === id);
|
|
if (index < 0) return -1;
|
|
city.buildings[index] = level;
|
|
state._gdpPerCapitaCache.clear();
|
|
state._touchModifiers();
|
|
return index;
|
|
}
|
|
|
|
export function unitsOf(state, civ) {
|
|
return state.units.filter((unit) => unit.civ === civ).length;
|
|
}
|
|
|
|
// Removes the port every naval city opens with, so a GDP-sensitive test starts
|
|
// from the same terrain-only economy it would have had before ports were seeded.
|
|
export function stripPorts(state, civ = null) {
|
|
const index = state.protoBuildings.findIndex((building) => building.id === "port");
|
|
if (index < 0) return;
|
|
for (const city of state.cities) {
|
|
if (civ !== null && city.civ !== civ) continue;
|
|
if (city.buildings[index]) delete city.buildings[index];
|
|
}
|
|
state._gdpPerCapitaCache.clear();
|
|
state._touchModifiers();
|
|
}
|
|
|
|
export function snapshotUnit(state, unitId, viewer = 0) {
|
|
return (state.snapshot(viewer).units || []).find((u) => u.id === unitId) || null;
|
|
}
|
|
|
|
export function gdpBuilding(overrides = {}) {
|
|
return {
|
|
id: "test_market",
|
|
name: "Test market",
|
|
baseCost: 100,
|
|
costAlpha: 2.0,
|
|
refundFraction: 0.5,
|
|
effects: [{ stat: "production", baseValue: 0.5, exponent: 1.0 }],
|
|
...overrides,
|
|
};
|
|
}
|