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.
142 lines
5.5 KiB
JavaScript
142 lines
5.5 KiB
JavaScript
import { TestCase } from "./framework/test_case.js";
|
|
import { smallState, cityOf } from "./framework/helpers.js";
|
|
import { key } from "../shared/hex.js";
|
|
|
|
// Places a resource building on a road tile next to `city` and returns its
|
|
// coords, so the trade BFS can reach it over the transport network.
|
|
function placeReachableMill(state, city, civ = 0) {
|
|
for (const neighbour of state.topology.neighbours(city.coords.x, city.coords.y)) {
|
|
const k = key(neighbour.x, neighbour.y);
|
|
if (!state._isLand(neighbour)) continue;
|
|
if (state.cityAt(neighbour) || state.tileImprovements.has(k)) continue;
|
|
state.tileImprovements.set(k, "steel_mill");
|
|
state.tileImprovementOwner.set(k, civ);
|
|
state.tileImprovementHp.set(k, 1000);
|
|
state._ensureResourceStock(k);
|
|
state.roads.add(k);
|
|
return neighbour;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export class TradeGraphTest extends TestCase {
|
|
// The graph carries every internal route as an edge, whether or not a delivery
|
|
// ran along it, so the map can draw the idle ones grey.
|
|
test_owned_routes_appear_as_idle_edges() {
|
|
const state = smallState();
|
|
const city = cityOf(state, 0);
|
|
const mill = placeReachableMill(state, city);
|
|
this.assertNotNull(mill, "the fixture found a tile for the mill");
|
|
|
|
const graph = state._serializeResourceGraph(0);
|
|
this.assertHas(graph, "edges");
|
|
const hasEdge = graph.edges.some(
|
|
(edge) =>
|
|
edge[0] === mill.x && edge[1] === mill.y &&
|
|
edge[2] === city.coords.x && edge[3] === city.coords.y
|
|
);
|
|
this.assertTrue(hasEdge, "the mill-to-city route is an edge");
|
|
this.assertEmpty(graph.links, "no delivery has run, so no link is active");
|
|
this.assertTrue(
|
|
graph.nodes.some((node) => node[0] === mill.x && node[1] === mill.y),
|
|
"the mill is a storage node"
|
|
);
|
|
}
|
|
|
|
// A delivery out of the viewer's own city is an export and belongs on the
|
|
// graph just as an import does, with the foreign buyer drawn as a node.
|
|
test_exports_from_own_nodes_are_kept() {
|
|
const state = smallState();
|
|
const city = cityOf(state, 0);
|
|
const mill = placeReachableMill(state, city);
|
|
const foreign = cityOf(state, 1);
|
|
this.assertNotNull(foreign, "the fixture has a foreign city");
|
|
state.resourceLinks = [{
|
|
from: [mill.x, mill.y],
|
|
to: [foreign.coords.x, foreign.coords.y],
|
|
resource: "steel",
|
|
amount: 12.5,
|
|
}];
|
|
state._resourceGraphCache = null;
|
|
|
|
const graph = state._serializeResourceGraph(0);
|
|
const link = graph.links.find(
|
|
(entry) => entry[0] === mill.x && entry[1] === mill.y &&
|
|
entry[2] === foreign.coords.x && entry[3] === foreign.coords.y
|
|
);
|
|
this.assertNotNull(link, "the export is on the graph");
|
|
this.assertEqual(link[4], "steel", "it names the commodity");
|
|
this.assertTrue(
|
|
graph.nodes.some((node) => node[0] === foreign.coords.x && node[1] === foreign.coords.y),
|
|
"the foreign buyer is a node"
|
|
);
|
|
}
|
|
|
|
// A delivery into the viewer's own city (an import) still shows, as before.
|
|
test_imports_into_own_cities_are_kept() {
|
|
const state = smallState();
|
|
const city = cityOf(state, 0);
|
|
const foreign = cityOf(state, 1);
|
|
state.resourceLinks = [{
|
|
from: [foreign.coords.x, foreign.coords.y],
|
|
to: [city.coords.x, city.coords.y],
|
|
resource: "food",
|
|
amount: 3,
|
|
}];
|
|
state._resourceGraphCache = null;
|
|
|
|
const graph = state._serializeResourceGraph(0);
|
|
this.assertTrue(
|
|
graph.links.some(
|
|
(entry) => entry[2] === city.coords.x && entry[3] === city.coords.y && entry[4] === "food"
|
|
),
|
|
"the import is on the graph"
|
|
);
|
|
}
|
|
|
|
// A spectator gets no graph at all.
|
|
test_a_spectator_sees_no_graph() {
|
|
const state = smallState();
|
|
const graph = state._serializeResourceGraph(-1);
|
|
this.assertEmpty(graph.nodes);
|
|
this.assertEmpty(graph.links);
|
|
this.assertEmpty(graph.edges);
|
|
}
|
|
|
|
// The walk is expensive (sea lanes flood the ocean), so it is kept across
|
|
// days until the network changes. Identity proves whether it was reused.
|
|
test_the_trade_walk_is_reused_until_the_network_changes() {
|
|
const state = smallState();
|
|
const city = cityOf(state, 0);
|
|
const nodes = state._resourceNodes();
|
|
const allowSea = state._cityMarketAccess(city);
|
|
const first = state._tradeGraph(city, nodes, allowSea);
|
|
const again = state._tradeGraph(city, nodes, allowSea);
|
|
this.assert(first === again, "an unchanged network reuses the cached walk");
|
|
|
|
state._improvementVersion += 1;
|
|
const rebuilt = state._tradeGraph(city, nodes, allowSea);
|
|
this.assert(first !== rebuilt, "a road change throws the cached walk away");
|
|
}
|
|
|
|
// A hostile camp can sever a route, so wartime movement drops the walk; in
|
|
// peacetime unit positions never enter it, so a move must not.
|
|
test_a_peacetime_unit_move_keeps_the_trade_walk() {
|
|
const state = smallState();
|
|
const city = cityOf(state, 0);
|
|
const nodes = state._resourceNodes();
|
|
const allowSea = state._cityMarketAccess(city);
|
|
const first = state._tradeGraph(city, nodes, allowSea);
|
|
|
|
state._invalidateUnitCaches();
|
|
const afterMove = state._tradeGraph(city, nodes, allowSea);
|
|
this.assert(first === afterMove, "with no war a unit move keeps the walk");
|
|
|
|
this.assertTrue(state.requestDeclareWar(0, 1), "the fixture declares a war");
|
|
const atWar = state._tradeGraph(city, nodes, allowSea);
|
|
state._invalidateUnitCaches();
|
|
const atWarAfterMove = state._tradeGraph(city, nodes, allowSea);
|
|
this.assert(atWar !== atWarAfterMove, "in war a unit move drops the walk");
|
|
}
|
|
}
|