File: BfsM.hs

package info (click to toggle)
haskell-lambdahack 0.11.0.1-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 4,056 kB
  • sloc: haskell: 45,636; makefile: 219
file content (628 lines) | stat: -rw-r--r-- 29,401 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
{-# LANGUAGE TupleSections #-}
-- | Breadth first search and related algorithms using the client monad.
module Game.LambdaHack.Client.BfsM
  ( invalidateBfsAid, invalidateBfsPathAid
  , invalidateBfsLid, invalidateBfsPathLid
  , invalidateBfsAll, invalidateBfsPathAll
  , createBfs, getCacheBfsAndPath, getCacheBfs
  , getCachePath, createPath, condBFS
  , furthestKnown, closestUnknown, closestSmell
  , FleeViaStairsOrEscape(..)
  , embedBenefit, closestTriggers, condEnoughGearM, closestItems, closestFoes
  , closestStashes, oursExploringAssocs, closestHideout
#ifdef EXPOSE_INTERNAL
  , unexploredDepth, updatePathFromBfs
#endif
  ) where

import Prelude ()

import Game.LambdaHack.Core.Prelude

import qualified Data.EnumMap.Strict as EM
import qualified Data.EnumSet as ES
import           Data.Word

import           Game.LambdaHack.Client.Bfs
import           Game.LambdaHack.Client.CommonM
import           Game.LambdaHack.Client.MonadClient
import           Game.LambdaHack.Client.State
import           Game.LambdaHack.Common.Actor
import           Game.LambdaHack.Common.ActorState
import           Game.LambdaHack.Common.Faction
import           Game.LambdaHack.Common.Item
import qualified Game.LambdaHack.Common.ItemAspect as IA
import           Game.LambdaHack.Common.Kind
import           Game.LambdaHack.Common.Level
import           Game.LambdaHack.Common.MonadStateRead
import           Game.LambdaHack.Common.Perception
import           Game.LambdaHack.Common.Point
import qualified Game.LambdaHack.Common.PointArray as PointArray
import           Game.LambdaHack.Common.State
import qualified Game.LambdaHack.Common.Tile as Tile
import           Game.LambdaHack.Common.Time
import           Game.LambdaHack.Common.Types
import           Game.LambdaHack.Common.Vector
import qualified Game.LambdaHack.Content.CaveKind as CK
import           Game.LambdaHack.Content.FactionKind
import qualified Game.LambdaHack.Content.ItemKind as IK
import           Game.LambdaHack.Content.RuleKind
import           Game.LambdaHack.Content.TileKind (isUknownSpace)
import           Game.LambdaHack.Core.Random
import qualified Game.LambdaHack.Definition.Ability as Ability
import           Game.LambdaHack.Definition.Defs

invalidateBfsAid :: MonadClient m => ActorId -> m ()
invalidateBfsAid aid =
  modifyClient $ \cli ->
    cli {sbfsD = EM.adjust (const BfsInvalid) aid (sbfsD cli)}

invalidateBfsPathAid :: MonadClient m => ActorId -> m ()
invalidateBfsPathAid aid = do
  let f BfsInvalid = BfsInvalid
      f (BfsAndPath bfsArr _) = BfsAndPath bfsArr EM.empty
  modifyClient $ \cli -> cli {sbfsD = EM.adjust f aid (sbfsD cli)}

-- Even very distant actors affected, e.g., when a hidden door found in a wall.
invalidateBfsLid :: MonadClient m => LevelId -> m ()
invalidateBfsLid lid = do
  lvl <- getLevel lid
  -- No need to filter, because foes won't be in our BFS map and looking up
  -- in our BFS map is faster than in all actors map.
  mapM_ invalidateBfsAid $ EM.elems $ lbig lvl

-- We invalidate, but not when actors move, since they are likely to move
-- out of the way in time. We only do, when they appear or disappear,
-- because they may be immobile or too close to move away before we get there.
-- We also don't consider far actors, since they are likely to disappear
-- again or to be far from our path. If they close enough to be lit
-- by our light, or one step further, that's worth taking seriously.
invalidateBfsPathLid :: MonadClient m => Actor -> m ()
invalidateBfsPathLid body = do
  lvl <- getLevel $ blid body
  let close (p, _) = chessDist p (bpos body) <= 3  -- heuristic
  -- No need to filter more, because foes won't be in our BFS map and looking up
  -- in our BFS map is faster than in all actors map.
  mapM_ (invalidateBfsPathAid . snd) $ filter close $ EM.assocs $ lbig lvl

invalidateBfsAll :: MonadClient m => m ()
invalidateBfsAll =
  modifyClient $ \cli -> cli {sbfsD = EM.map (const BfsInvalid) (sbfsD cli)}

invalidateBfsPathAll :: MonadClient m => m ()
invalidateBfsPathAll = do
  let f BfsInvalid = BfsInvalid
      f (BfsAndPath bfsArr _) = BfsAndPath bfsArr EM.empty
  modifyClient $ \cli -> cli {sbfsD = EM.map f (sbfsD cli)}

createBfs :: MonadClientRead m
          => Bool -> Word8 -> ActorId -> m (PointArray.Array BfsDistance)
createBfs canMove alterSkill0 aid =
  if canMove then do
    b <- getsState $ getActorBody aid
    salter <- getsClient salter
    let source = bpos b
        lalter = salter EM.! blid b
        alterSkill = max 1 alterSkill0
          -- We increase 0 skill to 1, to also path through unknown tiles.
          -- Since there are no other tiles that require skill 1, this is safe.
    stabs <- getsClient stabs
    return $! fillBfs lalter alterSkill source stabs
  else return PointArray.empty

updatePathFromBfs :: MonadClient m
                  => Bool -> BfsAndPath -> ActorId -> Point
                  -> m (PointArray.Array BfsDistance, Maybe AndPath)
updatePathFromBfs canMove bfsAndPathOld aid !target = do
  COps{coTileSpeedup} <- getsState scops
  let (oldBfsArr, oldBfsPath) = case bfsAndPathOld of
        (BfsAndPath bfsArr bfsPath) -> (bfsArr, bfsPath)
        BfsInvalid -> error $ "" `showFailure` (bfsAndPathOld, aid, target)
  let bfsArr = oldBfsArr
  if not canMove
  then return (bfsArr, Nothing)
  else do
    getActorB <- getsState $ flip getActorBody
    let b = getActorB aid
    fact <- getsState $ (EM.! bfid b) . sfactionD
    seps <- getsClient seps
    salter <- getsClient salter
    lvl <- getLevel (blid b)
    let !lalter = salter EM.! blid b
        fovLit p = Tile.isLit coTileSpeedup $ PointArray.fromUnboxRep
                                            $ ltile lvl `PointArray.accessI` p
        addFoeVicinity (p, aid2) =
          let b2 = getActorB aid2
          in if isFoe (bfid b) fact (bfid b2)
             then p : vicinityUnsafe p
             else [p]
        bigAdj = ES.fromList $ concatMap addFoeVicinity $ EM.assocs
                 $ EM.delete source $ lbig lvl  -- don't sidestep oneself
        !source = bpos b
        !mpath = findPathBfs bigAdj lalter fovLit source target seps bfsArr
        !bfsPath =
          maybe oldBfsPath (\path -> EM.insert target path oldBfsPath) mpath
        bap = BfsAndPath bfsArr bfsPath
    modifyClient $ \cli -> cli {sbfsD = EM.insert aid bap $ sbfsD cli}
    return (bfsArr, mpath)

-- | Get cached BFS array and path or, if not stored, generate and store first.
getCacheBfsAndPath :: forall m. MonadClient m
                   => ActorId -> Point
                   -> m (PointArray.Array BfsDistance, Maybe AndPath)
getCacheBfsAndPath aid target = do
  mbfs <- getsClient $ EM.lookup aid . sbfsD
  case mbfs of
    Just bap@(BfsAndPath bfsArr bfsPath) ->
      case EM.lookup target bfsPath of
        Nothing -> do
          (!canMove, _) <- condBFS aid
          updatePathFromBfs canMove bap aid target
        mpath@Just{} -> return (bfsArr, mpath)
    _ -> do
      (canMove, alterSkill) <- condBFS aid
      !bfsArr <- createBfs canMove alterSkill aid
      let bfsPath = EM.empty
      updatePathFromBfs canMove (BfsAndPath bfsArr bfsPath) aid target

-- | Get cached BFS array or, if not stored, generate and store first.
getCacheBfs :: MonadClient m => ActorId -> m (PointArray.Array BfsDistance)
getCacheBfs aid = do
  mbfs <- getsClient $ EM.lookup aid . sbfsD
  case mbfs of
    Just (BfsAndPath bfsArr _) -> return bfsArr
    _ -> do
      (canMove, alterSkill) <- condBFS aid
      !bfsArr <- createBfs canMove alterSkill aid
      let bfsPath = EM.empty
      modifyClient $ \cli ->
        cli {sbfsD = EM.insert aid (BfsAndPath bfsArr bfsPath) (sbfsD cli)}
      return bfsArr

-- | Get cached BFS path or, if not stored, generate and store first.
getCachePath :: MonadClient m => ActorId -> Point -> m (Maybe AndPath)
getCachePath aid target = do
  b <- getsState $ getActorBody aid
  let source = bpos b
  if source == target
  then return $ Just $ AndPath (bpos b) [] target 0  -- speedup
  else snd <$> getCacheBfsAndPath aid target

createPath :: MonadClient m => ActorId -> Target -> m TgtAndPath
createPath aid tapTgt = do
  COps{coTileSpeedup} <- getsState scops
  b <- getsState $ getActorBody aid
  lvl <- getLevel $ blid b
  let stopAtUnwalkable tapPath@(Just AndPath{..}) =
        let (walkable, rest) =
              -- Unknown tiles are not walkable, so path stops just before.
              -- which is good, because by the time actor reaches the tile,
              -- it is known and target is recalculated with new info,
              -- perhaps sidestepping the tile, e.g., if explosive.
              span (Tile.isWalkable coTileSpeedup . at lvl) pathList
        in case rest of
          _ | null walkable -> TgtAndPath{..}
          [] -> TgtAndPath{..}
          [g] | g == pathGoal -> TgtAndPath{..}
            -- the exception is when the tile is explicitly targeted
          newGoal : _ ->
            let newTgt = TPoint TBlock (blid b) newGoal
                newPath = AndPath{ pathSource = bpos b
                                 , pathList = walkable  -- no @newGoal@
                                 , pathGoal = newGoal
                                 , pathLen = length walkable + 1 }
            in TgtAndPath{tapTgt = newTgt, tapPath = Just newPath}
      stopAtUnwalkable Nothing = TgtAndPath{tapTgt, tapPath=Nothing}
  mpos <- getsState $ aidTgtToPos (Just aid) (blid b) (Just tapTgt)
  case mpos of
    Nothing -> return TgtAndPath{tapTgt, tapPath=Nothing}
    Just p -> do
      path <- getCachePath aid p
      return $! stopAtUnwalkable path

condBFS :: MonadClientRead m => ActorId -> m (Bool, Word8)
condBFS aid = do
  side <- getsClient sside
  -- We assume the actor eventually becomes a leader (or has the same
  -- set of skills as the leader, anyway). Otherwise we'd have
  -- to reset BFS after leader changes, but it would still lead to
  -- wasted movement if, e.g., non-leaders move but only leaders open doors
  -- and leader change is very rare.
  actorMaxSk <- getsState $ getActorMaxSkills aid
  let alterSkill =
        min (maxBound - 1)  -- @maxBound :: Word8@ means unalterable
            (toEnum $ max 0 $ Ability.getSk Ability.SkAlter actorMaxSk)
      canMove = Ability.getSk Ability.SkMove actorMaxSk > 0
                || Ability.getSk Ability.SkDisplace actorMaxSk > 0
                || Ability.getSk Ability.SkProject actorMaxSk > 0
  smarkSuspect <- getsClient smarkSuspect
  fact <- getsState $ (EM.! side) . sfactionD
  let -- Under UI, playing a hero party, we let AI set our target each
      -- turn for non-pointmen that can't move and can't alter,
      -- usually to TUnknown. This is rather useless, but correct.
      enterSuspect = smarkSuspect > 0 || gunderAI fact
      skill | enterSuspect = alterSkill  -- dig and search as skill allows
            | otherwise = 0  -- only walkable tiles
  return (canMove, skill)  -- keep it lazy

-- | Furthest (wrt paths) known position.
furthestKnown :: MonadClient m => ActorId -> m Point
furthestKnown aid = do
  bfs <- getCacheBfs aid
  getMaxIndex <- rndToAction $ oneOf [ PointArray.maxIndexA
                                     , PointArray.maxLastIndexA ]
  let furthestPos = getMaxIndex bfs
      dist = bfs PointArray.! furthestPos
  return $! assert (dist > apartBfs `blame` (aid, furthestPos, dist))
                   furthestPos

-- | Closest reachable unknown tile position, if any.
--
-- Note: some of these tiles are behind suspect tiles and they are chosen
-- in preference to more distant directly accessible unknown tiles.
-- This is in principle OK, but in dungeons with few hidden doors
-- AI is at a disadvantage (and with many hidden doors, it fares as well
-- as a human that deduced the dungeon properties). Changing Bfs to accomodate
-- all dungeon styles would be complex and would slow down the engine.
--
-- If the level has inaccessible open areas (at least from the stairs AI used)
-- the level will be nevertheless here finally labeled as explored,
-- to enable transition to other levels.
-- We should generally avoid such levels, because digging and/or trying
-- to find other stairs leading to disconnected areas is not KISS
-- so we don't do this in AI, so AI is at a disadvantage.
--
-- If the closest unknown is more than 126 tiles away from the targeting
-- actor, the level will marked as explored. We could complicate the code
-- and not mark if the unknown is too far as opposed to inaccessible,
-- but then if it is both too distant and inaccessible, AI would be
-- permanently stuck on such levels. To cope with this, escapes need to be
-- placed on open or small levels, or in dispersed enough that they don't
-- appear in such potentially unexplored potions of caves. Other than that,
-- this is rather harmless and hard to exploit, so let it be.
-- The principled way to fix this would be to extend BFS to @Word16@,
-- but then it takes too long to compute on maze levels, so we'd need
-- to optimize hard for JS.
closestUnknown :: MonadClient m => ActorId -> m (Maybe Point)
closestUnknown aid = do
  body <- getsState $ getActorBody aid
  lvl <- getLevel $ blid body
  bfs <- getCacheBfs aid
  let closestPoss = PointArray.minIndexesA bfs
      dist = bfs PointArray.! head closestPoss
      !_A = assert (lexpl lvl >= lseen lvl) ()
  return $!
    if lexpl lvl <= lseen lvl
         -- Some unknown may still be visible and even pathable, but we already
         -- know from global level info that they are inaccessible.
       || dist >= apartBfs
         -- Global level info may tell us that terrain was changed and so
         -- some new explorable tile appeared, but we don't care about those
         -- and we know we already explored all initially seen unknown tiles
         -- and it's enough for us (otherwise we'd need to hunt all around
         -- the map for tiles altered by enemies).
    then Nothing
    else let unknownAround pos =
               let vic = vicinityUnsafe pos
                   countUnknown :: Int -> Point -> Int
                   countUnknown c p =
                     if isUknownSpace $ lvl `at` p then c + 1 else c
               in foldl' countUnknown 0 vic
             cmp = comparing unknownAround
         in Just $ maximumBy cmp closestPoss

