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

142 lines
6.6 KiB
JavaScript

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");
}
}