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