142 lines
6.6 KiB
JavaScript
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");
|
|
}
|
|
}
|