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.
86 lines
3.7 KiB
JavaScript
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);
|
|
}
|