-- | Finds smells closest to the actor, except under the actor,
-- because actors consume smell only moving over them, not standing.
-- Of the closest, prefers the newest smell.
closestSmell :: MonadClient m => ActorId -> m [(Int, (Point, Time))]
closestSmell aid = do
  body <- getsState $ getActorBody aid
  Level{lsmell, ltime} <- getLevel $ blid body
  let smells = filter (\(p, sm) -> sm > ltime && p /= bpos body)
                      (EM.assocs lsmell)
  case smells of
    [] -> return []
    _ -> do
      bfs <- getCacheBfs aid
      let ts = mapMaybe (\x@(p, _) -> fmap (,x) (accessBfs bfs p)) smells
      return $! sortOn (fst &&& absoluteTimeNegate . snd . snd) ts

data FleeViaStairsOrEscape =
    ViaStairs
  | ViaStairsUp
  | ViaStairsDown
  | ViaEscape
  | ViaExit  -- can change whenever @sexplored@ changes
  | ViaNothing
  | ViaAnything
  deriving (Show, Eq)

embedBenefit :: MonadClientRead m
             => FleeViaStairsOrEscape -> ActorId
             -> [(Point, ItemBag)]
             -> m [(Double, (Point, ItemBag))]
embedBenefit fleeVia aid pbags = do
  COps{cocave, coTileSpeedup} <- getsState scops
  dungeon <- getsState sdungeon
  explored <- getsClient sexplored
  b <- getsState $ getActorBody aid
  fact <- getsState $ (EM.! bfid b) . sfactionD
  lvl <- getLevel (blid b)
  oursExploring <- getsState $ oursExploringAssocs (bfid b)
  let oursExploringLid =
        filter (\(_, body) -> blid body == blid b) oursExploring
      spawnFreqs = CK.cactorFreq $ okind cocave $ lkind lvl
      hasGroup grp = fromMaybe 0 (lookup grp spawnFreqs) > 0
      lvlSpawnsUs = any (hasGroup . fst) $ filter ((> 0) . snd)
                                         $ fgroups (gkind fact)
  actorSk <- if fleeVia `elem` [ViaAnything, ViaExit]
                  -- targeting, possibly when not a leader
             then getsState $ getActorMaxSkills aid
             else currentSkillsClient aid
  let alterSkill = Ability.getSk Ability.SkAlter actorSk
  condOurAdj <- getsState $ any (\(_, b2) -> isFriend (bfid b) fact (bfid b2))
                            . adjacentBigAssocs b
  unexploredTrue <- unexploredDepth True (blid b)
  unexploredFalse <- unexploredDepth False (blid b)
  condEnoughGear <- condEnoughGearM aid
  discoBenefit <- getsClient sdiscoBenefit
  getKind <- getsState $ flip getIidKind
  let alterMinSkill p = Tile.alterMinSkill coTileSpeedup $ lvl `at` p
      lidExplored = ES.member (blid b) explored
      allExplored = ES.size explored == EM.size dungeon
      -- Ignoring the number of items, because only one of each @iid@
      -- is triggered at the same time, others are left to be used later on.
      -- Taking the kind the item hides under into consideration, because
      -- it's a best guess only, for AI and UI.
      iidToEffs iid = IK.ieffects $ getKind iid
      feats bag = concatMap iidToEffs $ EM.keys bag
      -- For simplicity, we assume at most one exit at each position.
      -- AI uses exit regardless of traps or treasures at the spot.
      bens (_, bag) = case find IK.isEffEscapeOrAscend $ feats bag of
        Just IK.Escape{} ->
          -- Escape (or guard) only after exploring, for high score, etc.
          let escapeOrGuard =
                fcanEscape (gkind fact)
                || fleeVia == ViaExit  -- target to guard after explored
          in if fleeVia `elem` [ViaAnything, ViaEscape, ViaExit]
                && escapeOrGuard
                && allExplored
             then 10
             else 0  -- don't escape prematurely
        Just (IK.Ascend up) ->  -- change levels sensibly, in teams
          let easier = up /= (fromEnum (blid b) > 0)
              unexpForth = if up then unexploredTrue else unexploredFalse
              unexpBack = if not up then unexploredTrue else unexploredFalse
              -- Forbid loops via peeking at unexplored and getting back.
              aiCond = if unexpForth
                       then easier && condEnoughGear
                            || (not unexpBack || easier) && lidExplored
                       else not unexpBack && easier && allExplored
                            && null (lescape lvl)
              -- Prefer one direction of stairs, to team up
              -- and prefer embed (may, e.g., create loot) over stairs.
              v = if aiCond then if easier then 10 else 1 else 0
              guardingStash = case gstash fact of
                Nothing -> False
                Just (lid, p) ->
                  lid == blid b
                  && (length oursExploring > 1
                      || lvlSpawnsUs)
                  && (length oursExploringLid <= 1
                        -- not @==@ in case guard temporarily nonmoving
                      || p == bpos b && not condOurAdj)
                           -- don't leave the post; let the others explore
          in case fleeVia of
            _ | guardingStash -> 0
            ViaStairsUp | up -> 1
            ViaStairsDown | not up -> 1
            ViaStairs -> v
            ViaExit -> v
            ViaAnything -> v
            _ -> 0  -- don't ascend prematurely
        _ ->
          if fleeVia `elem` [ViaNothing, ViaAnything]
          then -- Actor uses the embedded item on himself, hence @benApply@.
               -- Let distance be the deciding factor and also prevent
               -- overflow on 32-bit machines.
               let sacrificeForExperiment = 101  -- single explosion acceptable
                   sumBen = sum $ map (\iid ->
                     benApply $ discoBenefit EM.! iid) (EM.keys bag)
               in min 1000 $ sacrificeForExperiment + sumBen
          else 0
      underFeet p = p == bpos b  -- if enter and alter, be more permissive
      -- Only actors with high enough @SkAlter@ can trigger terrain.
      -- Blocking actors and items not checked, because they can be moved
      -- before the actor gets to the location, or after.
      f (p, _) = underFeet p
                 || alterSkill >= fromEnum (alterMinSkill p)
                 || Tile.isSuspect coTileSpeedup (lvl `at` p)
                    && alterSkill >= 2
      benFeats = map (\pbag -> (bens pbag, pbag)) $ filter f pbags
      considered (benefitAndSacrifice, (p, _bag)) =
        benefitAndSacrifice > 0
        -- For speed and to avoid greedy AI loops, only experiment with few.
        && Tile.consideredByAI coTileSpeedup (lvl `at` p)
  return $! filter considered benFeats

