Files
Battle-for-Tismo/tests/simple_migration_test.js
adrien d5305da449 Gave the world a simple economy: one pool per nation and a global market
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.
2026-09-25 14:58:49 +02:00

132 lines
5.7 KiB
JavaScript

// 08-migrations.md, tested from the 11-tests.md plan: the simple economy
// retires only the two *economic* migration mechanisms -- the daily income-ratio
// sweep (`_migrateForIncome`) and the opening income-levelling pass
// (`_equalizeRegionIncomes`) -- while political/policy flows and natural growth
// carry on. The hard model's income migration stays pinned by
// `migration_test.js`.
import { TestCase } from "./framework/test_case.js";
import { simpleState, smallState } from "./framework/helpers.js";
import { key } from "../shared/hex.js";
import { POLICY_EXPEL } from "../shared/data/politics.js";
// A pair of neighbouring tiles where `from` is owned by `fromCiv` and the
// neighbour `to` is owned by a different civilisation.
function borderPair(state, fromCiv, toCiv) {
for (const coords of state._territoryByCiv.get(fromCiv) || []) {
for (const neighbour of state._neighbours(coords)) {
if (state.territory.get(key(neighbour.x, neighbour.y)) === toCiv) {
return { from: coords, to: neighbour };
}
}
}
return null;
}
// A sparse (so rich) region beside a packed (so poor) one. The hard income
// sweep sends people out of the packed region toward the sparse one.
function splitRichAndPoor(state) {
const cities = state.cities.filter((city) => city.civ === 0);
const rich = cities[0];
const poor = cities[1];
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();
return { rich, poor };
}
export class SimpleMigrationTest extends TestCase {
// Two regions with very different income do not exchange people: the daily
// sweep is a no-op, so thirty days of politics leave every region untouched.
test_two_regions_with_different_income_do_not_exchange_people() {
const state = simpleState();
splitRichAndPoor(state);
const before = state.cities.map((city) => state.regionPopulation(city));
for (let day = 0; day < 30; day++) state._tickPolitics();
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, "no income-driven movement");
}
}
// The two overrides themselves are the decision: called directly with a
// glaring income gap, neither moves a single person.
test_the_income_sweep_and_the_levelling_pass_are_no_ops() {
const state = simpleState();
const { rich, poor } = splitRichAndPoor(state);
let moved = 0;
const realMove = state._movePopulation.bind(state);
state._movePopulation = (...args) => {
moved += 1;
return realMove(...args);
};
state._migrateForIncome();
this.assertEqual(moved, 0, "the income sweep moved nobody");
const before = state.cities.map((city) => state.regionPopulation(city));
state._equalizeRegionIncomes();
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, "the levelling pass moved nobody");
}
// The gap is still there: nothing quietly flattened it.
this.assertGreater(
state.regionPopulation(poor),
state.regionPopulation(rich),
"the regions keep their different sizes"
);
delete state._movePopulation;
}
// Natural population growth is not migration and must survive: the simple
// model only removes the income flows.
test_natural_growth_still_applies() {
const state = simpleState();
const before = state.getPlayerPopulation(0);
state._tickPopulation();
this.assertGreater(state.getPlayerPopulation(0), before, "a day of natural growth");
}
// Option A keeps political migration in the simple economy: the income sweep
// is a no-op, but an expelling nation still drives a people across the border.
test_political_migration_still_applies() {
const state = simpleState();
const border = borderPair(state, 0, 1);
this.assertNotNull(border, "the two nations share a border");
state._setTileEthnicFractions(border.from, new Map([[0, 0.6], [1, 0.4]]));
state._setTileEthnicFractions(border.to, new Map([[1, 0.9], [0, 0.1]]));
this.assertTrue(state.requestPolicy(0, POLICY_EXPEL, 1));
state._tickMigration();
const leaving = state.migrations.get(0);
this.assertNotNull(leaving, "the expelling country has a flow");
this.assertGreater(leaving.outbound.get(1) || 0, 0, "people still leave under a policy");
}
// The migration snapshot fields stay present and empty rather than going
// missing, so the client's migration graph keeps its shape.
test_the_migration_snapshot_is_present_and_empty() {
const state = simpleState();
const snap = state.snapshot(0);
this.assertHas(snap, "migrations");
this.assertEqual(snap.migrations.length, state.civilisations.length, "one entry per nation");
this.assertHas(snap, "migrationGraph");
this.assertEmpty(snap.migrationGraph.links, "the opening graph draws no flows");
}
// The hard model is the control: the same sparse-next-to-packed setup does
// send people to the rich region, so the simple no-op is a real difference.
test_the_hard_model_still_moves_people_for_income() {
const state = smallState();
const { rich, poor } = splitRichAndPoor(state);
const richBefore = state.regionPopulation(rich);
const poorBefore = state.regionPopulation(poor);
state._migrateForIncome();
this.assertGreater(state.regionPopulation(rich), richBefore, "the hard rich region gained");
this.assertLess(state.regionPopulation(poor), poorBefore, "the hard poor region lost");
}
}