Files
Battle-for-Tismo/tests/migration_test.js
T
adrien 1324128245 Showed idle trade routes and flagged migrant flows on the economic maps
The delivery graph now ships every route the viewer's own storage nodes could trade over, not just the legs that ran, and keeps exports as well as imports so goods leaving the nation appear. The map draws the idle routes grey beneath the active white legs, with a key entry for them.

The population map replaces its bare flow arrows with the arrows plus a disc textured with the origin nation's flag drifting from city to city, mirroring the merchandise that rides the trade graph.
2026-09-22 21:44:29 +02:00

149 lines
6.5 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");
this.assertEqual(link[5], from.civ, "the link names the origin nation for its flag");
}
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");
}
}