The simple economy is now the default and the old simulation is frozen as the hard model, selectable per game. A nation keeps one resource pool available in every region, fed by public production works rather than private per-region stores, and construction and training begin at once, paced by the region's spare power instead of gathering materials over days. Money is euro only: a per-inhabitant daily tax funds a treasury spent in a global market, where buy and sell lots trade against the world stock and move its price. The hard economy keeps its per-nation currencies, central banks and exchange rates. Simple games skip the price-settling warmup and economic migration; the snapshot carries the economy model, the market and the pool figures the client draws. Added the matching test suites and the design briefs under docs/simple_economy.
181 lines
6.2 KiB
JavaScript
181 lines
6.2 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"],
|
|
// The integration fixtures do not care about the shape of the world, and
|
|
// the full default map costs about half a second to generate per fixture.
|
|
mapConfig: smallConfig(),
|
|
...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,
|
|
};
|
|
}
|
|
|
|
// The historical fixtures run the **hard** economy explicitly. The model
|
|
// defaults to simple everywhere, but these suites pin the original per-region
|
|
// simulation so their assertions stay valid; simple-economy tests use
|
|
// `simpleState` below.
|
|
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, { economyModel: "hard" });
|
|
return state;
|
|
}
|
|
|
|
// A configured state on the small fixture map running the simple economy: one
|
|
// resource pool per nation and public production buildings.
|
|
export function simpleState(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, { economyModel: "simple" });
|
|
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, { economyModel: "hard" });
|
|
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,
|
|
};
|
|
}
|