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.
204 lines
7.2 KiB
JavaScript
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"
|
|
);
|
|
}
|
|
}
|