-- | AI strategy operations implemented with the 'Action' monad.
module Game.LambdaHack.Client.StrategyAction
  ( targetStrategy, actionStrategy, visibleFoes
  ) where

import Control.Monad
import qualified Data.EnumMap.Strict as EM
import Data.Function
import Data.List
import Data.Maybe
import qualified Data.Traversable as Traversable

import Game.LambdaHack.Client.Action
import Game.LambdaHack.Client.State
import Game.LambdaHack.Client.Strategy
import Game.LambdaHack.Common.Ability (Ability)
import qualified Game.LambdaHack.Common.Ability as Ability
import Game.LambdaHack.Common.Action
import Game.LambdaHack.Common.Actor
import Game.LambdaHack.Common.ActorState
import qualified Game.LambdaHack.Common.Effect as Effect
import Game.LambdaHack.Common.Faction
import qualified Game.LambdaHack.Common.Feature as F
import Game.LambdaHack.Common.Item
import qualified Game.LambdaHack.Common.Kind as Kind
import Game.LambdaHack.Common.Level
import Game.LambdaHack.Common.Perception
import Game.LambdaHack.Common.Point
import qualified Game.LambdaHack.Common.Random as Random
import Game.LambdaHack.Common.ServerCmd
import Game.LambdaHack.Common.State
import qualified Game.LambdaHack.Common.Tile as Tile
import Game.LambdaHack.Common.Time
import Game.LambdaHack.Common.Vector
import Game.LambdaHack.Content.ActorKind
import Game.LambdaHack.Content.ItemKind
import Game.LambdaHack.Content.RuleKind
import Game.LambdaHack.Content.TileKind as TileKind
import Control.Exception.Assert.Sugar
import Game.LambdaHack.Utils.Frequency

-- | AI proposes possible targets for the actor. Never empty.
targetStrategy :: MonadClient m
               => ActorId -> [Ability] -> m (Strategy (Maybe Target))
targetStrategy aid factionAbilities = do
  btarget <- getsClient $ getTarget aid
  fper <- getsClient sfper
  reacquireTgt aid factionAbilities btarget fper

visibleFoes :: MonadActionRO m
            => ActorId -> FactionPers -> m [(ActorId, Actor)]
visibleFoes aid fper = do
  b <- getsState $ getActorBody aid
  assert (not $ bproj b) skip  -- would work, but is probably a bug
  let per = fper EM.! blid b
  fact <- getsState $ \s -> sfactionD s EM.! bfid b
  foes <- getsState $ actorNotProjAssocs (isAtWar fact) (blid b)
  return $! filter (actorSeesPos per aid . bpos . snd) foes

reacquireTgt :: MonadActionRO m
             => ActorId -> [Ability] -> Maybe Target -> FactionPers
             -> m (Strategy (Maybe Target))
