// 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" ); } }