{-# LANGUAGE DeriveGeneric, GeneralizedNewtypeDeriving, RankNTypes, TypeFamilies #-} -- | Breadth first search algorithm. module Game.LambdaHack.Client.Bfs ( BfsDistance, MoveLegal(..) , subtractBfsDistance, minKnownBfs, apartBfs, maxBfsDistance, fillBfs , AndPath(..), findPathBfs, accessBfs #ifdef EXPOSE_INTERNAL -- * Internal operations , succBfsDistance, predBfsDistance, abortedUnknownBfs, maskBfs, distanceBfs #endif ) where import Prelude () import Game.LambdaHack.Core.Prelude import Control.Monad.ST.Strict (ST, runST) import Data.Binary import Data.Bits (Bits, complement, (.&.), (.|.)) import qualified Data.EnumSet as ES import qualified Data.IntSet as IS import qualified Data.Primitive.PrimArray as PA import qualified Data.Vector.Unboxed as U import qualified Data.Vector.Unboxed.Mutable as VM import GHC.Exts (inline) import GHC.Generics (Generic) import Game.LambdaHack.Common.Point import qualified Game.LambdaHack.Common.PointArray as PointArray import Game.LambdaHack.Common.Vector import Game.LambdaHack.Definition.Defs -- @Word8@ is much faster, but in some very rare cases leads to AI loops, -- e.g., when a move through uknown terrain towards enemy stash -- goes beyond the @apartBfs@ range and makes AI abandon the stash target, -- only to pick it up after a step in the opposite direction. -- In normal LH maps, path length can get to around 200, -- in contrived mazes it could perhaps reach a few thousand. type DistanceWord = Word16 -- | Weighted distance between points along shortest paths. newtype BfsDistance = BfsDistance {bfsDistance :: DistanceWord} deriving (Show, Eq, Ord, Bits) instance PointArray.UnboxRepClass BfsDistance where type UnboxRep BfsDistance = DistanceWord toUnboxRepUnsafe = bfsDistance fromUnboxRep = BfsDistance -- | State of legality of moves between adjacent points. data MoveLegal = MoveBlocked | MoveToOpen | MoveToClosed | MoveToUnknown deriving Eq succBfsDistance :: BfsDistance -> BfsDistance succBfsDistance d = BfsDistance $ bfsDistance d + 1 predBfsDistance :: BfsDistance -> BfsDistance predBfsDistance d = BfsDistance $ bfsDistance d - 1 subtractBfsDistance :: BfsDistance -> BfsDistance -> Int subtractBfsDistance d1 d2 = fromEnum $ bfsDistance d1 - bfsDistance d2 -- | The minimal distance value assigned to paths that don't enter -- any unknown tiles. minKnownBfs :: BfsDistance minKnownBfs = BfsDistance $ 1 + maxBound `div` 2 -- | The distance value that denotes no legal path between points, -- either due to blocked tiles or pathfinding aborted at earlier tiles, -- e.g., due to unknown tiles. apartBfs :: BfsDistance apartBfs = predBfsDistance minKnownBfs -- | Maximum value of the type. maxBfsDistance :: BfsDistance maxBfsDistance = BfsDistance (maxBound :: DistanceWord) -- | The distance value that denotes that path search was aborted -- at this tile due to too large actual distance -- and that the tile was unknown. -- It is also a true distance value for this tile. abortedUnknownBfs :: BfsDistance abortedUnknownBfs = predBfsDistance apartBfs maskBfs :: BfsDistance -> BfsDistance {-# INLINE maskBfs #-} maskBfs distance = distance .&. complement minKnownBfs -- | Create and fill a BFS array for the given level. -- Unsafe array operations are OK here, because the intermediate -- values of the vector don't leak anywhere outside nor are kept unevaluated -- and so they can't be overwritten by the unsafe side-effect. -- -- When computing move cost, we assume doors openable at no cost, -- because other actors use them, too, so the cost is shared and the extra -- visiblity is valuable, too. We treat unknown tiles specially. -- Whether suspect tiles are considered openable depends on @smarkSuspect@. -- -- Instead of a BFS queue (list) we use the two tabs (arrays), for (JS) speed. fillBfs :: PointArray.Array Word8 -> Word8 -> Point -> (PA.PrimArray PointI, PA.PrimArray PointI) -> PointArray.Array BfsDistance fillBfs !lalter !alterSkill !source (!tabA, !tabB) = runST $ do let arr = PointArray.replicateA (PointArray.axsize lalter) (PointArray.aysize lalter) apartBfs vThawed <- U.unsafeThaw $ PointArray.avector arr tabAThawed <- PA.unsafeThawPrimArray tabA tabBThawed <- PA.unsafeThawPrimArray tabB fillBfsThawed lalter alterSkill (fromEnum source) (tabAThawed, tabBThawed) vThawed void $ PA.unsafeFreezePrimArray tabAThawed void $ PA.unsafeFreezePrimArray tabBThawed void $ U.unsafeFreeze vThawed return arr type QueueIx = Int type NextQueueIx = Int -- So very low-level that not even under EXPOSE_INTERNAL. fillBfsThawed :: forall s. PointArray.Array Word8 -> Word8 -> PointI -> (PA.MutablePrimArray s PointI, PA.MutablePrimArray s PointI) -> U.MVector s DistanceWord -> ST s () fillBfsThawed !lalter !alterSkill !sourceI (!tabAThawed, !tabBThawed) !vThawed = do let unsafeReadI :: PointI -> ST s BfsDistance {-# INLINE unsafeReadI #-} #ifdef WITH_EXPENSIVE_ASSERTIONS unsafeReadI p = BfsDistance <$> VM.read vThawed p -- index checking is sometimes an expensive (kind of) assertion #else unsafeReadI p = BfsDistance <$> VM.unsafeRead vThawed p #endif unsafeWriteI :: PointI -> BfsDistance -> ST s () {-# INLINE unsafeWriteI #-} #ifdef WITH_EXPENSIVE_ASSERTIONS unsafeWriteI p c = VM.write vThawed p (bfsDistance c) #else unsafeWriteI p c = VM.unsafeWrite vThawed p (bfsDistance c) #endif -- The two tabs (arrays) are used as a staged, optimized queue. -- The first tab is for writes, the second one for reads. -- They switch places in each recursive @bfs@ call. bfs :: PA.MutablePrimArray s PointI -> PA.MutablePrimArray s PointI -> BfsDistance -> QueueIx -> ST s () bfs !tabReadThawed !tabWriteThawed !distance !prevQueueIx = do let unsafeReadCurrent :: QueueIx -> ST s PointI {-# INLINE unsafeReadCurrent #-} unsafeReadCurrent = PA.readPrimArray tabReadThawed unsafeWriteNext :: QueueIx -> PointI -> ST s () {-# INLINE unsafeWriteNext #-} unsafeWriteNext = PA.writePrimArray tabWriteThawed -- The accumulator and the result represent the index into the next -- queue tab, incremented after each write. processQueue :: QueueIx -> NextQueueIx -> ST s NextQueueIx processQueue !currentQueueIx !acc1 = if currentQueueIx == -1 then return acc1 -- all queued positions inspected else do pos <- unsafeReadCurrent currentQueueIx let processMove :: (X, Y) -> NextQueueIx -> ST s NextQueueIx {-# INLINE processMove #-} processMove move acc2 = do let p = pos + inline fromEnum (uncurry Vector move) pDist <- unsafeReadI p if pDist /= apartBfs then return acc2 -- the position visited already else do let alter :: Word8 !alter = lalter `PointArray.accessI` p if | alterSkill < alter -> return acc2 | alter == 1 -> do let distCompl = maskBfs distance unsafeWriteI p distCompl return acc2 | otherwise -> do unsafeWriteI p distance unsafeWriteNext acc2 p return $! acc2 + 1 -- Innermost loop over @moves@ manually unrolled for (JS) speed: return acc1 >>= processMove (-1, -1) >>= processMove (0, -1) >>= processMove (1, -1) >>= processMove (1, 0) >>= processMove (1, 1) >>= processMove (0, 1) >>= processMove (-1, 1) >>= processMove (-1, 0) -- Recursive call to process next queue element: >>= processQueue (currentQueueIx - 1) acc3 <- processQueue (prevQueueIx - 1) 0 let distanceNew = succBfsDistance distance if acc3 == 0 || distanceNew == maxBfsDistance then return () -- no more close enough dungeon positions else bfs tabWriteThawed tabReadThawed distanceNew acc3 #ifdef WITH_EXPENSIVE_ASSERTIONS VM.write vThawed sourceI (bfsDistance minKnownBfs) #else VM.unsafeWrite vThawed sourceI (bfsDistance minKnownBfs) #endif PA.writePrimArray tabAThawed 0 sourceI bfs tabAThawed tabBThawed (succBfsDistance minKnownBfs) 1 data AndPath = AndPath { pathSource :: Point -- never included in @pathList@ , pathList :: [Point] , pathGoal :: Point -- needn't be @last pathList@ , pathLen :: Int -- needn't be @length pathList@ } deriving (Show, Generic) instance Binary AndPath -- | Find a path, without the source position, with the smallest length. -- The @eps@ coefficient determines which direction (of the closest -- directions available) that path should prefer, where 0 means north-west -- and 1 means north. The path tries hard to avoid actors and tries to avoid -- tiles that need altering and ambient light. Actors are avoided only close -- to the start of the path, because elsewhere they are likely to move -- before they are reached. Even projectiles are avoided, -- which sometimes has the effect of choosing a safer route -- (regardless if the projectiles are friendly fire or not). -- -- An unwelcome side effect of avoiding actors is that friends will sometimes -- avoid displacing and instead perform two separate moves, wasting 1 turn -- in total (only if they had opposed direction of their goals; unlikely). -- But in corridors they will still displace and elsewhere this scenario -- was quite rare already. findPathBfs :: ES.EnumSet Point -> PointArray.Array Word8 -> (PointI -> Bool) -> Point -> Point -> Int -> PointArray.Array BfsDistance -> Maybe AndPath {-# INLINE findPathBfs #-} findPathBfs lbig lalter fovLit pathSource pathGoal sepsRaw arr = let !pathGoalI = fromEnum pathGoal !pathSourceI = fromEnum pathSource eps = sepsRaw `mod` 4 (mc1, mc2) = splitAt eps movesCardinalI (md1, md2) = splitAt eps movesDiagonalI -- Prefer cardinal directions when closer to the target, so that -- the enemy can't easily disengage. prefMoves = mc2 ++ reverse mc1 ++ md2 ++ reverse md1 -- fuzz track :: PointI -> BfsDistance -> [Point] -> [Point] track !pos !oldDist !suffix | oldDist == minKnownBfs = assert (pos == pathSourceI) suffix track pos oldDist suffix | oldDist == succBfsDistance minKnownBfs = let !posP = toEnum pos in posP : suffix -- avoid calculating minP and dist for the last call track pos oldDist suffix = let !dist = predBfsDistance oldDist minChild :: PointI -> Bool -> Word8 -> [VectorI] -> PointI minChild !minP _ _ [] = minP minChild minP maxDark minAlter (mv : mvs) = let !p = pos + mv backtrackingMove = BfsDistance (arr `PointArray.accessI` p) /= dist in if backtrackingMove then minChild minP maxDark minAlter mvs else let free = p `IS.notMember` ES.enumSetToIntSet lbig alter | free = lalter `PointArray.accessI` p | otherwise = maxBound-1 -- occupied; disaster dark = not $ fovLit p -- Prefer paths without actors and through -- more easily opened tiles and, secondly, -- in the ambient dark (even if light carried, -- because it can be taken off at any moment). in if | alter == 0 && dark -> p -- speedup | alter < minAlter -> minChild p dark alter mvs | dark > maxDark && alter == minAlter -> minChild p dark alter mvs | otherwise -> minChild minP maxDark minAlter mvs -- @maxBound@ means not alterable, so some child will be lower !newPos = minChild pos{-dummy-} False maxBound prefMoves #ifdef WITH_EXPENSIVE_ASSERTIONS !_A = assert (newPos /= pos) () #endif !posP = toEnum pos in track newPos dist (posP : suffix) !goalDist = BfsDistance $ arr `PointArray.accessI` pathGoalI pathLen = fromEnum $ bfsDistance $ maskBfs goalDist pathList = track pathGoalI (goalDist .|. minKnownBfs) [] andPath = AndPath{..} in assert (BfsDistance (arr `PointArray.accessI` pathSourceI) == minKnownBfs) $ if goalDist /= apartBfs && pathLen < 2 * chessDist pathSource pathGoal then Just andPath else let f :: (Point, Int, Int, Int) -> Point -> BfsDistance -> (Point, Int, Int, Int) f acc@(pAcc, dAcc, chessAcc, sumAcc) p d = if d <= abortedUnknownBfs -- works in visible secrets mode only || d /= apartBfs && adjacent p pathGoal -- works for stairs then let dist = fromEnum $ bfsDistance $ maskBfs d chessNew = chessDist p pathGoal sumNew = dist + 2 * chessNew resNew = (p, dist, chessNew, sumNew) in case compare sumNew sumAcc of LT -> resNew EQ -> case compare chessNew chessAcc of LT -> resNew EQ -> case compare dist dAcc of LT -> resNew EQ | euclidDistSq p pathGoal < euclidDistSq pAcc pathGoal -> resNew _ -> acc _ -> acc _ -> acc else acc initAcc = (originPoint, maxBound, maxBound, maxBound) (pRes, dRes, _, sumRes) = PointArray.ifoldlA' f initAcc arr in if sumRes == maxBound || goalDist /= apartBfs && pathLen < sumRes then if goalDist /= apartBfs then Just andPath else Nothing else let pathList2 = track (fromEnum pRes) (BfsDistance (toEnum dRes) .|. minKnownBfs) [] in Just AndPath{pathList = pathList2, pathLen = sumRes, ..} -- | Access a BFS array and interpret the looked up distance value. accessBfs :: PointArray.Array BfsDistance -> Point -> Maybe Int accessBfs bfs p = if PointArray.axsize bfs == 0 then Nothing else distanceBfs $ bfs PointArray.! p distanceBfs :: BfsDistance -> Maybe Int {-# INLINE distanceBfs #-} distanceBfs dist = if dist == apartBfs then Nothing else Just $ fromEnum $ bfsDistance $ maskBfs dist