Files
Battle-for-Tismo/shared/economy_graph.js
adrien 4600b16399 Stockpiled a month of every material and cached the trade walks
Food, steel, luxury and high-tech now share one flat month-long reserve. A region keeps bidding for the shortfall while its store is thin, so a war that empties a pantry refills it from the grid instead of starving; the harvest is served before the city load, unmet food bids the price up, the army's rations count as demand, and a converter that cannot cover its power bill idles instead of making a loss.

The delivery graph walks sea lanes any distance, so its walks are cached across days behind a road/railway/work/territory stamp and only dropped by a wartime unit move. Units are indexed per tile, market access and node populations are memoised for the day, and the server can settle a fresh world before serving (--warmup, 150 days by default).

Roadmap records the remaining follow-ups -- the politics/migration hot path and the luxury/high-tech price plateau -- and that freight's heavy energy draw is intended.
2026-09-23 15:59:28 +02:00

86 lines
3.7 KiB
JavaScript

// The economic graph toolkit: a framework-free traversal over an abstract
// transport graph, and the transport-energy costs that weight its edges. It
// knows nothing about tiles, roads, ports or nations -- the caller supplies the
// adjacency and the per-step cost -- so the same engine serves the trade
// network, the delivery graph and any other economic spread over the map.
//
// Keeping this apart from `game_state/resources.js` means the simulation
// describes *how* its world is wired, while this module owns *how* to walk a
// graph and price a leg.
import { RESOURCE_RULES, resourceById } from "./data/resources.js";
// --------------------------------------------------------------- traversal --
// Breadth-first spread from `start` over an abstract graph, in rings of
// increasing distance. `neighbours(node)` returns the outward edges of a node,
// each `{ node, cost, weight }`; a directed graph is just a `neighbours` that
// omits the reverse edge. The traversal stops at `maxDistance` steps, or once
// its accumulated `weight` (one per edge, `edge.weight` or 1) exceeds
// `maxWeight` -- so a caller can bound one kind of leg without bounding all of
// them, e.g. land trade at eight tiles while sea legs run free.
//
// `accept(node, { distance, cost, weight })` is called for every newly reached
// node (never the start) and decides whether it joins the result; returning
// false still lets the search continue through it. The result is the accepted
// nodes with their accumulated `distance` (steps), `cost` (sum of edge costs)
// and `weight`, sorted by cost.
export function traverse(start, {
neighbours, id = (node) => node, accept = null,
maxDistance = Infinity, maxWeight = Infinity,
}) {
const results = [];
const visited = new Set([id(start)]);
let frontier = [{ node: start, distance: 0, cost: 0, weight: 0 }];
while (frontier.length > 0) {
const next = [];
for (const current of frontier) {
if (current.distance >= maxDistance) continue;
for (const edge of neighbours(current.node)) {
const key = id(edge.node);
if (visited.has(key)) continue;
const weight = current.weight + (edge.weight === undefined ? 1 : edge.weight);
if (weight > maxWeight) continue;
visited.add(key);
const reached = {
node: edge.node,
distance: current.distance + 1,
cost: current.cost + (edge.cost || 0),
weight,
};
if (!accept || accept(edge.node, reached)) results.push(reached);
next.push(reached);
}
}
frontier = next;
}
results.sort((a, b) => a.cost - b.cost);
return results;
}
// ---------------------------------------------------------------- delivery --
// The kWh of transport energy one material tonne costs over `tiles` tiles in
// the given mode. A material tonne and an energy tonne (10 GWh) share the
// formula, so material deliveries and energy deliveries are priced alike.
export function deliveryEnergyKwh(tonnes, tiles, mode, tileMovementCost = 1) {
const d = RESOURCE_RULES.deliveryEnergy;
const perTile = mode === "train"
? d.train
: mode === "ship"
? d.ship
: mode === "road"
? d.truckRoad
: d.truckOffRoad * Math.max(tileMovementCost, 0);
return tonnes * Math.max(tiles, 0) * perTile;
}
// The energy cost of delivering `amount` of a resource over `tiles`. Every
// resource carries a mass per canonical unit, so a carat or a chip is charged
// for the tonnes it actually weighs rather than as if it were a tonne.
export function deliveryEnergyForResource(id, amount, tiles, mode, tileMovementCost = 1) {
const proto = resourceById(id);
const tonnes = amount * (proto ? proto.tonnesPerUnit : 1);
return deliveryEnergyKwh(tonnes, tiles, mode, tileMovementCost);
}