reacquireTgt aid factionAbilities btarget fper = do
  cops@Kind.COps{coactor=Kind.Ops{okind}} <- getsState scops
  b <- getsState $ getActorBody aid
  assert (not $ bproj b) skip  -- would work, but is probably a bug
  lvl@Level{lxsize} <- getsState $ \s -> sdungeon s EM.! blid b
  visFoes <- visibleFoes aid fper
  actorD <- getsState sactorD
  -- TODO: set distant targets so that monsters behave as if they have
  -- a plan. We need pathfinding for that.
  noFoes :: Strategy (Maybe Target) <- do
    s <- getState
    str <- moveStrategy cops aid s Nothing
    return $ (Just . TPos . (bpos b `shift`)) `liftM` str
  let per = fper EM.! blid b
      mk = okind $ bkind b
      actorAbilities = acanDo mk `intersect` factionAbilities
      focused = bspeed b <= speedNormal
                -- Don't focus on a distant enemy, when you can't chase him.
                -- TODO: or only if another enemy adjacent? consider Flee?
                && Ability.Chase `elem` actorAbilities
      closest :: Strategy (Maybe Target)
      closest =
        let distB = chessDist lxsize (bpos b)
            foeDist = map (\(_, body) -> distB (bpos body)) visFoes
            minDist | null foeDist = maxBound
                    | otherwise = minimum foeDist
            minFoes =
              filter (\(_, body) -> distB (bpos body) == minDist) visFoes
            minTargets = map (\(a, body) ->
                                Just $ TEnemy a $ bpos body) minFoes
            minTgtS = liftFrequency $ uniformFreq "closest" minTargets
        in minTgtS .| noFoes .| returN "TCursor" Nothing  -- never empty
      reacquire :: Maybe Target -> Strategy (Maybe Target)
      reacquire tgt =
        case tgt of
          Just (TEnemy a ll) | focused ->  -- chase even if enemy dead, to loot
            case fmap bpos $ EM.lookup a actorD of
              Just l | actorSeesPos per aid l ->
                -- prefer visible (and alive) foes
                returN "TEnemy" $ Just $ TEnemy a l
              _ -> if null visFoes         -- prefer visible foes to positions
                      && bpos b /= ll      -- not yet reached the last pos
                   then returN "last known" $ Just $ TPos ll
                                           -- chase the last known pos
                   else closest
          Just TEnemy{} -> closest         -- just pick the closest foe
          Just (TPos pos) | bpos b == pos -> closest  -- already reached pos
          Just (TPos pos)
            | not $ bumpableHere cops lvl False (asight mk) pos ->
            closest  -- no longer bumpable, even assuming no foes
          Just TPos{} | null visFoes -> returN "TPos" tgt
                                           -- nothing visible, go to pos
          Just TPos{} -> closest           -- prefer visible foes
          Nothing -> closest
  return $! reacquire btarget

-- | AI strategy based on actor's sight, smell, intelligence, etc. Never empty.
actionStrategy :: MonadClient m
               => ActorId -> [Ability] -> m (Strategy CmdSerTakeTime)
actionStrategy aid factionAbilities = do
  disco <- getsClient sdisco
  btarget <- getsClient $ getTarget aid
  proposeAction disco aid factionAbilities btarget

proposeAction :: MonadActionRO m
              => Discovery -> ActorId -> [Ability] -> Maybe Target
              -> m (Strategy CmdSerTakeTime)
proposeAction disco aid factionAbilities btarget = do
  Kind.COps{coactor=Kind.Ops{okind}} <- getsState scops
  Actor{bkind, bpos, blid} <- getsState $ getActorBody aid
  lvl <- getLevel blid
  let mk = okind bkind
      (fpos, mfAid) =
        case btarget of
          Just (TEnemy foeAid l) -> (l, Just foeAid)
          Just (TPos l) -> (l, Nothing)
          Nothing -> (bpos, Nothing)  -- an actor blocked by friends or a missile
      foeVisible = isJust mfAid
      lootHere x = not $ EM.null $ lvl `atI` x
      actorAbilities = acanDo mk `intersect` factionAbilities
      isDistant = (`elem` [ Ability.Trigger
                          , Ability.Ranged
                          , Ability.Tools
                          , Ability.Chase ])
      -- TODO: this is too fragile --- depends on order of abilities
      (prefix, rest)    = break isDistant actorAbilities
      (distant, suffix) = partition isDistant rest
      -- TODO: Ranged and Tools should only be triggered in some situations.
      aFrequency :: MonadActionRO m => Ability -> m (Frequency CmdSerTakeTime)
      aFrequency Ability.Trigger = if foeVisible then return mzero
                                   else triggerFreq aid
      aFrequency Ability.Ranged = if not foeVisible then return mzero
                                  else rangedFreq disco aid fpos
      aFrequency Ability.Tools  = if not foeVisible then return mzero
                                  else toolsFreq disco aid
      aFrequency Ability.Chase  = if fpos == bpos then return mzero
                                  else chaseFreq
      aFrequency ab             = assert `failure` "unexpected ability"
                                          `twith` (ab, distant, actorAbilities)
      chaseFreq :: MonadActionRO m => m (Frequency CmdSerTakeTime)
      chaseFreq = do
        st <- chase aid (fpos, foeVisible)
        return $ scaleFreq 30 $ bestVariant st
      aStrategy :: MonadActionRO m => Ability -> m (Strategy CmdSerTakeTime)
      aStrategy Ability.Track  = track aid
      aStrategy Ability.Heal   = return mzero  -- TODO
      aStrategy Ability.Flee   = return mzero  -- TODO
      aStrategy Ability.Melee | Just foeAid <- mfAid = melee aid fpos foeAid
      aStrategy Ability.Melee  = return mzero
      aStrategy Ability.Pickup | not foeVisible && lootHere bpos = pickup aid
      aStrategy Ability.Pickup = return mzero
      aStrategy Ability.Wander = wander aid
      aStrategy ab             = assert `failure` "unexpected ability"
                                        `twith`(ab, actorAbilities)
      sumS abis = do
        fs <- mapM aStrategy abis
        return $ msum fs
      sumF abis = do
        fs <- mapM aFrequency abis
        return $ msum fs
      combineDistant as = fmap liftFrequency $ sumF as
  sumPrefix <- sumS prefix
  comDistant <- combineDistant distant
  sumSuffix <- sumS suffix
  return $ sumPrefix .| comDistant .| sumSuffix
           -- Wait until friends sidestep; ensures the strategy is never empty.
           -- TODO: try to switch leader away before that (we already
           -- switch him afterwards)
           .| waitBlockNow aid

