From ef3820de79c1fc049fc7406552a92616de4ac7ad Mon Sep 17 00:00:00 2001 From: Phaethon H Date: Sat, 6 Nov 2021 03:54:17 -0700 Subject: [PATCH] Change from Depth-First Search to Breadth-First Search; added bonus, the "135 closest" constraint comes much cheaper than with DFS. --- mods/ITEMS/mcl_sponges/register.lua | 218 +++++++++++++--------------- 1 file changed, 100 insertions(+), 118 deletions(-) diff --git a/mods/ITEMS/mcl_sponges/register.lua b/mods/ITEMS/mcl_sponges/register.lua index d49eaaec7..7bdb3dfc7 100644 --- a/mods/ITEMS/mcl_sponges/register.lua +++ b/mods/ITEMS/mcl_sponges/register.lua @@ -38,147 +38,129 @@ local function node_qualifies_for_sponging(scanpos, criteria) return qualified, liquidclass, planstep end --- absorption pattern 2: sponge reaches out to 7 blocks (taxicab distance). +-- absorption pattern 2: sponge reaches out to 7 blocks (taxicab distance) along contiguous blocks of liquid. -- absorbs maximum 135 blocks, closest blocks first. ---- bounds of check encompasses 15*15*15=3375 blocks; octahedral volume is 2703(?) blocks. ---- upwards of 2703 get_node() calls, and 135 set_node() calls. ---- may warrant use of voxel manipulator. +-- boundary encompasses 15*15*15=3375 blocks; octahedral volume is 2703(?) blocks. +-- upwards of 2703 get_node() calls, and 135 set_node() calls. +-- may warrant use of voxel manipulator. -- returns 2 values: table, string -local function absorption_strategy2(pos, criteria) - local plan = {} - local affected = 0 - local scores = {} -- table{score,pos,node} - local adjacency = {} -- adjacency graph, such that adjacency[x][y][z] -> { d=contiguous-distance } or "" for no contiguous path - local r = 7 - -- FIXME: this algorithm is dumb and slow. - -- adjacency graph to determine contiguous paths of liquid from sponge. +-- Breadth-First Search. +-- pos: world position of center of absorption (sponge). +-- criteria: table of acceptable liquid => sponge type when absorbing that. +local function absorption_strategy2(pos, criteria) + -- plan is list of {pos, node} + local plan = {} + -- radius of search (taxicab distance). + local r = 7 + -- remaining number of blocks that may be absorbed (countdown). + local absorption_limit = 135 + -- visitation chart while searching continguous adjacent blocks. + -- prevent unnecessary backtracking in BFS. + local visited = {} for i=-r,r do - adjacency[i] = {} + visited[i] = {} for j=-r,r do - adjacency[i][j] = {} - -- now adjacency[i][j][k] valid + visited[i][j] = {} + -- now visited[i][j][k] valid end end - local function scan_adjacency(graph, rx, ry, rz, d, liquidclass) - if rx == nil and ry == nil and rz == nil then - rx, ry, rz = 0, 0, 0 + visited[0][0][0] = true + + -- breadth-first search for water nodes to absorb; map out contiguous blocks of liquids. + -- FIXME: this algorithm is fairly dumb and slow. + + -- rx,ry,rz - relative Cartesian coordinates + local rx, ry, rz + -- d - distance (search depth) + local d + -- liquidclass - type of liquid to absorb (do not absorb mismatched). + -- start off as nil => seek a liquid to absorb. + -- once a(n acceptable) liquid is found, absorb only that liquid. + -- the resulting web sponge is specified in `criteria` + local liquidclass = nil + -- BFS visitation queue: list of { rx,ry,rz,d } + local queue = { [1]={0,0,0,0} } + local loop_steps = 0 + while #queue > 0 do + loop_steps = loop_steps + 1 + -- flag to prune searching. + local no_prune = true + -- next node to search. + rx, ry, rz, d = queue[1][1], queue[1][2], queue[1][3], queue[1][4] + table.remove(queue, 1) + -- don't scan center (the sponge). + local nodeliquid, planstep + if d > 0 then + -- scan node. + local scanpos = {x=pos.x+rx, y=pos.y+ry, z=pos.z+rz} + no_prune, nodeliquid, planstep = node_qualifies_for_sponging(scanpos, criteria) end - local v = graph[rx][ry][rz] - local skip_scan =false - if d == nil or d == 0 or (rx==0 and ry==0 and rz==0) then - skip_scan = true - d = 0 - end - if not skip_scan then - if v == "" then - -- already visited, deemed non-contiguous (not liquid). - return + if no_prune then + -- potentially absorbable. + if liquidclass == nil then + -- first liquid type found. + liquidclass = nodeliquid + end + if liquidclass == nodeliquid then + table.insert(plan, planstep) + -- count down absorption limit (max 135 absorbed). + absorption_limit = absorption_limit - 1 + if absorption_limit <= 0 then + -- terminate all searches. + no_prune, queue = false, {} + break + end else - if v ~= nil then - -- already visited, recheck distance. - if d >= v.d then - -- alternate path is worse. Terminate search. - return - end - -- TODO: test case with really perverse water configuration. - end - -- (re)scan node. - local scannode = minetest.get_node({x=pos.x+rx, y=pos.y+ry, z=pos.z+rz}) - local scandef = minetest.registered_nodes[scannode.name] - if liquidclass == nil then - -- adopt the liquidclass - liquidclass = scandef.liquid_alternative_source - if liquidclass == nil then - -- not liquid, terminate search. - graph[rx][ry][rz] = "" - return - end - elseif liquidclass ~= scandef.liquid_alternative_source then - -- change of substance. Leave blank, terminate search. - return - end - -- contiguous. - graph[rx][ry][rz] = { d=d, liquidclass=liquidclass } + -- mismatched liquid (no go); prune search. + no_prune = false end end -- adjacent nodes. - if d < r then - if rx > -r then - scan_adjacency(graph, rx-1, ry, rz, d+1, liquidclass) - end - if rx < r then - scan_adjacency(graph, rx+1, ry, rz, d+1, liquidclass) + if d < r and no_prune then + -- prioritize top to bottom. + if ry < r then + if not visited[rx][ry+1][rz] then + visited[rx][ry+1][rz] = true + table.insert(queue, {rx, ry+1, rz, d+1}) + end end if ry > -r then - scan_adjacency(graph, rx, ry-1, rz, d+1, liquidclass) + if not visited[rx][ry-1][rz] then + visited[rx][ry-1][rz] = true + table.insert(queue, {rx, ry-1, rz, d+1}) + end end - if ry < r then - scan_adjacency(graph, rx, ry+1, rz, d+1, liquidclass) + -- prioritize north to south. + if rz < r then + if not visited[rx][ry][rz+1] then + visited[rx][ry][rz+1] = true + table.insert(queue, {rx, ry, rz+1, d+1}) + end end if rz > -r then - scan_adjacency(graph, rx, ry, rz-1, d+1, liquidclass) + if not visited[rx][ry][rz-1] then + visited[rx][ry][rz-1] = true + table.insert(queue, {rx, ry, rz-1, d+1}) + end end - if rz < r then - scan_adjacency(graph, rx, ry, rz+1, d+1, liquidclass) + -- prioritize east to west. + if rx < r then + if not visited[rx+1][ry][rz] then + visited[rx+1][ry][rz] = true + table.insert(queue, {rx+1, ry, rz, d+1}) + end end - else - return - end - end - -- map out contiguous blocks of liquids. - scan_adjacency(adjacency, 0, 0, 0) - - -- collect the contiguous blocks of liquids for sponging. - for i=-r,r do - local YZ = adjacency[i] - for j=-r,r do - local Z = YZ[j] - for k=-r,r do - local v = Z[k] - if v == nil then - -- not reachable. - elseif v == "" then - -- not contiguous. - elseif (v.d <= r) then - -- consider. - local scanpos = { x=pos.x+i, y=pos.y+j, z=pos.z+k } - local qualified, liquidclass, nodeplan = node_qualifies_for_sponging(scanpos, criteria) - if qualified then - -- Scoring algorithm to prioritize blocks from which to pick 135 for absorption. - -- Could use improvement here. Right now, it's rather sloppy, but it's a start. -PH - local score = v.d * 8*r*r*r - score = score + ((-j)*4*r*r) - score = score + ((-i)*2*r) - score = score + (-k) - local entry = { score=score, liquidclass=liquidclass, nodeplan=nodeplan } - table.insert(scores, entry) - end + if rx > -r then + if not visited[rx-1][ry][rz] then + visited[rx-1][ry][rz] = true + table.insert(queue, {rx-1, ry, rz, d+1}) end end end end - - local new_spongename - if #scores > 0 then - -- sort by score. - table.sort(scores, function(a,b) return a.score < b.score end) - -- maximum 135 blocks absorbed by sponge. - local quota = 135 - -- absorb "nearest" liquid type. - local filterclass = scores[1].liquidclass - for i=1,#scores do - if scores[i].liquidclass == filterclass then - local nodeplan = scores[i].nodeplan - table.insert(plan, nodeplan) - quota = quota - 1 - end - if quota <= 0 then - break - end - end - new_spongename = criteria[filterclass] - end + -- determine the wet sponge from the liquid type absorbed. + local new_spongename = criteria[liquidclass] return plan, new_spongename end