LambdaHack-0.2.6.5: Game/LambdaHack/StrategyAction.hs
{-# LANGUAGE OverloadedStrings #-}
-- | AI strategy operations implemented with the 'Action' monad.
module Game.LambdaHack.StrategyAction
( targetStrategy, strategy
) where
import qualified Data.List as L
import qualified Data.IntMap as IM
import Data.Maybe
import Data.Function
import Control.Monad
import Control.Monad.State hiding (State, state)
import Control.Arrow
import qualified Data.Text as T
import Game.LambdaHack.Utils.Assert
import Game.LambdaHack.Ability (Ability)
import qualified Game.LambdaHack.Ability as Ability
import Game.LambdaHack.Point
import Game.LambdaHack.Vector
import Game.LambdaHack.Level
import Game.LambdaHack.Actor
import Game.LambdaHack.ActorState
import Game.LambdaHack.Content.ActorKind
import Game.LambdaHack.Utils.Frequency
import Game.LambdaHack.Perception
import Game.LambdaHack.Strategy
import Game.LambdaHack.State
import Game.LambdaHack.Action
import Game.LambdaHack.Msg
import Game.LambdaHack.EffectAction
import Game.LambdaHack.Actions
import Game.LambdaHack.ItemAction
import Game.LambdaHack.Content.ItemKind
import Game.LambdaHack.Content.RuleKind
import Game.LambdaHack.Item
import qualified Game.LambdaHack.Effect as Effect
import qualified Game.LambdaHack.Tile as Tile
import qualified Game.LambdaHack.Kind as Kind
import qualified Game.LambdaHack.Feature as F
import Game.LambdaHack.Time
import qualified Game.LambdaHack.Color as Color
-- | AI proposes possible targets for the actor. Never empty.
targetStrategy :: Kind.COps -> ActorId -> State -> Perception -> [Ability]
-> Strategy Target
targetStrategy cops actor state@State{splayer = pl} per factionAbilities =
retarget btarget
where
Kind.COps{ cotile
, coactor=coactor@Kind.Ops{okind}
} = cops
lvl@Level{lxsize} = slevel state
actorBody@Actor{ bkind, bloc = me, btarget, bfaction } =
getActor actor state
mk = okind bkind
enemyVisible a l =
asight mk
&& actorSeesActor cotile per lvl actor a me l pl
-- Enemy can be felt if adjacent (e. g., a player-controlled monster).
-- TODO: can this be replaced by setting 'lights' to [me]?
|| adjacent lxsize me l
&& (asmell mk || asight mk)
actorAbilities = acanDo (okind bkind) `L.intersect` factionAbilities
focused = actorSpeed coactor actorBody <= 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
retarget :: Target -> Strategy Target
retarget tgt =
case tgt of
TPath _ -> returN "TPath" tgt -- don't animate missiles
TEnemy a ll | focused
&& memActor a state -- present on this level
-- Don't hit a new player-controlled monster.
&& not (isAHero state actor && a == pl) ->
let l = bloc $ getActor a state
in if enemyVisible a l -- prefer visible foes
then returN "TEnemy" $ TEnemy a l
else if null visibleFoes -- prefer visible foes
&& me /= ll -- not yet reached the last enemy loc
then returN "last known" $ TLoc ll
-- chase the last known loc
else closest
TEnemy _ _ -> closest -- foe is gone and we forget
TLoc loc | me == loc -> closest -- already reached the loc
TLoc _ | null visibleFoes -> returN "TLoc" tgt
-- nothing visible, go to loc
TLoc _ -> closest -- prefer visible foes
TCursor -> closest
hs = hostileAssocs bfaction lvl
foes = if isAHero state actor
then L.filter ((pl /=) . fst) hs -- ignore player-controlled
else if not (isAHero state pl) && memActor pl state
then (pl, getPlayerBody state) : hs
else hs -- no player-controlled monster to add
visibleFoes = L.filter (uncurry enemyVisible) (L.map (second bloc) foes)
closest :: Strategy Target
closest =
let foeDist = L.map (\ (_, l) -> chessDist lxsize me l) visibleFoes
minDist = L.minimum foeDist
minFoes =
L.filter (\ (_, l) -> chessDist lxsize me l == minDist) visibleFoes
minTargets = map (\ (a, l) -> TEnemy a l) minFoes
minTgtS = liftFrequency $ uniformFreq "closest" minTargets
in minTgtS .| noFoes .| returN "TCursor" TCursor -- never empty
-- TODO: set distant targets so that monsters behave as if they have
-- a plan. We need pathfinding for that.
noFoes :: Strategy Target
noFoes =
(TLoc . (me `shift`)) `liftM` moveStrategy cops actor state Nothing
-- | AI strategy based on actor's sight, smell, intelligence, etc. Never empty.
strategy :: Kind.COps -> ActorId -> State -> [Ability] -> Strategy (Action ())
strategy cops actor state factionAbilities =
sumS prefix .| combineDistant distant .| sumS suffix
.| waitBlockNow actor -- wait until friends sidestep, ensures never empty
where
Kind.COps{coactor=Kind.Ops{okind}} = cops
Actor{ bkind, bloc, btarget } = getActor actor state
(floc, foeVisible) = case btarget of
TEnemy _ l -> (l, True)
TLoc l -> (l, False)
TPath _ -> (bloc, False) -- a missile
TCursor -> (bloc, False) -- an actor blocked by friends
combineDistant = liftFrequency . sumF
aFrequency :: Ability -> Frequency (Action ())
aFrequency Ability.Ranged = if foeVisible
then rangedFreq cops actor state floc
else mzero
aFrequency Ability.Tools = if foeVisible
then toolsFreq cops actor state
else mzero
aFrequency Ability.Chase = if (floc /= bloc)
then chaseFreq
else mzero
aFrequency _ = assert `failure` distant
chaseFreq =
scaleFreq 30 $ bestVariant $ chase cops actor state (floc, foeVisible)
aStrategy :: Ability -> Strategy (Action ())
aStrategy Ability.Track = track cops actor state
aStrategy Ability.Heal = mzero -- TODO
aStrategy Ability.Flee = mzero -- TODO
aStrategy Ability.Melee = foeVisible .=> melee actor state floc
aStrategy Ability.Pickup = not foeVisible .=> pickup actor state
aStrategy Ability.Wander = wander cops actor state
aStrategy _ = assert `failure` actorAbilities
actorAbilities = acanDo (okind bkind) `L.intersect` factionAbilities
isDistant = (`elem` [Ability.Ranged, Ability.Tools, Ability.Chase])
(prefix, rest) = L.break isDistant actorAbilities
(distant, suffix) = L.partition isDistant rest
sumS = msum . map aStrategy
sumF = msum . map aFrequency
dirToAction :: ActorId -> Bool -> Vector -> Action ()
dirToAction actor allowAttacks dir = do
-- set new direction
updateAnyActor actor $ \ m -> m { bdir = Just (dir, 0) }
-- perform action
tryWith (\ msg -> if T.null msg
then return ()
else assert `failure` msg <> "in AI") $ do
-- If the following action aborts, we just advance the time and continue.
-- TODO: ensure time is taken for other aborted actions in this file
-- TODO: or just fail at each abort in AI code? or use tryWithFrame?
moveOrAttack allowAttacks actor dir
-- | A strategy to always just wait.
waitBlockNow :: ActorId -> Strategy (Action ())
waitBlockNow actor = returN "wait" $ setWaitBlock actor
-- | A strategy to always just die.
dieNow :: ActorId -> Strategy (Action ())
dieNow actor = returN "die" $ do -- TODO: explode if a potion
bitems <- gets (getActorItem actor)
Actor{bloc} <- gets (getActor actor)
modify (updateLevel (dropItemsAt bitems bloc))
modify (deleteActor actor)
-- | Strategy for dumb missiles.
track :: Kind.COps -> ActorId -> State -> Strategy (Action ())
track cops actor state =
strat
where
lvl = slevel state
Actor{ bloc, btarget, bhp } = getActor actor state
darkenActor = updateAnyActor actor $ \ m -> m {bcolor = Just Color.BrBlack}
dieOrReset | bhp <= 0 = dieNow actor
| otherwise =
returN "reset TPath" $ updateAnyActor actor
$ \ m -> m {btarget = TCursor}
strat = case btarget of
TPath [] -> dieOrReset
TPath (d : _) | not $ accessible cops lvl bloc (shift bloc d) -> dieOrReset
-- TODO: perhaps colour differently the whole second turn of movement?
TPath [d] -> returN "last TPath" $ do
darkenActor
updateAnyActor actor $ \ m -> m { btarget = TPath [] }
dirToAction actor True d
TPath (d : lv) -> returN "follow TPath" $ do
updateAnyActor actor $ \ m -> m { btarget = TPath lv }
dirToAction actor True d
_ -> reject
pickup :: ActorId -> State -> Strategy (Action ())
pickup actor state =
lootHere bloc .=> actionPickup
where
lvl = slevel state
Actor{bloc} = getActor actor state
lootHere x = not $ L.null $ lvl `atI` x
actionPickup = returN "pickup" $ actorPickupItem actor
melee :: ActorId -> State -> Point -> Strategy (Action ())
melee actor state floc =
foeAdjacent .=> (returN "melee" $ dirToAction actor True dir)
where
Level{lxsize} = slevel state
Actor{bloc} = getActor actor state
foeAdjacent = adjacent lxsize bloc floc
dir = displacement bloc floc
rangedFreq :: Kind.COps -> ActorId -> State -> Point -> Frequency (Action ())
rangedFreq cops actor state@State{splayer = pl} floc =
toFreq "throwFreq" $
if not foesAdj
&& asight mk
&& accessible cops lvl bloc loc1 -- first accessible
&& isNothing (locToActor loc1 state) -- no friends on first
then throwFreq bitems 3 ++ throwFreq tis 6
else []
where
Kind.COps{ coactor=Kind.Ops{okind}
, coitem=Kind.Ops{okind=iokind}
, corule
} = cops
lvl@Level{lxsize, lysize} = slevel state
Actor{ bkind, bloc, bfaction } = getActor actor state
bitems = getActorItem actor state
mk = okind bkind
tis = lvl `atI` bloc
hs = hostileAssocs bfaction lvl
foes = if isAHero state actor
then L.filter ((pl /=) . fst) hs -- ignore player-controlled
else if not (isAHero state pl) && memActor pl state
then (pl, getPlayerBody state) : hs
else hs -- no player-controlled monster to add
foesAdj = foesAdjacent lxsize lysize bloc (map snd foes)
-- TODO: also don't throw if any loc 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.
eps = 0
bl = bla lxsize lysize eps bloc floc -- TODO:make an arg of projectGroupItem
loc1 = case bl of
Nothing -> bloc -- TODO
Just [] -> bloc -- TODO
Just (lbl:_) -> lbl
throwFreq is multi =
[ (benefit * multi,
projectGroupItem actor floc (iverbProject ik) i)
| i <- is,
let ik = iokind (jkind i),
let benefit = - (1 + jpower i) * Effect.effectToBenefit (ieffect ik),
benefit > 0,
-- Wasting weapons and armour would be too cruel to the player.
isymbol ik `elem` (ritemProject $ Kind.stdRuleset corule)]
toolsFreq :: Kind.COps -> ActorId -> State -> Frequency (Action ())
toolsFreq cops actor state =
toFreq "quaffFreq" $ quaffFreq bitems 1 ++ quaffFreq tis 2
where
Kind.COps{coitem=Kind.Ops{okind=iokind}} = cops
lvl = slevel state
Actor{bloc} = getActor actor state
bitems = getActorItem actor state
tis = lvl `atI` bloc
quaffFreq is multi =
[ (benefit * multi, applyGroupItem actor (iverbApply ik) i)
| i <- is,
let ik = iokind (jkind i),
let benefit = (1 + jpower i) * Effect.effectToBenefit (ieffect ik),
benefit > 0, isymbol ik == '!']
-- | 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 :: Kind.COps -> ActorId -> State -> Maybe (Point, Bool)
-> Strategy Vector
moveStrategy cops actor state mFoe =
case mFoe of
-- Target set and we chase the foe or his last position or another target.
Just (floc, foeVisible) ->
let towardsFoe =
let foeDir = towards lxsize bloc floc
tolerance | isUnit lxsize foeDir = 0
| otherwise = 1
in only (\ x -> euclidDistSq lxsize foeDir x <= tolerance)
in if floc == bloc
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)
$ L.groupBy ((==) `on` snd)
$ L.sortBy (flip compare `on` snd)
$ L.filter (\ (_, s) -> s > timeZero)
$ L.map (\ x ->
let sml = IM.findWithDefault
timeZero (bloc `shift` x) lsmell
in (x, sml `timeAdd` timeNegate ltime))
sensible
in asmell mk .=> L.foldr ((.|)
. liftFrequency
. uniformFreq "smell k") reject smells
.| moveOpenable -- no enemy in sight, explore doors
.| moveClear
where
Kind.COps{ cotile
, coactor=Kind.Ops{okind}
, coitem
} = cops
lvl@Level{lsmell, lxsize, lysize, ltime} = slevel state
Actor{ bkind, bloc, bdir, bfaction } = getActor actor state
bitems = getActorItem actor state
mk = okind bkind
lootHere x = not $ L.null $ lvl `atI` x
onlyLoot = onlyMoves lootHere bloc
interestHere x = let t = lvl `at` x
ts = map (lvl `at`) $ vicinity lxsize lysize x
in Tile.hasFeature cotile F.Exit t
-- Lit indirectly. E.g., a room entrance.
|| (not (Tile.hasFeature cotile F.Lit t)
&& L.any (Tile.hasFeature cotile F.Lit) ts)
onlyInterest = onlyMoves interestHere bloc
onlyKeepsDir k =
only (\ x -> maybe True (\ (d, _) -> euclidDistSq lxsize d x <= k) bdir)
onlyKeepsDir_9 = only (\ x -> maybe True (\ (d, _) -> neg x /= d) bdir)
moveIQ = aiq mk > 15 .=> onlyKeepsDir 0 moveRandomly
.| aiq mk > 10 .=> onlyKeepsDir 1 moveRandomly
.| aiq mk > 5 .=> onlyKeepsDir 2 moveRandomly
.| onlyKeepsDir_9 moveRandomly
interestFreq | interestHere bloc =
-- Don't detour towards an interest if already on one.
mzero
| otherwise =
-- Prefer interests, but don't exclude other focused moves.
scaleFreq 5 $ bestVariant $ onlyInterest $ onlyKeepsDir 2 moveRandomly
interestIQFreq = interestFreq `mplus` bestVariant moveIQ
moveClear = onlyMoves (not . openableHere) bloc moveFreely
moveOpenable = onlyMoves openableHere bloc moveFreely
moveFreely = onlyLoot moveRandomly
.| liftFrequency interestIQFreq
.| moveIQ -- sometimes interestIQFreq is excluded later on
.| moveRandomly
onlyMoves :: (Point -> Bool) -> Point -> Strategy Vector -> Strategy Vector
onlyMoves p l = only (\ x -> p (l `shift` x))
moveRandomly :: Strategy Vector
moveRandomly = liftFrequency $ uniformFreq "moveRandomly" sensible
-- Monsters don't see doors more secret than that. Enforced when actually
-- opening doors, too, so that monsters don't cheat. TODO: remove the code
-- duplication, though. TODO: make symmetric for playable monster faction?
openPower = timeScale timeTurn $
case strongestSearch coitem bitems of
Just i -> aiq mk + jpower i
Nothing -> aiq mk
openableHere = openable cotile lvl openPower
accessibleHere = accessible cops lvl bloc
noFriends | asight mk = unoccupied (factionList [bfaction] state)
| otherwise = const True
isSensible l = noFriends l && (accessibleHere l || openableHere l)
sensible = filter (isSensible . (bloc `shift`)) (moves lxsize)
chase :: Kind.COps -> ActorId -> State -> (Point, Bool) -> Strategy (Action ())
chase cops actor state foe@(_, foeVisible) =
-- 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
in dirToAction actor fight `liftM` moveStrategy cops actor state mFoe
wander :: Kind.COps -> ActorId -> State -> Strategy (Action ())
wander cops actor state =
-- Target set, but we don't chase the foe, e.g., because we are blocked
-- or we cannot chase at all.
let mFoe = Nothing
in dirToAction actor True `liftM` moveStrategy cops actor state mFoe