Files
Battle-for-Tismo/tests/simple_market_test.js
adrien d5305da449 Gave the world a simple economy: one pool per nation and a global market
The simple economy is now the default and the old simulation is frozen as the hard model, selectable per game. A nation keeps one resource pool available in every region, fed by public production works rather than private per-region stores, and construction and training begin at once, paced by the region's spare power instead of gathering materials over days.

Money is euro only: a per-inhabitant daily tax funds a treasury spent in a global market, where buy and sell lots trade against the world stock and move its price. The hard economy keeps its per-nation currencies, central banks and exchange rates.

Simple games skip the price-settling warmup and economic migration; the snapshot carries the economy model, the market and the pool figures the client draws. Added the matching test suites and the design briefs under docs/simple_economy.
2026-09-25 14:58:49 +02:00

247 lines
10 KiB
JavaScript

// The global market (06-global-market.md): one shared world stock per storable
// good, a price derived from that stock, and a buy/sell order in fixed lots.
// The hard economy keeps its drifting reference prices and has no market order.
import { TestCase } from "./framework/test_case.js";
import { simpleState, smallState, smallConfig, SEED } from "./framework/helpers.js";
import {
RESOURCE_IDS,
STORABLE_RESOURCE_IDS,
RESOURCE_MARKET_BASE,
MARKET_START_STOCK,
MARKET_LOTS,
} from "../shared/data.js";
import { GameServer } from "../server/game_server.js";
function configuredServer(economyModel) {
const server = new GameServer({ getPeerIds: () => [], send: () => {} });
server.configureGame({
seed: SEED,
player_civ: 0,
civilisations: ["france", "britain"],
mapConfig: smallConfig(),
economyModel,
});
server.peerCiv.set(1, 0);
return server;
}
export class SimpleMarketTest extends TestCase {
test_the_opening_price_is_the_base_price() {
const state = simpleState();
for (const id of STORABLE_RESOURCE_IDS) {
this.assertEqual(state.marketStock.get(id), MARKET_START_STOCK, `${id} opens full`);
this.assertApprox(state.marketPrice(id), RESOURCE_MARKET_BASE[id], 1e-9, `${id} at base`);
this.assertApprox(
state.getResourcePrice(id),
RESOURCE_MARKET_BASE[id],
1e-9,
`${id} reads at base`
);
}
// Energy is a regional flow, not a market good: it keeps its base price and
// is never stocked.
this.assertEqual(state.marketStock.has("energy"), false, "energy is not stocked");
this.assertApprox(
state.getResourcePrice("energy"),
RESOURCE_MARKET_BASE.energy,
1e-9,
"energy keeps its base price"
);
}
test_buying_drains_the_stock_and_raises_the_price() {
const state = simpleState();
const pool = state.getResourcePool(0);
const before = pool.steel;
const treasuryBefore = state.getBudget(0);
const price = state.marketPrice("steel");
const stockBefore = state.marketStock.get("steel");
this.assertTrue(state.requestMarketTrade(0, "steel", "buy", 2000), "the buy is accepted");
this.assertApprox(pool.steel, before + 2000, 1e-9, "the pool gained exactly the lot");
this.assertApprox(
state.getBudget(0),
treasuryBefore - 2000 * price,
1e-6,
"the treasury paid qty x price"
);
this.assertApprox(state.marketStock.get("steel"), stockBefore - 2000, 1e-9, "the stock fell");
this.assertGreater(state.marketPrice("steel"), price, "and the price rose");
}
test_selling_fills_the_stock_and_lowers_the_price() {
const state = simpleState();
const pool = state.getResourcePool(0);
pool.food = 100_000;
const before = pool.food;
const treasuryBefore = state.getBudget(0);
const price = state.marketPrice("food");
const stockBefore = state.marketStock.get("food");
this.assertTrue(state.requestMarketTrade(0, "food", "sell", 2000), "the sell is accepted");
this.assertApprox(pool.food, before - 2000, 1e-9, "the pool gave up exactly the lot");
this.assertApprox(
state.getBudget(0),
treasuryBefore + 2000 * price,
1e-6,
"the treasury was credited qty x price"
);
this.assertApprox(state.marketStock.get("food"), stockBefore + 2000, 1e-9, "the stock rose");
this.assertLess(state.marketPrice("food"), price, "and the price fell");
}
test_only_the_three_lots_are_accepted() {
const state = simpleState();
state.getResourcePool(0).steel = 1e9;
state.budgets.set(0, 1e15);
for (const lot of MARKET_LOTS) {
this.assertTrue(
state.requestMarketTrade(0, "steel", "buy", lot),
`the ${lot} lot is accepted`
);
}
for (const bad of [1, 50, 999, 2500, 100_000, 0, -100, NaN, undefined]) {
this.assertFalse(
state.requestMarketTrade(0, "steel", "buy", bad),
`the ${bad} quantity is refused`
);
}
}
test_a_refused_trade_moves_nothing() {
const state = simpleState();
const pool = state.getResourcePool(0);
const poolBefore = pool.hightech;
const stockBefore = state.marketStock.get("hightech");
// A treasury that cannot afford the lot.
state.budgets.set(0, 1);
this.assertFalse(state.requestMarketTrade(0, "hightech", "buy", 100), "no funds, no trade");
this.assertEqual(state.getBudget(0), 1, "the treasury is untouched");
this.assertEqual(pool.hightech, poolBefore, "the pool is untouched");
this.assertEqual(state.marketStock.get("hightech"), stockBefore, "the stock is untouched");
// A pool that does not hold the goods being sold.
state.budgets.set(0, 1e12);
pool.steel = 50;
this.assertFalse(state.requestMarketTrade(0, "steel", "sell", 100), "nothing to sell");
this.assertEqual(pool.steel, 50, "the pool keeps what it had");
this.assertEqual(state.getBudget(0), 1e12, "and the treasury does not move");
// A world stock that cannot cover the buy.
state.marketStock.set("steel", 50);
this.assertFalse(state.requestMarketTrade(0, "steel", "buy", 100), "the world is short");
this.assertEqual(state.marketStock.get("steel"), 50, "the stock keeps what it had");
// A malformed side or good.
this.assertFalse(state.requestMarketTrade(0, "steel", "hold", 100), "a bad side is refused");
this.assertFalse(state.requestMarketTrade(0, "energy", "buy", 100), "energy is not traded");
this.assertFalse(state.requestMarketTrade(0, "nonsense", "buy", 100), "an unknown good");
}
test_the_market_is_shared_by_every_player() {
const state = simpleState();
const before = state.marketPrice("steel");
this.assertTrue(state.requestMarketTrade(0, "steel", "buy", 50_000), "civ 0 buys");
const after = state.marketPrice("steel");
this.assertGreater(after, before, "the buy moved the price");
this.assertApprox(state.marketPrice("steel"), after, 1e-9, "civ 1 reads the same market");
this.assertApprox(state.getResourcePrice("steel"), after, 1e-9, "and the price read agrees");
const view = state.getMarketView(1).find((entry) => entry.id === "steel");
this.assertApprox(view.price, after, 1e-9, "the market view shows the shared price");
this.assertApprox(view.stock, MARKET_START_STOCK - 50_000, 1e-9, "and the shared stock");
}
test_a_near_empty_stock_stays_finite() {
const state = simpleState();
for (const stock of [0, -5, NaN, undefined]) {
state.marketStock.set("steel", stock);
const price = state.marketPrice("steel");
this.assertTrue(Number.isFinite(price), `a stock of ${stock} gives a finite price`);
this.assertGreater(price, 0, "and a positive one");
}
}
test_the_simple_tick_leaves_the_price_to_the_market() {
const state = simpleState();
state.applyStartingScenario();
const steel = state.marketPrice("steel");
const market = new Map();
for (const id of RESOURCE_IDS) {
market.set(id, { supply: 0, demand: 1e6, price: state.getResourcePrice(id) });
}
state._updateMarketPrices(market);
this.assertApprox(state.marketPrice("steel"), steel, 1e-9, "no supply/demand drift");
for (let hour = 0; hour < 24; hour++) state.advanceHour();
this.assertApprox(state.marketPrice("steel"), steel, 1e-9, "a whole day does not drift it");
}
test_the_snapshot_carries_the_stock_and_price() {
const state = simpleState();
const market = state.snapshot(0).resourceMarket;
this.assertSize(market, RESOURCE_IDS.length, "one entry per resource");
const steel = market.find((entry) => entry.id === "steel");
this.assertEqual(steel.stock, MARKET_START_STOCK, "the stock ships");
this.assertApprox(steel.price, RESOURCE_MARKET_BASE.steel, 1e-9, "the price ships");
const energy = market.find((entry) => entry.id === "energy");
this.assertEqual(energy.stock, null, "energy ships no stock");
}
test_a_trade_bumps_the_market_version() {
const state = simpleState();
const before = state.snapshot(0).versions.market;
this.assertGreater(before, 0, "the version starts set");
this.assertTrue(state.requestMarketTrade(0, "food", "sell", 100), "a trade is accepted");
this.assertGreater(state.snapshot(0).versions.market, before, "the version moved with it");
}
test_the_server_validates_a_market_trade() {
const server = configuredServer("simple");
const state = server.state;
const pool = state.getResourcePool(0);
const steelBefore = pool.steel;
this.assertTrue(
server.handleOrder(1, { t: "market_trade", side: "buy", resource: "steel", lot: 2000 }),
"a valid trade is accepted"
);
this.assertEqual(pool.steel, steelBefore + 2000, "and the pool moved");
this.assertFalse(
server.handleOrder(1, { t: "market_trade", side: "buy", resource: "steel", lot: 1234 }),
"a bad lot is refused"
);
this.assertFalse(
server.handleOrder(1, { t: "market_trade", side: "hold", resource: "steel", lot: 100 }),
"a bad side is refused"
);
this.assertFalse(
server.handleOrder(1, { t: "market_trade", side: "buy", resource: "energy", lot: 100 }),
"energy cannot be bought"
);
this.assertFalse(
server.handleOrder(1, { t: "market_trade", side: "buy", resource: "nonsense", lot: 100 }),
"an unknown good is refused"
);
}
test_the_hard_server_has_no_market_order() {
const server = configuredServer("hard");
this.assertFalse(
server.handleOrder(1, { t: "market_trade", side: "buy", resource: "steel", lot: 2000 }),
"the hard model refuses the market order"
);
}
test_the_hard_prices_still_drift_with_supply_and_demand() {
const state = smallState();
const base = state.getResourcePrice("food");
this.assertApprox(base, RESOURCE_MARKET_BASE.food, 1e-9, "a hard game opens at base");
const market = new Map();
for (const id of RESOURCE_IDS) {
market.set(id, { supply: 1000, demand: 0, price: state.getResourcePrice(id) });
}
market.get("food").supply = 1;
market.get("food").demand = 1000;
state._updateMarketPrices(market);
this.assertGreater(state.getResourcePrice("food"), base, "a shortage lifts the hard price");
this.assertEqual(typeof state.marketPrice, "undefined", "the hard model has no market stock API");
}
}