module StrategyState where import qualified Data.List as L import qualified Data.IntMap as IM import Data.Maybe import Control.Monad import Control.Arrow import Loc import Dir import Level import Actor import ActorState import Content.ActorKind import Frequency import Perception import Strategy import State import Action import Actions import ItemAction import Content.ItemKind import Item import qualified Effect import qualified Tile import qualified Kind {- Monster movement ---------------- Not all monsters use the same algorithm to find the hero. Some implemented and unimplemented methods are listed below: * Random The simplest way to have a monster move is at random. * Sight If a monster can see the hero (as an approximation, we assume it is the case when the hero can see the monster), the monster should move toward the hero. * Smell The hero leaves a trail when moving toward the dungeon. For a certain timespan (100--200 moves), it is possible for certain monsters to detect that a hero has been at a certain field. Once a monster is following a trail, it should move to the neighboring field where the hero has most recently visited. * Noise The hero makes noise. If the distance between the hero and the monster is small enough, the monster can hear the hero and moves into the approximate direction of the hero. -} strategy :: ActorId -> State -> Perceptions -> Strategy (Action ()) strategy actor oldState@State{splayer = pl, stime = time} per = strat where lvl@Level{lsmell = nsmap, lxsize} = slevel oldState Actor { bkind = ak, bloc = me, bdir = ad, btarget = tgt, bitems = items } = getActor actor oldState mk = Kind.getKind ak delState = deleteActor actor oldState enemyVisible a l = -- We assume monster sight is infravision, so light has no significance. asight mk && actorReachesActor a actor l me per Nothing || -- Any enemy is visible if adjacent (e. g., a monster player). memActor a delState && adjacent lxsize me l -- If no heroes on the level, monsters go at each other. TODO: let them -- earn XP by killing each other to make this dangerous to the player. hs = L.map (AHero *** bloc) $ IM.assocs $ lheroes $ slevel delState ms = L.map (AMonster *** bloc) $ IM.assocs $ lmonsters $ slevel delState -- Below, "foe" is the hero (or a monster, or loc) chased by the actor. (newTgt, floc) = case tgt of TEnemy a ll | focusedMonster -> if memActor a delState then let l = bloc $ getActor a delState in if enemyVisible a l then (TEnemy a l, Just l) else if isJust (snd closest) || me == ll then closest -- prefer visible foes else (tgt, Just ll) -- last known location of enemy else closest -- enemy not on the level, temporarily chase others TLoc loc -> if me == loc then closest else (tgt, Just loc) -- ignore everything and go to loc _ -> closest closest = let hsAndTraitor = if isAMonster pl then (pl, bloc $ getPlayerBody delState) : hs else hs foes = if L.null hsAndTraitor then ms else hsAndTraitor -- We assume monster sight is infravision, so light has no effect. foeVisible = L.filter (uncurry enemyVisible) foes foeDist = L.map (\ (a, l) -> (distance lxsize me l, l, a)) foeVisible in case foeDist of [] -> (TCursor, Nothing) _ -> let (_, l, a) = L.minimum foeDist in (TEnemy a l, Just l) onlyFoe = onlyMoves (maybe (const False) (==) floc) me towardsFoe = case floc of Nothing -> const mzero Just loc -> let foeDir = towards lxsize me loc in only (\ x -> dirDistSq lxsize foeDir x <= 1) lootHere x = not $ L.null $ lvl `iat` x onlyLoot = onlyMoves lootHere me exitHere x = let t = lvl `at` x in Tile.isExit t onlyExit = onlyMoves exitHere me onlyKeepsDir k = only (\ x -> maybe True (\ d -> dirDistSq lxsize d x <= k) ad) onlyKeepsDir_9 = only (\ x -> maybe True (\ d -> neg x /= d) ad) onlyNoMs = onlyMoves (unoccupied (levelMonsterList delState)) me -- 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. openPower = Tile.SecretStrength $ case strongestItem items "ring" of Just i -> aiq mk + jpower i Nothing -> aiq mk openableHere = openable lvl openPower onlyOpenable = onlyMoves openableHere me accessibleHere = accessible lvl me onlySensible = onlyMoves (\ l -> accessibleHere l || openableHere l) me focusedMonster = aiq mk > 10 smells = L.map fst $ L.sortBy (\ (_, s1) (_, s2) -> compare s2 s1) $ L.filter (\ (_, s) -> s > 0) $ L.map (\ x -> let sm = smelltime $ IM.findWithDefault (SmellTime 0) (me `shift` x) nsmap in (x, (sm - time) `max` 0)) (moves lxsize) fromDir allowAttacks d = dirToAction actor newTgt allowAttacks `liftM` d strat = fromDir True (onlyFoe moveFreely) .| isJust floc .=> liftFrequency (msum freqs) .| lootHere me .=> actionPickup .| fromDir True moveAround actionPickup = return $ actorPickupItem actor tis = lvl `iat` me freqs = [applyFreq items 1, applyFreq tis 2, throwFreq items 2, throwFreq tis 5, towardsFreq] applyFreq is multi = Frequency [ (benefit * multi, actionApply (iname ik) i) | i <- is, let ik = Kind.getKind (jkind i), let benefit = (1 + jpower i) * Effect.effectToBenefit (ieffect ik), benefit > 0, asight mk || iname ik /= "scroll"] actionApply groupName = applyGroupItem actor (applyToVerb groupName) throwFreq is multi = if not $ asight mk then mzero else Frequency [ (benefit * multi, actionThrow (iname ik) i) | i <- is, let ik = Kind.getKind (jkind i), let benefit = - (1 + jpower i) * Effect.effectToBenefit (ieffect ik), benefit > 0, -- Wasting swords would be too cruel to the player. iname ik /= "sword"] actionThrow groupName = zapGroupItem actor (fromJust floc) (zapToVerb groupName) towardsFreq = let freqs2 = runStrategy $ fromDir False moveTowards in if asight mk && not (L.null freqs2) then scale 30 $ head freqs2 else mzero moveTowards = onlySensible $ onlyNoMs (towardsFoe moveFreely) moveAround = onlySensible $ (if asight mk then onlyNoMs else id) $ asmell mk .=> L.foldr ((.|) . return) reject smells .| onlyOpenable moveFreely .| moveFreely moveFreely = onlyLoot moveRandomly .| onlyExit (onlyKeepsDir 2 moveRandomly) .| aiq mk > 15 .=> onlyKeepsDir 0 moveRandomly .| aiq mk > 10 .=> onlyKeepsDir 1 moveRandomly .| aiq mk > 5 .=> onlyKeepsDir 2 moveRandomly .| onlyKeepsDir_9 moveRandomly .| moveRandomly onlyMoves :: (Loc -> Bool) -> Loc -> Strategy Dir -> Strategy Dir onlyMoves p l = only (\ x -> p (l `shift` x)) moveRandomly :: Strategy Dir moveRandomly = liftFrequency $ uniform (moves lxsize) dirToAction :: ActorId -> Target -> Bool -> Dir -> Action () dirToAction actor tgt allowAttacks dir = do -- set new direction updateAnyActor actor $ \ m -> m { bdir = Just dir, btarget = tgt } -- perform action tryWith (advanceTime actor) $ -- if the following action aborts, we just advance the time and continue -- TODO: ensure time is taken for other aborted actions in this file moveOrAttack allowAttacks True actor dir wait :: ActorId -> Strategy (Action ()) wait actor = return $ advanceTime actor