- the nation summary shows own-government approval, white in the neutral 40-60% band, red below and green above - the city and government panels keep a fixed size per tab, dock below the summary and no longer cover it, and their tab bar wraps instead of being clipped - propaganda campaigns grow exponentially harder near their opinion ceiling or floor, so the last percent are the hardest to win - memoised city, nation and matrix approval against the politics version, invalidated by territory redraws, so snapshots stop recomputing it every frame - extracted the duplicated modal tab wiring into modals/tabs.js - tests: campaign-difficulty, approval-colour and cache-invalidation cases (513 pass)
504 lines
20 KiB
JavaScript
504 lines
20 KiB
JavaScript
import { TestCase } from "./framework/test_case.js";
|
|
import { smallState } from "./framework/helpers.js";
|
|
import {
|
|
OPINION,
|
|
APPROVAL,
|
|
CAMPAIGN_ETHNICITY,
|
|
CAMPAIGN_GOVERNMENT,
|
|
POLICY_CHAMPION,
|
|
POLICY_EXPEL,
|
|
POLICY_IMMIGRATION,
|
|
POLICY_PROTECT,
|
|
PROPAGANDA,
|
|
WAR_CRIME,
|
|
} from "../shared/data/politics.js";
|
|
import { NEWS_POLICY } from "../shared/data/relations.js";
|
|
import { BUILDINGS } from "../shared/data.js";
|
|
import { HOURS_PER_YEAR, HOURS_PER_DAY } from "../shared/game_state/constants.js";
|
|
|
|
const BIG_BUDGET = 1.0e15;
|
|
|
|
function daysPass(state, days) {
|
|
for (let i = 0; i < days * HOURS_PER_DAY; i++) state.tickHour();
|
|
}
|
|
|
|
export class PoliticsModelTest extends TestCase {
|
|
test_opinions_start_neutral() {
|
|
const state = smallState();
|
|
this.assertApprox(state.getEthnicOpinion(0, 1), OPINION.baseline);
|
|
this.assertApprox(state.getGovOpinion(0, 1), OPINION.baseline);
|
|
this.assertApprox(state.getGovOpinion(1, 0), OPINION.baseline);
|
|
}
|
|
|
|
test_ethnic_makeup_sums_to_one_and_owner_leads() {
|
|
const state = smallState();
|
|
this.assertNotEmpty(state.tileEthnicity);
|
|
for (const [k, shares] of state.tileEthnicity) {
|
|
let total = 0;
|
|
let best = -1;
|
|
let bestShare = -1;
|
|
for (const [eth, share] of shares) {
|
|
total += share;
|
|
if (share > bestShare) {
|
|
bestShare = share;
|
|
best = eth;
|
|
}
|
|
}
|
|
this.assertApprox(total, 1, 1e-6, `shares of ${k} sum to one`);
|
|
const owner = state.territory.get(k);
|
|
if (owner !== undefined && owner >= 0) {
|
|
this.assertEqual(best, owner, `owner majority on ${k}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
test_territory_makeup_matches_tile_shares() {
|
|
const state = smallState();
|
|
const makeup = state.getCivEthnicMakeup(0);
|
|
let total = 0;
|
|
for (const entry of makeup) total += entry.share;
|
|
this.assertApprox(total, 1, 1e-6);
|
|
const own = makeup.find((entry) => entry.ethnicity === 0);
|
|
this.assertNotNull(own, "the nation's own ethnicity is present");
|
|
this.assertGreater(own.share, 0.5, "the majority ethnicity dominates");
|
|
}
|
|
|
|
test_campaign_moves_opinion_and_costs_money() {
|
|
const state = smallState();
|
|
state.budgets.set(0, BIG_BUDGET);
|
|
this.assertTrue(
|
|
state.requestCampaign(0, 0, CAMPAIGN_GOVERNMENT, 1, -1),
|
|
"campaign accepted"
|
|
);
|
|
this.assertGreater(state.getCampaignUpkeep(0), 0, "it accrues an hourly upkeep");
|
|
daysPass(state, 10);
|
|
this.assertLess(state.getGovOpinion(0, 1), OPINION.baseline, "opinion cooled");
|
|
}
|
|
|
|
test_campaign_spending_is_adjustable_and_zero_stops_it() {
|
|
const state = smallState();
|
|
state.budgets.set(0, BIG_BUDGET);
|
|
this.assertTrue(state.requestCampaign(0, 0, CAMPAIGN_GOVERNMENT, 1, 1));
|
|
const id = state.campaigns[0].id;
|
|
this.assertTrue(state.requestSetCampaignSpending(0, id, 20_000));
|
|
this.assertEqual(state.campaigns[0].hourlyCost, 20_000);
|
|
this.assertEqual(state.getCampaignUpkeep(0), 20_000);
|
|
this.assertTrue(state.requestSetCampaignSpending(0, id, 10_000_000));
|
|
this.assertEqual(state.campaigns[0].hourlyCost, PROPAGANDA.maxHourlyCost, "capped");
|
|
this.assertTrue(state.requestSetCampaignSpending(0, id, 0));
|
|
this.assertEmpty(state.campaigns, "spending nothing stops the campaign");
|
|
}
|
|
|
|
test_more_spending_moves_opinion_faster() {
|
|
const low = smallState();
|
|
low.budgets.set(0, BIG_BUDGET);
|
|
low.requestCampaign(0, 0, CAMPAIGN_GOVERNMENT, 1, 1);
|
|
low.requestSetCampaignSpending(0, low.campaigns[0].id, PROPAGANDA.defaultHourlyCost / 2);
|
|
const high = smallState();
|
|
high.budgets.set(0, BIG_BUDGET);
|
|
high.requestCampaign(0, 0, CAMPAIGN_GOVERNMENT, 1, 1);
|
|
high.requestSetCampaignSpending(0, high.campaigns[0].id, PROPAGANDA.defaultHourlyCost * 2);
|
|
daysPass(low, 10);
|
|
daysPass(high, 10);
|
|
this.assertGreater(
|
|
high.getGovOpinion(0, 1),
|
|
low.getGovOpinion(0, 1),
|
|
"the bigger spender warms the people faster"
|
|
);
|
|
}
|
|
|
|
test_campaign_warm_reverses_the_cooling() {
|
|
const state = smallState();
|
|
state.budgets.set(0, BIG_BUDGET);
|
|
state._shiftGovOpinion(0, 1, -0.2);
|
|
this.assertTrue(state.requestCampaign(0, 0, CAMPAIGN_GOVERNMENT, 1, 1));
|
|
daysPass(state, 10);
|
|
this.assertGreater(state.getGovOpinion(0, 1), OPINION.baseline - 0.2, "opinion warmed");
|
|
}
|
|
|
|
test_campaign_has_diminishing_returns_near_the_goal() {
|
|
const mid = smallState();
|
|
mid.budgets.set(0, BIG_BUDGET);
|
|
const midStart = mid.getGovOpinion(0, 1);
|
|
mid.requestCampaign(0, 0, CAMPAIGN_GOVERNMENT, 1, 1);
|
|
daysPass(mid, 10);
|
|
const midGain = mid.getGovOpinion(0, 1) - midStart;
|
|
|
|
const near = smallState();
|
|
near.budgets.set(0, BIG_BUDGET);
|
|
near._shiftGovOpinion(0, 1, 0.4);
|
|
const nearStart = near.getGovOpinion(0, 1);
|
|
near.requestCampaign(0, 0, CAMPAIGN_GOVERNMENT, 1, 1);
|
|
daysPass(near, 10);
|
|
const nearGain = near.getGovOpinion(0, 1) - nearStart;
|
|
|
|
this.assertGreater(nearGain, 0, "the campaign still moves a nearly convinced people");
|
|
this.assertLess(
|
|
nearGain,
|
|
midGain,
|
|
"the same spend wins less of the last stretch toward the goal"
|
|
);
|
|
}
|
|
|
|
test_cooling_campaign_also_slows_near_its_floor() {
|
|
const mid = smallState();
|
|
mid.budgets.set(0, BIG_BUDGET);
|
|
const midStart = mid.getGovOpinion(0, 1);
|
|
mid.requestCampaign(0, 0, CAMPAIGN_GOVERNMENT, 1, -1);
|
|
daysPass(mid, 10);
|
|
const midDrop = midStart - mid.getGovOpinion(0, 1);
|
|
|
|
const near = smallState();
|
|
near.budgets.set(0, BIG_BUDGET);
|
|
near._shiftGovOpinion(0, 1, -0.4);
|
|
const nearStart = near.getGovOpinion(0, 1);
|
|
near.requestCampaign(0, 0, CAMPAIGN_GOVERNMENT, 1, -1);
|
|
daysPass(near, 10);
|
|
const nearDrop = nearStart - near.getGovOpinion(0, 1);
|
|
|
|
this.assertGreater(nearDrop, 0, "a hated people is still pushed a little further");
|
|
this.assertLess(nearDrop, midDrop, "cooling likewise slows near the floor");
|
|
}
|
|
|
|
test_campaign_never_pushes_opinion_past_its_bound() {
|
|
const state = smallState();
|
|
state.budgets.set(0, BIG_BUDGET);
|
|
state._shiftGovOpinion(0, 1, 0.49);
|
|
state.requestCampaign(0, 0, CAMPAIGN_GOVERNMENT, 1, 1);
|
|
state.requestSetCampaignSpending(0, state.campaigns[0].id, PROPAGANDA.maxHourlyCost);
|
|
daysPass(state, 4000);
|
|
this.assertLess(
|
|
state.getGovOpinion(0, 1),
|
|
OPINION.max + 1e-9,
|
|
"opinion approaches but never passes its ceiling"
|
|
);
|
|
}
|
|
|
|
test_campaign_persists_until_cancelled() {
|
|
const state = smallState();
|
|
state.budgets.set(0, BIG_BUDGET);
|
|
this.assertTrue(state.requestCampaign(0, 0, CAMPAIGN_ETHNICITY, 1, 1));
|
|
daysPass(state, 60);
|
|
this.assertSize(state.campaigns, 1, "the campaign keeps running");
|
|
const id = state.campaigns[0].id;
|
|
this.assertTrue(state.requestCancelCampaign(0, id));
|
|
this.assertEmpty(state.campaigns);
|
|
this.assertFalse(state.requestCancelCampaign(0, id), "already stopped");
|
|
}
|
|
|
|
test_duplicate_campaign_is_refused() {
|
|
const state = smallState();
|
|
state.budgets.set(0, BIG_BUDGET);
|
|
this.assertTrue(state.requestCampaign(0, 0, CAMPAIGN_GOVERNMENT, 1, 1));
|
|
const before = state.getBudget(0);
|
|
this.assertFalse(
|
|
state.requestCampaign(0, 0, CAMPAIGN_GOVERNMENT, 1, -1),
|
|
"no duplicate, even in the opposite direction"
|
|
);
|
|
this.assertApprox(state.getBudget(0), before, 1e-9, "nothing was charged");
|
|
this.assertSize(state.campaigns, 1);
|
|
this.assertTrue(
|
|
state.requestCampaign(0, 0, CAMPAIGN_GOVERNMENT, 0, 1),
|
|
"a different target is allowed"
|
|
);
|
|
this.assertSize(state.campaigns, 2);
|
|
}
|
|
|
|
test_campaign_starts_without_an_upfront_cost() {
|
|
const state = smallState();
|
|
state.budgets.set(0, 0);
|
|
this.assertTrue(state.requestCampaign(0, 0, CAMPAIGN_GOVERNMENT, 1, -1));
|
|
this.assertSize(state.campaigns, 1);
|
|
this.assertEqual(state.getBudget(0), 0, "nothing was charged up front");
|
|
}
|
|
|
|
test_approval_cache_is_invalidated_by_territory_changes() {
|
|
const state = smallState();
|
|
// Make the two peoples feel very differently about civ 0's government.
|
|
state._shiftGovOpinion(0, 0, 0.5);
|
|
state._shiftGovOpinion(1, 0, -0.5);
|
|
const before = state.getCivApproval(0);
|
|
state.getCivApproval(0);
|
|
const tile = (state._territoryByCiv.get(1) || [])[0];
|
|
this.assertNotNull(tile, "civ 1 holds at least one tile");
|
|
state.territory.set(`${tile.x},${tile.y}`, 0);
|
|
state._reindexTerritory();
|
|
const after = state.getCivApproval(0);
|
|
this.assertLess(after, before, "the newly held, hostile people pull approval down");
|
|
}
|
|
|
|
test_low_approval_slows_building_and_taxes() {
|
|
const state = smallState();
|
|
const city = state.cities.find((c) => c.civ === 0);
|
|
const baseline = state.getCityEconomy(city).income;
|
|
for (let eth = 0; eth < state.civilisations.length; eth++) {
|
|
state._shiftGovOpinion(eth, 0, -0.5);
|
|
}
|
|
const approval = state.getCityApproval(city);
|
|
this.assertLess(approval, APPROVAL.rebellionThreshold, "the region turned against the government");
|
|
this.assertGreater(state.constructionApprovalMultiplier(city), 1, "building slows down");
|
|
this.assertLess(state.getCityEconomy(city).income, baseline, "the tax take drops");
|
|
this.assertLess(
|
|
state.taxApprovalMultiplier(city),
|
|
1,
|
|
"the tax multiplier is penalised"
|
|
);
|
|
}
|
|
|
|
test_war_on_a_friendly_government_costs_popularity() {
|
|
const state = smallState();
|
|
state._shiftGovOpinion(0, 1, 0.45);
|
|
state._shiftGovOpinion(1, 1, 0.45);
|
|
const before = state.getGovOpinion(1, 0);
|
|
this.assertTrue(state.requestDeclareWar(0, 1));
|
|
this.assertLess(state.getGovOpinion(1, 0), before, "the aggressor grew unpopular");
|
|
}
|
|
|
|
test_war_on_a_loathed_government_can_please() {
|
|
const state = smallState();
|
|
state._shiftGovOpinion(0, 1, -0.45);
|
|
const before = state.getGovOpinion(0, 0);
|
|
this.assertTrue(state.requestDeclareWar(0, 1));
|
|
this.assertGreater(state.getGovOpinion(0, 0), before, "the war was popular");
|
|
}
|
|
|
|
test_pillaging_is_a_long_remembered_war_crime() {
|
|
const state = smallState();
|
|
const unit = state.units.find((u) => u.civ === 0);
|
|
const target = state._territoryByCiv.get(1).find(
|
|
(coords) =>
|
|
!state.cityAt(coords) &&
|
|
state.unitsAt(coords).length === 0 &&
|
|
(state.tilePopulation.get(`${coords.x},${coords.y}`) || 0) > 0
|
|
);
|
|
this.assertNotNull(target, "an undefended enemy tile");
|
|
unit.coords = { x: target.x, y: target.y };
|
|
unit.path = [];
|
|
unit.pathIndex = 0;
|
|
const victim = state._dominantEthnicity(target);
|
|
const before = state.getGovOpinion(victim, 0);
|
|
this.assertTrue(state.requestPillage([unit.id]), "the pillage happened");
|
|
this.assertLess(state.getGovOpinion(victim, 0), before, "the aggressor is hated on the tile");
|
|
// The grudge outlives a generation.
|
|
state.totalHours = 24 * HOURS_PER_YEAR;
|
|
this.assertLess(
|
|
state.getGovOpinion(victim, 0),
|
|
before,
|
|
"the crime is still remembered 24 years on"
|
|
);
|
|
state.totalHours = WAR_CRIME.memoryYears * HOURS_PER_YEAR + 1;
|
|
state._expireGrudges();
|
|
this.assertApprox(
|
|
state.getGovOpinion(victim, 0),
|
|
before,
|
|
1e-9,
|
|
"the grudge has finally expired"
|
|
);
|
|
}
|
|
|
|
test_champion_policy_buys_minority_love_at_majority_cost() {
|
|
const state = smallState();
|
|
const minorityBefore = state.getGovOpinion(1, 0);
|
|
const majorityBefore = state.getGovOpinion(0, 0);
|
|
this.assertTrue(state.requestPolicy(0, POLICY_CHAMPION, 1));
|
|
this.assertTrue(state.hasPolicy(0, POLICY_CHAMPION, 1));
|
|
this.assertGreater(state.getGovOpinion(1, 0), minorityBefore);
|
|
this.assertLess(state.getGovOpinion(0, 0), majorityBefore);
|
|
}
|
|
|
|
test_expel_policy_moves_people_and_hurts_the_government() {
|
|
const state = smallState();
|
|
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]]));
|
|
const before = state.getGovOpinion(1, 0);
|
|
this.assertTrue(state.requestPolicy(0, POLICY_EXPEL, 1));
|
|
this.assertLess(state.getGovOpinion(1, 0), before, "expelling enrages the people");
|
|
const shareBefore = state.tileEthnicity.get(`${border.from.x},${border.from.y}`).get(1);
|
|
daysPass(state, 5);
|
|
const shareAfter = state.tileEthnicity.get(`${border.from.x},${border.from.y}`).get(1) || 0;
|
|
this.assertLess(shareAfter, shareBefore, "the expelled people are leaving");
|
|
}
|
|
|
|
test_immigration_policy_draws_people_from_unpopular_lands() {
|
|
const state = smallState();
|
|
const border = borderPair(state, 1, 0);
|
|
this.assertNotNull(border, "the two nations share a border");
|
|
// `from` is a Tile-1 tile rich in ethnicity 1; `to` is the Tile-0 tile next
|
|
// to it that the immigrants will move onto.
|
|
state._setTileEthnicFractions(border.from, new Map([[1, 0.9], [0, 0.1]]));
|
|
state._setTileEthnicFractions(border.to, new Map([[0, 0.9], [1, 0.1]]));
|
|
// Ethnicity 1 hates its own government, so its people emigrate.
|
|
state._shiftGovOpinion(1, 1, -0.45);
|
|
const target = border.to;
|
|
const before = fractionOf(state, target, 1);
|
|
this.assertTrue(state.requestPolicy(0, POLICY_IMMIGRATION, 1));
|
|
daysPass(state, 10);
|
|
this.assertGreater(fractionOf(state, target, 1), before, "the invited people arrived");
|
|
}
|
|
|
|
test_cultural_impact_is_zero_sum_and_wins_friends() {
|
|
const state = smallState();
|
|
const museum = BUILDINGS.findIndex((b) => b.id === "museum");
|
|
const city = state.cities.find((c) => c.civ === 0);
|
|
city.buildings[museum] = 1;
|
|
const impact0 = state.getCulturalImpact(0);
|
|
const impact1 = state.getCulturalImpact(1);
|
|
this.assertApprox(impact0 + impact1, 1, 1e-9, "shares add to one");
|
|
this.assertEqual(impact1, 0);
|
|
daysPass(state, 60);
|
|
this.assertGreater(state.getGovOpinion(1, 0), OPINION.baseline, "culture won the world over");
|
|
}
|
|
|
|
test_oppressed_minority_rises_up_for_its_own_nation() {
|
|
const state = smallState();
|
|
const tile = (state._territoryByCiv.get(0) || [])[0];
|
|
this.assertNotNull(tile);
|
|
state._setTileEthnicFractions(tile, new Map([[0, 0.4], [1, 0.6]]));
|
|
state._shiftGovOpinion(1, 0, -0.5);
|
|
state._shiftGovOpinion(1, 1, 0.45);
|
|
state._tickPolitics();
|
|
const rebels = state.units.filter((u) => u.civ === 1 && u.rebel);
|
|
this.assertNotEmpty(rebels, "the minority took up arms for its own nation");
|
|
this.assertEqual(
|
|
state.territory.get(`${rebels[0].coords.x},${rebels[0].coords.y}`),
|
|
0,
|
|
"the rebels rose inside the host nation"
|
|
);
|
|
}
|
|
|
|
test_snapshot_carries_politics() {
|
|
const state = smallState();
|
|
const snapshot = state.snapshot(0);
|
|
this.assertNotNull(snapshot.approval);
|
|
this.assertSize(snapshot.approval, 2);
|
|
this.assertNotNull(snapshot.tileEthnicity);
|
|
this.assertNotNull(snapshot.opinions);
|
|
this.assertNotNull(snapshot.opinions.ethnic);
|
|
this.assertNotNull(snapshot.policies);
|
|
this.assertNotNull(snapshot.policies[0].list);
|
|
this.assertNotNull(snapshot.migrations);
|
|
this.assertNotNull(snapshot.cultureImpact);
|
|
this.assertNotNull(snapshot.viewerStats.approval);
|
|
this.assertNotNull(snapshot.viewerStats.ethnicMakeup);
|
|
this.assertNotNull(snapshot.cityStats[0].ethnicMakeup);
|
|
}
|
|
|
|
test_duplicate_policy_is_refused_but_other_policies_are_allowed() {
|
|
const state = smallState();
|
|
this.assertTrue(state.requestPolicy(0, POLICY_CHAMPION, 1));
|
|
this.assertFalse(state.requestPolicy(0, POLICY_CHAMPION, 1), "no duplicate");
|
|
this.assertEqual(state.getPolicies(0).length, 1);
|
|
this.assertTrue(
|
|
state.requestPolicy(0, POLICY_PROTECT, 1),
|
|
"a different policy for the same people is fine"
|
|
);
|
|
this.assertEqual(state.getPolicies(0).length, 2);
|
|
}
|
|
|
|
test_revoke_policy_removes_it() {
|
|
const state = smallState();
|
|
this.assertTrue(state.requestPolicy(0, POLICY_CHAMPION, 1));
|
|
const policy = state.getPolicies(0)[0];
|
|
this.assertTrue(state.requestRevokePolicy(0, policy.id));
|
|
this.assertEmpty(state.getPolicies(0));
|
|
this.assertFalse(state.requestRevokePolicy(0, policy.id), "already gone");
|
|
}
|
|
|
|
test_enacting_a_policy_is_news() {
|
|
const state = smallState();
|
|
const before = state.getNews().length;
|
|
this.assertTrue(state.requestPolicy(0, POLICY_PROTECT, 1));
|
|
this.assertGreater(state.getNews().length, before);
|
|
const entry = state.getNews().find((item) => item.type === NEWS_POLICY);
|
|
this.assertNotNull(entry, "policy news recorded");
|
|
this.assertEqual(entry.civ, 0);
|
|
this.assertEqual(entry.policy, POLICY_PROTECT);
|
|
this.assertEqual(entry.ethnicity, 1);
|
|
}
|
|
|
|
test_running_campaigns_are_upkeep_in_the_budget() {
|
|
const state = smallState();
|
|
state.budgets.set(0, BIG_BUDGET);
|
|
const before = state.getPlayerUpkeep(0);
|
|
this.assertEqual(state.getCampaignUpkeep(0), 0);
|
|
this.assertTrue(state.requestCampaign(0, 0, CAMPAIGN_GOVERNMENT, 1, 1));
|
|
const cost = state.getCampaignUpkeep(0);
|
|
this.assertGreater(cost, 0);
|
|
this.assertApprox(state.getPlayerUpkeep(0) - before, cost);
|
|
this.assertGreater(state.budgetBreakdown(0).campaignUpkeep.total, 0);
|
|
}
|
|
|
|
test_migration_flows_are_recorded() {
|
|
const state = smallState();
|
|
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 left");
|
|
const arriving = state.migrations.get(1);
|
|
this.assertNotNull(arriving, "the neighbour has a flow");
|
|
this.assertGreater(arriving.inbound.get(1) || 0, 0, "people arrived");
|
|
}
|
|
|
|
test_makeup_counts_match_the_headline_population() {
|
|
const state = smallState();
|
|
const makeup = state.getCivEthnicMakeup(0);
|
|
const total = makeup.reduce((sum, entry) => sum + entry.count, 0);
|
|
this.assertApprox(total, state.getPlayerPopulation(0), 1, "counts match the shown population");
|
|
}
|
|
|
|
test_makeup_trends_follow_migration() {
|
|
const state = smallState();
|
|
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 home = state.getCivEthnicMakeup(0).find((entry) => entry.ethnicity === 1);
|
|
this.assertNotNull(home);
|
|
this.assertLess(home.trend, 0, "the expelled people fall at home");
|
|
const host = state.getCivEthnicMakeup(1).find((entry) => entry.ethnicity === 1);
|
|
this.assertNotNull(host);
|
|
this.assertGreater(host.trend, 0, "they rise in the neighbour");
|
|
}
|
|
|
|
test_tile_distribution_carries_count_and_trend() {
|
|
const state = smallState();
|
|
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 dist = state.getTileEthnicityDistribution(border.from);
|
|
const moved = dist.find((entry) => entry.ethnicity === 1);
|
|
this.assertNotNull(moved);
|
|
this.assertGreater(moved.count, 0, "the tile still counts that people");
|
|
this.assertLess(moved.trend, 0, "the tile lost some of them");
|
|
}
|
|
}
|
|
|
|
// 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)) {
|
|
const owner = state.territory.get(`${neighbour.x},${neighbour.y}`);
|
|
if (owner === toCiv) return { from: coords, to: neighbour };
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function fractionOf(state, coords, eth) {
|
|
const shares = state.tileEthnicity.get(`${coords.x},${coords.y}`);
|
|
return shares ? shares.get(eth) || 0 : 0;
|
|
}
|