-- | Closest (wrt paths) AI-triggerable tiles with embedded items.
-- In AI, the level the actor is on is either explored or the actor already
-- has a weapon equipped, so no need to explore further, he tries to find
-- enemies on other levels, but before that, he triggers other tiles
-- in hope of some loot or beneficial effect to enter next level with.
closestTriggers :: MonadClient m => FleeViaStairsOrEscape -> ActorId
                -> m [(Int, (Point, (Point, ItemBag)))]
closestTriggers fleeVia aid = do
  COps{corule=RuleContent{rWidthMax, rHeightMax}} <- getsState scops
  b <- getsState $ getActorBody aid
  lvl <- getLevel (blid b)
  let pbags = EM.assocs $ lembed lvl
  efeat <- embedBenefit fleeVia aid pbags
  -- The advantage of targeting the tiles in vicinity of triggers is that
  -- triggers don't need to be pathable (and so AI doesn't bump into them
  -- by chance while walking elsewhere) and that many accesses to the tiles
  -- are more likely to be targeted by different AI actors (even starting
  -- from the same location), so there is less risk of clogging stairs and,
  -- OTOH, siege of stairs or escapes is more effective.
  bfs <- getCacheBfs aid
  let vicTrigger (cid, (p0, bag)) =
        map (\p -> (cid, (p, (p0, bag))))
            (vicinityBounded rWidthMax rHeightMax p0)
      vicAll = concatMap vicTrigger efeat
  return $!
    let mix (benefit, ppbag) dist =
          let maxd = subtractBfsDistance maxBfsDistance apartBfs
              v = intToDouble $ maxd `div` (dist + 1)
          in (ceiling $ benefit * v, ppbag)
    in mapMaybe (\bpp@(_, (p, _)) ->
         mix bpp <$> accessBfs bfs p) vicAll