-- | A strategy to always just wait.
waitBlockNow :: ActorId -> Strategy CmdSerTakeTime
waitBlockNow aid = returN "wait" $ WaitSer aid

-- | Strategy for dumb missiles.
track :: MonadActionRO m => ActorId -> m (Strategy CmdSerTakeTime)
track aid = do
  cops <- getsState scops
  b@Actor{bpos, bpath, blid} <- getsState $ getActorBody aid
  lvl <- getLevel blid
  let clearPath = returN "ClearPathSer" $ SetPathSer aid []
      strat = case bpath of
        Nothing -> reject
        Just [] -> assert `failure` "null path" `twith` (aid, b)
        -- TODO: instead let server do this in MoveSer, abort, handle in loop
        Just (d : _) | not $ accessibleDir cops lvl bpos d -> clearPath
        Just lv -> returN "SetPathSer" $ SetPathSer aid lv
  return strat

-- TODO: (most?) animals don't pick up. Everybody else does.
pickup :: MonadActionRO m => ActorId -> m (Strategy CmdSerTakeTime)
pickup aid = do
  body@Actor{bpos, blid} <- getsState $ getActorBody aid
  lvl <- getLevel blid
  actionPickup <- case EM.minViewWithKey $ lvl `atI` bpos of
    Nothing -> assert `failure` "pickup of empty pile" `twith` (aid, bpos, lvl)
    Just ((iid, k), _) -> do  -- pick up first item
      item <- getsState $ getItemBody iid
      let l = if jsymbol item == '$' then Just $ InvChar '$' else Nothing
      return $ case assignLetter iid l body of
        Just l2 -> returN "pickup" $ PickupSer aid iid k l2
        Nothing -> returN "pickup" $ WaitSer aid  -- TODO
  return actionPickup

-- Everybody melees in a pinch, even though some prefer ranged attacks.
melee :: MonadActionRO m
      => ActorId -> Point -> ActorId -> m (Strategy CmdSerTakeTime)
melee aid fpos foeAid = do
  Actor{bpos, blid} <- getsState $ getActorBody aid
  Level{lxsize} <- getLevel blid
  let foeAdjacent = adjacent lxsize bpos fpos  -- MeleeDistant
  return $ foeAdjacent .=> returN "melee" (MeleeSer aid foeAid)

