Files
Battle-for-Tismo/tests/migration_test.js
T
adrien 6f73e2cc51 Reworked the world around a fixed sea share and settled coasts
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.
2026-09-22 17:00:38 +02:00

148 lines
6.4 KiB
JavaScript

import { TestCase } from "./framework/test_case.js";
import { smallState, cityOf } from "./framework/helpers.js";
import { key } from "../shared/hex.js";
export class MigrationTest extends TestCase {
test_food_output_comes_from_the_land_not_the_headcount() {
const state = smallState();
const city = cityOf(state, 0);
const before = state._regionProductionValue(city);
// Pack many more people onto the same land: the harvest must not change.
for (const coords of state.regionTiles(city)) {
const k = key(coords.x, coords.y);
state.tilePopulation.set(k, (state.tilePopulation.get(k) || 0) * 4 + 1);
}
state._gdpPerCapitaCache.clear();
const after = state._regionProductionValue(city);
this.assertApprox(after, before, 1e-6, "the land yields the same however many live on it");
}
test_regions_of_a_nation_open_with_equal_income_per_head() {
const state = smallState();
for (let civ = 0; civ < state.civilisations.length; civ++) {
const cities = state.cities.filter((city) => city.civ === civ);
if (cities.length < 2) continue;
const perHead = cities.map((city) => {
const economy = state.getCityEconomy(city);
return economy.population > 0 ? economy.gdp / economy.population : 0;
});
const highest = Math.max(...perHead);
const lowest = Math.min(...perHead);
this.assertTrue(
highest > 0 && (highest - lowest) / highest <= 0.02,
`regions of civ ${civ} open level`
);
}
}
test_moving_people_conserves_the_total() {
const state = smallState();
const cities = state.cities.filter((city) => city.civ === 0);
this.assertGreaterOrEqual(cities.length, 2, "the fixture has two regions");
const from = cities[0];
const to = cities[1];
const fromBefore = state.regionPopulation(from);
const toBefore = state.regionPopulation(to);
const moved = state._movePopulation(from, to, 100_000, false);
this.assertApprox(moved, 100_000, 1);
this.assertApprox(state.regionPopulation(from), fromBefore - moved, 1e-6);
this.assertApprox(state.regionPopulation(to), toBefore + moved, 1e-6);
}
test_people_move_toward_the_richer_region() {
const state = smallState();
const cities = state.cities.filter((city) => city.civ === 0);
const rich = cities[0];
const poor = cities[1];
// Dense on one side, sparse on the other: with land-based food that makes
// the sparse region the rich one.
for (const coords of state.regionTiles(rich)) {
state.tilePopulation.set(key(coords.x, coords.y), 1_000);
}
for (const coords of state.regionTiles(poor)) {
state.tilePopulation.set(key(coords.x, coords.y), 5_000_000);
}
state._gdpPerCapitaCache.clear();
const richBefore = state.regionPopulation(rich);
const poorBefore = state.regionPopulation(poor);
state._migrateForIncome();
this.assertGreater(state.regionPopulation(rich), richBefore, "people moved to the rich region");
this.assertLess(state.regionPopulation(poor), poorBefore, "and left the poor one");
}
test_a_region_only_moves_to_a_much_richer_one() {
const state = smallState();
// Migration levelled the regions at generation, so no one is twice as rich
// as anyone else and nobody moves.
const before = state.cities.map((city) => state.regionPopulation(city));
state._migrateForIncome();
const after = state.cities.map((city) => state.regionPopulation(city));
for (let i = 0; i < before.length; i++) {
this.assertApprox(after[i], before[i], 1e-6, "nobody moved to a merely equal region");
}
}
test_an_airport_caps_its_daily_intake() {
const state = smallState();
const cities = state.cities;
const from = cityOf(state, 0);
const to = cities.find((city) => city.civ === 1);
// Every region is dense and equally poor except `to`, which is sparse and
// so far richer. Only `to` is worth moving to.
for (const city of cities) {
const people = city.id === to.id ? 1_000 : 5_000_000;
for (const coords of state.regionTiles(city)) {
state.tilePopulation.set(key(coords.x, coords.y), people);
}
}
state._gdpPerCapitaCache.clear();
const airport = state.protoBuildings.findIndex((b) => b.id === "airport");
from.buildings[airport] = 1;
to.buildings[airport] = 1;
const fromBefore = state.regionPopulation(from);
state._migrateForIncome();
const lost = fromBefore - state.regionPopulation(from);
this.assertGreater(lost, 0, "people flew to the rich region");
// 1000 people per airport level a day, and the route's level is 1.
this.assertTrue(lost <= 1_001, `the flight carried at most its capacity, lost ${lost}`);
}
test_the_migration_graph_tallies_city_to_city_flows() {
const state = smallState();
const from = cityOf(state, 0);
const to = state.cities.find((city) => city.id !== from.id);
this.assertNotNull(from, "the fixture has an origin");
this.assertNotNull(to, "and a destination");
const moved = state._movePopulation(from, to, 100_000, true);
this.assertGreater(moved, 0, "people moved");
const graph = state.snapshot(0).migrationGraph;
this.assertHas(graph, "links");
const link = graph.links.find(
(entry) =>
entry[0] === from.coords.x && entry[1] === from.coords.y &&
entry[2] === to.coords.x && entry[3] === to.coords.y
);
this.assertNotNull(link, "the city-to-city flow was tallied");
this.assertApprox(link[4], moved, 1, "the link carries everyone who moved");
}
test_the_migration_graph_rolls_over_a_month() {
const state = smallState();
const from = cityOf(state, 0);
const to = state.cities.find((city) => city.id !== from.id);
state._movePopulation(from, to, 100_000, true);
this.assertNotEmpty(state.snapshot(0).migrationGraph.links, "the flow is on the graph");
// Each day archives the last, so a month's worth of empties eventually
// pushes the only flow out of the rolling window.
for (let day = 0; day < 31; day++) state._tickMigration();
this.assertEmpty(state.snapshot(0).migrationGraph.links, "the month-old flow has fallen off");
}
test_internal_equalisation_does_not_pollute_the_migration_graph() {
// The start-of-game levelling moved people with recording off, so the
// population map opens with no migration arrows.
const state = smallState();
this.assertEmpty(state.snapshot(0).migrationGraph.links, "the opening graph is empty");
}
}