Files
Battle-for-Tismo/shared/game_state/money.js
T

550 lines
20 KiB
JavaScript

// Money and currencies. Each nation issues its own currency; a central bank
// sets an interest rate and holds foreign reserves. The private sector is the
// regions: every region keeps its own cash reserve, a basket of currencies, and
// chases profit trading with its neighbours.
//
// Stage 1 keeps every currency at par (value 1 against the global unit) and
// every region's cash in its own national currency. Later stages let the rates
// float, fill the baskets with foreign money and move real trade onto private
// cash; the shapes here (baskets, reserves, rates) are already the final ones.
import { MONEY } from "../data/economy.js";
import { DAYS_PER_YEAR } from "./constants.js";
const FALLBACK_CURRENCY = { code: "XAU", symbol: "¤", name: "Credit" };
function emptyBasket() {
return new Map();
}
export const moneyMethods = {
// ------------------------------------------------------- setup --
_initMoney() {
this.regionCash = new Map();
// What each region owes its national central bank, in the national currency.
this.regionDebt = new Map();
// What each nation's central bank owes foreign central banks, by currency:
// the offsetting side of the currency it issues when it settles trade in a
// foreign seller's money.
this.centralBankDebt = new Map();
this.centralBankRate = new Map();
this.centralBankReserves = new Map();
this.currencyValues = new Map();
this._currencyDemand = new Map();
this._currencyCivMap = null;
for (let civ = 0; civ < this.civilisations.length; civ++) {
this.centralBankRate.set(civ, MONEY.defaultInterestRate);
this.centralBankReserves.set(civ, emptyBasket());
this.centralBankDebt.set(civ, emptyBasket());
this.currencyValues.set(civ, 1);
this._currencyDemand.set(civ, 0);
}
for (const city of this.cities) {
this.regionCash.set(city.id, this._openingRegionBasket(city));
}
},
// A region opens with a private-sector cash reserve in its nation's currency,
// sized off its yearly GDP.
_openingRegionBasket(city) {
const economy = this.getCityEconomy(city);
const cash = Math.max(0, economy.gdp) * MONEY.privateCashFraction;
return new Map([[this.currencyOf(city.civ).code, cash]]);
},
// ---------------------------------------------------- currencies --
currencyOf(civ) {
const civEntry = this.civilisations[civ];
return (civEntry && civEntry.currency) || FALLBACK_CURRENCY;
},
// The price of one unit of a nation's currency in global units. Every
// currency starts at par and floats from there.
currencyValue(civ) {
if (!this.currencyValues) return 1;
return this.currencyValues.get(civ) || 1;
},
setCurrencyValue(civ, value) {
const floor = 1 - MONEY.maxRateDrift;
const ceiling = 1 + MONEY.maxRateDrift;
this.currencyValues.set(civ, Math.max(floor, Math.min(ceiling, value)));
},
_civByCurrencyCode() {
if (!this._currencyCivMap) {
this._currencyCivMap = new Map();
this.civilisations.forEach((entry, index) => {
if (entry.currency) this._currencyCivMap.set(entry.currency.code, index);
});
}
return this._currencyCivMap;
},
currencyValueOf(code) {
const civ = this._civByCurrencyCode().get(code);
return civ === undefined ? 1 : this.currencyValue(civ);
},
// How many units of `toCiv`'s currency one unit of `fromCiv`'s buys.
exchangeRate(fromCiv, toCiv) {
if (fromCiv === toCiv) return 1;
return this.currencyValue(fromCiv) / this.currencyValue(toCiv);
},
// --------------------------------------------------- region cash --
// Takes `amount` of one currency out of a region's basket. Returns false (and
// charges nothing) when the region does not hold enough.
_debitRegionCash(city, code, amount) {
if (!(amount > 0)) return true;
const basket = this.regionCash ? this.regionCash.get(city.id) : null;
if (!basket) return false;
const held = basket.get(code) || 0;
if (held + 1e-9 < amount) return false;
basket.set(code, held - amount);
return true;
},
_creditRegionCash(city, code, amount) {
if (!(amount > 0)) return;
const basket = this.regionCash ? this.regionCash.get(city.id) : null;
if (!basket) return;
basket.set(code, (basket.get(code) || 0) + amount);
},
// Charges a region, spending its cash and borrowing whatever is missing from
// its national central bank at the bank's rate. A region never fails to pay:
// it takes on debt instead.
_chargeRegionCash(city, code, amount) {
if (!(amount > 0)) return true;
const basket = this.regionCash ? this.regionCash.get(city.id) : null;
if (!basket) return false;
const held = basket.get(code) || 0;
if (held >= amount) {
basket.set(code, held - amount);
return true;
}
basket.set(code, 0);
const shortfall = amount - held;
this.regionDebt.set(city.id, (this.regionDebt.get(city.id) || 0) + shortfall);
return true;
},
// Pays money into a nation's regions, split by population: the private sector
// is what ultimately receives government spending, so the money stays in the
// system rather than vanishing.
_creditRegions(civ, amount, code = null) {
if (!(amount > 0)) return;
const cities = this.cities.filter((city) => city.civ === civ);
if (cities.length === 0) return;
const currency = code || this.currencyOf(civ).code;
const shares = this._populationShares(cities);
for (let i = 0; i < cities.length; i++) {
this._creditRegionCash(cities[i], currency, amount * shares[i]);
}
},
// Takes money from a nation's regions, split by population, borrowing from the
// central bank where a region is short. This is how taxes are collected
// without minting money.
_chargeRegions(civ, amount) {
if (!(amount > 0)) return;
const cities = this.cities.filter((city) => city.civ === civ);
if (cities.length === 0) return;
const currency = this.currencyOf(civ).code;
const shares = this._populationShares(cities);
for (let i = 0; i < cities.length; i++) {
this._chargeRegionCash(cities[i], currency, amount * shares[i]);
}
},
// Each region's share of a set's total population, defaulting to an even split
// when there are no people at all.
_populationShares(cities) {
let total = 0;
const populations = cities.map((city) => {
const people = this.regionPopulation(city);
total += people;
return people;
});
if (total <= 0) return cities.map(() => 1 / cities.length);
return populations.map((people) => people / total);
},
getRegionDebt(city) {
return this.regionDebt ? (this.regionDebt.get(city.id) || 0) : 0;
},
// Everything the nation's regions owe their central bank.
getRegionDebtTotal(civ) {
let total = 0;
for (const city of this.cities) {
if (city.civ === civ) total += this.getRegionDebt(city);
}
return total;
},
// The central bank's outstanding loans: the government's overdraft, every
// region's borrowing, and what it has issued to settle foreign trade.
getCentralBankLoans(civ) {
return this.getGovernmentDebt(civ) + this.getRegionDebtTotal(civ) +
this._centralBankDebtValue(civ);
},
// The city of `civ` nearest `coords`, so a sale to a resource building can be
// paid into the private cash of the region that owns it.
_regionCityNear(civ, coords) {
let best = null;
let bestDistance = Infinity;
for (const city of this.cities) {
if (city.civ !== civ) continue;
const distance = this.topology.tileDistance(city.coords, coords);
if (distance < bestDistance) {
bestDistance = distance;
best = city;
}
}
return best;
},
// --------------------------------------------------- central bank --
_addReserve(civ, code, amount) {
if (!(amount > 0)) return;
const reserves = this.centralBankReserves.get(civ)
|| (this.centralBankReserves.set(civ, emptyBasket()), this.centralBankReserves.get(civ));
reserves.set(code, (reserves.get(code) || 0) + amount);
},
// The debt a central bank takes on when it issues its currency to settle a
// foreign seller's bill. It is the loan side of the money the sale created, so
// the world's money is conserved.
_addCentralBankDebt(civ, code, amount) {
if (!(amount > 0)) return;
let debts = this.centralBankDebt.get(civ);
if (!debts) {
debts = emptyBasket();
this.centralBankDebt.set(civ, debts);
}
debts.set(code, (debts.get(code) || 0) + amount);
},
getCentralBankDebt(civ) {
return this.centralBankDebt ? (this.centralBankDebt.get(civ) || emptyBasket()) : emptyBasket();
},
// What a central bank owes foreign banks, in its own currency.
_centralBankDebtValue(civ) {
const value = this.currencyValue(civ);
let total = 0;
for (const [code, amount] of this.getCentralBankDebt(civ)) {
total += amount * this.currencyValueOf(code) / value;
}
return total;
},
// A buyer acquires `globalValue` (in global units) worth of `targetCiv`'s
// currency: first from what it already holds, then from that nation's central
// bank, paying with its own national currency (borrowing from its own bank if
// it is short). The target's central bank keeps the buyer's currency as a
// foreign reserve. Returns the amount of the target currency acquired, plus
// the buyer currency actually spent on it (the held portion is free).
//
// `charge` overrides who pays the national-currency cost: a region does by
// default, but a government buys with the treasury. `useHeld` is false for the
// treasury, which holds no basket of its own.
_regionAcquireCurrency(buyerCity, targetCiv, globalValue, charge = null, useHeld = true) {
if (!(globalValue > 0)) return { code: this.currencyOf(targetCiv).code, amount: 0 };
const target = this.currencyOf(targetCiv);
const buyer = this.currencyOf(buyerCity.civ);
const vTarget = this.currencyValue(targetCiv);
const vBuyer = this.currencyValue(buyerCity.civ);
const needTarget = globalValue / vTarget;
const basket = useHeld ? this.regionCash.get(buyerCity.id) : null;
const heldTarget = basket ? basket.get(target.code) || 0 : 0;
const fromHeld = Math.min(heldTarget, needTarget);
const remaining = needTarget - fromHeld;
// The buyer currency actually spent acquiring the foreign currency. The
// held portion costs nothing, so an import tariff can fall only on what was
// really exchanged.
let spent = 0;
if (remaining > 0) {
const costBuyer = remaining * vTarget / vBuyer;
spent = costBuyer;
// The buyer pays with its national currency, borrowing the rest from its
// own central bank if it is short; the seller's bank holds that currency
// as a reserve and issues its own, taking on the matching debt so no money
// is conjured from nothing.
if (charge) charge(costBuyer);
else this._chargeRegionCash(buyerCity, buyer.code, costBuyer);
this._addReserve(targetCiv, buyer.code, costBuyer);
this._addCentralBankDebt(targetCiv, target.code, remaining);
}
if (fromHeld > 0) basket.set(target.code, heldTarget - fromHeld);
return { code: target.code, amount: fromHeld + remaining, spent };
},
// --------------------------------------------------- demand/rates --
_recordCurrencyDemand(civ, amount) {
if (!this._currencyDemand || civ < 0 || !(amount !== 0)) return;
this._currencyDemand.set(civ, (this._currencyDemand.get(civ) || 0) + amount);
},
// All of a nation's currency in circulation, in global units: its own
// treasury, every region's holding and every central bank's reserve.
_currencyMass(civ) {
const code = this.currencyOf(civ).code;
const value = this.currencyValue(civ);
// An overdrawn treasury is a claim on the bank, not circulating money.
let amount = Math.max(0, this.getBudget(civ) || 0);
for (const city of this.cities) amount += this.getRegionCash(city).get(code) || 0;
for (const reserves of this.centralBankReserves.values()) {
amount += reserves.get(code) || 0;
}
return Math.max(1, amount * value);
},
// ----------------------------------------------------- holdings --
getRegionCash(city) {
if (!this.regionCash) return emptyBasket();
return this.regionCash.get(city.id) || emptyBasket();
},
getCentralBankReserves(civ) {
if (!this.centralBankReserves) return emptyBasket();
return this.centralBankReserves.get(civ) || emptyBasket();
},
getCentralBankRate(civ) {
if (!this.centralBankRate) return MONEY.defaultInterestRate;
const rate = this.centralBankRate.get(civ);
return rate === undefined ? MONEY.defaultInterestRate : rate;
},
setCentralBankRate(civ, rate) {
const value = Math.max(0, Math.min(MONEY.maxInterestRate, Number(rate) || 0));
this.centralBankRate.set(civ, value);
},
// A player sets the central bank's interest rate: the rate regions and the
// government borrow at, compounding on their debts.
requestSetInterestRate(civ, rate) {
if (civ < 0 || civ >= this.civilisations.length) return false;
const value = Number(rate);
if (!Number.isFinite(value) || value < 0) return false;
this.setCentralBankRate(civ, value);
this._emitChanged();
return true;
},
// The value of a basket in `inCiv`'s currency at the current rates.
_basketValue(basket, inCiv = 0) {
let total = 0;
const denom = this.currencyValue(inCiv);
for (const [code, amount] of basket) {
total += amount * this.currencyValueOf(code) / denom;
}
return total;
},
// The private-sector cash of one region, expressed in a nation's currency.
getRegionCashValue(city, inCiv = city.civ) {
return this._basketValue(this.getRegionCash(city), inCiv);
},
// Everything the private sector of a nation holds, in that nation's currency.
getPrivateSectorCash(civ, inCiv = civ) {
let total = 0;
for (const city of this.cities) {
if (city.civ !== civ) continue;
total += this._basketValue(this.getRegionCash(city), inCiv);
}
return total;
},
// Every place a nation's own currency is stored: its treasury, its own
// regions' cash, any foreign region still holding some, and every central
// bank that keeps it as a reserve. Foreign holdings are what exports earned.
getCurrencyHoldings(civ) {
const currency = this.currencyOf(civ);
const code = currency.code;
const own = [];
const foreign = [];
let ownTotal = 0;
let foreignTotal = 0;
for (const city of this.cities) {
const amount = this.getRegionCash(city).get(code) || 0;
if (!(amount > 0)) continue;
const host = this.civilisations[city.civ];
const entry = {
cityId: city.id,
name: city.name,
civ: city.civ,
civName: host ? host.name : "",
amount,
};
if (city.civ === civ) {
own.push(entry);
ownTotal += amount;
} else {
foreign.push(entry);
foreignTotal += amount;
}
}
const banks = [];
let bankTotal = 0;
for (let c = 0; c < this.civilisations.length; c++) {
const amount = this.getCentralBankReserves(c).get(code) || 0;
if (!(amount > 0)) continue;
const host = this.civilisations[c];
banks.push({ civ: c, name: host ? host.name : "", amount });
bankTotal += amount;
}
const budget = this.getBudget(civ) || 0;
const treasury = Math.max(0, budget);
const treasuryDebt = Math.max(0, -budget);
return {
code,
symbol: currency.symbol,
name: currency.name,
value: this.currencyValue(civ),
treasury,
treasuryDebt,
own,
ownTotal,
foreign,
foreignTotal,
banks,
bankTotal,
total: treasury + ownTotal + foreignTotal + bankTotal,
};
},
// --------------------------------------------------- daily tick --
// The day's monetary work: debts to the central bank accrue interest, regions
// repatriate any foreign money they still hold and pay down what they owe, and
// the day's trading leaves each currency's value drifting with net demand. The
// interest rate is the borrowing cost, not a direct inflation lever.
_tickMoney() {
if (!this.regionCash) return;
for (const city of this.cities) {
const basket = this.regionCash.get(city.id);
const rate = this.getCentralBankRate(city.civ) / DAYS_PER_YEAR;
// Interest compounds on whatever the region already owes.
const debt = this.regionDebt.get(city.id) || 0;
if (debt > 0) this.regionDebt.set(city.id, debt * (1 + rate));
if (!basket) continue;
this._repatriateRegion(city, basket);
this._repayRegionDebt(city, basket);
}
for (let civ = 0; civ < this.civilisations.length; civ++) {
// A treasury in the red is an overdraft that compounds too.
const budget = this.getBudget(civ);
if (budget < 0) {
const rate = this.getCentralBankRate(civ) / DAYS_PER_YEAR;
this.budgets.set(civ, budget * (1 + rate));
}
const demand = this._currencyDemand.get(civ) || 0;
const mass = this._currencyMass(civ);
const value = this.currencyValue(civ) * (1 + MONEY.demandSensitivity * (demand / mass));
this.setCurrencyValue(civ, value);
this._currencyDemand.set(civ, 0);
}
},
// A region uses any cash it has to pay down its central-bank debt, so the
// interest does not run away.
_repayRegionDebt(city, basket) {
const debt = this.regionDebt.get(city.id) || 0;
if (!(debt > 0)) return;
const code = this.currencyOf(city.civ).code;
const cash = basket.get(code) || 0;
const repay = Math.min(cash, debt);
if (!(repay > 0)) return;
basket.set(code, cash - repay);
this.regionDebt.set(city.id, debt - repay);
},
// A region turns a slice of every foreign currency it holds back into its own;
// the private sector prefers to carry national money. The conversion is priced
// through the global desk at today's rates.
_repatriateRegion(city, basket) {
const national = this.currencyOf(city.civ).code;
let changed = false;
for (const [code, amount] of Array.from(basket)) {
if (code === national || !(amount > 0)) continue;
const move = amount * MONEY.repatriationPerDay;
if (!(move > 0)) continue;
const value = move * this.currencyValueOf(code);
basket.set(code, amount - move);
const issuer = this._civByCurrencyCode().get(code);
if (issuer !== undefined) this._recordCurrencyDemand(issuer, -value);
this._recordCurrencyDemand(city.civ, value);
this._creditRegionCash(city, national, value / this.currencyValue(city.civ));
changed = true;
}
if (changed) this.regionCash.set(city.id, basket);
},
// ------------------------------------------------- serialization --
_serializeCurrencies() {
return this.civilisations.map((_, civ) => {
const currency = this.currencyOf(civ);
return {
civ,
code: currency.code,
symbol: currency.symbol,
name: currency.name,
value: this.currencyValue(civ),
};
});
},
// The exchange rate from the viewer's currency to every other nation's, for
// the national Economy tab.
_serializeExchangeRates(viewerCiv) {
return this.civilisations.map((_, civ) => {
const currency = this.currencyOf(civ);
return {
civ,
code: currency.code,
symbol: currency.symbol,
name: currency.name,
rate: this.exchangeRate(viewerCiv, civ),
};
});
},
_serializeCentralBank(civ) {
const reserves = [];
for (const [code, amount] of this.getCentralBankReserves(civ)) {
if (!(amount > 0)) continue;
const issuer = this._civByCurrencyCode().get(code);
const currency = issuer === undefined ? FALLBACK_CURRENCY : this.currencyOf(issuer);
reserves.push({ code, symbol: currency.symbol, name: currency.name, amount });
}
return {
rate: this.getCentralBankRate(civ),
currency: this.currencyOf(civ),
reserves,
// What the bank has lent: the government's overdraft plus region borrowing.
loans: this.getCentralBankLoans(civ),
};
},
_serializeRegionCash(city) {
const cash = {};
for (const [code, amount] of this.getRegionCash(city)) {
if (amount > 0) cash[code] = amount;
}
return cash;
},
};