-- Fast monsters don't pay enough attention to features.
triggerFreq :: MonadActionRO m
            => ActorId -> m (Frequency CmdSerTakeTime)
triggerFreq aid = do
  cops@Kind.COps{cotile=Kind.Ops{okind}} <- getsState scops
  b@Actor{bpos, blid, bfid, boldpos} <- getsState $ getActorBody aid
  fact <- getsState $ \s -> sfactionD s EM.! bfid
  lvl <- getLevel blid
  let spawn = isSpawnFact cops fact
      t = lvl `at` bpos
      feats = TileKind.tfeature $ okind t
      ben feat = case feat of
        F.Cause Effect.Escape | spawn -> 0  -- spawners lose if they escape
        F.Cause ef -> effectToBenefit cops b ef
        _ -> 0
      benFeat = zip (map ben feats) feats
      -- Probably recently switched levels or was pushed to another level.
      -- Do not repeatedly switch levels or push each other between levels.
      -- Consequently, AI won't dive many levels down with linked staircases.
      -- TODO: beware of stupid monsters that backtrack and so occupy stairs.
      recentlyAscended = bpos == boldpos
      -- Too fast to notice and use features.
      fast = bspeed b > speedNormal
  if recentlyAscended || fast then
    return mzero
  else
    return $ toFreq "triggerFreq" $ [ (benefit, TriggerSer aid (Just feat))
                                    | (benefit, feat) <- benFeat
                                    , benefit > 0 ]

-- Actors require sight to use ranged combat and intelligence to throw
-- or zap anything else than obvious physical missiles.
rangedFreq :: MonadActionRO m
           => Discovery -> ActorId -> Point -> m (Frequency CmdSerTakeTime)
rangedFreq disco aid fpos = do
  cops@Kind.COps{ coactor=Kind.Ops{okind}
                , coitem=Kind.Ops{okind=iokind}
                , corule
                , cotile
                } <- getsState scops
  b@Actor{bkind, bpos, bfid, blid, bbag, binv} <- getsState $ getActorBody aid
  lvl@Level{lxsize, lysize} <- getLevel blid
  let mk = okind bkind
      tis = lvl `atI` bpos
  fact <- getsState $ \s -> sfactionD s EM.! bfid
  foes <- getsState $ actorNotProjList (isAtWar fact) blid
  let foesAdj = foesAdjacent lxsize lysize bpos foes
      posClear pos1 = Tile.hasFeature cotile F.Clear (lvl `at` pos1)
  as <- getsState $ actorList (const True) blid
  -- TODO: also don't throw if any pos on path is visibly not accessible
  -- from previous (and tweak eps in bla to make it accessible).
  -- Also don't throw if target not in range.
  s <- getState
  let eps = 0
      bl = bla lxsize lysize eps bpos fpos  -- TODO:make an arg of projectGroupItem
      permitted = (if aiq mk >= 10 then ritemProject else ritemRanged)
                  $ Kind.stdRuleset corule
      throwFreq bag multi container =
        [ (- benefit * multi,
          ProjectSer aid fpos eps iid (container iid))
        | (iid, i) <- map (\iid -> (iid, getItemBody iid s))
                      $ EM.keys bag
        , let benefit =
                case jkind disco i of
                  Nothing -> -- TODO: (undefined, 0)   --- for now, cheating
                    effectToBenefit cops b (jeffect i)
                  Just _ki ->
                    let _kik = iokind _ki
                        _unneeded = isymbol _kik
                    in effectToBenefit cops b (jeffect i)
        , benefit < 0
        , jsymbol i `elem` permitted ]
  return $ toFreq "throwFreq" $
    case bl of
      Just (pos1 : _) ->
        if not foesAdj  -- ProjectBlockFoes
           && asight mk
           && posClear pos1  -- ProjectBlockTerrain
           && unoccupied as pos1  -- ProjectBlockActor
        then throwFreq bbag 3 (actorContainer aid binv)
             ++ throwFreq tis 6 (const $ CFloor blid bpos)
        else []
      _ -> []  -- ProjectAimOnself

