From 2e06d5c9f20ede1117c0aeeb30355c9fec1c4eee Mon Sep 17 00:00:00 2001 From: crazydubc Date: Tue, 30 Nov 2021 20:38:20 -0700 Subject: [PATCH] Update to mincut Transition to a class and added functions to improve user adaptability into their code base. Several minor performance edits were made (less iterations through for loops) --- .../JavaScript/minCutWallRampartsPlacement.js | 685 +++++++++--------- 1 file changed, 341 insertions(+), 344 deletions(-) diff --git a/src/misc/JavaScript/minCutWallRampartsPlacement.js b/src/misc/JavaScript/minCutWallRampartsPlacement.js index 16ef12c..b0d5b56 100644 --- a/src/misc/JavaScript/minCutWallRampartsPlacement.js +++ b/src/misc/JavaScript/minCutWallRampartsPlacement.js @@ -1,411 +1,408 @@ -// require('util.min_cut').test('W5N9'); +//MinCut By Carson Burke. Modified by Michael Braecklein -/** - * Posted 10 may 2018 by @saruss - * - * Code for calculating the minCut in a room, written by Saruss - * adapted (for Typescript by Chobobobo , is it somewhere?) - * some readability added by Chobobobo @typescript was included here - * (15Aug2019) Updated Game.map.getTerrainAt to Game.map.getRoomTerrain method -Shibdib - */ +//USAGE Example +//const minCut = require('class.mincut'); +//const mc = new minCut(room); +//add all protected positions with radiuses +//mc.addAreaToProtect(controller.pos, 2); //3 is the default radius if not given. Do this for all areas to protect. +//mc.addAreaToProtect(bunker.pos, 10); +//mc.addAreaToProtect(source.pos, 3); +//const barrierPositions = mc.getCutTiles(); //returns an array of RoomPositions. + +// Terrain types const UNWALKABLE = -1; const NORMAL = 0; const PROTECTED = 1; const TO_EXIT = 2; const EXIT = 3; +class Graph { + constructor(v) { + this.v = v; // Vertex count + this.level = Array(v); + this.edges = Array(v).fill(0).map(x => []); // Array: for every vertex an edge Array mit {v,r,c,f} vertex_to,res_edge,capacity,flow + } + newEdge(u, v, c) { // Adds new edge from u to v + this.edges[u].push({ v: v, r: this.edges[v].length, c: c, f: 0 }); // Normal forward Edge + this.edges[v].push({ v: u, r: this.edges[u].length - 1, c: 0, f: 0 }); // reverse Edge for Residal Graph + } + Bfs(s, t) { // calculates Level Graph and if theres a path from s to t + if (t >= this.v) return false; + this.level.fill(-1); // reset old levels + this.level[s] = 0; + const q = []; // queue with s as starting point -/** - * An Array with Terrain information: -1 not usable, 2 Sink (Leads to Exit) - */ -function room_2d_array(roomname,bounds={x1:0,y1:0,x2:49,y2:49}) { - let room_2d=Array(50).fill(0).map( x=>Array(50).fill(UNWALKABLE)); // Array for room tiles - let i=bounds.x1;const imax=bounds.x2; - let j=bounds.y1;const jmax=bounds.y2; - const terrain = Game.map.getRoomTerrain(roomname); - for (;i<=imax;i++) { - j=bounds.y1; - for(;j<=jmax;j++) { - if (terrain.get(i,j) !== TERRAIN_MASK_WALL){ - room_2d[i][j]=NORMAL; // mark unwalkable - if (i===bounds.x1 || j===bounds.y1 || i===bounds.x2 || j===bounds.y2) - room_2d[i][j]=TO_EXIT; // Sink Tiles mark from given bounds - if (i===0 || j===0 || i===49 || j===49) - room_2d[i][j]=EXIT; // Exit Tiles mark + q.push(s); + while (q.length) { + const u = q.splice(0, 1)[0]; + for (let i = 0; i < this.edges[u].length; i++) { + const edge = this.edges[u][i]; + if (this.level[edge.v] < 0 && edge.f < edge.c) { + this.level[edge.v] = this.level[u] + 1; + q.push(edge.v); + } + } + } - } - } - } + return this.level[t] >= 0; // return if theres a path to t -> no level, no path! + }; - /* OLD CODE - let terrain_array=room.lookForAtArea(LOOK_TERRAIN,0,0,49,49,true); - if (terrain_array.length == 0) { - console.log('get_room_array in room_layout, look-at-for-Area Fehler'); - } - let terrain=''; - let x_pos=0; - let y_pos=0; - let i=0;const imax=terrain_array.length; - for (;it recursivly while increasing the level of the visited vertices by one + // u vertex, f flow on path, t =Sink , c Array, c[i] saves the count of edges explored from vertex i + Dfsflow(u, f, t, c) { + if (u === t) // Sink reached , aboard recursion + return f; + + while (c[u] < this.edges[u].length) { // Visit all edges of the vertex one after the other + let edge = this.edges[u][c[u]]; + + if (this.level[edge.v] === this.level[u] + 1 && edge.f < edge.c) { // Edge leads to Vertex with a level one higher, and has flow left + const flow_till_here = Math.min(f, edge.c - edge.f); + const flow_to_t = this.Dfsflow(edge.v, flow_till_here, t, c); + if (flow_to_t > 0) { + edge.f += flow_to_t; // Add Flow to current edge + this.edges[edge.v][edge.r].f -= flow_to_t; // subtract from reverse Edge -> Residual Graph neg. Flow to use backward direction of BFS/DFS + return flow_to_t; + } + } + c[u]++; + } + return 0; + } + + Bfsthecut(s) { // breadth-first-search which uses the level array to mark the vertices reachable from s + const e_in_cut = []; + this.level.fill(-1); + this.level[s] = 1; + const q = [s]; + + while (q.length) { + const u = q.splice(0, 1)[0]; + const imax = this.edges[u].length; + for (let i = 0; i < imax; i++) { + let edge = this.edges[u][i]; + if (edge.f < edge.c) { + if (this.level[edge.v] < 1) { + this.level[edge.v] = 1; + q.push(edge.v); + } + } + if (edge.f === edge.c && edge.c > 0) { // blocking edge -> could be in min cut + edge.u = u; + e_in_cut.push(edge); + } + } + } + const min_cut = []; + for (let i = 0; i < e_in_cut.length; i++) { + if (this.level[e_in_cut[i].v] === -1) // Only edges which are blocking and lead to from s unreachable vertices are in the min cut + min_cut.push(e_in_cut[i].u); + } + return min_cut; + } + Calcmincut(s, t) { // calculates min-cut graph (Dinic Algorithm) + if (s === t)return -1; + + let returnvalue = 0; + while (this.Bfs(s, t) === true) { + const count = Array(this.v + 1).fill(0); + let flow = 0; + do { + flow = this.Dfsflow(s, Number.MAX_VALUE, t, count); + if (flow > 0) + returnvalue += flow; + } while (flow) + } + return returnvalue; + } } -function Graph(menge_v) { - this.v=menge_v; // Vertex count - this.level=Array(menge_v); - this.edges=Array(menge_v).fill(0).map( x=>[]); // Array: for every vertex an edge Array mit {v,r,c,f} vertex_to,res_edge,capacity,flow - this.New_edge=function(u,v,c) { // Adds new edge from u to v - this.edges[u].push({v: v, r: this.edges[v].length, c:c, f:0}); // Normal forward Edge - this.edges[v].push({v: u, r: this.edges[u].length-1, c:0, f:0}); // reverse Edge for Residal Graph - }; - this.Bfs=function(s, t) { // calculates Level Graph and if theres a path from s to t - if (t>=this.v) - return false; - this.level.fill(-1); // reset old levels - this.level[s]=0; - let q=[]; // queue with s as starting point - q.push(s); - let u=0; - let edge=null; - while (q.length) { - u=q.splice(0,1)[0]; - let i=0;const imax=this.edges[u].length; - for (;i= 0; // return if theres a path to t -> no level, no path! - }; - // DFS like: send flow at along path from s->t recursivly while increasing the level of the visited vertices by one - // u vertex, f flow on path, t =Sink , c Array, c[i] saves the count of edges explored from vertex i - this.Dfsflow = function(u,f,t,c) { - if (u===t) // Sink reached , aboard recursion - return f; - let edge=null; - let flow_till_here=0; - let flow_to_t=0; - while (c[u] < this.edges[u].length) { // Visit all edges of the vertex one after the other - edge=this.edges[u][c[u]]; - if (this.level[edge.v] === this.level[u]+1 && edge.f < edge.c) { // Edge leads to Vertex with a level one higher, and has flow left - flow_till_here=Math.min(f,edge.c-edge.f); - flow_to_t=this.Dfsflow(edge.v,flow_till_here,t,c); - if (flow_to_t > 0 ) { - edge.f+=flow_to_t; // Add Flow to current edge - this.edges[edge.v][edge.r].f-=flow_to_t; // subtract from reverse Edge -> Residual Graph neg. Flow to use backward direction of BFS/DFS - return flow_to_t; - } - } - c[u]++; - } - return 0; - }; - this.Bfsthecut=function(s) { // breadth-first-search which uses the level array to mark the vertices reachable from s - let e_in_cut=[]; - this.level.fill(-1); - this.level[s]=1; - let q=[]; - q.push(s); - let u=0; - let edge=null; - while (q.length) { - u=q.splice(0,1)[0]; - let i=0;const imax=this.edges[u].length; - for (;i Array(50).fill(UNWALKABLE)); // Array for room tiles + const terrain = Game.map.getRoomTerrain(roomName); + + // Loop through each tile and find terrain type, assign to usable terrain values + for (let i = bounds.x1; i <= bounds.x2; i++) { + for (let j = bounds.y1; j <= bounds.y2; j++) { + if (terrain.get(i, j) !== TERRAIN_MASK_WALL) { + room_2d[i][j] = NORMAL; // mark unwalkable + if (i === bounds.x1 || j === bounds.y1 || i === bounds.x2 || j === bounds.y2) { + room_2d[i][j] = TO_EXIT; // user specified bounds, this will mark the exit of the bounds + } + if (i === 0 || j === 0 || i === 49 || j === 49) { + room_2d[i][j] = EXIT; // Exit Tiles mark of theroom. } - } - if (edge.f===edge.c && edge.c>0) { // blocking edge -> could be in min cut - edge.u=u; - e_in_cut.push(edge); } } } - let min_cut=[]; - let i=0;const imax=e_in_cut.length; - for (;i 0 ) - returnvalue+=flow; - } while (flow) + + // mark Border Tiles near room edge as unwalkable + for (let i = 1; i < 49; i++) { + room_2d[0][i] == UNWALKABLE; + room_2d[49][i] == UNWALKABLE; + room_2d[i][0] == UNWALKABLE; + room_2d[i][49] == UNWALKABLE; } - return returnvalue; + return room_2d; } -} -var util_mincut={ + // Function to create Source, Sink, Tiles arrays: takes a rectangle-Array as input for Tiles that are to Protect - // rects have top-left/bot_right Coordinates {x1,y1,x2,y2} - create_graph: function(roomname,rect,bounds) { - let room_array=room_2d_array(roomname,bounds); // An Array with Terrain information: -1 not usable, 2 Sink (Leads to Exit) - // For all Rectangles, set edges as source (to protect area) and area as unused - let r=null; - let j=0;const jmax=rect.length; - // Check bounds - if (bounds.x1 >= bounds.x2 || bounds.y1 >= bounds.y2 || - bounds.x1 < 0 || bounds.y1 < 0 || bounds.x2 > 49 || bounds.y2 > 49) - return console.log('ERROR: Invalid bounds', JSON.stringify(bounds)); - for (;j= r.x2 || r.y1 >= r.y2) { - return console.log('ERROR: Rectangle Nr.',j, JSON.stringify(r), 'invalid.'); - } else if (r.x1 < bounds.x1 || r.x2 > bounds.x2 || r.y1 < bounds.y1 || r.y2 > bounds.y2) { - return console.log('ERROR: Rectangle Nr.',j, JSON.stringify(r), 'out of bounds:', JSON.stringify(bounds)); + // rects have top-left/bottom_right Coordinates {x1,y1,x2,y2} + createGraph(roomName, rect, bounds) { + // Create array with terrain usable information + const roomArray = this.formatRoomTerrain(roomName, bounds) + + // Check if near exit + const exits = this.room.find(FIND_EXIT) + + for (const exit of exits) { + if (exit.getRangeTo(rect.x1, rect.y1) === 0 || exit.getRangeTo(rect.x2, rect.y2) === 0) { + return console.log("ERROR: Too close to exit") } + } - let x=r.x1;const maxx=r.x2+1; - let y=r.y1;const maxy=r.y2+1; - for (;x= r.x2 || r.y1 >= r.y2) { + return console.log('ERROR: Rectangle Nr.', j, JSON.stringify(r), 'invalid.') } - } - // ********************** Visualisierung - if (true) { - let visual=new RoomVisual(roomname); - let x=0;let y=0;const max=50; - for (;x pos 0 // top vertices <-> x,y : v=y*50+x and x= v % 50 y=v/50 (math.floor?) - // bot vertices <-> top + 2500 - let source=2*50*50; - let sink=2*50*50+1; - let top=0; - let bot=0; - let dx=0; - let dy=0; - let x=1;let y=1;const max=49; - for (;x top + 2500 + const sink = 2 * 50 * 50 + 1 + + for (let x = 1; x < 49; x++) { + for (let y = 1; y < 49; y++) { + + const top = y * 50 + x; + const bottom = top + 2500; + + if (roomArray[x][y] === NORMAL) { // normal Tile + // If normal tile do x + g.newEdge(top, bottom, 1); + for (let i = 0; i < 8; i++) { + const dx = x + surr[i][0]; + const dy = y + surr[i][1]; + if (roomArray[dx][dy] === NORMAL || roomArray[dx][dy] === TO_EXIT) g.newEdge(bottom, dy * 50 + dx, infini); } - } else if (room_array[x][y] === PROTECTED ) { // protected Tile - g.New_edge(source,top, infini ); - g.New_edge(top,bot, 1 ); - for (let i=0;i<8;i++) { - dx=x+surr[i][0]; - dy=y+surr[i][1]; - if (room_array[dx][dy] === NORMAL || room_array[dx][dy] === TO_EXIT) - g.New_edge(bot,dy*50+dx,infini); + } else if (roomArray[x][y] === PROTECTED) { + // If protected tile do x + g.newEdge(5000, top, infini); + g.newEdge(top, bottom, 1); + for (let i = 0; i < 8; i++) { + const dx = x + surr[i][0]; + const dy = y + surr[i][1]; + if (roomArray[dx][dy] === NORMAL || roomArray[dx][dy] === TO_EXIT) g.newEdge(bottom, dy * 50 + dx, infini); } - } else if (room_array[x][y] === TO_EXIT) { // near Exit - g.New_edge(top,sink, infini ); + } else if (roomArray[x][y] === TO_EXIT) { + // If exit tile do x + g.newEdge(top, sink, infini); } } - } // graph finished + } + return g; - }, - delete_tiles_to_dead_ends: function(roomname,cut_tiles_array) { // Removes unneccary cut-tiles if bounds are set to include some dead ends - // Get Terrain and set all cut-tiles as unwalkable - let room_array=room_2d_array(roomname); - for (let i=cut_tiles_array.length-1;i>=0;i--) { - room_array[cut_tiles_array[i].x][cut_tiles_array[i].y]=UNWALKABLE; + } + + deleteTilesToDeadEnds(cut_tiles_array) { + // make any tiles that don't have a path to the exits unwalkable terrain + const roomArray = room_2d_array(this.roomName); + for (let i = cut_tiles_array.length - 1; i >= 0; i--) { + roomArray[cut_tiles_array[i].x][cut_tiles_array[i].y] = UNWALKABLE; } + // Floodfill from exits: save exit tiles in array and do a bfs-like search - let unvisited_pos=[]; - let y=0;const max=49; - for(;y 0) { - index=unvisited_pos.pop(); - x=index % 50; - y=Math.floor(index/50); - for (let i=0;i<8;i++) { - dx=x+surr[i][0]; - dy=y+surr[i][1]; - if (room_array[dx][dy] === NORMAL ) { - unvisited_pos.push(50*dy+dx); - room_array[dx][dy] = TO_EXIT; + // Take the last tile from the unvisited tiles array, and set it as the current tile to be "inspected" + const index = unvisited_pos.pop(); + + const x = index % 50; + const y = Math.floor(index / 50); + + // Loop through all neighboring tiles as determined by the relative positions in "surr" + for (let i = 0; i < 8; i++) { + // Current neighbor + const dx = x + surr[i][0]; + const dy = y + surr[i][1]; + + // If the neighboring tile is walkable (NORMAL), add it to the unvisited tiles array to continue the Breadths first search + // Since the search began at the exit, we know that if this tile has been reached, it has a path to the exit, so we mark it as such + if (roomArray[dx][dy] === NORMAL) { + unvisited_pos.push(50 * dy + dx); + roomArray[dx][dy] = TO_EXIT; } } } - // Remove min-Cut-Tile if there is no TO-EXIT surrounding it - let leads_to_exit=false; - for (let i=cut_tiles_array.length-1;i>=0;i--) { - leads_to_exit=false; - x=cut_tiles_array[i].x; - y=cut_tiles_array[i].y; - for (let i=0;i<8;i++) { - dx=x+surr[i][0]; - dy=y+surr[i][1]; - if (room_array[dx][dy] === TO_EXIT ) { - leads_to_exit=true; + + // Remove tile if there is no TO-EXIT surrounding it + for (let i = cut_tiles_array.length - 1; i >= 0; i--) { + let leads_to_exit = false; + + // Loop through the tile's neighbors once again + const x = cut_tiles_array[i].x; + const y = cut_tiles_array[i].y; + for (let i = 0; i < 8; i++) { + const dx = x + surr[i][0]; + const dy = y + surr[i][1]; + + // If the tile has a path to the exit, then set the flag to skip it + if (roomArray[dx][dy] === TO_EXIT) { + leads_to_exit = true; } } + + // If the tile doesn't lead to an exit, remove it from the array (this should remove it from the "positions" array that was originally passed to this function) if (!leads_to_exit) { - cut_tiles_array.splice(i,1); + cut_tiles_array.splice(i, 1); } } - }, + } + //pass center point and radius + addAreaToProtect(pos, dist = 3) { + const exits = this.room.find(FIND_EXIT) + for (const exit of exits) { + if (exit.getRangeTo(pos) <= dist) { + return; + } + } + // If not near give position for protection + this.protectedAreas.push({ x1: pos.x - dist, y1: pos.y - dist, x2: pos.x + dist, y2: pos.y + dist }); + } // Function for user: calculate min cut tiles from room, rect[] - GetCutTiles: function(roomname, rect, bounds={x1:0,y1:0,x2:49,y2:49}, verbose=false) { - let graph=util_mincut.create_graph(roomname, rect, bounds); - let source=2*50*50; // Position Source / Sink in Room-Graph - let sink=2*50*50+1; - let count=graph.Calcmincut(source,sink); - if (verbose) console.log('NUmber of Tiles in Cut:',count); - let positions=[]; + getCutTiles(rect = this.protectedAreas, bounds = { x1: 0, y1: 0, x2: 49, y2: 49 }) { + const graph = this.createGraph(this.roomName, rect); // Get the map + const source = 5000 + const sink = 2 * 50 * 50 + 1; + const count = graph.Calcmincut(source, sink); + + const positions = []; if (count > 0) { - let cut_edges=graph.Bfsthecut(source); + // I think by cut_edges, they mean any edge that is not unwalkable + const cut_edges = graph.Bfsthecut(source); // Get Positions from Edge - let u,x,y; - let i=0;const imax=cut_edges.length; - for (;i 0 && !whole_room) - util_mincut.delete_tiles_to_dead_ends(roomname,positions); + this.deleteTilesToDeadEnds(positions); + // Visualise Result - if (true && positions.length > 0) { - let visual=new RoomVisual(roomname); - for (let i=positions.length-1;i>=0;i--) { - visual.circle(positions[i].x,positions[i].y,{radius: 0.5, fill:'#ff7722',opacity: 0.9}); + if (positions.length > 0) { + const visual = new RoomVisual(this.roomName); + for (let i = positions.length - 1; i >= 0; i--) { + // These must be the walls + visual.circle(positions[i].x, positions[i].y, { radius: 0.4, fill: COLOR_GREEN, opacity: 0.8 }); } } - return positions; - }, - // Example function: demonstrates how to get a min cut with 2 rectangles, which define a "to protect" area - test: function(roomname) { - //let room=Game.rooms[roomname]; - //if (!room) - // return 'O noes, no room'; - let cpu=Game.cpu.getUsed(); - // Rectangle Array, the Rectangles will be protected by the returned tiles - let rect_array=[]; - rect_array.push({x1: 20, y1: 6, x2:28, y2: 27}); - rect_array.push({x1: 29, y1: 13, x2:34, y2: 16}); - // Boundary Array for Maximum Range - let bounds={x1: 0, y1: 0, x2:49, y2: 49}; - // Get Min cut - let positions=util_mincut.GetCutTiles(roomname,rect_array,bounds); // Positions is an array where to build walls/ramparts - // Test output - console.log('Positions returned',positions.length); - cpu=Game.cpu.getUsed()-cpu; - console.log('Needed',cpu,' cpu time'); - return 'Finished'; - }, - + return _.map(positions, coord => new RoomPosition(coord.x, coord.y, this.roomName)); + } } -module.exports = util_mincut; +module.exports = minCut