-- | Check whether the actor has enough gear to go look for enemies.
-- We assume weapons in equipment are better than any among organs
-- or at least provide some essential diversity.
-- Disabled if, due to doctrine, actors follow leader and so would
-- repeatedly move towards and away from stairs at leader change,
-- depending on current leader's gear.
-- Number of items of a single kind is ignored, because variety is needed.
condEnoughGearM :: MonadClientRead m => ActorId -> m Bool
condEnoughGearM aid = do
  b <- getsState $ getActorBody aid
  fact <- getsState $ (EM.! bfid b) . sfactionD
  let followDoctrine =
        gdoctrine fact `elem` [Ability.TFollow, Ability.TFollowNoItems]
  eqpAssocs <- getsState $ fullAssocs aid [CEqp]
  return $ not followDoctrine  -- keep it lazy
           && (any (IA.checkFlag Ability.Meleeable
                    . aspectRecordFull . snd) eqpAssocs
               || length eqpAssocs >= 3)

unexploredDepth :: MonadClientRead m => Bool -> LevelId -> m Bool
unexploredDepth !up !lidCurrent = do
  dungeon <- getsState sdungeon
  explored <- getsClient sexplored
  let allExplored = ES.size explored == EM.size dungeon
      unexploredD =
        let unex !lid = allExplored
                        && not (null $ lescape $ dungeon EM.! lid)
                        || ES.notMember lid explored
                        || unexploredD lid
        in any unex . ascendInBranch dungeon up
  return $ unexploredD lidCurrent  -- keep it lazy