-- Tools use requires significant intelligence and sometimes literacy.
toolsFreq :: MonadActionRO m
          => Discovery -> ActorId -> m (Frequency CmdSerTakeTime)
toolsFreq disco aid = do
  cops@Kind.COps{coactor=Kind.Ops{okind}} <- getsState scops
  b@Actor{bkind, bpos, blid, bbag, binv} <- getsState $ getActorBody aid
  lvl <- getLevel blid
  s <- getState
  let tis = lvl `atI` bpos
      mk = okind bkind
      mastered | aiq mk < 5 = ""
               | aiq mk < 10 = "!"
               | otherwise = "!?"  -- literacy required
      useFreq bag multi container =
        [ (benefit * multi, ApplySer aid iid (container iid))
        | (iid, i) <- map (\iid -> (iid, getItemBody iid s))
                      $ EM.keys bag
        , let benefit =
                case jkind disco i of
                  Nothing -> 30  -- experimenting is fun
                  Just _ki -> effectToBenefit cops b $ jeffect i
        , benefit > 0
        , jsymbol i `elem` mastered ]
  return $ toFreq "useFreq" $
    useFreq bbag 1 (actorContainer aid binv)
    ++ useFreq tis 2 (const $ CFloor blid bpos)

-- TODO: express fully in MonadActionRO
-- TODO: separate out bumping into solid tiles
-- TODO: also close doors; then stupid members of the party won't see them,
-- but it's assymetric warfare: rather harm humans than help party members
-- | AI finds interesting moves in the absense of visible foes.
-- This strategy can be null (e.g., if the actor is blocked by friends).
moveStrategy :: MonadActionRO m
             => Kind.COps -> ActorId -> State -> Maybe (Point, Bool)
             -> m (Strategy Vector)
