Files
Battle-for-Tismo/tests/trade_graph_test.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

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