import { TestCase } from "./framework/test_case.js"; import { smallState, cityOf } from "./framework/helpers.js"; import { key } from "../shared/hex.js"; import { TAXES } from "../shared/data.js"; import { HOURS_PER_DAY } from "../shared/game_state/constants.js"; // Trade taxes are the government's only revenue, so these lock in who pays // what: a region pays a sales tax on home purchases, a foreign buyer pays an // export tariff to the seller's government, and a region pays an import tariff // on the national currency it spends buying a foreign currency. // A buyer with a road to its land neighbours, so crafted suppliers are // reachable over the transport network. function reachableBuyer(state) { const buyer = cityOf(state, 0); const land = state.topology.neighbours(buyer.coords.x, buyer.coords.y) .filter((n) => state._isLand(n)); for (const n of land) state.roads.add(key(n.x, n.y)); return { buyer, land }; } export class TaxesTest extends TestCase { test_rates_default_and_override_per_resource() { const state = smallState(); this.assertApprox(state.getTaxRate(0, "sales"), TAXES.defaultSales); this.assertApprox(state.getTaxRate(0, "export"), TAXES.defaultExport); this.assertApprox(state.getTaxRate(0, "import"), TAXES.defaultImport); state.setTaxRate(0, "sales", 0.3); this.assertApprox(state.getTaxRate(0, "sales"), 0.3); this.assertApprox(state.getTaxRate(0, "sales", "food"), 0.3); state.setTaxRate(0, "sales", 0.1, "steel"); this.assertApprox(state.getTaxRate(0, "sales", "steel"), 0.1); this.assertApprox(state.getTaxRate(0, "sales", "food"), 0.3); // A change is clamped to the catalogue ceiling. state.setTaxRate(0, "export", 5); this.assertApprox(state.getTaxRate(0, "export"), TAXES.maxRate); } test_a_sales_tax_falls_on_a_home_purchase() { const state = smallState(); const { buyer, land } = reachableBuyer(state); const homeNode = { coords: land[0], civ: 0, stock: { food: 1e9 }, kind: "building" }; const buyerStock = state.getCityResourceStock(buyer); buyerStock.food = 0; const treasuryBefore = state.getBudget(0); const bought = state._procureFromNeighbours(buyer, "food", 1000, buyerStock, [homeNode]); this.assertGreater(bought, 0, "the home supplier sold food"); const tax = state.getTaxTake(0).sales; this.assertGreater(tax, 0, "the sales tax was charged"); this.assertApprox(state.getBudget(0) - treasuryBefore, tax, 1e-6, "the tax reached the treasury"); } test_an_export_tariff_falls_on_a_foreign_buyer() { const state = smallState(); const { buyer, land } = reachableBuyer(state); const foreignNode = { coords: land[0], civ: 1, stock: { food: 1e9 }, kind: "building" }; const buyerStock = state.getCityResourceStock(buyer); buyerStock.food = 0; const treasuryBefore = state.getBudget(1); const bought = state._procureFromNeighbours(buyer, "food", 1000, buyerStock, [foreignNode]); this.assertGreater(bought, 0, "the foreign region sold food"); const tax = state.getTaxTake(1).export; this.assertGreater(tax, 0, "the export tariff was charged"); this.assertApprox( state.getBudget(1) - treasuryBefore, tax, 1e-6, "the tariff reached the seller's treasury" ); this.assertEqual(state.getTaxTake(1).sales, 0, "no sales tax crosses a border"); } test_an_import_tariff_falls_on_the_buyer_currency_spent() { const state = smallState(); const { buyer, land } = reachableBuyer(state); const foreignNode = { coords: land[0], civ: 1, stock: { food: 1e9 }, kind: "building" }; const buyerStock = state.getCityResourceStock(buyer); buyerStock.food = 0; const treasuryBefore = state.getBudget(0); const bought = state._procureFromNeighbours(buyer, "food", 1000, buyerStock, [foreignNode]); this.assertGreater(bought, 0, "the foreign region sold food"); const tax = state.getTaxTake(0).import; this.assertGreater(tax, 0, "the import tariff was charged"); this.assertApprox( state.getBudget(0) - treasuryBefore, tax, 1e-6, "the tariff reached the buyer's treasury" ); } test_holding_the_foreign_currency_avoids_the_import_tariff() { const state = smallState(); const { buyer, land } = reachableBuyer(state); const foreignNode = { coords: land[0], civ: 1, stock: { food: 1e9 }, kind: "building" }; const buyerStock = state.getCityResourceStock(buyer); buyerStock.food = 0; // Fill the buyer's basket with the seller's currency: nothing of the buyer's // own currency need be spent, so nothing is taxed. const code = state.currencyOf(1).code; state.getRegionCash(buyer).set(code, 1e12); const bought = state._procureFromNeighbours(buyer, "food", 1000, buyerStock, [foreignNode]); this.assertGreater(bought, 0, "the foreign region sold food"); this.assertApprox(state.getTaxTake(0).import, 0, 1e-6, "no currency was spent, no tariff"); } test_only_tariff_changes_are_news() { const state = smallState(); const before = state.news.length; state.requestSetTaxRate(0, "sales", 0.3); state.requestSetTaxRate(0, "import", 0.08); state.requestSetTaxRate(0, "export", 0.07, "steel"); const news = state.news.slice(before); this.assertSize(news.filter((entry) => entry.type === "tariff"), 2, "two tariff notices"); const kinds = news.map((entry) => entry.kind).sort(); this.assertEqual(kinds.join(","), "export,import"); } test_commodity_inflation_is_weighted_by_consumption() { const state = smallState(); const previous = new Map(); for (const id of state.resourcePrices.keys()) { previous.set(id, state.getResourcePrice(id)); } // Steel is consumed (weight 100) and doubles; nothing else is demanded. const market = new Map(); for (const id of previous.keys()) market.set(id, { id, demand: 0 }); market.get("steel").demand = 100; state.setResourcePrice("steel", state.getResourcePrice("steel") * 2); state._recordCommodityInflation(market, previous); this.assertApprox(state.commodityInflation, 1.0, 1e-9, "a doubling reads as 100%"); } test_viewer_stats_carry_taxes_and_inflation() { const state = smallState(); state.setTaxRate(0, "sales", 0.25); state._recordTax(0, "sales", "steel", 480); const stats = state.viewerStats(0); this.assertApprox(stats.taxes.sales.rate, 0.25); this.assertApprox(stats.taxTake.sales, 480); this.assertEqual(typeof stats.commodityInflation, "number"); for (let h = 0; h < HOURS_PER_DAY; h++) state.advanceHour(); const market = state.snapshot(0).resourceMarket; this.assertTrue(market.every((entry) => typeof entry.prevPrice === "number"), "every market row ships yesterday's price"); } }