From 22edfb0dec554b6d3ef0399b8b2122bd47c490d4 Mon Sep 17 00:00:00 2001 From: Azgaar Date: Sat, 27 Apr 2024 13:33:33 +0200 Subject: [PATCH] feat: searoute - change pathfinding algo --- modules/routes-generator-old.js | 273 -------------------------------- modules/routes-generator.js | 60 ++++++- modules/ui/layers.js | 12 +- 3 files changed, 64 insertions(+), 281 deletions(-) delete mode 100644 modules/routes-generator-old.js diff --git a/modules/routes-generator-old.js b/modules/routes-generator-old.js deleted file mode 100644 index 3019763d..00000000 --- a/modules/routes-generator-old.js +++ /dev/null @@ -1,273 +0,0 @@ -window.RoutesOld = (function () { - const getRoads = function () { - TIME && console.time("generateMainRoads"); - const cells = pack.cells; - const burgs = pack.burgs.filter(b => b.i && !b.removed); - const capitals = burgs.filter(b => b.capital).sort((a, b) => a.population - b.population); - - if (capitals.length < 2) return []; // not enough capitals to build main roads - const paths = []; // array to store path segments - - for (const b of capitals) { - const connect = capitals.filter(c => c.feature === b.feature && c !== b); - for (const t of connect) { - const [from, exit] = findLandPath(b.cell, t.cell, true); - const segments = restorePath(b.cell, exit, "main", from); - segments.forEach(s => paths.push(s)); - } - } - - cells.i.forEach(i => (cells.s[i] += cells.route[i] / 2)); // add roads to suitability score - TIME && console.timeEnd("generateMainRoads"); - return paths; - }; - - const getTrails = function () { - TIME && console.time("generateTrails"); - const cells = pack.cells; - const burgs = pack.burgs.filter(b => b.i && !b.removed); - - if (burgs.length < 2) return []; // not enough burgs to build trails - - let paths = []; // array to store path segments - for (const f of pack.features.filter(f => f.land)) { - const isle = burgs.filter(b => b.feature === f.i); // burgs on island - if (isle.length < 2) continue; - - isle.forEach(function (b, i) { - let path = []; - if (!i) { - // build trail from the first burg on island - // to the farthest one on the same island or the closest road - const farthest = d3.scan( - isle, - (a, c) => (c.y - b.y) ** 2 + (c.x - b.x) ** 2 - ((a.y - b.y) ** 2 + (a.x - b.x) ** 2) - ); - const to = isle[farthest].cell; - if (cells.route[to]) return; - const [from, exit] = findLandPath(b.cell, to, true); - path = restorePath(b.cell, exit, "small", from); - } else { - // build trail from all other burgs to the closest road on the same island - if (cells.route[b.cell]) return; - const [from, exit] = findLandPath(b.cell, null, true); - if (exit === null) return; - path = restorePath(b.cell, exit, "small", from); - } - if (path) paths = paths.concat(path); - }); - } - - TIME && console.timeEnd("generateTrails"); - return paths; - }; - - const getSearoutes = function () { - TIME && console.time("generateSearoutes"); - const {cells, burgs, features} = pack; - const allPorts = burgs.filter(b => b.port > 0 && !b.removed); - - if (!allPorts.length) return []; - - const bodies = new Set(allPorts.map(b => b.port)); // water features with ports - let paths = []; // array to store path segments - const connected = []; // store cell id of connected burgs - - bodies.forEach(f => { - const ports = allPorts.filter(b => b.port === f); // all ports on the same feature - if (!ports.length) return; - - if (features[f]?.border) addOverseaRoute(f, ports[0]); - - // get inner-map routes - for (let s = 0; s < ports.length; s++) { - const source = ports[s].cell; - if (connected[source]) continue; - - for (let t = s + 1; t < ports.length; t++) { - const target = ports[t].cell; - if (connected[target]) continue; - - const [from, exit, passable] = findOceanPath(target, source, true); - if (!passable) continue; - - const path = restorePath(target, exit, "ocean", from); - paths = paths.concat(path); - - connected[source] = 1; - connected[target] = 1; - } - } - }); - - function addOverseaRoute(f, port) { - const {x, y, cell: source} = port; - const dist = p => Math.abs(p[0] - x) + Math.abs(p[1] - y); - const [x1, y1] = [ - [0, y], - [x, 0], - [graphWidth, y], - [x, graphHeight] - ].sort((a, b) => dist(a) - dist(b))[0]; - const target = findCell(x1, y1); - - if (cells.f[target] === f && cells.h[target] < 20) { - const [from, exit, passable] = findOceanPath(target, source, true); - - if (passable) { - const path = restorePath(target, exit, "ocean", from); - paths = paths.concat(path); - last(path).push([x1, y1]); - } - } - } - - TIME && console.timeEnd("generateSearoutes"); - return paths; - }; - - const draw = function (main, small, water) { - TIME && console.time("drawRoutes"); - const {cells, burgs} = pack; - const {burg, p} = cells; - - const getBurgCoords = b => [burgs[b].x, burgs[b].y]; - const getPathPoints = cells => cells.map(i => (Array.isArray(i) ? i : burg[i] ? getBurgCoords(burg[i]) : p[i])); - const getPath = segment => round(lineGen(getPathPoints(segment)), 1); - const getPathsHTML = (paths, type) => - paths.map((path, i) => ``).join(""); - - lineGen.curve(d3.curveCatmullRom.alpha(0.1)); - roads.html(getPathsHTML(main, "road")); - trails.html(getPathsHTML(small, "trail")); - - lineGen.curve(d3.curveBundle.beta(1)); - searoutes.html(getPathsHTML(water, "searoute")); - - TIME && console.timeEnd("drawRoutes"); - }; - - const regenerate = function () { - routes.selectAll("path").remove(); - pack.cells.route = new Uint16Array(pack.cells.i.length); - pack.cells.crossroad = new Uint16Array(pack.cells.i.length); - const main = getRoads(); - const small = getTrails(); - const water = getSearoutes(); - draw(main, small, water); - }; - - return {getRoads, getTrails, getSearoutes, draw, regenerate}; - - // Find a land path to a specific cell (exit), to a closest road (toRoad), or to all reachable cells (null, null) - function findLandPath(start, exit = null, toRoad = null) { - const cells = pack.cells; - const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p}); - const cost = [], - from = []; - queue.queue({e: start, p: 0}); - - while (queue.length) { - const next = queue.dequeue(), - n = next.e, - p = next.p; - if (toRoad && cells.route[n]) return [from, n]; - - for (const c of cells.c[n]) { - if (cells.h[c] < 20) continue; // ignore water cells - const stateChangeCost = cells.state && cells.state[c] !== cells.state[n] ? 400 : 0; // trails tend to lay within the same state - const habitability = biomesData.habitability[cells.biome[c]]; - if (!habitability) continue; // avoid inhabitable cells (eg. lava, glacier) - const habitedCost = habitability ? Math.max(100 - habitability, 0) : 400; // routes tend to lay within populated areas - const heightChangeCost = Math.abs(cells.h[c] - cells.h[n]) * 10; // routes tend to avoid elevation changes - const heightCost = cells.h[c] > 80 ? cells.h[c] : 0; // routes tend to avoid mountainous areas - const cellCoast = 10 + stateChangeCost + habitedCost + heightChangeCost + heightCost; - const totalCost = p + (cells.route[c] || cells.burg[c] ? cellCoast / 3 : cellCoast); - - if (from[c] || totalCost >= cost[c]) continue; - from[c] = n; - if (c === exit) return [from, exit]; - cost[c] = totalCost; - queue.queue({e: c, p: totalCost}); - } - } - return [from, exit]; - } - - function restorePath(start, end, type, from) { - const cells = pack.cells; - const path = []; // to store all segments; - let segment = [], - current = end, - prev = end; - const score = type === "main" ? 5 : 1; // to increase road score at cell - - if (type === "ocean" || !cells.route[prev]) segment.push(end); - if (!cells.route[prev]) cells.route[prev] = score; - - for (let i = 0, limit = 1000; i < limit; i++) { - if (!from[current]) break; - current = from[current]; - - if (cells.route[current]) { - if (segment.length) { - segment.push(current); - path.push(segment); - if (segment[0] !== end) { - cells.route[segment[0]] += score; - cells.crossroad[segment[0]] += score; - } - if (current !== start) { - cells.route[current] += score; - cells.crossroad[current] += score; - } - } - segment = []; - prev = current; - } else { - if (prev) segment.push(prev); - prev = null; - segment.push(current); - } - - cells.route[current] += score; - if (current === start) break; - } - - if (segment.length > 1) path.push(segment); - return path; - } - - // find water paths - function findOceanPath(start, exit = null, toRoute = null) { - const cells = pack.cells, - temp = grid.cells.temp; - const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p}); - const cost = [], - from = []; - queue.queue({e: start, p: 0}); - - while (queue.length) { - const next = queue.dequeue(), - n = next.e, - p = next.p; - if (toRoute && n !== start && cells.route[n]) return [from, n, true]; - - for (const c of cells.c[n]) { - if (c === exit) { - from[c] = n; - return [from, exit, true]; - } - if (cells.h[c] >= 20) continue; // ignore land cells - if (temp[cells.g[c]] <= -5) continue; // ignore cells with term <= -5 - const dist2 = (cells.p[c][1] - cells.p[n][1]) ** 2 + (cells.p[c][0] - cells.p[n][0]) ** 2; - const totalCost = p + (cells.route[c] ? 1 + dist2 / 2 : dist2 + (cells.t[c] ? 1 : 100)); - - if (from[c] || totalCost >= cost[c]) continue; - (from[c] = n), (cost[c] = totalCost); - queue.queue({e: c, p: totalCost}); - } - } - return [from, exit, false]; - } -})(); diff --git a/modules/routes-generator.js b/modules/routes-generator.js index 44b33f66..dcaac978 100644 --- a/modules/routes-generator.js +++ b/modules/routes-generator.js @@ -1,3 +1,20 @@ +// suggested data format + +// pack.cells.connectivity = { +// cellId1: { +// toCellId2: routeId2, +// toCellId3: routeId2, +// }, +// cellId2: { +// toCellId1: routeId2, +// toCellId3: routeId1, +// } +// } + +// pack.routes = [ +// {i, group: "roads", feature: featureId, cells: [cellId], points?: [[x, y], [x, y]]} +// ]; + window.Routes = (function () { const ROUTES = { MAIN_ROAD: 1, @@ -91,14 +108,36 @@ window.Routes = (function () { TIME && console.time("generateSeaRoutes"); const seaRoutes = []; + let skip = false; + for (const [featureId, featurePorts] of Object.entries(portsByFeature)) { const points = featurePorts.map(burg => [burg.x, burg.y]); const urquhartEdges = calculateUrquhartEdges(points); - console.log(urquhartEdges); + urquhartEdges.forEach(([fromId, toId]) => { const start = featurePorts[fromId].cell; const exit = featurePorts[toId].cell; + if (skip) return; + if (start === 444 && exit === 297) { + // if (segment.join(",") === "124,122,120") debugger; + skip = true; + + for (const con of connections) { + const [from, to] = con[0].split("-").map(Number); + const [x1, y1] = cells.p[from]; + const [x2, y2] = cells.p[to]; + debug + .append("line") + .attr("x1", x1) + .attr("y1", y1) + .attr("x2", x2) + .attr("y2", y2) + .attr("stroke", "red") + .attr("stroke-width", 0.2); + } + } + const segments = findPathSegments({isWater: true, connections, start, exit}); for (const segment of segments) { addConnections(segment, ROUTES.SEA_ROUTE); @@ -170,6 +209,8 @@ window.Routes = (function () { const queue = new FlatQueue(); queue.push(start, 0); + const isDebug = start === 444 && exit === 297; + return isWater ? findWaterPath() : findLandPath(); function findLandPath() { @@ -188,7 +229,7 @@ window.Routes = (function () { const habitabilityModifier = 1 + Math.max(100 - habitability, 0) / 1000; // [1, 1.1]; const heightModifier = 1 + Math.max(cells.h[neibCellId] - 25, 25) / 25; // [1, 3]; const connectionModifier = connections.has(`${next}-${neibCellId}`) ? 1 : 3; - const burgModifier = cells.burg[neibCellId] ? 1 : 2; + const burgModifier = cells.burg[neibCellId] ? 1 : 3; const cellsCost = distanceCost * habitabilityModifier * heightModifier * connectionModifier * burgModifier; const totalCost = priority + cellsCost; @@ -210,13 +251,16 @@ window.Routes = (function () { while (queue.length) { const priority = queue.peekValue(); const next = queue.pop(); + isDebug && console.log("next", next); for (const neibCellId of cells.c[next]) { if (neibCellId === exit) { + isDebug && console.log(`neib ${neibCellId} is exit`); from[neibCellId] = next; return from; } + // if (from[neibCellId]) continue; // don't go back if (cells.h[neibCellId] >= 20) continue; // ignore land cells if (temp[cells.g[neibCellId]] < MIN_PASSABLE_SEA_TEMP) continue; // ignore too cold cells @@ -227,7 +271,17 @@ window.Routes = (function () { const cellsCost = distanceCost * typeModifier * connectionModifier; const totalCost = priority + cellsCost; - if (from[neibCellId] || totalCost >= cost[neibCellId]) continue; + if (isDebug) { + const lost = totalCost >= cost[neibCellId]; + console.log( + `neib ${neibCellId}`, + `cellCost ${rn(cellsCost)}`, + `new ${rn(totalCost)} ${lost ? ">=" : "<"} prev ${rn(cost[neibCellId])}.`, + `${lost ? "lost" : "won"}` + ); + } + + if (totalCost >= cost[neibCellId]) continue; from[neibCellId] = next; cost[neibCellId] = totalCost; diff --git a/modules/ui/layers.js b/modules/ui/layers.js index b018ed10..ad826bc7 100644 --- a/modules/ui/layers.js +++ b/modules/ui/layers.js @@ -1653,11 +1653,11 @@ function drawRoutes() { }; for (const {i, group, cells} of pack.routes) { - if (group !== "searoutes") straightenPathAngles(cells); // mutates points + // if (group !== "searoutes") straightenPathAngles(cells); // mutates points const pathPoints = getPathPoints(cells); // TODO: temporary view for searoutes - if (group === "searoutes2") { + if (group) { const pathPoints = cells.map(cellId => points[cellId]); const color = getMixedColor("#000000", 0.6); const line = "M" + pathPoints.join("L"); @@ -1667,9 +1667,9 @@ function drawRoutes() { if (!routePaths[group]) routePaths[group] = []; routePaths[group].push(``); - lineGen.curve(curves[group] || curves.default); - const path = round(lineGen(pathPoints), 1); - routePaths[group].push(` `); + // lineGen.curve(curves[group] || curves.default); + // const path = round(lineGen(pathPoints), 1); + // routePaths[group].push(` `); continue; } @@ -1685,6 +1685,8 @@ function drawRoutes() { routes.select("#" + group).html(routePaths[group].join("")); } + drawCellsValue(pack.cells.i); + TIME && console.timeEnd("drawRoutes"); function adjustBurgPoints() {