-- | Closest (wrt paths) items.
closestItems :: MonadClient m => ActorId -> m [(Int, (Point, ItemBag))]
closestItems aid = do
  body <- getsState $ getActorBody aid
  Level{lfloor, lbig} <- getLevel $ blid body
  factionD <- getsState sfactionD
  per <- getPerFid $ blid body
  let canSee p = ES.member p (totalVisible per)
  -- Don't consider items at any stash location that an actor stands over
  -- or can stand over, but it's out of our LOS.
  -- In case of the own stash, don't consider regardless of actors and LOS.
  -- Own stash items are already owned, enemy stash is already targetted
  -- and allied or neutral stashes with actors on top are unlikely
  -- to be vacated and cause AI to wonder around forever or look up,
  -- leave, return hopeful, find a guard, repeat.
  let stashes = map (second gstash) $ EM.assocs factionD
      stashToRemove :: (FactionId, Maybe (LevelId, Point)) -> [Point]
      stashToRemove (fid, Just (lid, pos))
        | lid == blid body
          && (fid == bfid body || pos `EM.member` lbig || not (canSee pos)) =
            [pos]
      stashToRemove _ = []
      stashesToRemove = ES.fromList $ concatMap stashToRemove stashes
      lfloorBarStashes = EM.withoutKeys lfloor stashesToRemove
  if EM.null lfloorBarStashes then return [] else do
    bfs <- getCacheBfs aid
    let mix pbag dist =
          let maxd = subtractBfsDistance maxBfsDistance apartBfs
              -- Beware of overflowing 32-bit integers.
              -- Here distance is the only factor influencing frequency.
              -- Whether item is desirable is checked later on.
              v = (maxd * 10) `div` (dist + 1)
          in (v, pbag)
    return $! mapMaybe (\(p, bag) ->
      mix (p, bag) <$> accessBfs bfs p) (EM.assocs lfloorBarStashes)

