From 8d97cac61bc24561f838b36b24ba5b3858379782 Mon Sep 17 00:00:00 2001
From: dg <dg@51575b47-30f0-44d4-a5cc-537603b46e54>
Date: Tue, 6 Dec 2011 10:57:14 +0000
Subject: [PATCH] Avoid avoidable ambush immediately after new target is
 attained when autoexploring Explore more efficiently by inferring likely wall
 tiles when autoexploring If autoexplore is stopped on the way to an exit
 (such as by a trap), then it will still go that target when AE is initiated
 again When autoexploring don't go to items that the player dropped

git-svn-id: http://svn.net-core.org/repos/t-engine4@4720 51575b47-30f0-44d4-a5cc-537603b46e54
---
 game/modules/tome/class/Actor.lua             |   2 +
 game/modules/tome/class/Player.lua            |   4 +
 .../tome/class/interface/PlayerExplore.lua    | 534 ++++++++++++++++--
 3 files changed, 482 insertions(+), 58 deletions(-)

diff --git a/game/modules/tome/class/Actor.lua b/game/modules/tome/class/Actor.lua
index ef45620107..fca1dcffe7 100644
--- a/game/modules/tome/class/Actor.lua
+++ b/game/modules/tome/class/Actor.lua
@@ -3241,6 +3241,8 @@ end
 --- Called upon dropping an object
 function _M:onDropObject(o)
 	if self:attr("has_transmo") then o.__transmo = false end
+	if self.player then game.level.map.attrs(self.x, self.y, "obj_seen", true)
+	elseif game.level.map.attrs(self.x, self.y, "obj_seen") then game.level.map.attrs(self.x, self.y, "obj_seen", false) end
 end
 
 function _M:transmoPricemod(o) if o.type == "gem" then return 0.40 else return 0.05 end end
diff --git a/game/modules/tome/class/Player.lua b/game/modules/tome/class/Player.lua
index 911510c844..f27bec39d8 100644
--- a/game/modules/tome/class/Player.lua
+++ b/game/modules/tome/class/Player.lua
@@ -724,6 +724,10 @@ function _M:runStopped()
 			node.actor:addParticles(engine.Particles.new("notice_enemy", 1))
 		end
 	end
+
+	-- if you stop at an object (such as on a trap), then mark it as seen
+	local obj = game.level.map:getObject(x, y, 1)
+	if obj then game.level.map.attrs(x, y, "obj_seen", true) end
 end
 
 --- Activates a hotkey with a type "inventory"
diff --git a/game/modules/tome/class/interface/PlayerExplore.lua b/game/modules/tome/class/interface/PlayerExplore.lua
index e26e2ee8fe..004d8360fd 100644
--- a/game/modules/tome/class/interface/PlayerExplore.lua
+++ b/game/modules/tome/class/interface/PlayerExplore.lua
@@ -19,8 +19,6 @@
 
 --- This file implements auto-explore whereby a single command can explore unseen tiles and objects,
 -- go to unexplored doors, and go to the level exit all while avoiding known traps and water if possible.
--- Implemented hastily by "tiger_eye", so please direct all complaints and code criticisms to the ToME
--- forum where they can be promptly ignored ;) (I jest--compliments and suggestions will be most welcome!)
 --
 -- Note that the floodfill algorithm in this file can handle grids with different movement costs
 
@@ -39,15 +37,16 @@ local function toDouble(c)
 	return c - y * game.level.map.w, y
 end
 
-local function listAdjacentTiles(node, no_diagonal, no_cardinal)
+-- a flexible but slow function to list all adjacent tile
+local function listAdjacentTiles(tile, no_diagonal, no_cardinal)
 	local tiles = {}
 	local x, y, c, val
-	if type(node) == "table" then
-		x, y, c, val = unpack(node)
+	if type(tile) == "table" then
+		x, y, c, val = unpack(tile)
 		val = val + 1
-	elseif type(node) == "number" then
-		x, y = toDouble(node)
-		c = node
+	elseif type(tile) == "number" then
+		x, y = toDouble(tile)
+		c = tile
 		val = 1
 	else
 		return tiles
@@ -74,12 +73,15 @@ local function listAdjacentTiles(node, no_diagonal, no_cardinal)
 end
 
 -- Performing a flood-fill algorithm in lua with robust logic is going to be relatively slow, so we
--- need to make things more efficient wherever we can.  "adjacentTiles" below is an example of this.
+-- need to make things more efficient wherever we can.  "getNextNodes" below is an example of this.
 -- Every node knows from which direction it was explored, and it only explores adjacent tiles that
 -- may not have previously been explored.  Nodes that were explored from a cardinal direction only
 -- have three new adjacent tiles to iterate over, and diagonal directions have five new tiles.
 -- Therefore, we should favor cardinal direction tile propagation for speed whenever possible.
