Change from Depth-First Search to Breadth-First Search; added bonus, the "135 closest" constraint comes much cheaper than with DFS.

This commit is contained in:
Phaethon H 2021-11-06 03:54:17 -07:00
parent 9df16d8dc3
commit ef3820de79
1 changed files with 100 additions and 118 deletions

View File

@ -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