From 5e2b994adff14c889e5a7be6f325b223d4cc2f60 Mon Sep 17 00:00:00 2001
From: dg <dg@51575b47-30f0-44d4-a5cc-537603b46e54>
Date: Mon, 7 Nov 2011 09:17:02 +0000
Subject: [PATCH] plop

git-svn-id: http://svn.net-core.org/repos/t-engine4@4619 51575b47-30f0-44d4-a5cc-537603b46e54
---
 .../engine/interface/PlayerExplore.lua        | 500 ++++++++++++++++++
 1 file changed, 500 insertions(+)
 create mode 100644 game/engines/default/engine/interface/PlayerExplore.lua

diff --git a/game/engines/default/engine/interface/PlayerExplore.lua b/game/engines/default/engine/interface/PlayerExplore.lua
new file mode 100644
index 0000000000..c35af67fd9
--- /dev/null
+++ b/game/engines/default/engine/interface/PlayerExplore.lua
@@ -0,0 +1,500 @@
+-- TE4 - T-Engine 4
+-- Copyright (C) 2009, 2010, 2011 Nicolas Casalini
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of the GNU General Public License as published by
+-- the Free Software Foundation, either version 3 of the License, or
+-- (at your option) any later version.
+--
+-- This program is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+-- GNU General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see <http://www.gnu.org/licenses/>.
+--
+-- Nicolas Casalini "DarkGod"
+-- darkgod@te4.org
+
+--- This file implements a simple auto-explore whereby a single command can explore unseen tiles and objects.
+-- To see how a module can make auto-explore more robust, see "game/modules/tome/class/interface/PlayerExplore.lua"
+--
+-- Note that the floodfill algorithm in this file assumes that the movement costs for all grids are equal
+
+require "engine.class"
+local Map = require "engine.Map"
+local Dialog = require "engine.ui.Dialog"
+
+module(..., package.seeall, class.make)
+
+local function toSingle(x, y)
+	return x + y * game.level.map.w
+end
+
+local function toDouble(c)
+	local y = math.floor(c / game.level.map.w)
+	return c - y * game.level.map.w, y
+end
+
+local function listAdjacentTiles(node, no_diagonal, no_cardinal)
+	local tiles = {}
+	local x, y, c
+	if type(node) == "table" then
+		x, y, c = unpack(node)
+	elseif type(node) == "number" then
+		x, y = toDouble(node)
+		c = node
+	else
+		return tiles
+	end
+
+	local left_okay = x > 0
+	local right_okay = x < game.level.map.w - 1
+	local lower_okay = y > 0
+	local upper_okay = y < game.level.map.h - 1
+
+	if not no_cardinal then
+		if upper_okay then tiles[1]        = {x,     y + 1, c + game.level.map.w, 2 } end
+		if left_okay  then tiles[#tiles+1] = {x - 1, y,     c - 1,                4 } end
+		if right_okay then tiles[#tiles+1] = {x + 1, y,     c + 1,                6 } end
+		if lower_okay then tiles[#tiles+1] = {x,     y - 1, c - game.level.map.w, 8 } end
+	end
+	if not no_diagonal then
+		if left_okay  and upper_okay then tiles[#tiles+1] = {x - 1, y + 1, c - 1 + game.level.map.w, 1 } end
+		if right_okay and upper_okay then tiles[#tiles+1] = {x + 1, y + 1, c + 1 + game.level.map.w, 3 } end
+		if left_okay  and lower_okay then tiles[#tiles+1] = {x - 1, y - 1, c - 1 - game.level.map.w, 7 } end
+		if right_okay and lower_okay then tiles[#tiles+1] = {x + 1, y - 1, c + 1 - game.level.map.w, 9 } end
+	end
+	return tiles
+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.
+-- 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 = {
+	-- Dir 1
+	function(node, cardinal_tiles, diagonal_tiles)
+		local x, y, c = unpack(node)
+
+		if y < game.level.map.h - 1 then
+			cardinal_tiles[#cardinal_tiles+1]         = {x,     y + 1, c     + game.level.map.w, 2 }
+			diagonal_tiles[#diagonal_tiles+1]         = {x + 1, y + 1, c + 1 + game.level.map.w, 3 }
+			if x > 0 then
+				diagonal_tiles[#diagonal_tiles+1] = {x - 1, y + 1, c - 1 + game.level.map.w, 1 }
+				cardinal_tiles[#cardinal_tiles+1] = {x - 1, y,     c - 1,                    4 }
+				diagonal_tiles[#diagonal_tiles+1] = {x - 1, y - 1, c - 1 - game.level.map.w, 7 }
+			end
+		elseif x > 0 then
+			cardinal_tiles[#cardinal_tiles+1]         = {x - 1, y,     c - 1,                    4 }
+			diagonal_tiles[#diagonal_tiles+1]         = {x - 1, y - 1, c - 1 - game.level.map.w, 7 }
+		end
+	end,
+	--Dir 2
+	function(node, cardinal_tiles, diagonal_tiles)
+		local x, y, c = unpack(node)
+		if y > game.level.map.h - 2 then return end
+
+		if x > 0 then diagonal_tiles[#diagonal_tiles+1]                    = {x - 1, y + 1, c - 1 + game.level.map.w, 1 } end
+		cardinal_tiles[#cardinal_tiles+1]                                  = {x,     y + 1, c     + game.level.map.w, 2 }
+		if x < game.level.map.w - 1 then diagonal_tiles[#diagonal_tiles+1] = {x + 1, y + 1, c + 1 + game.level.map.w, 3 } end
+	end,
+	-- Dir 3
+	function(node, cardinal_tiles, diagonal_tiles)
+		local x, y, c = unpack(node)
+
+		if y < game.level.map.h - 1 then
+			diagonal_tiles[#diagonal_tiles+1]         = {x - 1, y + 1, c - 1 + game.level.map.w, 1 }
+			cardinal_tiles[#cardinal_tiles+1]         = {x,     y + 1, c     + game.level.map.w, 2 }
+			if x < game.level.map.w - 1 then
+				diagonal_tiles[#diagonal_tiles+1] = {x + 1, y + 1, c + 1 + game.level.map.w, 3 }
+				cardinal_tiles[#cardinal_tiles+1] = {x + 1, y,     c + 1,                    6 }
+				diagonal_tiles[#diagonal_tiles+1] = {x + 1, y - 1, c + 1 - game.level.map.w, 9 }
+			end
+		elseif x < game.level.map.w - 1 then
+			cardinal_tiles[#cardinal_tiles+1]         = {x + 1, y,     c + 1,                    6 }
+			diagonal_tiles[#diagonal_tiles+1]         = {x + 1, y - 1, c + 1 - game.level.map.w, 9 }
+		end
+	end,
+	--Dir 4
+	function(node, cardinal_tiles, diagonal_tiles)
+		local x, y, c = unpack(node)
+		if x < 1 then return end
+
+		if y < game.level.map.h - 1 then diagonal_tiles[#diagonal_tiles+1] = {x - 1, y + 1, c - 1 + game.level.map.w, 1 } end
+		cardinal_tiles[#cardinal_tiles+1]                                  = {x - 1, y,     c - 1,                    4 }
+		if y > 0 then diagonal_tiles[#diagonal_tiles+1]                    = {x - 1, y - 1, c - 1 - game.level.map.w, 7 } end
+	end,
+	--Dir 5 (all adjacent, slow)
+	function(node, cardinal_tiles, diagonal_tiles)
+		local x, y, c = unpack(node)
+	
+		local left_okay = x > 0
+		local right_okay = x < game.level.map.w - 1
+		local lower_okay = y > 0
+		local upper_okay = y < game.level.map.h - 1
+	
+		if upper_okay then cardinal_tiles[#cardinal_tiles+1] = {x,     y + 1, c + game.level.map.w, 2 } end
+		if left_okay  then cardinal_tiles[#cardinal_tiles+1] = {x - 1, y,     c - 1,                4 } end
+		if right_okay then cardinal_tiles[#cardinal_tiles+1] = {x + 1, y,     c + 1,                6 } end
+		if lower_okay then cardinal_tiles[#cardinal_tiles+1] = {x,     y - 1, c - game.level.map.w, 8 } end
+
+		if left_okay  and upper_okay then diagonal_tiles[#diagonal_tiles+1] = {x - 1, y + 1, c - 1 + game.level.map.w, 1 } end
+		if right_okay and upper_okay then diagonal_tiles[#diagonal_tiles+1] = {x + 1, y + 1, c + 1 + game.level.map.w, 3 } end
+		if left_okay  and lower_okay then diagonal_tiles[#diagonal_tiles+1] = {x - 1, y - 1, c - 1 - game.level.map.w, 7 } end
+		if right_okay and lower_okay then diagonal_tiles[#diagonal_tiles+1] = {x + 1, y - 1, c + 1 - game.level.map.w, 9 } end
+	end,
+	--Dir 6
+	function(node, cardinal_tiles, diagonal_tiles)
+		local x, y, c = unpack(node)
+		if x > game.level.map.w - 2 then return end
+
+		if y < game.level.map.h - 1 then diagonal_tiles[#diagonal_tiles+1] = {x + 1, y + 1, c + 1 + game.level.map.w, 3 } end
+		cardinal_tiles[#cardinal_tiles+1]                                  = {x + 1, y,     c + 1,                    6 }
+		if y > 0 then diagonal_tiles[#diagonal_tiles+1]                    = {x + 1, y - 1, c + 1 - game.level.map.w, 9 } end
+	end,
+	-- Dir 7
+	function(node, cardinal_tiles, diagonal_tiles)
+		local x, y, c = unpack(node)
+
+		if x > 0 then
+			diagonal_tiles[#diagonal_tiles+1]         = {x - 1, y + 1, c - 1 + game.level.map.w, 1 }
+			cardinal_tiles[#cardinal_tiles+1]         = {x - 1, y,     c - 1,                    4 }
+			if y > 0 then
+				diagonal_tiles[#diagonal_tiles+1] = {x - 1, y - 1, c - 1 - game.level.map.w, 7 }
+				cardinal_tiles[#cardinal_tiles+1] = {x,     y - 1, c     - game.level.map.w, 8 }
+				diagonal_tiles[#diagonal_tiles+1] = {x + 1, y - 1, c + 1 - game.level.map.w, 9 }
+			end
+		elseif y > 0 then
+			cardinal_tiles[#cardinal_tiles+1]         = {x,     y - 1, c     - game.level.map.w, 8 }
+			diagonal_tiles[#diagonal_tiles+1]         = {x + 1, y - 1, c + 1 - game.level.map.w, 9 }
+		end
+	end,
+	--Dir 8
+	function(node, cardinal_tiles, diagonal_tiles)
+		local x, y, c = unpack(node)
+		if y < 1 then return end
+
+		if x > 0 then diagonal_tiles[#diagonal_tiles+1]                    = {x - 1, y - 1, c - 1 - game.level.map.w, 7 } end
+		cardinal_tiles[#cardinal_tiles+1]                                  = {x,     y - 1, c     - game.level.map.w, 8 }
+		if x < game.level.map.w - 1 then diagonal_tiles[#diagonal_tiles+1] = {x + 1, y - 1, c + 1 - game.level.map.w, 9 } end
+	end,
+	-- Dir 9
+	function(node, cardinal_tiles, diagonal_tiles)
+		local x, y, c = unpack(node)
+
+		if x < game.level.map.w - 1 then
+			diagonal_tiles[#diagonal_tiles+1]         = {x + 1, y + 1, c + 1 + game.level.map.w, 3 }
+			cardinal_tiles[#cardinal_tiles+1]         = {x + 1, y,     c + 1,                    6 }
+			if y > 0 then
+				diagonal_tiles[#diagonal_tiles+1] = {x - 1, y - 1, c - 1 - game.level.map.w, 7 }
+				cardinal_tiles[#cardinal_tiles+1] = {x,     y - 1, c     - game.level.map.w, 8 }
+				diagonal_tiles[#diagonal_tiles+1] = {x + 1, y - 1, c + 1 - game.level.map.w, 9 }
+			end
+		elseif y > 0 then
+			diagonal_tiles[#diagonal_tiles+1]         = {x - 1, y - 1, c - 1 - game.level.map.w, 7 }
+			cardinal_tiles[#cardinal_tiles+1]         = {x,     y - 1, c     - game.level.map.w, 8 }
+		end
+	end
+}
+
+function _M:autoExplore()
+	local node = { self.x, self.y, toSingle(self.x, self.y), 5 }
+	local current_tiles = { node }
+	local unseen_tiles = {}
+	local unseen_singlets = {}
+	local unseen_items = {}
+	local exits = {}
+	local values = {}
+	values[node[3]] = 0
+	local iter = 1
+	local running = true
+	local minval = 999999999999999
+	local minval_items = 999999999999999
+	local val, _, anode, tile_list
+
+	-- 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 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 and unvisited items
+	while running do
+		-- construct lists of adjacent tiles to iterate over, and iterate in cardinal directions first
+		local current_tiles_next = {}
+		local cardinal_tiles = {}
+		local diagonal_tiles = {}
+		for _, node in ipairs(current_tiles) do
+			adjacentTiles[node[4]](node, cardinal_tiles, diagonal_tiles)
+		end
+
+		for _, tile_list in ipairs({cardinal_tiles, diagonal_tiles}) do
+			for _, node in ipairs(tile_list) do
+				local x, y, c = unpack(node)
+
+				if not values[c] then
+					if not game.level.map.has_seens(x, y) then
+						unseen_tiles[#unseen_tiles + 1] = c
+						values[c] = iter
+						if iter < minval then
+							minval = iter
+						end
+						-- Try to not abandon lone unseen tiles
+						local is_singlet = true
+						for _, anode in ipairs(listAdjacentTiles(node)) do
+							if not game.level.map.has_seens(anode[1], anode[2]) then
+								is_singlet = false
+								break
+							end
+						end
+						if is_singlet then
+							unseen_singlets[#unseen_singlets + 1] = c
+						end
+					else
+						-- propagate "current_tiles" for next iteration
+						local trap = game.level.map(x, y, Map.TRAP)
+						if not (game.level.map:checkAllEntities(x, y, "block_move", self) or trap and trap:knownBy(self)) then
+							values[c] = iter
+							current_tiles_next[#current_tiles_next + 1] = node
+						end
+						-- only go to objects we haven't walked over yet
+						local obj = game.level.map:getObject(x, y, 1)
+						if obj and not game.level.map.attrs(x, y, "obj_seen") then
+							unseen_items[#unseen_items + 1] = c
+							values[c] = iter
+							if iter < minval_items then
+								minval_items = iter
+							end
+						end
+					end
+				end
+			end
+		end
+		-- Continue the loop if we haven't found any destination tiles
+		running = #unseen_tiles == 0 and #unseen_items == 0
+		-- performing a few extra iteration will help us find items and "singlets"
+		if not running and extra_iters > 0 then
+			running = true
+			extra_iters = extra_iters - 1
+		end
+
+		-- stop the loop if there are no more tiles to iterate over
+		running = running and #current_tiles_next > 0
+		current_tiles = current_tiles_next
+
+		iter = iter + 1
+	end
+
+	-- Choose target
+	if #unseen_tiles > 0 or #unseen_items > 0 then
+		local target_type
+		local choices = {}
+		local distances = {}
+		local mindist = 999999999999999
+		-- try to explore cleanly--don't leave single unseen tiles by themselves
+		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)
+				distances[c] = dist
+				if dist < mindist then
+					mindist = dist
+				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 = "item"
+					choices[#choices + 1] = c
+					local x, y = toDouble(c)
+					local dist = core.fov.distance(self.x, self.y, x, y, true)
+					distances[c] = dist
+					if dist < mindist then
+						mindist = dist
+					end
+				end
+			end
+		end
+		-- if no nearby items, go to nearest unseen tile
+		if #choices == 0 then
+			for _, c in ipairs(unseen_tiles) do
+				if values[c] == minval 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)
+					distances[c] = dist
+					if dist < mindist then
+						mindist = dist
+					end
+				end
+			end
+		end
+
+		-- if multiple choices, then choose nearest one based on fov distance metric
+		if #choices > 1 then
+			local choices2 = {}
+			for _, c in ipairs(choices) do
+				if distances[c] == mindist then
+					choices2[#choices2 + 1] = c
+				end
+			end
+			choices = choices2
+		end
+		-- if still multiple choices, then choose one randomly
+		local target = #choices > 0 and rng.table(choices) or nil
+
+		-- Now create the path to the target (constructed from target to source)
+		if target then
+			local target_x, target_y = toDouble(target)
+			local x, y = toDouble(target)
+			local path = {{x=x, y=y}}
+			local current_val = values[target]
+			-- the idiot check condition should NEVER occur, but, well, if it does, it'll save us from being stuck in an infinite loop
+			local idiot_counter = 0
+			local idiot_check = current_val + 5
+			while (path[#path].x ~= self.x or path[#path].y ~= self.y) and idiot_counter <= idiot_check do
+				idiot_counter = idiot_counter + 1
+				-- perform a greedy minimization that prefers cardinal directions
+				local cardinals = {}
+				local min_cardinal = current_val
+				for _, node in ipairs(listAdjacentTiles(target, true)) do
+					local c = node[3]
+					if values[c] and values[c] <= min_cardinal then
+						min_cardinal = values[c]
+						cardinals[#cardinals + 1] = node
+					end
+				end
+				local diagonals = {}
+				local min_diagonal = current_val
+				for _, node in ipairs(listAdjacentTiles(target, false, true)) do
+					local c = node[3]
+					if values[c] and values[c] < min_diagonal then
+						min_diagonal = values[c]
+						diagonals[#diagonals + 1] = node
+					end
+				end
+
+				-- Favor cardinal directions since we are constructing the path in reverse.
+				-- This results in dog-leg (or hockey stick)-like movement.  If desired, we could try adding an A*-like heuristic
+				-- to favor straighter line movement (i.e., alternate between diagonal and cardinal moves), but, meh, whatever ;)
+				if #cardinals == 0 or min_diagonal < min_cardinal then
+					current_val = min_diagonal
+					for _, node in ipairs(diagonals) do
+						if values[node[3]] == min_diagonal then
+							path[#path + 1] = {x=node[1], y=node[2]}
+							target = node[3]
+							break
+						end
+					end
+				else
+					current_val = min_cardinal
+					for _, node in ipairs(cardinals) do
+						if values[node[3]] == min_cardinal then
+							path[#path + 1] = {x=node[1], y=node[2]}
+							target = node[3]
+							break
+						end
+					end
+				end
+			end
+
+			-- sanity check.  This should NEVER occur, but if it does by freak accident, let's be prepared
+			if path[#path].x ~= self.x or path[#path].y ~= self.y then
+				path = {}
+			else
+				-- un-reverse the path
+				local temp_path = {}
+				for i = #path-1, 1, -1 do temp_path[#temp_path + 1] = path[i] end
+				path = temp_path
+			end
+
+			if #path > 0 then
+				if self.running and self.running.explore then
+					self.running.path = path
+					self.running.cnt = 1
+					self.running.explore = target_type
+				else
+					self.running = {
+						path = path,
+						cnt = 1,
+						dialog = Dialog:simplePopup("Running...", "You are exploring, press any key to stop.", function()
+							self:runStop()
+						end, false, true),
+						explore = target_type,
+					}
+					self.running.dialog.__showup = nil
+					self.running.dialog.__hidden = true
+				
+					self:runStep()
+				end
+				return true
+			end
+		end
+	end
+	return false
+end
+
+function _M:checkAutoExplore()
+	if not self.running or not self.running.explore then return false end
+
+	-- if we're at the end of the path and we're searching for unseen tiles, then continue with a new path
+	local x, y = self.running.path[#self.running.path].x, self.running.path[#self.running.path].y
+	local obj = game.level.map:getObject(x, y, 1)
+	local node = self.running.path[self.running.cnt]
+	if not node then
+		if self.running.explore == "unseen" or self.running.explore == "item" and not obj then
+			return self:autoExplore()
+		else
+			return false
+		end
+	end
+
+	-- if the next spot in the path is blocked, explore a new path if we are searching for unseen tiles, otherwise stop
+	if game.level.map.has_seens(node.x, node.y) and game.level.map:checkEntity(node.x, node.y, Map.TERRAIN, "block_move", self, nil, true) then
+	-- game.level.map:checkAllEntities(node.x, node.y, "block_move", self) then
+		if self.running.explore == "unseen" then 
+			return self:autoExplore()
+		else
+			self:runStop("the path is blocked")
+			return false
+		end
+	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.
+	-- 
+	--   .tx      Moving onto 't' puts us adjacent to an unseen tile, '?'
+	--   ?#@      --> Pick 'x' instead
+	if math.abs(self.x - node.x) == 1 and math.abs(self.y - node.y) == 1 then
+		if game.level.map:checkAllEntities(self.x, node.y, "block_move", self) and not game.level.map:checkAllEntities(node.x, self.y, "block_move", self) and
+				game.level.map:isBound(self.x, 2*node.y - self.y) and not game.level.map.has_seens(self.x, 2*node.y - self.y) then
+			table.insert(self.running.path, self.running.cnt, {x=node.x, y=self.y})
+		elseif game.level.map:checkAllEntities(node.x, self.y, "block_move", self) and not game.level.map:checkAllEntities(self.x, node.y, "block_move", self) and
+				game.level.map:isBound(2*node.x - self.x, self.y) and not game.level.map.has_seens(2*node.x - self.x, self.y) then
+			table.insert(self.running.path, self.running.cnt, {x=self.x, y=node.y})
+		end
+	end
+
+	-- continue current path if we haven't seen the target tile or object yet
+	if not game.level.map.has_seens(x, y) then return true end
+	if obj and not game.level.map.attrs(x, y, "obj_seen") then return true end
+
+	-- if we have explored the unseen node, then continue auto-exploring somewhere else
+	if self.running.explore == "unseen" or self.running.explore == "item" and not obj then
+		return self:autoExplore()
+	else
+	-- otherwise, try to continue running on the current path to reach our target
+		return true
+	end
+end
+
-- 
GitLab