-local adjacentTiles = {
+--
+-- Note: if we want this to be faster such as using a floodfill for NPCs (better ai!), then we should
+-- perform the floodfill in C, where we could use more advanced tricks to make it blazingly fast.
+local getNextNodes = {
 	-- Dir 1
 	function(node, cardinal_tiles, diagonal_tiles)
 		local x, y, c, val = node[1], node[2], node[3], node[4]+1
@@ -205,8 +207,394 @@ local adjacentTiles = {
 	end
 }
 
+-- Use directional information to list all adjacent tiles more efficiently
+local listAdjacentNodes = {
+	-- Dir 1
+	function(node)
+		local x, y, c, val = node[1], node[2], node[3], node[4]+1
+		local tiles = {{x + 1, y,     c + 1,                    val, 6 },
+		               {x,     y - 1, c     - game.level.map.w, val, 8 },
+		               {x + 1, y - 1, c + 1 - game.level.map.w, val, 9 }}
+		if y < game.level.map.h - 1 then
+			tiles[4]         = {x,     y + 1, c     + game.level.map.w, val, 2 }
+			tiles[5]         = {x + 1, y + 1, c + 1 + game.level.map.w, val, 3 }
+			if x > 0 then
+				tiles[6] = {x - 1, y + 1, c - 1 + game.level.map.w, val, 1 }
+				tiles[7] = {x - 1, y,     c - 1,                    val, 4 }
+				tiles[8] = {x - 1, y - 1, c - 1 - game.level.map.w, val, 7 }
+			end
+		elseif x > 0 then
+			tiles[4]         = {x - 1, y,     c - 1,                    val, 4 }
+			tiles[5]         = {x - 1, y - 1, c - 1 - game.level.map.w, val, 7 }
+		end
+		return tiles
+	end,
+	-- Dir 2
+	function(node)
+		local x, y, c, val = node[1], node[2], node[3], node[4]+1
+		local tiles = {{x,     y - 1, c     - game.level.map.w, val, 8 }}
+
+		if y < game.level.map.h - 1 then
+			tiles[2] = {x,     y + 1, c     + game.level.map.w, val, 2 }
+			if x > 0 then
+				tiles[3] = {x - 1, y + 1, c - 1 + game.level.map.w, val, 1 }
+				tiles[4] = {x - 1, y,     c - 1,                    val, 4 }
+				tiles[5] = {x - 1, y - 1, c - 1 - game.level.map.w, val, 7 }
+			end
+			if x < game.level.map.w - 1 then
+				tiles[#tiles+1] = {x + 1, y + 1, c + 1 + game.level.map.w, val, 3 }
+				tiles[#tiles+1] = {x + 1, y,     c + 1,                    val, 6 }
+				tiles[#tiles+1] = {x + 1, y - 1, c + 1 - game.level.map.w, val, 9 }
+			end
+		else
+			if x > 0 then
+				tiles[2] = {x - 1, y,     c - 1,                    val, 4 }
+				tiles[3] = {x - 1, y - 1, c - 1 - game.level.map.w, val, 7 }
+			end
+			if x < game.level.map.w - 1 then
+				tiles[#tiles+1] = {x + 1, y,     c + 1,                    val, 6 }
+				tiles[#tiles+1] = {x + 1, y - 1, c + 1 - game.level.map.w, val, 9 }
+			end
+		end
+		return tiles
+	end,
+	-- Dir 3
+	function(node)
+		local x, y, c, val = node[1], node[2], node[3], node[4]+1
+		local tiles = {{x - 1, y,     c - 1,                    val, 4 },
+		               {x - 1, y - 1, c - 1 - game.level.map.w, val, 7 },
+		               {x,     y - 1, c     - game.level.map.w, val, 8 }}
+		if y < game.level.map.h - 1 then
+			tiles[4]         = {x - 1, y + 1, c - 1 + game.level.map.w, val, 1 }
+			tiles[5]         = {x,     y + 1, c     + game.level.map.w, val, 2 }
+			if x < game.level.map.w - 1 then
+				tiles[6] = {x + 1, y + 1, c + 1 + game.level.map.w, val, 3 }
+				tiles[7] = {x + 1, y,     c + 1,                    val, 6 }
+				tiles[8] = {x + 1, y - 1, c + 1 - game.level.map.w, val, 9 }
+			end
+		elseif x < game.level.map.w - 1 then
+			tiles[4]         = {x + 1, y,     c + 1,                    val, 6 }
+			tiles[5]         = {x + 1, y - 1, c + 1 - game.level.map.w, val, 9 }
+		end
+		return tiles
+	end,
+	-- Dir 4
+	function(node)
+		local x, y, c, val = node[1], node[2], node[3], node[4]+1
+		local tiles = {{x + 1, y,     c + 1,                    val, 6 }}
+
+		if x > 0 then
+			tiles[2] = {x - 1, y,     c - 1,                    val, 4 }
+			if y < game.level.map.h - 1 then
+				tiles[3] = {x - 1, y + 1, c - 1 + game.level.map.w, val, 1 }
+				tiles[4] = {x,     y + 1, c     + game.level.map.w, val, 2 }
+				tiles[5] = {x + 1, y + 1, c + 1 + game.level.map.w, val, 3 }
+			end
+			if y > 0 then
+				tiles[#tiles+1] = {x - 1, y - 1, c - 1 - game.level.map.w, val, 7 }
+				tiles[#tiles+1] = {x,     y - 1, c     - game.level.map.w, val, 8 }
+				tiles[#tiles+1] = {x + 1, y - 1, c + 1 - game.level.map.w, val, 9 }
+			end
+		else
+			if y < game.level.map.h - 1 then
+				tiles[2] = {x,     y + 1, c     + game.level.map.w, val, 2 }
+				tiles[3] = {x + 1, y + 1, c + 1 + game.level.map.w, val, 3 }
+			end
+			if y > 0 then
+				tiles[#tiles+1] = {x,     y - 1, c     - game.level.map.w, val, 8 }
+				tiles[#tiles+1] = {x + 1, y - 1, c + 1 - game.level.map.w, val, 9 }
+			end
+		end
+		return tiles
+	end,
+	-- Dir 5
+	function(node)
+		local tiles = {}
+		getNextNodes[5](node, tiles, tiles)
+		return tiles
+	end,
+	-- Dir 6
+	function(node)
+		local x, y, c, val = node[1], node[2], node[3], node[4]+1
+		local tiles = {{x - 1, y,     c - 1,                    val, 4 }}
+
+		if x < game.level.map.w - 1 then
+			tiles[2] = {x + 1, y,     c + 1,                    val, 6 }
+			if y < game.level.map.h - 1 then
+				tiles[3] = {x - 1, y + 1, c - 1 + game.level.map.w, val, 1 }
+				tiles[4] = {x,     y + 1, c     + game.level.map.w, val, 2 }
+				tiles[5] = {x + 1, y + 1, c + 1 + game.level.map.w, val, 3 }
+			end
+			if y > 0 then
+				tiles[#tiles+1] = {x - 1, y - 1, c - 1 - game.level.map.w, val, 7 }
+				tiles[#tiles+1] = {x,     y - 1, c     - game.level.map.w, val, 8 }
+				tiles[#tiles+1] = {x + 1, y - 1, c + 1 - game.level.map.w, val, 9 }
+			end
+		else
+			if y < game.level.map.h - 1 then
+				tiles[2] = {x - 1, y + 1, c - 1 + game.level.map.w, val, 1 }
+				tiles[3] = {x,     y + 1, c     + game.level.map.w, val, 2 }
+			end
+			if y > 0 then
+				tiles[#tiles+1] = {x - 1, y - 1, c - 1 - game.level.map.w, val, 7 }
+				tiles[#tiles+1] = {x,     y - 1, c     - game.level.map.w, val, 8 }
+			end
+		end
+		return tiles
+	end,
+	-- Dir 7
+	function(node)
+		local x, y, c, val = node[1], node[2], node[3], node[4]+1
+		local tiles = {{x,     y + 1, c     + game.level.map.w, val, 2 },
+		               {x + 1, y + 1, c + 1 + game.level.map.w, val, 3 },
+		               {x + 1, y,     c + 1,                    val, 6 }}
+		if x > 0 then
+			tiles[4]         = {x - 1, y + 1, c - 1 + game.level.map.w, val, 1 }
+			tiles[5]         = {x - 1, y,     c - 1,                    val, 4 }
+			if y > 0 then
+				tiles[6] = {x - 1, y - 1, c - 1 - game.level.map.w, val, 7 }
+				tiles[7] = {x,     y - 1, c     - game.level.map.w, val, 8 }
+				tiles[8] = {x + 1, y - 1, c + 1 - game.level.map.w, val, 9 }
+			end
+		elseif y > 0 then
+			tiles[4]         = {x,     y - 1, c     - game.level.map.w, val, 8 }
+			tiles[5]         = {x + 1, y - 1, c + 1 - game.level.map.w, val, 9 }
+		end
+		return tiles
+	end,
+	-- Dir 8
+	function(node)
+		local x, y, c, val = node[1], node[2], node[3], node[4]+1
+		local tiles = {{x,     y + 1, c     + game.level.map.w, val, 2 }}
+
+		if y > 0 then
+			tiles[2] = {x,     y - 1, c     - game.level.map.w, val, 8 }
+			if x > 0 then
+				tiles[3] = {x - 1, y + 1, c - 1 + game.level.map.w, val, 1 }
+				tiles[4] = {x - 1, y,     c - 1,                    val, 4 }
+				tiles[5] = {x - 1, y - 1, c - 1 - game.level.map.w, val, 7 }
+			end
+			if x < game.level.map.w - 1 then
+				tiles[#tiles+1] = {x + 1, y + 1, c + 1 + game.level.map.w, val, 3 }
+				tiles[#tiles+1] = {x + 1, y,     c + 1,                    val, 6 }
+				tiles[#tiles+1] = {x + 1, y - 1, c + 1 - game.level.map.w, val, 9 }
+			end
+		else
+			if x > 0 then
+				tiles[2] = {x - 1, y + 1, c - 1 + game.level.map.w, val, 1 }
+				tiles[3] = {x - 1, y,     c - 1,                    val, 4 }
+			end
+			if x < game.level.map.w - 1 then
+				tiles[#tiles+1] = {x + 1, y + 1, c + 1 + game.level.map.w, val, 3 }
+				tiles[#tiles+1] = {x + 1, y,     c + 1,                    val, 6 }
+			end
+		end
+		return tiles
+	end,
+	-- Dir 9
+	function(node)
+		local x, y, c, val = node[1], node[2], node[3], node[4]+1
+		local tiles = {{x - 1, y + 1, c - 1 + game.level.map.w, val, 1 },
+		               {x,     y + 1, c     + game.level.map.w, val, 2 },
+		               {x - 1, y,     c - 1,                    val, 4 }}
+		if x < game.level.map.w - 1 then
+			tiles[4]         = {x + 1, y + 1, c + 1 + game.level.map.w, val, 3 }
+			tiles[5]         = {x + 1, y,     c + 1,                    val, 6 }
+			if y > 0 then
+				tiles[6] = {x - 1, y - 1, c - 1 - game.level.map.w, val, 7 }
+				tiles[7] = {x,     y - 1, c     - game.level.map.w, val, 8 }
+				tiles[8] = {x + 1, y - 1, c + 1 - game.level.map.w, val, 9 }
+			end
+		elseif y > 0 then
+			tiles[4]         = {x - 1, y - 1, c - 1 - game.level.map.w, val, 7 }
+			tiles[5]         = {x,     y - 1, c     - game.level.map.w, val, 8 }
+		end
+		return tiles
+	end
+}
+
+-- List tiles that are adjacent to both current tile and previous tile that the previous tile iterated over.
+-- Right now these (and "listSharedNodesPrevious") are used to infer what might be a wall, and may be useful later.
+-- c = current, p = previous     c*    *c*
+-- * = returned tile             *p    .p.
+local listSharedNodes = {
+	-- Dir 1
+	function(node)
+		local x, y, c, val = unpack(node)
+		return {{x + 1, y,     c + 1,                    val, 6 },
+		        {x,     y - 1, c     - game.level.map.w, val, 8 }}
+	end,
+	-- Dir 2
+	function(node)
+		local x, y, c, val = unpack(node)
+		if     x < 1                    then return {{x + 1, y,     c + 1,                    val, 6 }}
+		elseif x > game.level.map.w - 2 then return {{x - 1, y,     c - 1,                    val, 4 }}
+		else return {{x - 1, y,     c - 1,                    val, 4 },
+		             {x + 1, y,     c + 1,                    val, 6 }}
+		end
+	end,
+	-- Dir 3
+	function(node)
+		local x, y, c, val = unpack(node)
+		return {{x - 1, y,     c - 1,                    val, 4 },
+		        {x,     y - 1, c     - game.level.map.w, val, 8 }}
+	end,
+	-- Dir 4
+	function(node)
+		local x, y, c, val = unpack(node)
+		if     y < 1                    then return {{x,     y + 1, c     + game.level.map.w, val, 2 }}
+		elseif y > game.level.map.h - 2 then return {{x,     y - 1, c     - game.level.map.w, val, 8 }}
+		else return {{x,     y + 1, c     + game.level.map.w, val, 2 },
+		             {x,     y - 1, c     - game.level.map.w, val, 8 }}
+		end
+	end,
+	-- Dir 5
+	function(node) return {} end,
+	-- Dir 6
+	function(node)
+		local x, y, c, val = unpack(node)
+		if     y < 1                    then return {{x,     y + 1, c     + game.level.map.w, val, 2 }}
+		elseif y > game.level.map.h - 2 then return {{x,     y - 1, c     - game.level.map.w, val, 8 }}
+		else return {{x,     y + 1, c     + game.level.map.w, val, 2 },
+		             {x,     y - 1, c     - game.level.map.w, val, 8 }}
+		end
+	end,
+	-- Dir 7
+	function(node)
+		local x, y, c, val = unpack(node)
+		return {{x,     y + 1, c     + game.level.map.w, val, 2 },
+		        {x + 1, y,     c + 1,                    val, 6 }}
+	end,
+	-- Dir 8
+	function(node)
+		local x, y, c, val = unpack(node)
+		if     x < 1                    then return {{x + 1, y,     c + 1,                    val, 6 }}
+		elseif x > game.level.map.w - 2 then return {{x - 1, y,     c - 1,                    val, 4 }}
+		else return {{x - 1, y,     c - 1,                    val, 4 },
+		             {x + 1, y,     c + 1,                    val, 6 }}
+		end
+	end,
+	-- Dir 9
+	function(node)
+		local x, y, c, val = unpack(node)
+		return {{x,     y + 1, c     + game.level.map.w, val, 2 },
+		        {x - 1, y,     c - 1,                    val, 4 }}
+	end
+}
+
+-- A partial complement to "listSharedNodes".  "listSharedNodes" and "listSharedNodesPrevious" allow us to easily
+-- check specific configurations, which will come in handy if/when I rewrite the "hack" for exploring large areas.
+-- c = current, p = previous     c.    .c.
+-- * = returned tile             .p    *p*
+local listSharedNodesPrevious = {
+	-- Dir 1
+	function(node) return {} end,
+	-- Dir 2
+	function(node)
+		local x, y, c, val = unpack(node)
+		if     x < 1                    then return {{x + 1, y - 1, c + 1 - game.level.map.w, val, 9 }}
+		elseif x > game.level.map.w - 2 then return {{x - 1, y - 1, c - 1 - game.level.map.w, val, 7 }}
+		else return {{x + 1, y - 1, c + 1 - game.level.map.w, val, 9 },
+		             {x - 1, y - 1, c - 1 - game.level.map.w, val, 7 }}
+		end
+	end,
+	-- Dir 3
+	function(node) return {} end,
+	-- Dir 4
+	function(node)
+		local x, y, c, val = unpack(node)
+		if     y < 1                    then return {{x + 1, y + 1, c + 1 + game.level.map.w, val, 3 }}
+		elseif y > game.level.map.h - 2 then return {{x + 1, y - 1, c + 1 - game.level.map.w, val, 9 }}
+		else return {{x + 1, y + 1, c + 1 + game.level.map.w, val, 3 },
+		             {x + 1, y - 1, c + 1 - game.level.map.w, val, 9 }}
+		end
+	end,
+	-- Dir 5
+	function(node) return {} end,
+	-- Dir 6
+	function(node)
+		local x, y, c, val = unpack(node)
+		if     y < 1                    then return {{x - 1, y + 1, c - 1 + game.level.map.w, val, 1 }}
+		elseif y > game.level.map.h - 2 then return {{x - 1, y - 1, c - 1 - game.level.map.w, val, 7 }}
+		else return {{x - 1, y + 1, c - 1 + game.level.map.w, val, 1 },
+		             {x - 1, y - 1, c - 1 - game.level.map.w, val, 7 }}
+		end
+	end,
+	-- Dir 7
+	function(node) return {} end,
+	-- Dir 8
+	function(node)
+		local x, y, c, val = unpack(node)
+		if     x < 1                    then return {{x + 1, y + 1, c + 1 + game.level.map.w, val, 3 }}
+		elseif x > game.level.map.w - 2 then return {{x - 1, y + 1, c - 1 + game.level.map.w, val, 1 }}
+		else return {{x + 1, y + 1, c + 1 + game.level.map.w, val, 3 },
+		             {x - 1, y + 1, c - 1 + game.level.map.w, val, 1 }}
+		end
+	end,
+	-- Dir 9
+	function(node) return {} end
+}
+
+local previousNode = {
+	-- Dir 1
+	function(node) local x, y, c, val = unpack(node) ; return {x + 1, y - 1, c + 1 - game.level.map.w, val-1, 9 } end,
+	-- Dir 2
+	function(node) local x, y, c, val = unpack(node) ; return {x,     y - 1, c     - game.level.map.w, val-1, 8 } end,
+	-- Dir 3
+	function(node) local x, y, c, val = unpack(node) ; return {x - 1, y - 1, c - 1 - game.level.map.w, val-1, 7 } end,
+	-- Dir 4
+	function(node) local x, y, c, val = unpack(node) ; return {x + 1, y,     c + 1,                    val-1, 6 } end,
+	-- Dir 5
+	function(node) local x, y, c, val = unpack(node) ; return {x,     y,     c,                        val-1, 5 } end,
+	-- Dir 6
+	function(node) local x, y, c, val = unpack(node) ; return {x - 1, y,     c - 1,                    val-1, 4 } end,
+	-- Dir 7
+	function(node) local x, y, c, val = unpack(node) ; return {x + 1, y + 1, c + 1 + game.level.map.w, val-1, 3 } end,
+	-- Dir 8
+	function(node) local x, y, c, val = unpack(node) ; return {x,     y + 1, c     + game.level.map.w, val-1, 2 } end,
+	-- Dir 9
+	function(node) local x, y, c, val = unpack(node) ; return {x - 1, y + 1, c - 1 + game.level.map.w, val-1, 1 } end
+}
+
+-- One more kindness to the player: take advantage of asymmetric LoS in this one specific case.
+-- If an enemy is at '?', the player is able to prevent an ambush by moving to 'x' instead of 't'.
+-- This is the only sensibly preventable ambush (that I know of) in which the player can move
+-- in a way to see the would-be ambusher and the would-be ambusher can't see the player.
+-- However, don't do this if it will step onto a known trap
+--
+--   .tx      Moving onto 't' puts us adjacent to an unseen tile, '?'
+--   ?#@      --> Pick 'x' instead
+local function checkAmbush(self)
+	if not self.running or not self.running.explore or not self.running.path or not self.running.path[self.running.cnt] then return end
+
+	local cx, cy = self.running.path[self.running.cnt].x, self.running.path[self.running.cnt].y
+	if math.abs(self.x - cx) == 1 and math.abs(self.y - cy) == 1 then
+		if game.level.map:checkAllEntities(self.x, cy, "block_move", self) and not game.level.map:checkAllEntities(cx, self.y, "block_move", self) and
+				game.level.map:isBound(self.x, 2*cy - self.y) and not game.level.map.has_seens(self.x, 2*cy - self.y) then
+			local trap = game.level.map(cx, self.y, Map.TRAP)
+			if not trap or not trap:knownBy(self) then
+				table.insert(self.running.path, self.running.cnt, {x=cx, y=self.y})
+			end
+		elseif game.level.map:checkAllEntities(cx, self.y, "block_move", self) and not game.level.map:checkAllEntities(self.x, cy, "block_move", self) and
+				game.level.map:isBound(2*cx - self.x, self.y) and not game.level.map.has_seens(2*cx - self.x, self.y) then
+			local trap = game.level.map(self.x, cy, Map.TRAP)
+			if not trap or not trap:knownBy(self) then
+				table.insert(self.running.path, self.running.cnt, {x=self.x, y=cy})
+			end
+		end
+	end
+end
+
+-- If a target destination is found, then it creates (or updates) "self.running" and returns true.
+-- If no target, or if we shouldn't explore for some reason, then return false.
 function _M:autoExplore()
+	-- levels that use "all_remembered" (like towns) don't set "has_seen" values to true for all grids,
+	-- so this lets us behave reasonably in these zones: go to objects and then exit
 	local do_unseen = not (game.level.all_remembered or game.zone and game.zone.all_remembered)
+
+	-- if we changed levels, then remove previous auto-explore information
+	if self.running_prev and self.running_prev.level ~= game.level then self.running_prev = nil end
+
 	local node = { self.x, self.y, toSingle(self.x, self.y), 0, 5 }
 	local current_tiles = { node }
 	local unseen_tiles = {}
@@ -230,7 +618,7 @@ function _M:autoExplore()
 
 	-- a few tunable parameters
 	local extra_iters = 5     -- number of extra iterations to do after we found an item or unseen tile
-	local singlet_greed = 5   -- number of additional moves we're willing to do to explore a single unseen tile
+	local singlet_greed = 4   -- number of additional moves we're willing to do to explore a single unseen tile
 	local item_greed = 5      -- number of additional moves we're willing to do to visit an unseen item rather than an unseen tile
 
 	-- Create a distance map array via flood-fill to locate unseen tiles, unvisited items, closed doors, and exits
@@ -241,13 +629,13 @@ function _M:autoExplore()
 		local diagonal_tiles = {}
 		-- Nearly half the time is spent here.  This could be implemented in C if desired, but I think it's fast enough
 		for _, node in ipairs(current_tiles) do
-			adjacentTiles[node[5]](node, cardinal_tiles, diagonal_tiles)
+			getNextNodes[node[5]](node, cardinal_tiles, diagonal_tiles)
 		end
 
 		-- The other half of the time is spent in this loop
 		for _, tile_list in ipairs({cardinal_tiles, diagonal_tiles}) do
 			for _, node in ipairs(tile_list) do
-				local x, y, c, move_cost = unpack(node)
+				local x, y, c, move_cost, dir = unpack(node)
 
 				if not game.level.map.has_seens(x, y) and do_unseen then
 					if not values[c] or values[c] > move_cost then
@@ -258,12 +646,50 @@ function _M:autoExplore()
 						end
 						-- Try to not abandon lone unseen tiles
 						local is_singlet = true
-						for _, anode in ipairs(listAdjacentTiles(node)) do
+						for _, anode in ipairs(listAdjacentNodes[dir](node)) do
 							if not game.level.map.has_seens(anode[1], anode[2]) then
 								is_singlet = false
 								break
 							end
 						end
+
+						-- look for tiles that are probably walls so we can hopefully explore more efficiently by preventing unnecessary return trips.
+						--               ?#       #?#
+						-- For example:  #.  and  ...  are probably walls (in most zones)
+						if not is_singlet then
+							is_singlet = true
+							for _, anode in ipairs(listSharedNodes[dir](node)) do
+								if not game.level.map.has_seens(anode[1], anode[2]) or not game.level.map:checkEntity(anode[1], anode[2], Map.TERRAIN, "does_block_move") then
+									is_singlet = false
+								-- if we propagated diagonally, then check if this might be a wall side, not corner
+								--  c = current,  1 = supposed wall    #c1
+								--  p = previous, 2 = supposed floor   p.2
+								elseif dir % 2 == 1 then
+									local x1 = 2*x - anode[1]
+									local y1 = 2*y - anode[2]
+									if game.level.map.has_seens(x1, y1) and game.level.map:checkEntity(x1, y1, Map.TERRAIN, "does_block_move") then
+										local pnode = previousNode[dir](node)
+										x1 = x1 + pnode[1] - anode[1]
+										y1 = y1 + pnode[2] - anode[2]
+										if game.level.map.has_seens(x1, y1) and not game.level.map:checkEntity(x1, y1, Map.TERRAIN, "does_block_move") then
+											is_singlet = true
+											break
+										end
+									end
+								end
+							end
+							-- if walls are where we expect them to be, check that floors are where we expect:
+							-- c = current, ? = supposed floor   #c#
+							-- p = previous,                     ?p?
+							if is_singlet then
+								for _, anode in ipairs(listSharedNodesPrevious[dir](node)) do
+									if not game.level.map.has_seens(anode[1], anode[2]) or game.level.map:checkEntity(anode[1], anode[2], Map.TERRAIN, "does_block_move") then
+										is_singlet = false
+										break
+									end
+								end
+							end
+						end
 						if is_singlet then
 							unseen_singlets[#unseen_singlets + 1] = c
 						end
@@ -315,7 +741,7 @@ function _M:autoExplore()
 						-- only go to closed doors with unseen grids behind them. We can go through "safe" doors
 						elseif terrain.door_opened and do_unseen then
 							local is_unexplored = false
-							for _, anode in ipairs(listAdjacentTiles(node)) do
+							for _, anode in ipairs(listAdjacentNodes[dir](node)) do
 								if not game.level.map.has_seens(anode[1], anode[2]) then
 									is_unexplored = true
 									break
@@ -339,7 +765,7 @@ function _M:autoExplore()
 						elseif terrain.orb_portal then
 							local is_portal_center = true
 							local is_small_portal = true
-							for _, anode in ipairs(listAdjacentTiles(node)) do
+							for _, anode in ipairs(listAdjacentNodes[dir](node)) do
 								if not game.level.map:checkEntity(anode[1], anode[2], Map.TERRAIN, "orb_portal") then
 									is_portal_center = false
 								else
@@ -408,11 +834,11 @@ function _M:autoExplore()
 		local mindist = 999999999999999
 		-- If we already have a suitable target that we haven't reached yet, then use that as our target.  This will be more useful
 		-- if or when we save info between between instances of running auto-explore.  For now it's useful when dodging traps on the fly.
-		if self.running and (self.running.explore == "exit" or self.running.explore == "portal") and (self.x ~= self.running.target.x or self.y ~= self.running.target.y) then
+		if self.running_prev and (self.running_prev.explore == "exit" or self.running_prev.explore == "portal") and (self.x ~= self.running_prev.target.x or self.y ~= self.running_prev.target.y) then
 			-- verify that the target is currently reachable
 			for _, c in ipairs(exits) do
 				local x, y = toDouble(c)
-				if x == self.running.target.x and y == self.running.target.y then
+				if x == self.running_prev.target.x and y == self.running_prev.target.y then
 					target_type = "exit"
 					choices[1] = c
 					distances[c] = 1
@@ -423,7 +849,7 @@ function _M:autoExplore()
 			if #choices == 0 then
 				for _, c in ipairs(portals) do
 					local x, y = toDouble(c)
-					if x == self.running.target.x and y == self.running.target.y then
+					if x == self.running_prev.target.x and y == self.running_prev.target.y then
 						target_type = "portal"
 						choices[1] = c
 						distances[c] = 1
@@ -433,11 +859,11 @@ function _M:autoExplore()
 				end
 			end
 		end
-		-- try to explore cleanly--don't leave single unseen tiles by themselves
-		if #choices == 0 then
-			for _, c in ipairs(unseen_singlets) do
-				if values[c] <= minval + singlet_greed then
-					target_type = "unseen"
+		-- go to closest items first
+		if #choices == 0 and minval_items <= minval + item_greed then
+			for _, c in ipairs(unseen_items) do
+				if values[c] == minval_items then
+					target_type = "object"
 					choices[#choices + 1] = c
 					local x, y = toDouble(c)
 					local dist = core.fov.distance(self.x, self.y, x, y, true)
@@ -448,11 +874,11 @@ function _M:autoExplore()
 				end
 			end
 		end
-		-- go to closest items first
-		if #choices == 0 and minval_items <= minval + item_greed then
-			for _, c in ipairs(unseen_items) do
-				if values[c] == minval_items then
-					target_type = "object"
+		-- try to explore cleanly--don't leave single unseen tiles by themselves
+		if #choices == 0 then
+			for _, c in ipairs(unseen_singlets) do
+				if values[c] <= minval + singlet_greed then
+					target_type = "unseen"
 					choices[#choices + 1] = c
 					local x, y = toDouble(c)
 					local dist = core.fov.distance(self.x, self.y, x, y, true)
@@ -741,10 +1167,10 @@ function _M:autoExplore()
 					-- don't run into adjacent interesting terrain if we've already been running
 					local x, y = path[1].x, path[1].y
 					local terrain = game.level.map(x, y, Map.TERRAIN)
-					if terrain.notice and (target_type ~= "exit" and target_type ~= "portal" or #path ~= 1 and not game.level.map.attrs(x, y, "noticed")) then
+					if terrain.notice and (target_type ~= "exit" and target_type ~= "portal" or #path ~= 1) then
 						if safe_doors[toSingle(x, y)] and not self.running.busy then
 							self.running.busy = { type = "opening door", do_move = true, no_energy = true }
-						else
+						elseif not game.level.map.attrs(x, y, "noticed") then
 							if terrain.change_level or terrain.orb_portal then game.level.map.attrs(x, y, "noticed", true) end
 							self:runStop("interesting terrain")
 							return false
@@ -759,6 +1185,8 @@ function _M:autoExplore()
 					self.running.ave_x = (self.running.ave_x*self.running.ave_N + 2*(target_x + self.x)) / (self.running.ave_N + 4)
 					self.running.ave_y = (self.running.ave_y*self.running.ave_N + 2*(target_y + self.y)) / (self.running.ave_N + 4)
 					self.running.ave_N = self.running.ave_N + 2
+					-- end hack!
+					checkAmbush(self)
 				else
 					-- another fringe case: if we target an item in an adjacent wall that we've probably already targeted, then mark it as seen and find a new target
 					if #path == 1 and target_type == "object" and game.level.map:checkEntity(target_x, target_y, Map.TERRAIN, "block_move", self, nil, true) then
@@ -776,13 +1204,24 @@ function _M:autoExplore()
 						end, false, true),
 						explore = target_type,
 						target = {x=target_x, y=target_y},
-						-- hack!
-						ave_x = 0.5*(target_x + self.x),
-						ave_y = 0.5*(target_y + self.y),
-						ave_N = 2,
+						level = game.level
 					}
+					-- hack!
+					if self.running_prev then
+						self.running.ave_N = 0.6*self.running_prev.ave_N
+						self.running.ave_x = (self.running_prev.ave_x*self.running.ave_N + 2*(target_x + self.x)) / (self.running.ave_N + 4)
+						self.running.ave_y = (self.running_prev.ave_y*self.running.ave_N + 2*(target_y + self.y)) / (self.running.ave_N + 4)
+						self.running.ave_N = 2*math.floor(0.5*self.running.ave_N + 1)
+					else
+						self.running.ave_x = 0.5*(target_x + self.x)
+						self.running.ave_y = 0.5*(target_y + self.y)
+						self.running.ave_N = 2
+					end
+					-- end hack!
+
 					self.running.dialog.__showup = nil
 					self.running.dialog.__hidden = true
+					self.running_prev = self.running
 
 					self:runStep()
 				end
@@ -860,29 +1299,8 @@ function _M:checkAutoExplore()
 		return self:autoExplore()
 	end
 
-	-- One more kindness to the player: take advantage of asymmetric LoS in this one specific case.
-	-- If an enemy is at '?', the player is able to prevent an ambush by moving to 'x' instead of 't'.
-	-- This is the only sensibly preventable ambush (that I know of) in which the player can move
-	-- in a way to see the would-be ambusher and the would-be ambusher can't see the player.
-	-- However, don't do this if it will step onto a known trap
-	--
-	--   .tx      Moving onto 't' puts us adjacent to an unseen tile, '?'
-	--   ?#@      --> Pick 'x' instead
-	if math.abs(self.x - cx) == 1 and math.abs(self.y - cy) == 1 then
-		if game.level.map:checkAllEntities(self.x, cy, "block_move", self) and not game.level.map:checkAllEntities(cx, self.y, "block_move", self) and
-				game.level.map:isBound(self.x, 2*cy - self.y) and not game.level.map.has_seens(self.x, 2*cy - self.y) then
-			local trap = game.level.map(cx, self.y, Map.TRAP)
-			if not trap or not trap:knownBy(self) then
-				table.insert(self.running.path, self.running.cnt, {x=cx, y=self.y})
-			end
-		elseif game.level.map:checkAllEntities(cx, self.y, "block_move", self) and not game.level.map:checkAllEntities(self.x, cy, "block_move", self) and
-				game.level.map:isBound(2*cx - self.x, self.y) and not game.level.map.has_seens(2*cx - self.x, self.y) then
-			local trap = game.level.map(self.x, cy, Map.TRAP)
-			if not trap or not trap:knownBy(self) then
-				table.insert(self.running.path, self.running.cnt, {x=self.x, y=cy})
-			end
-		end
-	end
+	-- avoid a simple preventable ambush
+	checkAmbush(self)
 
 	-- continue current path if we haven't seen the target tile or object yet
 	if not game.level.map.has_seens(tx, ty) then return true end
-- 
GitLab