moveStrategy cops aid s mFoe =
  return $ case mFoe of
    -- Target set and we chase the foe or his last position or another target.
    Just (fpos, _) ->
      let towardsFoe =
            let tolerance | adjacent lxsize bpos fpos = 0
                          | otherwise = 1
                foeDir = towards lxsize bpos fpos
            in only (\x -> euclidDistSq lxsize foeDir x <= tolerance)
      in if fpos == bpos
         then reject
         else towardsFoe
              $ if foeVisible
                then moveClear  -- enemies in sight, don't waste time for doors
                     .| moveOpenable
                else moveOpenable  -- no enemy in sight, explore doors
                     .| moveClear
    Nothing ->
      let smells =
            map (map fst)
            $ groupBy ((==) `on` snd)
            $ sortBy (flip compare `on` snd)
            $ filter (\(_, sm) -> sm > timeZero)
            $ map (\x ->
                      let sml = EM.findWithDefault
                                  timeZero (bpos `shift` x) lsmell
                      in (x, sml `timeAdd` timeNegate ltime))
                sensible
      in asmell mk .=> foldr ((.|)
                              . liftFrequency
                              . uniformFreq "smell k") reject smells
         .| moveOpenable  -- no enemy in sight, explore doors
         .| moveClear
 where
  Kind.COps{cotile, coactor=Kind.Ops{okind}} = cops
  lvl@Level{lsmell, lxsize, lysize, ltime} = sdungeon s EM.! blid
  Actor{bkind, bpos, boldpos, bfid, blid} = getActorBody aid s
  mk = okind bkind
  lootHere x = not $ EM.null $ lvl `atI` x
  onlyLoot = onlyMoves lootHere
  interestHere x = let t = lvl `at` x
                       ts = map (lvl `at`) $ vicinity lxsize lysize x
                   in Tile.hasFeature cotile F.Exit t
                      -- Blind actors tend to reveal/forget repeatedly.
                      || asight mk && Tile.hasFeature cotile F.Suspect t
                      -- Lit indirectly. E.g., a room entrance.
                      || (not (Tile.hasFeature cotile F.Lit t)
                          && (x == bpos || accessible cops lvl x bpos)
                          && any (Tile.hasFeature cotile F.Lit) ts)
  onlyInterest = onlyMoves interestHere
  bdirAI | bpos == boldpos = Nothing
         | otherwise = Just $ towards lxsize boldpos bpos
  onlyKeepsDir k =
    only (\x -> maybe True (\d -> euclidDistSq lxsize d x <= k) bdirAI)
  onlyKeepsDir_9 = only (\x -> maybe True (\d -> neg x /= d) bdirAI)
  foeVisible = fmap snd mFoe == Just True
  -- TODO: aiq 16 leads to robotic, repetitious, looping movement;
  -- either base it off some new stat or wait until pathfinding,
  -- which will eliminate the loops
  moveIQ | foeVisible = onlyKeepsDir_9 moveRandomly  -- danger, be flexible
         | otherwise =
       aiq mk > 15 .=> onlyKeepsDir 0 moveRandomly
    .| aiq mk > 10 .=> onlyKeepsDir 1 moveRandomly
    .| aiq mk > 5  .=> onlyKeepsDir 2 moveRandomly
    .| onlyKeepsDir_9 moveRandomly
  interestFreq | interestHere bpos =
    -- Don't detour towards an interest if already on one.
    mzero
               | otherwise =
    -- Prefer interests, but don't exclude other focused moves.
    scaleFreq 10 $ bestVariant $ onlyInterest $ onlyKeepsDir 2 moveRandomly
  interestIQFreq = interestFreq `mplus` bestVariant moveIQ
  moveClear    =
    onlyMoves (not . bumpableHere cops lvl foeVisible (asight mk)) moveFreely
  moveOpenable =
    onlyMoves (bumpableHere cops lvl foeVisible (asight mk)) moveFreely
  -- Ignore previously ignored loot, to prevent repetition.
  moveNewLoot = onlyLoot (onlyKeepsDir 2 moveRandomly)
  moveFreely = moveNewLoot
               .| liftFrequency interestIQFreq
               .| moveIQ  -- @bestVariant moveIQ@ may be excluded elsewhere
               .| moveRandomly
  onlyMoves :: (Point -> Bool) -> Strategy Vector -> Strategy Vector
  onlyMoves p = only (\x -> p (bpos `shift` x))
  moveRandomly :: Strategy Vector
  moveRandomly = liftFrequency $ uniformFreq "moveRandomly" sensible
  accessibleHere = accessible cops lvl bpos
  fact = sfactionD s EM.! bfid
  friends = actorList (not . isAtWar fact) blid s
  noFriends | asight mk = unoccupied friends
            | otherwise = const True
  isSensible l = noFriends l
                 && (accessibleHere l
                     || bumpableHere cops lvl foeVisible (asight mk) l)
  sensible = filter (isSensible . (bpos `shift`)) (moves lxsize)

bumpableHere :: Kind.COps -> Level -> Bool -> Bool -> Point -> Bool
bumpableHere Kind.COps{cotile} lvl foeVisible asight pos =
  let t = lvl `at` pos  -- cannot hold items, so OK
  in Tile.openable cotile t
     || -- Try to find hidden doors only if no foe in sight and not blind.
        -- Blind actors forget their search results too quickly.
        asight && not foeVisible && Tile.hasFeature cotile F.Suspect t

chase :: MonadActionRO m
      => ActorId -> (Point, Bool) -> m (Strategy CmdSerTakeTime)
chase aid foe@(_, foeVisible) = do
  cops <- getsState scops
  -- Target set and we chase the foe or offer null strategy if we can't.
  -- The foe is visible, or we remember his last position.
  let mFoe = Just foe
      fight = not foeVisible  -- don't pick fights if the real foe is close
  s <- getState
  str <- moveStrategy cops aid s mFoe
  if fight
    then Traversable.mapM (moveRunAid False aid) str
    else Traversable.mapM (moveRunAid True aid) str

