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
|
-- | Picking the AI actor to move and refreshing leader and non-leader targets.
module Game.LambdaHack.Client.AI.PickActorM
( pickActorToMove, setTargetFromDoctrines
) where
import Prelude ()
import Game.LambdaHack.Core.Prelude
import qualified Data.EnumMap.Strict as EM
import Data.Ratio
import Game.LambdaHack.Client.AI.ConditionM
import Game.LambdaHack.Client.AI.PickTargetM
import Game.LambdaHack.Client.Bfs
import Game.LambdaHack.Client.BfsM
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.Kind
import Game.LambdaHack.Common.Level
import Game.LambdaHack.Common.Misc
import Game.LambdaHack.Common.MonadStateRead
import Game.LambdaHack.Common.Point
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.Content.FactionKind (fskillsOther)
import Game.LambdaHack.Core.Frequency
import Game.LambdaHack.Core.Random
import qualified Game.LambdaHack.Definition.Ability as Ability
-- | Pick a new leader from among the actors on the current level.
-- Refresh the target of the new leader, even if unchanged.
pickActorToMove :: MonadClient m
=> [(ActorId, Actor)] -> [(ActorId, Actor)] -> Maybe ActorId
-> m ActorId
pickActorToMove foeAssocs friendAssocs maidToAvoid = do
COps{coTileSpeedup} <- getsState scops
actorMaxSkills <- getsState sactorMaxSkills
mleader <- getsClient sleader
let oldAid = fromMaybe (error $ "" `showFailure` maidToAvoid) mleader
oldBody <- getsState $ getActorBody oldAid
let side = bfid oldBody
arena = blid oldBody
lvl <- getLevel arena
localTime <- getsState $ getLocalTime arena
condInMelee <- condInMeleeM arena
fact <- getsState $ (EM.! side) . sfactionD
-- Find our actors on the current level only.
ours <- getsState $ fidActorRegularAssocs side arena
let pickOld = do
void $ refreshTarget foeAssocs friendAssocs (oldAid, oldBody)
return oldAid
oursNotSleeping = filter (\(_, b) -> bwatch b /= WSleep) ours
-- Faction discourages client leader change on level, because
-- non-leader actors have the same skills as leader, so no point.
-- Server is guaranteed to switch leader within a level occasionally,
-- e.g., when the old leader dies, so this works fine.
discouragedPointmanSwitchOnLevel =
fskillsOther (gkind fact) == Ability.zeroSkills
case oursNotSleeping of
_ | -- Keep the leader: client is discouraged from leader switching,
-- so it will only be changed if pointman waits (maidToAvoid)
-- to avoid wasting his higher mobility.
-- This is OK for monsters even if in melee, because both having
-- a meleeing actor a leader (and higher DPS) and rescuing actor
-- a leader (and so faster to get in melee range) is good.
-- And we are guaranteed that only the two classes of actors are
-- not waiting, with some exceptions (urgent unequip, flee via starts,
-- melee-less trying to flee, first aid, etc.).
discouragedPointmanSwitchOnLevel && isNothing maidToAvoid -> pickOld
[] -> pickOld
[(aidNotSleeping, bNotSleeping)] -> do
-- Target of asleep actors won't change unless foe adjacent,
-- which is caught without recourse to targeting.
void $ refreshTarget foeAssocs friendAssocs (aidNotSleeping, bNotSleeping)
return aidNotSleeping
_ -> do
-- At this point we almost forget who the old leader was
-- and treat all party actors the same, eliminating candidates
-- until we can't distinguish them any more, at which point we slightly
-- prefer the old leader, if he is among the best candidates
-- (to make the AI appear more human-like and easier to observe).
let refresh aidBody = do
mtgt <- refreshTarget foeAssocs friendAssocs aidBody
return (aidBody, mtgt)
oursTgtRaw <- mapM refresh oursNotSleeping
oldFleeD <- getsClient sfleeD
let recentlyFled aid = maybe False
(\(_, time) -> timeRecent5 localTime time)
(aid `EM.lookup` oldFleeD)
goodGeneric (_, Nothing) = Nothing
goodGeneric (_, Just TgtAndPath{tapPath=Nothing}) = Nothing
-- this case means melee-less heroes adjacent to foes, etc.
-- will never flee if melee is happening; but this is rare;
-- this also ensures even if a lone actor melees and nobody
-- can come to rescue, he will become and remain the leader,
-- because otherwise an explorer would need to become a leader
-- and fighter will be 1 clip slower for the whole fight,
-- just for a few turns of exploration in return;
--
-- also note that when the fighter then becomes a leader
-- he may gain quite a lot of time via @swapTime@,
-- and so be able to get a double blow on opponents
-- or a safe blow and a withdraw (but only once); this is a mild
-- exploit that encourages ambush camping (with a non-leader),
-- but it's also a rather fun exploit and a straightforward
-- consequence of the game mechanics, so it's OK for now
goodGeneric ((aid, b), Just tgt) = case maidToAvoid of
_ | aid == oldAid && actorWaits b -> Nothing
-- Not the old leader that was stuck last turn
-- because he is likely to be still stuck.
Nothing -> Just ((aid, b), tgt)
Just aidToAvoid ->
if aid == aidToAvoid
then Nothing -- not an attempted leader stuck this turn
else Just ((aid, b), tgt)
oursTgt = mapMaybe goodGeneric oursTgtRaw
-- This should be kept in sync with @actionStrategy@,
-- because it's a part of the condition for @flee@ in @PickActionM@.
-- Comments are in the full copy.
actorVulnerable ((aid, body), _) = do
let actorMaxSk = actorMaxSkills EM.! aid
condAnyHarmfulFoeAdj <-
getsState $ anyHarmfulFoeAdj actorMaxSkills aid
threatDistL <- getsState $ meleeThreatDistList foeAssocs aid
(fleeL, _) <- fleeList foeAssocs aid
condSupport1 <- condSupport friendAssocs 1 aid
condSolo <- condAloneM friendAssocs aid
let condCanFlee = not (null fleeL)
heavilyDistressed =
deltasSerious (bcalmDelta body)
speed1_5 = speedScale (3%2) (gearSpeed actorMaxSk)
condCanMelee = actorCanMelee actorMaxSkills aid body
threatAdj = takeWhile ((== 1) . fst) threatDistL
condManyThreatAdj = length threatAdj >= 2
condFastThreatAdj =
any (\(_, (aid2, _)) ->
let ar2 = actorMaxSkills EM.! aid2
in gearSpeed ar2 > speed1_5)
threatAdj
condNonStealthyThreatAdj =
any (\(_, (aid2, b2)) ->
let ar2 = actorMaxSkills EM.! aid2
in Ability.getSk Ability.SkShine ar2 > 0
|| isLit (bpos b2))
threatAdj
isLit pos = Tile.isLit coTileSpeedup (lvl `at` pos)
fleeingMakesSense =
not condCanMelee
|| (Ability.getSk Ability.SkSight actorMaxSk > 2
|| Ability.getSk Ability.SkNocto actorMaxSk > 2)
&& (Ability.getSk Ability.SkShine actorMaxSk > 2
|| condNonStealthyThreatAdj || null threatAdj)
return $!
not condFastThreatAdj
&& fleeingMakesSense
&& if | condAnyHarmfulFoeAdj ->
not condCanMelee
|| condManyThreatAdj && not condSupport1 && not condSolo
| condInMelee -> False
| heavilyDistressed -> True
-- Different from @PickActionM@:
-- If under fire, do something quickly, always,
-- because the actor clearly vulnerable,
-- but don't make a leader only because threats close.
| otherwise -> False
&& condCanFlee
actorFled ((aid, _), _) = recentlyFled aid
actorHearning (_, TgtAndPath{ tapTgt=TPoint TEnemyPos{} _ _
, tapPath=Nothing }) =
return False
actorHearning (_, TgtAndPath{ tapTgt=TPoint TEnemyPos{} _ _
, tapPath=Just AndPath{pathLen} })
| pathLen <= 2 =
return False -- noise probably due to fleeing target
actorHearning ((_aid, b), _) = do
let closeFoes = filter ((<= 3) . chessDist (bpos b) . bpos . snd)
foeAssocs
actorHears = deltasHears (bcalmDelta b)
return $! actorHears -- e.g., actor hears an enemy
&& null closeFoes -- the enemy not visible; a trap!
-- AI has to be prudent and not lightly waste leader for meleeing.
actorMeleeing ((aid, _), _) =
getsState $ anyHarmfulFoeAdj actorMaxSkills aid
(oursVulnerable, oursSafe) <- partitionM actorVulnerable oursTgt
let (oursFled, oursNotFled) = partition actorFled oursSafe
(oursMeleeingRaw, oursNotMeleeingRaw) <-
partitionM actorMeleeing oursNotFled
let actorMeleeingCanDisplace ( (aid, sb)
, TgtAndPath{tapTgt=TEnemy target} ) = do
tb <- getsState $ getActorBody target
let actorMaxSk = actorMaxSkills EM.! target
dEnemy <- getsState $ dispEnemy aid target actorMaxSk
-- Some usual conditions ignored, because transient or rare.
return $! checkAdjacent sb tb && dEnemy
actorMeleeingCanDisplace _ = return False
(oursMeleeingCanDisplace, oursMeleeing) <-
partitionM actorMeleeingCanDisplace oursMeleeingRaw
let adjStash
( (_, b)
, TgtAndPath{tapTgt=TPoint TStash{} lid pos} ) =
lid == arena
&& adjacent pos (bpos b)
&& isNothing (posToBigLvl pos lvl)
adjStash _ = False
(oursAdjStash, oursNotMeleeing) =
partition adjStash oursNotMeleeingRaw
(oursHearing, oursNotHearing) <- partitionM actorHearning oursNotMeleeing
let actorRanged ((aid, body), _) =
not $ actorCanMelee actorMaxSkills aid body
targetTEnemy (_, TgtAndPath{tapTgt=TEnemy _}) = True
targetTEnemy (_, TgtAndPath{tapTgt=TPoint TEnemyPos{} lid _}) =
lid == arena
targetTEnemy ((_, b), TgtAndPath{tapTgt=TPoint TStash{} lid pos}) =
lid == arena && pos /= bpos b
-- stashes as crucial as enemies. except when guarding them
targetTEnemy _ = False
actorNoSupport ((aid, _), _) = do
threatDistL <- getsState $ meleeThreatDistList foeAssocs aid
condSupport2 <- condSupport friendAssocs 2 aid
let condThreat n = not $ null $ takeWhile ((<= n) . fst) threatDistL
-- If foes far, friends may still come, so we let him move.
-- The net effect is that lone heroes close to foes freeze
-- until support comes.
return $! condThreat 5 && not condSupport2
(oursRanged, oursNotRanged) = partition actorRanged oursNotHearing
(oursTEnemyAll, oursOther) = partition targetTEnemy oursNotRanged
notSwapReady ((_, b), TgtAndPath{tapTgt=TPoint TStash{} lid pos}) _ =
lid == arena && pos == bpos b
-- not ready to follow goal if already guarding the stash
notSwapReady abt@((_, b), _)
(ab2, Just t2@TgtAndPath{tapPath=
Just AndPath{pathList=q : _}}) =
let source = bpos b
tenemy = targetTEnemy abt
tenemy2 = targetTEnemy (ab2, t2)
-- Copied from 'displaceTowards':
in not (q == source -- friend wants to swap
|| tenemy && not tenemy2)
notSwapReady _ _ = True
-- These are not necessarily stuck (perhaps can go around),
-- but their current path is blocked by friends.
-- As soon as friends move, path is recalcuated and they may
-- become unstuck.
targetBlocked abt@((aid, _), TgtAndPath{tapPath}) = case tapPath of
Just AndPath{pathList= q : _} ->
any (\abt2@((aid2, body2), _) ->
aid2 /= aid -- in case pushed on goal
&& bpos body2 == q
&& notSwapReady abt abt2)
oursTgtRaw
_ -> False
(oursTEnemyBlocked, oursTEnemy) =
partition targetBlocked oursTEnemyAll
(oursNoSupportRaw, oursSupportRaw) <-
if length oursTEnemy <= 2
then return ([], oursTEnemy)
else partitionM actorNoSupport oursTEnemy
let (oursNoSupport, oursSupport) =
if length oursSupportRaw <= 1 -- make sure picks random enough
then ([], oursTEnemy)
else (oursNoSupportRaw, oursSupportRaw)
(oursBlocked, oursPos) =
partition targetBlocked $ oursRanged ++ oursOther
guarding ((_, b), Just TgtAndPath{tapTgt=TPoint TStash{} lid pos}) =
lid == arena && pos == bpos b
guarding _ = False
-- Don't try to include a stash guard in formation, even if attacking
-- or being attacked. Attackers would be targetted anyway.
oursNotSleepingNorGuarding = filter (not . guarding) oursTgtRaw
-- Lower overhead is better.
overheadOurs :: ((ActorId, Actor), TgtAndPath) -> Int
overheadOurs (_, TgtAndPath{tapPath=Nothing}) = 100
overheadOurs ((_, b), TgtAndPath{tapTgt=TPoint TStash{} lid pos})
| lid == arena && pos == bpos b = 200 -- guarding, poor choice
overheadOurs abt@( (aid, b)
, TgtAndPath{tapPath=Just AndPath{pathLen=d, ..}} ) =
-- Keep proper formation. Too dense and exploration takes
-- too long; too sparse and actors fight alone.
-- Note that right now, while we set targets separately for each
-- hero, perhaps on opposite borders of the map,
-- we can't help that sometimes heroes are separated.
let maxSpread = 3 + length oursNotSleepingNorGuarding
lDist p = [ chessDist (bpos b2) p
| ((aid2, b2), _) <- oursNotSleepingNorGuarding
, aid2 /= aid ]
pDist p = let ld = lDist p
in if null ld then 0 else minimum ld
aidDist = pDist (bpos b)
-- Negative, if the goal gets us closer to the party.
diffDist = pDist pathGoal - aidDist
-- If actor already at goal or equidistant, count it as closer.
sign = if diffDist <= 0 then -1 else 1
formationValue =
sign * (abs diffDist `max` maxSpread)
* (aidDist `max` maxSpread) ^ (2 :: Int)
targetsEnemy = targetTEnemy abt
fightValue = if targetsEnemy
then - fromEnum (bhp b `div` (10 * oneM))
else 0
isLit pos = Tile.isLit coTileSpeedup (lvl `at` pos)
-- solid tiles ignored, because not obvious if dark
-- after removed
actorMaxSk = actorMaxSkills EM.! aid
actorShines = Ability.getSk Ability.SkShine actorMaxSk > 0
stepsIntoLight =
not actorShines
&& not (isLit $ bpos b)
&& case pathList of
[] -> False
q : _ -> isLit q
-- shortest path is through light even though may
-- sidestep through dark in @chase@ or @flee@
in formationValue `div` 3
+ fightValue
+ (case d of
0 -> -400 -- do your thing ASAP and retarget
1 | not targetsEnemy -> -200
-- prevent others from trying to occupy the tile;
-- TStash that obscures a foe correctly handled here
_ -> if d < 8 then d `div` 4 else 2 + d `div` 10)
+ (if aid == oldAid then 0 else 10)
+ (if stepsIntoLight then 30 else 0)
-- Overheads above @maxBoundInt32@ are unlikely (and unsuppored in JS)
-- and also capping the value does not distort the choice too much.
positiveOverhead abt =
min maxBoundInt32 $ max 1 $ 200 - overheadOurs abt
candidates = [ oursAdjStash
, oursVulnerable
, oursSupport
, oursNoSupport
, oursPos
, oursFled -- if just fled, but not vulnerable,
-- keep him passive and safe, out of action
, oursMeleeingCanDisplace
-- prefer melee actors displacing than blocked
-- actors trying to walk around them
, oursTEnemyBlocked
-- prefer blocked actors trying to walk around
-- even if that causes overhead for the meleeing
, oursMeleeing
, oursHearing
, oursBlocked
]
case filter (not . null) candidates of
l : _ -> do
let freq = toFreq "candidates for AI leader"
$ map (positiveOverhead &&& id) l
((aid, b), _) <- rndToAction $ frequency freq
s <- getState
modifyClient $ updateLeader aid s
-- When you become a leader, stop following old leader, but follow
-- his target, if still valid, to avoid distraction.
when (gdoctrine fact `elem` [Ability.TFollow, Ability.TFollowNoItems]
&& not condInMelee) $
void $ refreshTarget foeAssocs friendAssocs (aid, b)
return aid
_ -> return oldAid
-- | Inspect the doctrines of the actor and set his target according to it.
setTargetFromDoctrines :: MonadClient m
=> [(ActorId, Actor)] -> [(ActorId, Actor)] -> ActorId
-> m ()
setTargetFromDoctrines foeAssocs friendAssocs oldAid = do
mleader <- getsClient sleader
let !_A = assert (mleader /= Just oldAid) ()
oldBody <- getsState $ getActorBody oldAid
moldTgt <- getsClient $ EM.lookup oldAid . stargetD
let side = bfid oldBody
arena = blid oldBody
fact <- getsState $ (EM.! side) . sfactionD
let explore = void $ refreshTarget foeAssocs friendAssocs (oldAid, oldBody)
setPath mtgt = case (mtgt, moldTgt) of
(Nothing, _) -> return False
( Just TgtAndPath{tapTgt=leaderTapTgt},
Just TgtAndPath{tapTgt=oldTapTgt,tapPath=Just oldTapPath} )
| leaderTapTgt == oldTapTgt -- targets agree
&& bpos oldBody == pathSource oldTapPath -> do -- nominal path
void $ refreshTarget foeAssocs friendAssocs (oldAid, oldBody)
return True -- already on target
(Just TgtAndPath{tapTgt=leaderTapTgt}, _) -> do
tap <- createPath oldAid leaderTapTgt
case tap of
TgtAndPath{tapPath=Nothing} -> return False
_ -> do
modifyClient $ \cli ->
cli {stargetD = EM.insert oldAid tap (stargetD cli)}
return True
follow = case mleader of
-- If no leader at all (forced @TFollow@ doctrine on an actor
-- from a leaderless faction), fall back to @TExplore@.
Nothing -> explore
_ | bwatch oldBody == WSleep ->
-- We could check skills, but it would be more complex.
explore
Just leader -> do
onLevel <- getsState $ memActor leader arena
condInMelee <- condInMeleeM arena
-- If leader not on this level or if we are meleeing,
-- and so following is not important, fall back to @TExplore@.
if not onLevel || condInMelee then explore
else do
-- Copy over the leader's target, if any, or follow his position.
mtgt <- getsClient $ EM.lookup leader . stargetD
tgtPathSet <- setPath mtgt
unless tgtPathSet $ do
let nonEnemyPath = Just TgtAndPath { tapTgt = TNonEnemy leader
, tapPath = Nothing }
nonEnemyPathSet <- setPath nonEnemyPath
unless nonEnemyPathSet
-- If no path even to the leader himself, explore.
explore
case gdoctrine fact of
Ability.TExplore -> explore
Ability.TFollow -> follow
Ability.TFollowNoItems -> follow
Ability.TMeleeAndRanged -> explore -- needs to find ranged targets
Ability.TMeleeAdjacent -> explore -- probably not needed, but may change
Ability.TBlock -> return () -- no point refreshing target
Ability.TRoam -> explore -- @TRoam@ is checked again inside @explore@
Ability.TPatrol -> explore -- WIP
|