550 lines
20 KiB
JavaScript
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;
|
|
},
|
|
};
|