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