-- | Closest (wrt paths) enemy actors.
closestFoes :: MonadClient m
            => [(ActorId, Actor)] -> ActorId -> m [(Int, (ActorId, Actor))]
closestFoes foes aid =
  case foes of
    [] -> return []
    _ -> do
      bfs <- getCacheBfs aid
      let ds = mapMaybe (\x@(_, b) -> fmap (,x) (accessBfs bfs (bpos b))) foes
      return $! sortBy (comparing fst) ds

-- | Closest (wrt paths) enemy or our unguarded stash locations. If it's ours,
-- we want to guard it, it enemy, loot it. Neutral and friendly stashes
-- not chased to avoid loops of bloodless takeovers.
closestStashes :: MonadClient m => ActorId -> m [(Int, (FactionId, Point))]
closestStashes aid = do
  COps{cocave} <- getsState scops
  factionD <- getsState sfactionD
  b <- getsState $ getActorBody aid
  lvl <- getLevel (blid b)
  oursExploring <- getsState $ oursExploringAssocs (bfid b)
  let fact = factionD EM.! bfid b
      spawnFreqs = CK.cactorFreq $ okind cocave $ lkind lvl
      hasGroup grp = fromMaybe 0 (lookup grp spawnFreqs) > 0
      lvlSpawnsUs = any (hasGroup . fst) $ filter ((> 0) . snd)
                                         $ fgroups (gkind fact)
      qualifyStash (fid2, Faction{gstash}) = case gstash of
        Nothing -> Nothing
        Just (lid, pos) ->
          -- The condition below is more strict that in @updateTgt@
          -- to avoid loops by changing target of actor displacing
          -- and walking over stash to @TStash@.
          if lid == blid b
             && (fid2 == bfid b
                 && isNothing (posToBigLvl pos lvl)  -- unguarded
                 && (length oursExploring > 1  -- other actors able to explore
                     || lvlSpawnsUs)  -- or future spawned will be able
                 || isFoe (bfid b) fact fid2)
          then Just (fid2, pos)
          else Nothing
  case mapMaybe qualifyStash $ EM.assocs factionD of
    [] -> return []
    stashes -> do
      bfs <- getCacheBfs aid
      let ds = mapMaybe (\x@(_, pos) -> fmap (,x) (accessBfs bfs pos)) stashes
      return $! sortBy (comparing fst) ds

