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
|
-- | Assorted conditions used later on in AI logic.
module Game.LambdaHack.Client.AI.ConditionM
( condAimEnemyTargetedM
, condAimEnemyOrStashM
, condAimEnemyOrRememberedM
, condAimNonEnemyPresentM
, condAimCrucialM
, condTgtNonmovingEnemyM
, condAdjTriggerableM
, meleeThreatDistList
, condBlocksFriendsM
, condFloorWeaponM
, condNoEqpWeaponM
, condCanProjectM
, condProjectListM
, benAvailableItems
, hinders
, condDesirableFloorItemM
, benGroundItems
, desirableItem
, condSupport
, condAloneM
, condShineWouldBetrayM
, fleeList
) where
import Prelude ()
import Game.LambdaHack.Core.Prelude
import qualified Data.EnumMap.Strict as EM
import Game.LambdaHack.Client.Bfs
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.Point
import Game.LambdaHack.Common.ReqFailure
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 Game.LambdaHack.Content.FactionKind
import qualified Game.LambdaHack.Content.ItemKind as IK
import qualified Game.LambdaHack.Content.RuleKind as RK
import qualified Game.LambdaHack.Core.Dice as Dice
import qualified Game.LambdaHack.Definition.Ability as Ability
import Game.LambdaHack.Definition.Defs
-- All conditions are (partially) lazy, because they are not always
-- used in the strict monadic computations they are in.
-- | Require that a target enemy is visible by the party.
condAimEnemyTargetedM :: MonadClientRead m => ActorId -> m Bool
condAimEnemyTargetedM aid = do
btarget <- getsClient $ getTarget aid
return $ case btarget of
Just (TEnemy _) -> True
_ -> False
-- | Require that a target enemy or enemy stash is visible by the party.
condAimEnemyOrStashM :: MonadClientRead m => ActorId -> m Bool
condAimEnemyOrStashM aid = do
btarget <- getsClient $ getTarget aid
return $ case btarget of
Just (TEnemy _) -> True
Just (TPoint (TStash _) _ _) -> True -- speedup from: lid == blid b
_ -> False
-- | Require that a target enemy is remembered on the actor's level.
condAimEnemyOrRememberedM :: MonadClientRead m => ActorId -> m Bool
condAimEnemyOrRememberedM aid = do
b <- getsState $ getActorBody aid
btarget <- getsClient $ getTarget aid
return $ case btarget of
Just (TEnemy _) -> True
Just (TPoint (TEnemyPos _) lid _) -> lid == blid b
Just (TPoint (TStash _) lid _) -> lid == blid b
_ -> False
-- | Require that a target non-enemy is visible by the party.
condAimNonEnemyPresentM :: MonadClientRead m => ActorId -> m Bool
condAimNonEnemyPresentM aid = do
btarget <- getsClient $ getTarget aid
return $ case btarget of
Just (TNonEnemy _) -> True
_ -> False
-- | Require that the target is crucial to success, e.g., an item,
-- or that it's not too far away and so the changes to get it are high.
condAimCrucialM :: MonadClientRead m => ActorId -> m Bool
condAimCrucialM aid = do
b <- getsState $ getActorBody aid
mtgtMPath <- getsClient $ EM.lookup aid . stargetD
return $ case mtgtMPath of
Just TgtAndPath{tapTgt=TEnemy _} -> True
Just TgtAndPath{tapTgt=TPoint tgoal lid _, tapPath=Just AndPath{pathLen}} ->
lid == blid b
&& (pathLen < 10 -- close enough to get there first
|| tgoal `notElem` [TUnknown, TKnown])
Just TgtAndPath{tapTgt=TVector{}, tapPath=Just AndPath{pathLen}} ->
pathLen < 7 -- can't say if the target important, but the constants
-- from @take6@ and @traSlack7@ ensure target is
-- already approached or close to level edge
-- or not a random @traSlack7@ wandering
_ -> False -- includes the case of target with no path
-- | Check if the target is a nonmoving enemy.
condTgtNonmovingEnemyM :: MonadClientRead m => ActorId -> m Bool
condTgtNonmovingEnemyM aid = do
btarget <- getsClient $ getTarget aid
case btarget of
Just (TEnemy enemy) -> do
actorMaxSk <- getsState $ getActorMaxSkills enemy
return $ Ability.getSk Ability.SkMove actorMaxSk <= 0
_ -> return False
-- | Require the actor stands on or adjacent to a triggerable tile
-- (e.g., stairs).
condAdjTriggerableM :: MonadStateRead m => Ability.Skills -> ActorId -> m Bool
condAdjTriggerableM actorSk aid = do
COps{coTileSpeedup} <- getsState scops
b <- getsState $ getActorBody aid
lvl <- getLevel $ blid b
let alterSkill = Ability.getSk Ability.SkAlter actorSk
alterMinSkill p = Tile.alterMinSkill coTileSpeedup $ lvl `at` p
underFeet p = p == bpos b -- if enter and alter, be more permissive
-- Before items are applied (which AI attempts even if apply
-- skills too low), tile must be alerable, hence both checks.
hasTriggerable p = (underFeet p
|| alterSkill >= fromEnum (alterMinSkill p))
&& p `EM.member` lembed lvl
return $ any hasTriggerable $ bpos b : vicinityUnsafe (bpos b)
-- | Produce the chess-distance-sorted list of non-low-HP,
-- melee-cabable foes on the level. We don't consider path-distance,
-- because we are interested in how soon the foe can close in to hit us,
-- which can diverge greately from path distance for short distances,
-- e.g., when terrain gets revealed. We don't consider non-moving actors,
-- because they can't chase us and also because they can't be aggresive
-- so to resolve the stalemate, the opposing AI has to be aggresive
-- by ignoring them and closing in to melee distance.
meleeThreatDistList :: [(ActorId, Actor)] -> ActorId -> State
-> [(Int, (ActorId, Actor))]
meleeThreatDistList foeAssocs aid s =
let actorMaxSkills = sactorMaxSkills s
b = getActorBody aid s
strongActor (aid2, b2) =
let actorMaxSk = actorMaxSkills EM.! aid2
nonmoving = Ability.getSk Ability.SkMove actorMaxSk <= 0
in not (hpTooLow b2 actorMaxSk || nonmoving)
&& actorCanMeleeToHarm actorMaxSkills aid2 b2
allThreats = filter strongActor foeAssocs
addDist (aid2, b2) = (chessDist (bpos b) (bpos b2), (aid2, b2))
in sortBy (comparing fst) $ map addDist allThreats
-- | Require the actor blocks the paths of any of his party members.
condBlocksFriendsM :: MonadClientRead m => ActorId -> m Bool
condBlocksFriendsM aid = do
b <- getsState $ getActorBody aid
targetD <- getsClient stargetD
let blocked aid2 = aid2 /= aid &&
case EM.lookup aid2 targetD of
Just TgtAndPath{tapPath=Just AndPath{pathList=q : _}} | q == bpos b ->
True
_ -> False
any blocked <$> getsState (fidActorRegularIds (bfid b) (blid b))
-- | Require the actor stands over a weapon that would be auto-equipped,
-- if only it was a desirable item (checked elsewhere).
condFloorWeaponM :: MonadStateRead m => ActorId -> m Bool
condFloorWeaponM aid =
any (IA.checkFlag Ability.Meleeable . aspectRecordFull . snd) <$>
getsState (fullAssocs aid [CGround])
-- | Check whether the actor has no weapon in equipment.
condNoEqpWeaponM :: MonadStateRead m => ActorId -> m Bool
condNoEqpWeaponM aid =
not . any (IA.checkFlag Ability.Meleeable . aspectRecordFull . snd) <$>
getsState (fullAssocs aid [CEqp])
-- | Require that the actor can project any items.
condCanProjectM :: MonadClientRead m => Int -> ActorId -> m Bool
condCanProjectM skill aid = do
side <- getsClient sside
curChal <- getsClient scurChal
fact <- getsState $ (EM.! side) . sfactionD
if skill < 1
|| ckeeper curChal && fhasUI (gkind fact)
then return False
else -- shortcut
-- Compared to conditions in @projectItem@, range and charge are ignored,
-- because they may change by the time the position for the fling
-- is reached.
not . null <$> condProjectListM skill aid
condProjectListM :: MonadClientRead m
=> Int -> ActorId
-> m [(Double, CStore, ItemId, ItemFull, ItemQuant)]
condProjectListM skill aid = do
condShineWouldBetray <- condShineWouldBetrayM aid
condAimEnemyOrRemembered <- condAimEnemyOrRememberedM aid
discoBenefit <- getsClient sdiscoBenefit
getsState $ projectList discoBenefit skill aid
condShineWouldBetray condAimEnemyOrRemembered
projectList :: DiscoveryBenefit -> Int -> ActorId -> Bool -> Bool -> State
-> [(Double, CStore, ItemId, ItemFull, ItemQuant)]
projectList discoBenefit skill aid
condShineWouldBetray condAimEnemyOrRemembered s =
let b = getActorBody aid s
actorMaxSk = getActorMaxSkills aid s
calmE = calmEnough b actorMaxSk
heavilyDistressed = -- Actor hit by a projectile or similarly distressed.
deltasSerious (bcalmDelta b)
uneasy = condAimEnemyOrRemembered
|| not calmE
|| heavilyDistressed
-- don't take recent fleeing into account when item can be lost
coeff CGround = 2 -- pickup turn saved
coeff COrgan = error $ "" `showFailure` benList
coeff CEqp = 1000 -- must hinder currently (or be very potent);
-- note: not larger, to avoid Int32 overflow
coeff CStash = 1
-- This detects if the value of keeping the item in eqp is in fact < 0.
hind = hinders condShineWouldBetray uneasy actorMaxSk
goodMissile (Benefit{benInEqp, benFling}, cstore, iid, itemFull, kit) =
let arItem = aspectRecordFull itemFull
benR = coeff cstore * benFling
in if benR < -1 -- ignore very weak projectiles
&& (not benInEqp -- can't wear, so OK to risk losing or breaking
|| not (IA.checkFlag Ability.Meleeable arItem)
-- anything else expendable
&& hind itemFull) -- hinders now, so possibly often
&& permittedProjectAI skill calmE itemFull
then Just (benR, cstore, iid, itemFull, kit)
else Nothing
stores = [CStash, CGround] ++ [CEqp | calmE]
benList = benAvailableItems discoBenefit aid stores s
in mapMaybe goodMissile benList
-- | Produce the list of items from the given stores available to the actor
-- and the items' values.
benAvailableItems :: DiscoveryBenefit -> ActorId -> [CStore] -> State
-> [(Benefit, CStore, ItemId, ItemFull, ItemQuant)]
benAvailableItems discoBenefit aid cstores s =
let b = getActorBody aid s
mstash = gstash $ sfactionD s EM.! bfid b
ben _ CGround | mstash == Just (blid b, bpos b) = []
ben bag cstore =
[ (discoBenefit EM.! iid, cstore, iid, itemToFull iid s, kit)
| (iid, kit) <- EM.assocs bag]
benCStore cs = ben (getBodyStoreBag b cs s) cs
in concatMap benCStore cstores
hinders :: Bool -> Bool -> Ability.Skills -> ItemFull -> Bool
hinders condShineWouldBetray uneasy actorMaxSk itemFull =
let arItem = aspectRecordFull itemFull
itemShine = 0 < IA.getSkill Ability.SkShine arItem
-- @condAnyFoeAdj@ is not checked, because it's transient and also item
-- management is unlikely to happen during melee, anyway
itemShineBad = condShineWouldBetray && itemShine
in -- In the presence of enemies (seen, remembered or unseen but distressing)
-- actors want to hide in the dark.
uneasy && itemShineBad -- even if it's a weapon, take it off
-- Fast actors want to hit hard, because they hit much more often
-- than receive hits.
|| gearSpeed actorMaxSk > speedWalk
&& not (IA.checkFlag Ability.Meleeable arItem)
-- in case it's the only weapon
&& 0 > IA.getSkill Ability.SkHurtMelee arItem
-- | Require that the actor stands over a desirable item.
condDesirableFloorItemM :: MonadClientRead m => ActorId -> m Bool
condDesirableFloorItemM aid = not . null <$> benGroundItems aid
-- | Produce the list of items on the ground beneath the actor
-- that are worth picking up.
benGroundItems :: MonadClientRead m
=> ActorId
-> m [(Benefit, CStore, ItemId, ItemFull, ItemQuant)]
benGroundItems aid = do
cops <- getsState scops
b <- getsState $ getActorBody aid
fact <- getsState $ (EM.! bfid b) . sfactionD
discoBenefit <- getsClient sdiscoBenefit
let canEsc = fcanEscape (gkind fact)
isDesirable (ben, _, _, itemFull, _) =
desirableItem cops canEsc (benPickup ben)
(aspectRecordFull itemFull) (itemKind itemFull)
99 -- fake, because no time is wasted walking to item
filter isDesirable
<$> getsState (benAvailableItems discoBenefit aid [CGround])
desirableItem :: COps -> Bool -> Double -> IA.AspectRecord -> IK.ItemKind -> Int
-> Bool
desirableItem COps{corule}
canEsc benPickup arItem itemKind k =
let loneProjectile =
IK.isymbol itemKind == IK.rsymbolProjectile (RK.ritemSymbols corule)
&& k == 1
&& Dice.infDice (IK.icount itemKind) > 1
-- never generated as lone; usually means weak
useful = if canEsc
then benPickup > 0
|| IA.checkFlag Ability.Precious arItem
else -- A hack to prevent monsters from picking up
-- treasure meant for heroes.
let preciousNotUseful = IA.isHumanTrinket itemKind
in benPickup > 0 && not preciousNotUseful
in useful && not loneProjectile
condSupport :: MonadClientRead m
=> [(ActorId, Actor)] -> Int -> ActorId -> m Bool
{-# INLINE condSupport #-}
condSupport friendAssocs param aid = do
mtgtMPath <- getsClient $ EM.lookup aid . stargetD
getsState $ strongSupport friendAssocs param aid mtgtMPath
strongSupport :: [(ActorId, Actor)]
-> Int -> ActorId -> Maybe TgtAndPath -> State
-> Bool
strongSupport friendAssocs param aid mtgtMPath s =
-- The smaller the area scanned for friends, the lower number required.
let actorMaxSkills = sactorMaxSkills s
actorMaxSk = actorMaxSkills EM.! aid
n = min 2 param - Ability.getSk Ability.SkAggression actorMaxSk
b = getActorBody aid s
approaching b2 = case mtgtMPath of
Just TgtAndPath{tapTgt=TEnemy{},tapPath=Just AndPath{pathGoal}} ->
chessDist (bpos b2) pathGoal <= 1 + param -- will soon melee anyway
_ -> False
closeEnough b2 = let dist = chessDist (bpos b) (bpos b2)
in dist > 0 && (dist <= max 2 param || approaching b2)
closeAndStrong (aid2, b2) = closeEnough b2
&& actorCanMeleeToHarm actorMaxSkills aid2 b2
closeAndStrongFriends = filter closeAndStrong friendAssocs
in n <= 0 || not (null (drop (n - 1) closeAndStrongFriends))
-- optimized: length closeAndStrongFriends >= n
-- The numbers reflect fleeing AI conditions for non-aggresive actors
-- so that actors don't wait for support that is not possible due to not
-- enough friends on the level, even counting sleeping ones.
condAloneM :: MonadStateRead m => [(ActorId, Actor)] -> ActorId -> m Bool
condAloneM friendAssocs aid = do
b <- getsState $ getActorBody aid
mstash <- getsState $ \s -> gstash $ sfactionD s EM.! bfid b
let onStashLevel = case mstash of
Nothing -> False
Just (lid, _) -> lid == blid b
return $! length friendAssocs <= if onStashLevel then 3 else 2
-- | Require that the actor stands in the dark and so would be betrayed
-- by his own equipped light,
condShineWouldBetrayM :: MonadStateRead m => ActorId -> m Bool
condShineWouldBetrayM aid = do
b <- getsState $ getActorBody aid
aInAmbient <- getsState $ actorInAmbient b
return $ not aInAmbient -- tile is dark, so actor could hide
-- | Produce a list of acceptable adjacent points to flee to.
fleeList :: MonadClientRead m
=> [(ActorId, Actor)] -> ActorId -> m ([(Int, Point)], [(Int, Point)])
fleeList foeAssocs aid = do
COps{coTileSpeedup} <- getsState scops
mtgtMPath <- getsClient $ EM.lookup aid . stargetD
-- Prefer fleeing along the path to target, unless the target is a foe,
-- in which case flee in the opposite direction.
let etgtPath = case mtgtMPath of
Just TgtAndPath{ tapPath=Just AndPath{pathList, pathGoal}
, tapTgt } -> case tapTgt of
TEnemy{} -> Left pathGoal
TPoint TEnemyPos{} _ _ -> Left pathGoal
-- this is too weak, because only one is recorded and sometimes
-- many are needed to decide to flee next turn as well
_ -> Right pathList
_ -> Right []
b <- getsState $ getActorBody aid
lvl <- getLevel $ blid b
localTime <- getsState $ getLocalTime (blid b)
fleeD <- getsClient sfleeD
-- But if fled recently, prefer even more fleeing further this turn.
let eOldFleeOrTgt = case EM.lookup aid fleeD of
Just (fleeStart, time) | timeRecent5 localTime time -> Left fleeStart
_ -> etgtPath
myVic = vicinityUnsafe $ bpos b
dist p | null foeAssocs = 100
| otherwise = minimum $ map (chessDist p . bpos . snd) foeAssocs
dVic = map (dist &&& id) myVic
-- Flee, if possible. Direct access required; not enough time to open.
-- Can't be occupied.
accWalkUnocc p = Tile.isWalkable coTileSpeedup (lvl `at` p)
&& not (occupiedBigLvl p lvl)
&& not (occupiedProjLvl p lvl)
accWalkVic = filter (accWalkUnocc . snd) dVic
gtVic = filter ((> dist (bpos b)) . fst) accWalkVic
eqVicRaw = filter ((== dist (bpos b)) . fst) accWalkVic
(eqVicOld, eqVic) = partition ((== boldpos b) . Just . snd) eqVicRaw
accNonWalkUnocc p = not (Tile.isWalkable coTileSpeedup (lvl `at` p))
&& Tile.isEasyOpen coTileSpeedup (lvl `at` p)
&& not (occupiedBigLvl p lvl)
&& not (occupiedProjLvl p lvl)
accNonWalkVic = filter (accNonWalkUnocc . snd) dVic
gtEqNonVic = filter ((>= dist (bpos b)) . fst) accNonWalkVic
ltAllVic = filter ((< dist (bpos b)) . fst) dVic
rewardPath mult (d, p) = case eOldFleeOrTgt of
Right tgtPathList | p `elem` tgtPathList ->
(100 * mult * d, p)
Right tgtPathList | any (adjacent p) tgtPathList ->
(10 * mult * d, p)
Left pathGoal | bpos b /= pathGoal ->
let venemy = towards (bpos b) pathGoal
vflee = towards (bpos b) p
sq = euclidDistSqVector venemy vflee
skew = case compare sq 2 of
GT -> 100 * sq
EQ -> 10 * sq
LT -> sq -- going towards enemy (but may escape adjacent foes)
in (mult * skew * d, p)
_ -> (mult * d, p) -- far from target path or even on target goal
goodVic = map (rewardPath 10000) gtVic
++ map (rewardPath 100) eqVic
badVic = map (rewardPath 1) $ gtEqNonVic ++ eqVicOld ++ ltAllVic
return (goodVic, badVic)
|