Files
Battle-for-Tismo/tests/budget_test.js

225 lines
10 KiB
JavaScript

import { TestCase } from "./framework/test_case.js";
import { smallState } from "./framework/helpers.js";
import { HOURS_PER_DAY } from "../shared/game_state/constants.js";
import { BUDGET_HISTORY_MONTHS } from "../shared/game_state/budget.js";
import { key } from "../shared/hex.js";
// Plants a state-owned work on a free land tile, so the day's works upkeep is
// billed to the treasury rather than settled by a private producer's cash.
function placeFortification(state, civ = 0) {
const coords = state.landCells.find((c) => {
if (state.civAt(c) !== civ || state.cityAt(c)) return false;
const k = key(c.x, c.y);
return !state.tileImprovements.has(k) && !state.roads.has(k) && !state.railways.has(k);
});
const k = key(coords.x, coords.y);
state.tileImprovements.set(k, "fortifications");
state.tileImprovementOwner.set(k, civ);
state.tileImprovementHp.set(k, 1000);
}
export class BudgetTest extends TestCase {
test_the_opening_month_is_the_only_report() {
const state = smallState();
const months = state.budgetMonthReports(0);
this.assertSize(months, 1, "only the live month exists at the start");
this.assertTrue(months[0].isCurrent, "the live month is flagged current");
this.assertEqual(months[0].label, "January 2000");
this.assertEqual(months[0].expenses.length, 0, "nothing has been spent yet");
this.assertEqual(months[0].net, 0, "the month opens balanced");
}
test_spending_is_filed_under_its_category() {
const state = smallState();
state._spendBudget(0, 1_000, "training");
state._spendBudget(0, 4_000, "construction");
state._recordTax(0, "sales", "steel", 9_000);
const report = state.budgetMonthReports(0).at(-1);
const byId = new Map(report.expenses.map((entry) => [entry.id, entry]));
this.assertApprox(byId.get("training").cash, 1_000);
this.assertApprox(byId.get("construction").cash, 4_000);
this.assertApprox(report.income, 9_000);
this.assertApprox(report.totalCash, 5_000);
this.assertApprox(report.net, 4_000);
}
test_a_spend_on_an_empty_treasury_becomes_debt_and_is_recorded() {
const state = smallState();
state.budgets.set(0, 0);
// The government always pays; the overdraft is the central bank's loan.
this.assertTrue(state._spendBudget(0, 500, "training"));
this.assertLess(state.getBudget(0), 0, "the treasury went into the red");
const report = state.budgetMonthReports(0).at(-1);
const training = report.expenses.find((entry) => entry.id === "training");
this.assertApprox(training.cash, 500);
}
test_a_refund_cancels_the_category() {
const state = smallState();
state._spendBudget(0, 1_000, "construction");
state._refundBudget(0, 1_000, "construction");
const report = state.budgetMonthReports(0).at(-1);
this.assertEqual(report.expenses.length, 0, "a refunded order does not linger");
this.assertEqual(report.net, 0);
}
test_daily_upkeep_records_cash_and_market_value() {
const state = smallState();
placeFortification(state);
state.advanceHour();
for (let i = 0; i < 23; i++) state.advanceHour();
const report = state.budgetMonthReports(0).at(-1);
const byId = new Map(report.expenses.map((entry) => [entry.id, entry]));
for (const id of ["buildings", "units", "works", "transport"]) {
const entry = byId.get(id);
this.assertNotNull(entry, `${id} upkeep is recorded`);
this.assertTrue(entry.economic >= entry.cash - 1e-6, `${id} market value covers its cash`);
}
}
test_rolling_a_month_freezes_a_digest_and_resets() {
const state = smallState();
state._spendBudget(0, 2_000, "training");
// January 2000 has 31 days.
for (let i = 0; i < 31 * HOURS_PER_DAY; i++) state.tickHour();
const months = state.budgetMonthReports(0);
this.assertSize(months, 2, "January is frozen behind the live February");
this.assertEqual(months[0].label, "January 2000");
this.assertTrue(!months[0].isCurrent, "the frozen month is not current");
this.assertEqual(months[1].label, "February 2000");
this.assertTrue(months[1].isCurrent, "the live month is February");
const training = months[0].expenses.find((entry) => entry.id === "training");
this.assertApprox(training.cash, 2_000, 1e-6);
this.assertEqual(
months[1].expenses.find((entry) => entry.id === "training"),
undefined,
"the new month starts empty"
);
}
test_the_history_is_capped() {
const state = smallState();
for (let i = 0; i < BUDGET_HISTORY_MONTHS + 5; i++) state._rolloverBudget(24000 + i);
this.assertSize(state.budgetHistory, BUDGET_HISTORY_MONTHS, "only two years are kept");
}
test_resource_costs_are_their_own_budget_line() {
const state = smallState();
state._recordResourceSpend(0, "energy", 2_400);
state._recordResourceSpend(0, "goods", 480, "steel");
state._recordResourceSpend(0, "transport", 240);
const breakdown = state.budgetBreakdown(0);
const byId = new Map(breakdown.resourceCosts.map((entry) => [entry.id, entry]));
this.assertApprox(byId.get("energy").amount, 100, 1e-9, "the day's power bill, hourly");
this.assertApprox(byId.get("goods").amount, 20, 1e-9);
this.assertApprox(byId.get("transport").amount, 10, 1e-9);
this.assertEqual(byId.get("combat"), undefined, "an unused category is left out");
}
test_resource_trade_is_mirrored_into_the_month() { const state = smallState();
state._recordResourceSpend(0, "goods", 5_000, "steel");
state._recordResourceEarn(0, "steel", 2_000);
const report = state.budgetMonthReports(0).at(-1);
const trade = report.resourceTrade.find((entry) => entry.id === "steel");
this.assertNotNull(trade);
this.assertApprox(trade.bought, 5_000);
this.assertApprox(trade.sold, 2_000);
}
test_expense_rows_carry_their_resource_detail() {
const state = smallState();
state._recordBudgetCash(0, "buildings", -500);
state._recordBudgetCategoryResource(0, "buildings", "steel", 300);
state._recordBudgetCategoryResource(0, "buildings", "hightech", 200);
const report = state.budgetMonthReports(0).at(-1);
const buildings = report.expenses.find((entry) => entry.id === "buildings");
this.assertApprox(buildings.cash, 500);
this.assertEqual(buildings.buys.length, 2, "both resources are listed");
const steel = buildings.buys.find((entry) => entry[0] === "steel");
this.assertApprox(steel[1], 300);
}
test_expense_rows_carry_the_quantity_that_was_bought() {
const state = smallState();
state._recordBudgetCash(0, "tilebuild", -1_000);
state._recordBudgetCategoryResource(0, "tilebuild", "steel", 1_000, "Road", 50);
const report = state.budgetMonthReports(0).at(-1);
const tilebuild = report.expenses.find((entry) => entry.id === "tilebuild");
const steel = tilebuild.buys.find((entry) => entry[0] === "steel");
this.assertApprox(steel[1], 1_000);
this.assertApprox(steel[3], 50, 1e-9, "the quantity travels with the money");
this.assertApprox(steel[1] / steel[3], 20, 1e-9, "so the price per unit can be read");
this.assertApprox(steel[2][0][2], 50, 1e-9, "the source carries its quantity too");
}
test_expense_rows_name_the_buildings_that_consumed_a_resource() {
const state = smallState();
state._recordBudgetCash(0, "buildings", -500);
state._recordBudgetCategoryResource(0, "buildings", "steel", 300, "Barracks");
state._recordBudgetCategoryResource(0, "buildings", "steel", 100, "Factory");
const report = state.budgetMonthReports(0).at(-1);
const buildings = report.expenses.find((entry) => entry.id === "buildings");
const steel = buildings.buys.find((entry) => entry[0] === "steel");
this.assertApprox(steel[1], 400);
const sources = new Map(steel[2]);
this.assertApprox(sources.get("Barracks"), 300);
this.assertApprox(sources.get("Factory"), 100);
}
test_upkeep_records_the_resources_it_bought() {
const state = smallState();
// Empty the viewer's stores and stock a foreign region, so the day's upkeep
// has to be bought across the border.
for (const city of state.cities) {
const store = state.getCityResourceStock(city);
if (city.civ === 0) {
store.steel = 0;
store.hightech = 0;
} else {
store.steel = 1.0e9;
store.hightech = 1.0e9;
}
}
state.budgets.set(0, 1.0e15);
for (let i = 0; i < HOURS_PER_DAY; i++) state.tickHour();
const report = state.budgetMonthReports(0).at(-1);
const buildings = report.expenses.find((entry) => entry.id === "buildings");
this.assertNotNull(buildings, "building upkeep is recorded");
this.assertGreater(buildings.buys.length, 0, "the row names the resource it bought");
const steel = buildings.buys.find((entry) => entry[0] === "steel");
this.assertGreater(steel[3], 0, "the row knows how much steel it wore");
const sourceQty = steel[2].reduce((sum, source) => sum + (source[2] || 0), 0);
this.assertApprox(sourceQty, steel[3], 1e-9, "the sources carry the worn quantity too");
}
test_upkeep_splits_the_worn_quantity_across_its_sources() {
const state = smallState();
state._recordBudgetCash(0, "transport", -900);
state._recordResourceBySource(
0, "transport", "steel", 900,
[{ label: "Roads", steel: 2 }, { label: "Railways", steel: 1 }],
"steel", 300
);
const report = state.budgetMonthReports(0).at(-1);
const transport = report.expenses.find((entry) => entry.id === "transport");
const steel = transport.buys.find((entry) => entry[0] === "steel");
this.assertApprox(steel[3], 300, 1e-9, "the resource line carries the total quantity");
const sources = new Map(steel[2].map((source) => [source[0], source]));
this.assertApprox(sources.get("Roads")[1], 600, 1e-9);
this.assertApprox(sources.get("Roads")[2], 200, 1e-9, "two thirds of the wear");
this.assertApprox(sources.get("Railways")[1], 300, 1e-9);
this.assertApprox(sources.get("Railways")[2], 100, 1e-9, "one third of the wear");
}
test_the_snapshot_ships_the_month_reports() {
const state = smallState();
const snapshot = state.snapshot(0);
const months = snapshot.viewerStats.budgetMonths;
this.assertGreaterOrEqual(months.length, 1);
const current = months[months.length - 1];
this.assertTrue(current.isCurrent, "the last shipped report is the live month");
this.assertTrue(Array.isArray(current.expenses), "the report carries its expenses");
this.assertEqual(current.label, "January 2000", "the shipped month is named");
}
}