oursExploringAssocs :: FactionId -> State -> [(ActorId, Actor)]
oursExploringAssocs fid s =
  let f (!aid, !b) = bfid b == fid
                     && not (bproj b)
                     && bhp b > 0  -- dead can stay forever on a frozen level
                     && (bwatch b `elem` [WSleep, WWake]
                           -- if asleep, probably has walking skill normally;
                           -- when left alone will wake up and guard or explore
                        || let actorMaxSk = sactorMaxSkills s EM.! aid
                           in Ability.getSk Ability.SkMove actorMaxSk > 0
                              || Ability.getSk Ability.SkMove actorMaxSk < -50)
                                   -- a hacky way to rule out tmp immobile
  in filter f $ EM.assocs $ sactorD s

-- | Find the nearest walkable position in dark, if any. Deterministic,
-- to let all friends gather up and defend in the same shelter.
-- Ignore position underfoot.
closestHideout :: MonadClient m => ActorId -> m (Maybe (Point, Int))
closestHideout aid = do
  COps{coTileSpeedup} <- getsState scops
  b <- getsState $ getActorBody aid
  lvl <- getLevel (blid b)
  bfs <- getCacheBfs aid
  let minHideout :: (Point, BfsDistance) -> Point -> BfsDistance
                 -> (Point, BfsDistance)
      minHideout (pMin, distMin) p dist =
        if dist > minKnownBfs && dist < distMin
           && Tile.isHideout coTileSpeedup (lvl `at` p)
        then (p, dist)
        else (pMin, distMin)
      (p1, dist1) = PointArray.ifoldlA' minHideout (bpos b, maxBfsDistance) bfs
  return $! if p1 == bpos b  -- possibly hideout underfoot; ignore
            then Nothing
            else Just (p1, subtractBfsDistance dist1 apartBfs)