Files
Battle-for-Tismo/tests/simple_snapshot_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

204 lines
7.2 KiB
JavaScript

// The simple economy's wire shape (10-snapshot-and-orders.md): which snapshot
// fields the client needs to render the pool and the global market, that the
// market is delta-shipped on its own version, and that `market_trade` is
// validated on the server. The hard model's snapshot must stay untouched.
import { TestCase } from "./framework/test_case.js";
import { simpleState, smallState, smallConfig, SEED } from "./framework/helpers.js";
import { GameServer } from "../server/game_server.js";
import {
RESOURCE_IDS,
STORABLE_RESOURCE_IDS,
RESOURCE_MARKET_BASE,
MARKET_START_STOCK,
} from "../shared/data.js";
function fakeNetwork() {
return {
peers: new Set(),
sent: [],
getPeerIds() {
return Array.from(this.peers);
},
send(peerId, message) {
this.sent.push({ peerId, message });
},
};
}
// A configured simple server with peer 1 mapped to civ 0, so a snapshot can be
// walked through the same delta path the real broadcast uses.
function configuredServer(economyModel) {
const network = fakeNetwork();
const server = new GameServer(network);
server.configureGame({
seed: SEED,
player_civ: 0,
civilisations: ["france", "britain"],
mapConfig: smallConfig(),
economyModel,
});
network.peers.add(1);
server.peerCiv.set(1, 0);
return { server, network };
}
function lastState(network) {
return network.sent[network.sent.length - 1].message.state;
}
export class SimpleSnapshotTest extends TestCase {
test_a_simple_snapshot_carries_the_model_the_pool_and_the_market() {
const state = simpleState();
const snap = state.snapshot(0);
// The top-level model, so the client picks the simple UI.
this.assertEqual(snap.economyModel, "simple", "the model ships at the top level");
// The viewer's own pool is the `stock` of its resource summary; every
// region reads the same four numbers.
const pool = state.getResourcePool(0);
for (const id of STORABLE_RESOURCE_IDS) {
this.assertEqual(
snap.viewerStats.resources.stock[id],
pool[id],
`${id} ships the viewer's pool`
);
}
// The world market ships one entry per resource: stock plus the price the
// client renders without recomputing it. Energy is a flow, never stocked.
const market = snap.resourceMarket;
this.assertSize(market, RESOURCE_IDS.length, "one market entry per resource");
const steel = market.find((entry) => entry.id === "steel");
this.assertEqual(steel.stock, MARKET_START_STOCK, "the world stock ships");
this.assertApprox(steel.price, RESOURCE_MARKET_BASE.steel, 1e-9, "and its price");
const energy = market.find((entry) => entry.id === "energy");
this.assertEqual(energy.stock, null, "energy ships no stock");
}
test_the_shipped_pool_tracks_the_live_pool() {
const state = simpleState();
state.getResourcePool(0).steel = 4242;
this.assertEqual(
state.snapshot(0).viewerStats.resources.stock.steel,
4242,
"an edit to the pool is visible to the very next snapshot"
);
}
test_a_hard_snapshot_keeps_its_own_market() {
const state = smallState();
state.applyStartingScenario();
const snap = state.snapshot(0);
this.assertEqual(snap.economyModel, "hard", "the hard model is reported");
this.assertEqual(typeof state.getResourcePool, "undefined", "there is no pool API");
// The hard market is still the supply/demand listing: it ships no stock and
// keeps its own price and supply/demand fields.
const steel = snap.resourceMarket.find((entry) => entry.id === "steel");
this.assertEqual(steel.stock, null, "the hard market ships no stock");
this.assertEqual(typeof steel.supply, "number", "but it keeps its supply");
this.assertEqual(typeof steel.demand, "number", "and its demand");
const versionBefore = snap.versions.market;
for (let hour = 0; hour < 24; hour++) state.advanceHour();
this.assertGreater(
state.snapshot(0).versions.market,
versionBefore,
"and the hard day still moves its market version"
);
}
test_an_unchanged_market_is_left_out_of_a_delta() {
const { server, network } = configuredServer("simple");
server._broadcastState();
const full = lastState(network);
this.assertFalse(full.delta, "the first snapshot is full");
this.assertNotNull(full.resourceMarket, "and carries the whole market");
// Nothing moved, so the next broadcast is a delta without the market.
server._broadcastState();
const quiet = lastState(network);
this.assertTrue(quiet.delta, "the second snapshot is a delta");
this.assertNull(quiet.resourceMarket, "an unchanged market is omitted");
// The pool lives in viewerStats, which every snapshot carries.
this.assertNotNull(
quiet.viewerStats.resources.stock,
"the pool is still shipped inline"
);
}
test_a_trade_reships_the_market_in_the_next_delta() {
const { server, network } = configuredServer("simple");
server._broadcastState();
server._broadcastState();
const before = lastState(network);
this.assertNull(before.resourceMarket, "the quiet delta held no market");
const versionBefore = server.state.snapshot(0).versions.market;
const poolBefore = server.state.getResourcePool(0).steel;
this.assertTrue(
server.state.requestMarketTrade(0, "steel", "buy", 100),
"a buy is accepted"
);
server._broadcastState();
const after = lastState(network);
this.assertNotNull(after.resourceMarket, "the changed market is re-shipped");
this.assertGreater(
server.state.snapshot(0).versions.market,
versionBefore,
"the market version moved"
);
this.assertEqual(
after.viewerStats.resources.stock.steel,
poolBefore + 100,
"and the pool rode along in the same delta"
);
}
test_the_server_validates_a_market_trade() {
const { server } = configuredServer("simple");
const pool = server.state.getResourcePool(0);
const steelBefore = pool.steel;
const stockBefore = server.state.marketStock.get("steel");
this.assertTrue(
server.handleOrder(1, { type: "market_trade", side: "buy", resource: "steel", lot: 2000 }),
"a valid lot is accepted"
);
this.assertEqual(pool.steel, steelBefore + 2000, "and the pool moved by the lot");
this.assertEqual(
server.state.marketStock.get("steel"),
stockBefore - 2000,
"and the world stock paid for it"
);
for (const bad of [
{ side: "buy", resource: "steel", lot: 1234 },
{ side: "hold", resource: "steel", lot: 100 },
{ side: "buy", resource: "energy", lot: 100 },
{ side: "buy", resource: "nonsense", lot: 100 },
]) {
this.assertFalse(
server.handleOrder(1, { type: "market_trade", ...bad }),
`refused: ${JSON.stringify(bad)}`
);
}
this.assertEqual(pool.steel, steelBefore + 2000, "a refused trade moves no pool");
this.assertEqual(
server.state.marketStock.get("steel"),
stockBefore - 2000,
"and no stock"
);
}
test_the_hard_server_has_no_market_order() {
const { server } = configuredServer("hard");
this.assertFalse(
server.handleOrder(1, { type: "market_trade", side: "buy", resource: "steel", lot: 2000 }),
"the hard model refuses the simple market order"
);
}
}