wander :: MonadActionRO m
       => ActorId -> m (Strategy CmdSerTakeTime)
wander aid = do
  cops <- getsState scops
  -- Target set, but we don't chase the foe, e.g., because we are blocked
  -- or we cannot chase at all.
  let mFoe = Nothing
  s <- getState
  str <- moveStrategy cops aid s mFoe
  Traversable.mapM (moveRunAid False aid) str

-- | Actor moves or searches or alters or attacks. Displaces if @run@.
moveRunAid :: MonadActionRO m
           => Bool -> ActorId -> Vector -> m CmdSerTakeTime
moveRunAid run source dir = do
  cops@Kind.COps{cotile} <- getsState scops
  sb <- getsState $ getActorBody source
  let lid = blid sb
  lvl <- getLevel lid
  let spos = bpos sb           -- source position
      tpos = spos `shift` dir  -- target position
      t = lvl `at` tpos
  -- We start by checking actors at the the target position,
  -- which gives a partial information (actors can be invisible),
  -- as opposed to accessibility (and items) which are always accurate
  -- (tiles can't be invisible).
  tgt <- getsState $ posToActor tpos lid
  case tgt of
    Just target | run ->  -- can be a foe, as well as a friend
      if accessible cops lvl spos tpos then
        -- Displacing requires accessibility.
        return $ DisplaceSer source target
      else
        -- If cannot displace, hit. No DisplaceAccess.
        return $ MeleeSer source target
    Just target ->  -- can be a foe, as well as a friend
      -- Attacking does not require full access, adjacency is enough.
      return $ MeleeSer source target
    Nothing -> do  -- move or search or alter
      if accessible cops lvl spos tpos then
        -- Movement requires full access.
        return $ MoveSer source dir
        -- The potential invisible actor is hit.
      else if not $ EM.null $ lvl `atI` tpos then
        -- This is, e.g., inaccessible open door with an item in it.
        assert `failure` "AI causes AlterBlockItem" `twith` (run, source, dir)
      else if not (Tile.hasFeature cotile F.Walkable t)  -- not implied
              && (Tile.hasFeature cotile F.Suspect t
                  || Tile.openable cotile t
                  || Tile.closable cotile t
                  || Tile.changeable cotile t) then
        -- No access, so search and/or alter the tile.
        return $ AlterSer source tpos Nothing
      else
        -- Boring tile, no point bumping into it, do WaitSer if really idle.
        assert `failure` "AI causes MoveNothing or AlterNothing"
               `twith` (run, source, dir)

-- | How much AI benefits from applying the effect. Multipllied by item p.
-- Negative means harm to the enemy when thrown at him. Effects with zero
-- benefit won't ever be used, neither actively nor passively.
effectToBenefit :: Kind.COps -> Actor -> Effect.Effect Int -> Int
effectToBenefit Kind.COps{coactor=Kind.Ops{okind}} b eff =
  let kind = okind $ bkind b
      deep k = signum k == signum (fromEnum $ blid b)
  in case eff of
    Effect.NoEffect -> 0
    (Effect.Heal p) -> 10 * min p (Random.maxDice (ahp kind) - bhp b)
    (Effect.Hurt _ p) -> -(p * 10)     -- TODO: dice ignored, not capped
    Effect.Mindprobe{} -> 0            -- AI can't benefit yet
    Effect.Dominate -> -100
    (Effect.CallFriend p) -> p * 100
    Effect.Summon{} -> 1               -- may or may not spawn a friendly
    (Effect.CreateItem p) -> p * 20
    Effect.ApplyPerfume -> 0
    Effect.Regeneration{} -> 0         -- bigger benefit from carrying around
    Effect.Searching{} -> 0
    (Effect.Ascend k) | deep k -> 500  -- AI likes to explore deep down
    Effect.Ascend{} -> 1
    Effect.Escape -> 1000              -- AI wants to win