{-# LANGUAGE LambdaCase #-}
module Board where
import Control.Monad (foldM, guard, when)
import Control.Monad.Random (Rand, StdGen)
import Control.Monad.Writer (Writer, WriterT, lift, mapWriterT,
runWriter, tell)
import Data.Bifunctor (second)
import Data.Function (on)
import Data.Functor (($>))
import Data.Functor.Identity (runIdentity)
import Data.Maybe (fromMaybe, mapMaybe)
import Data.Monoid (Any (..), Sum (..))
import Safe (atMay, headMay, minimumMay)
import qualified Data.HashSet as HS
import qualified Data.Map.Strict as M
import qualified Data.Set as S
import qualified BoardConf as BC
import qualified Pos as P
import qualified Power as Pow
import qualified RollFrom as RF
import AStar
import Creature
import Exit
import Group
import Item
import Rand
import Wall
w,h :: Int
w = 5
h = 5
inBounds :: P.Pos -> Bool
inBounds (P.Pos x y) = 0 <= x && x < w && 0 <= y && y < h
inBoundsW :: P.WPos -> Bool
inBoundsW = all inBounds . P.adjPoss
poss :: S.Set P.Pos
poss = S.fromList [ P.Pos x y | x <- [0..w-1], y <- [0..h-1] ]
wPoss :: S.Set P.WPos
wPoss = S.fromList [ wp | p <- S.toList poss, up <- [True,False], let wp = P.WPos p up, all inBounds $ P.adjPoss wp ]
boundaryWPoss :: S.Set P.WPos
boundaryWPoss = S.fromList $
[ P.WPos (P.Pos x y) True | x <- [0..w-1], y <- [-1,h-1] ]
<> [ P.WPos (P.Pos x y) False | x <- [-1,w-1], y <- [0..h-1] ]
isBoundaryWPos :: P.WPos -> Bool
isBoundaryWPos (P.WPos (P.Pos _ y) True) = y `elem` [-1,h-1]
isBoundaryWPos (P.WPos (P.Pos x _) False) = x `elem` [-1,w-1]
treasuresPerBoard :: Int
treasuresPerBoard = 5
-- XXX If all active, status line is 58 long -- watch out with renaming!
data Status = Dazzled | Smoke | Haste | Ghost | Foresight deriving (Eq, Ord, Show)
type Tagged = S.Set P.WPos
data Board = Board
{ visible :: S.Set P.Pos
, unrevealed :: S.Set P.Pos
, creatures :: M.Map P.Pos Creature
, items :: M.Map P.Pos Item
, walls :: M.Map P.WPos Wall
, exits :: M.Map P.WPos Exit
, powers :: M.Map P.Pos Pow.Power
, treasures :: Int
, timer :: Int
, possItems :: [Item]
, safe :: Bool
, statuses :: M.Map Status Int
, expected :: M.Map P.Pos (Maybe Creature)
, tagged :: Tagged
, diffs :: BC.BoardConfDiffs
, conf :: BC.BoardConf
}
new :: BC.BoardConf -> Board
new = Board S.empty poss M.empty M.empty M.empty initBoundary M.empty treasuresPerBoard 0 [] True M.empty M.empty S.empty M.empty where
initBoundary = M.fromSet (const UnseenBoundary) boundaryWPoss
empty :: Board
empty = new BC.emptyBoardConf
modVisible :: (S.Set P.Pos -> S.Set P.Pos) -> Board -> Board
modVisible f bd = bd { visible = f $ visible bd }
setVisible :: S.Set P.Pos -> Board -> Board
setVisible = modVisible . const
modUnrevealed :: (S.Set P.Pos -> S.Set P.Pos) -> Board -> Board
modUnrevealed f bd = bd { unrevealed = f $ unrevealed bd }
setUnrevealed :: S.Set P.Pos -> Board -> Board
setUnrevealed = modUnrevealed . const
modCreatures :: (M.Map P.Pos Creature -> M.Map P.Pos Creature) -> Board -> Board
modCreatures f bd = bd { creatures = f $ creatures bd }
modItems :: (M.Map P.Pos Item -> M.Map P.Pos Item) -> Board -> Board
modItems f bd = bd { items = f $ items bd }
modWalls :: (M.Map P.WPos Wall -> M.Map P.WPos Wall) -> Board -> Board
modWalls f bd = bd { walls = f $ walls bd }
modExits :: (M.Map P.WPos Exit -> M.Map P.WPos Exit) -> Board -> Board
modExits f bd = bd { exits = f $ exits bd }
modPowers :: (M.Map P.Pos Pow.Power -> M.Map P.Pos Pow.Power) -> Board -> Board
modPowers f bd = bd { powers = f $ powers bd }
modTreasures, modTimer :: (Int -> Int) -> Board -> Board
modTreasures f bd = bd { treasures = f $ treasures bd }
modTimer f bd = bd { timer = f $ timer bd }
setTimer :: Int -> Board -> Board
setTimer = modTimer . const
setSafe :: Bool -> Board -> Board
setSafe s bd = bd { safe = s }
modStatuses :: (M.Map Status Int -> M.Map Status Int) -> Board -> Board
modStatuses f bd = bd { statuses = f $ statuses bd }
modExpected :: (M.Map P.Pos (Maybe Creature) -> M.Map P.Pos (Maybe Creature)) -> Board -> Board
modExpected f bd = bd { expected = f $ expected bd }
setExpected :: M.Map P.Pos (Maybe Creature) -> Board -> Board
setExpected = modExpected . const
modTagged :: (Tagged -> Tagged) -> Board -> Board
modTagged f bd = bd { tagged = f $ tagged bd }
modConf :: (BC.BoardConf -> BC.BoardConf) -> Board -> Board
modConf f bd = bd { conf = f $ conf bd }
wPosVisible :: S.Set P.Pos -> P.WPos -> Bool
wPosVisible vis wp = any (`S.member` vis) $ P.adjPoss wp
visibleWPoss, invisibleWPoss :: Tagged -> S.Set P.Pos -> S.Set P.WPos
visibleWPoss tags vis = tags `S.union` S.filter (wPosVisible vis) wPoss
invisibleWPoss tags vis = S.filter (not . wPosVisible vis) wPoss S.\\ tags
invisible :: Board -> S.Set P.Pos
invisible bd = poss S.\\ visible bd
onSpawnTreasure :: Board -> Board
onSpawnTreasure bd = (if treasures bd > 1 then setUnrevealed poss else id)
$ modTreasures (+ (-1)) bd
setPlayerDead :: Board -> Board
setPlayerDead = modCreatures . M.map $ \case
Player -> DeadPlayer
c -> c
treasureAt :: Board -> P.Pos -> Item
treasureAt bd (P.Pos x y)
| treasures bd == 1 = Gem
| 2*x > w = treasureAt bd (P.Pos (w-1-x) y)
| 2*y > h = treasureAt bd (P.Pos x (h-1-y))
treasureAt _ (P.Pos 2 2) = Junk
treasureAt _ (P.Pos 1 1) = Potion
treasureAt _ (P.Pos 0 2) = MiniPotion
treasureAt bd (P.Pos 1 0) | Just i <- headMay $ possItems bd = i
treasureAt bd (P.Pos 0 y) | Just i <- possItems bd `atMay` (y+1) = i
treasureAt bd (P.Pos 2 0) = treasureAt bd $ P.Pos 0 2
treasureAt _ _ = ScoreTreasure
setFov :: Bool -> Board -> Rand StdGen Board
setFov hasKey bd0 = setSafe False <$> iter bd0 where
iter bd =
let oldVis = visible bd
newVis = fov bd
oldWVis = visibleWPoss (tagged bd) oldVis
newWVis = visibleWPoss (tagged bd) newVis
in if newVis == oldVis
then seeBoundary hasKey $ destroyInvisStuffs bd
else do
bd' <- destroyInvisStuffs . updateVis . destroyInvisWalls . updateVis <$>
createAtW (newWVis S.\\ oldWVis) bd
newVis' <- shuffle . S.toList $ visible bd' S.\\ oldVis
let newInvis' = S.toList $ oldVis S.\\ visible bd'
bd'' <- revealVis <$> (expectAtMaybe newInvis' =<< createAt newVis' bd')
-- created orbs may lead to new vision, so need to iterate
iter bd''
destroyInvisWalls, destroyInvisStuffs :: Board -> Board
destroyInvisStuffs bd = modCreatures delPInvis . modItems delPInvis $ bd where
delPInvis :: M.Map P.Pos a -> M.Map P.Pos a
delPInvis = (M.\\ M.fromSet (const ()) (invisible bd))
destroyInvisWalls bd = modWalls delWPInvis bd where
delWPInvis :: M.Map P.WPos a -> M.Map P.WPos a
delWPInvis = (M.\\ M.fromSet (const ()) (invisibleWPoss (tagged bd) (visible bd)))
expectAtMaybe
| expectant bd0 = expectAt
| otherwise = const pure
createAt :: [P.Pos] -> Board -> Rand StdGen Board
createAt = flip $ foldM createP
createAtW :: S.Set P.WPos -> Board -> Rand StdGen Board
createAtW = flip $ foldM createW
bc = conf bd0
createP :: Board -> P.Pos -> Rand StdGen Board
createW :: Board -> P.WPos -> Rand StdGen Board
createP bd p = modExpected (M.delete p) <$> createP' (modUnrevealed (S.delete p) bd) p
createP' bd p
| S.null (unrevealed bd) && treasures bd > 0 =
pure . onSpawnTreasure . modItems (M.insert p $ treasureAt bd p) $ bd
| safe bd = pure bd
| p `M.member` creatures bd = pure bd
| expectant bd = pure $ case expected bd M.!? p of
Just (Just c) -> modCreatures (M.insert p c) bd
_ -> bd
| otherwise =
maybe bd (($ bd) . modCreatures . M.insert p) <$> RF.roll (BC.creatureRoll bc)
createW bd wp =
maybe bd (($ bd) . modWalls . M.insert wp) <$> RF.roll (BC.wallRoll bc)
revealVis bd
-- | Needed when all poss revealed on spawning treasure
| S.null (unrevealed bd) && treasures bd > 0 = setUnrevealed (poss `S.difference` visible bd) bd
-- | Needed only when we created a treasure
| otherwise = modUnrevealed (`S.difference` visible bd) bd
seeBoundary :: Bool -> Board -> Rand StdGen Board
seeBoundary hasKey bd = do
v <- newVisBdd
pure $ modExits (flip (foldr set) v) bd
where
newVisBdd = shuffle . S.toList . S.filter (wPosVisible $ visible bd) .
S.filter ((== Just UnseenBoundary) . (exits bd M.!?)) $ boundaryWPoss
set :: P.WPos -> M.Map P.WPos Exit -> M.Map P.WPos Exit
set wp exs = M.insert wp ex exs where
ex | S.null . S.delete wp $ S.filter (sameWall wp) unseen =
case S.toList $ S.filter (not . sameWall wp) unseen of
[] -> Exit
wp':_ | hasKey && all (sameWall wp') (S.delete wp unseen) -> KeyExit
_ -> SeenBoundary
| otherwise = SeenBoundary
unseen = S.filter ((== Just UnseenBoundary) . (exs M.!?)) boundaryWPoss
sameWall = (==) `on` P.exitDir
fov, playerFov :: Board -> S.Set P.Pos
fov bd = addOrbFovs plFov plFov where
plFov = fovs $ playerPoss bd <> cameraPoss bd
fovs = S.unions . map (fovAt bd)
addOrbFovs :: S.Set P.Pos -> S.Set P.Pos -> S.Set P.Pos
addOrbFovs v news
| S.null news = v
| otherwise = let
orbPoss = [ p | (p, i) <- M.assocs $ M.restrictKeys (items bd) news, isOrb i ]
orbFovs = fovs orbPoss
isOrb (ItemInvItem Orb) = True
isOrb (RollingOrb _ _) = True
isOrb _ = False
in addOrbFovs (v `S.union` orbFovs) (orbFovs S.\\ v)
playerFov bd = S.unions . map (fovAt bd) $ playerPoss bd
playerPoss :: Board -> [ P.Pos ]
playerPoss bd = [ p | (p,Player) <- M.assocs $ creatures bd ]
cameraPoss :: Board -> [ P.Pos ]
cameraPoss bd = [ p | (p, ItemInvItem (Camera _)) <- M.assocs $ items bd ]
fovAt :: Board -> P.Pos -> S.Set P.Pos
fovAt bd base = S.filter inRange $ poss S.\\ blocked where
blocked = S.unions [ wallCone (short wl) base wp
| (wp,wl) <- M.assocs $ walls bd, wl `notElem` [Window, BrokenWindow]
] where
short Pillar = True
short (UmbrellaWall _) = True
short _ = False
inRange
| Just r <- sightRadius = (<= r*r) . ((smokeFac*smokeFac)*) . P.distSquared base
| otherwise = const True
where
sightRadius = minimumMay $ mapMaybe rad (M.assocs (statuses bd))
rad (Dazzled,_) = Just 0
rad (Smoke,n) = Just $ max 0 (maxSmoke - n) + smokeFac
rad _ = Nothing
wallCone :: Bool -> P.Pos -> P.WPos -> S.Set P.Pos
wallCone short base wp = (poss `S.intersection`) $ (base +^) `S.map` wallCone' (neg base +^ wp) where
wallCone' (P.WPos p False) = flipPos `S.map` wallCone' (P.WPos (flipPos p) True)
wallCone' (P.WPos (P.Pos x y) True)
| x < 0 = (\(P.Pos x' y') -> P.Pos (-x') y') `S.map` wallCone' (P.WPos (P.Pos (-x) y) True)
| y < 0 = (\(P.Pos x' y') -> P.Pos x' (-y')) `S.map` wallCone' (P.WPos (P.Pos x (-y-1)) True)
| otherwise =
S.fromList [ P.Pos x' y'
| x' <- [-w..w], y' <- [y+1..h]
, ineq x' y'
] where
-- (y+1/2)/(x-w/2) <= y'/x' <= (y+1/2)/(x+w/2)
ineq x' y' | short = abs (x'*(8*y+4) - 8*y'*x) <= 3*y' -- w = 3/4
| otherwise = abs (x'*(2*y+1) - 2*y'*x) <= y' -- w = 1
flipPos (P.Pos x y) = P.Pos y x
smokeFac, maxSmoke :: Int
smokeFac = 4
maxSmoke = 20
updateVis :: Board -> Board
updateVis bd = setVisible (fov bd) bd
data Move = Move {mvCreature :: Creature, mvFrom :: P.Pos, mvDir :: P.Dir}
data Alert
= AlertMoveCreature Creature P.Pos P.Dir
| AlertMoveItem Item P.Pos P.Dir
| AlertUseItem Item P.Pos P.Dir
| AlertHighlight (S.Set P.Pos) (S.Set P.WPos)
data Transition = Transition { transBase :: Board, transAlerts :: [Alert] }
data MoveResults = MoveResults
{ damage :: Sum Int
, exitings :: [P.WPos]
, alerts :: [Alert]
, someAction :: Any
}
instance Semigroup MoveResults where
MoveResults a b c d <> MoveResults a' b' c' d' = MoveResults (a<>a') (b<>b') (c<>c') (d<>d')
instance Monoid MoveResults where
mempty = MoveResults mempty mempty mempty mempty
action :: MoveResults
action = mempty { someAction = Any True }
movePlayers :: P.Dir -> Board -> Writer MoveResults Board
movePlayers dir bd = foldM (flip $ tryMoveCreature dir) bd (playerPoss bd)
throughWall :: Board -> Creature -> Maybe Wall -> Bool
throughWall _ _ Nothing = True
throughWall _ GhostMonster _ = True
throughWall bd Player _ | ghostly bd = True
throughWall _ Player (Just Hedge) = True
throughWall _ (InflatedBalloon _) (Just _) = False
throughWall _ _ (Just BrokenWindow) = True
throughWall _ _ _ = False
creatureCanMove :: Board -> Creature -> P.Pos -> P.Dir -> Bool
creatureCanMove bd c p dir =
let p' = P.dirPos dir +^ p
wp = P.wposInDir p dir
in inBounds p' && (c == Player || M.notMember p' (creatures bd)) && throughWall bd c (walls bd M.!? wp)
creatureCanTryMove :: Board -> Creature -> P.Pos -> P.Dir -> Bool
creatureCanTryMove bd c p dir =
let p' = P.dirPos dir +^ p
wp = P.wposInDir p dir
in and
[ inBounds p'
, creatures bd M.!? p' `elem` [Nothing, Just Player]
, throughWall bd c $ walls bd M.!? wp
, c /= SmartMonster || p' `S.member` playerFov bd
]
exitInDir :: Board -> P.Pos -> P.Dir -> Bool
exitInDir bd p d = exits bd M.!? P.wposInDir p d `elem` [Just Exit, Just KeyExit]
enterAt :: P.WPos -> Board -> Board
enterAt wp@(P.WPos (P.Pos x y) _) = modVisible (S.insert p') . modCreatures (M.insert p' Player) . modExits (M.insert wp Entrance) where
p' = P.Pos (max 0 x) (max 0 y)
oppositeWPos :: P.WPos -> P.WPos
oppositeWPos (P.WPos (P.Pos x y) True) = P.WPos (P.Pos x (h-2-y)) True
oppositeWPos (P.WPos (P.Pos x y) False) = P.WPos (P.Pos (w-2-x) y) False
tryMoveCreature :: P.Dir -> P.Pos -> Board -> Writer MoveResults Board
tryMoveCreature dir p bd
| Just Player <- creatures bd M.!? p
, not $ ghostly bd
, let wp = P.wposInDir p dir
, Just wl <- walls bd M.!? wp
, Just cost <- wallDestructionCost wl
= do
tell $ action { damage = Sum cost }
pure . modTagged (S.delete wp) . modWalls (M.update damageWall wp) $ bd
| Just c <- creatures bd M.!? p, creatureCanMove bd c p dir = do
let p' = P.dirPos dir +^ p
bd' <- case creatures bd M.!? p' of
Just (InflatedBalloon _) | c == Player ->
tryMoveCreature dir p' bd
_ -> pure bd
let creatureDestroyed = maybe False isMonster $ creatures bd' M.!? p'
throughPainful = c == Player && not (ghostly bd) &&
(walls bd M.!? P.wposInDir p dir) `elem` [Just Hedge, Just BrokenWindow]
dmg = sum $ [2 | creatureDestroyed] <> [1 | throughPainful]
modMove = modCreatures $ M.insert p' c . M.delete p
modBalloon
| Just (InflatedBalloon charges) <- creatures bd' M.!? p'
, charges > 0
, p' `M.notMember` items bd'
= modItems . M.insert p' . ItemInvItem $ Balloon charges
| otherwise = id
tell $ action { damage = Sum dmg, alerts = move c }
pure . modBalloon . modMove $ bd'
| Just Player <- creatures bd M.!? p, exitInDir bd p dir = do
tell $ action { exitings = [P.wposInDir p dir], alerts = move Player }
pure $ modCreatures (M.delete p) bd
| otherwise = pure bd
where
move c = [AlertMoveCreature c p dir]
damageWall = \case
ThickHedge -> Just Hedge
Window -> Just BrokenWindow
_ -> Nothing
collectItems :: Board -> Writer [Item] Board
collectItems bd
| p:_ <- playerPoss bd
= collectItemsAt p bd
| otherwise = pure bd
collectItemsAt :: P.Pos -> Board -> Writer [Item] Board
collectItemsAt p bd
| Just item <- items bd M.!? p = let
takeUmbrella
| UmbrellaHandle d <- item =
let wp' = P.wposInDir p d
in modTagged (S.delete wp') . modWalls (M.delete wp')
| otherwise = id
getItem
| UmbrellaHandle d <- item = P.wposInDir p d `M.member` walls bd
| otherwise = True
in do
when getItem $ tell [item]
pure . modItems (M.delete p) . takeUmbrella $ bd
| otherwise = pure bd
grabItem :: P.Dir -> Board -> Writer [Item] Board
grabItem d bd
| p:_ <- playerPoss bd
= collectItemsAt (p +^ P.dirPos d) bd
| otherwise = pure bd
findPathDir, chaseDir :: Creature -> Board -> P.Pos -> P.Pos -> Rand StdGen (Maybe P.Dir)
findPathDir c bd from goal = do
let firsts = headMay `mapMaybe` aStarAll graph (P.sqDist goal) (== goal) from
dirs = (P.posDir . (+^ neg from)) `mapMaybe` firsts
randElem dirs
where
graph :: P.Pos -> HS.HashSet P.Pos
graph p = HS.fromList [ p +^ P.dirPos d | d <- P.dirs, creatureCanTryMove bd c p d ]
chaseDir c bd from goal =
let diff = from +^ neg goal
ok = creatureCanTryMove bd c from
dirs = filter ok <$> P.dirsTowardsZero diff
in headMay . concat <$> mapM shuffle dirs
npcs :: Board -> M.Map P.Pos Creature
npcs = M.filter (/= Player) . creatures
incStatus :: Status -> Int -> Board -> Board
incStatus st n = modStatuses $ M.alter f st where
f (Just n') = Just $ n + n'
f _ = Just n
hasted,isHasteRound,ghostly,expectant :: Board -> Bool
hasted = M.member Haste . statuses
ghostly = M.member Ghost . statuses
expectant = M.member Foresight . statuses
isHasteRound bd
| Just n <- statuses bd M.!? Haste = n `mod` 2 == 1
| otherwise = False
beginExpect :: Board -> Rand StdGen Board
beginExpect bd
| expectant bd = pure bd
| otherwise = (expectAt . S.toList $ invisible bd) bd
expectAt :: [P.Pos] -> Board -> Rand StdGen Board
expectAt ps bd0 = foldM expectP bd0 ps
where
expectP bd p = ($ bd) . modExpected . M.insert p <$> RF.roll (BC.creatureRoll $ conf bd0)
npcsAct :: Board -> WriterT [Alert] (Rand StdGen) Board
npcsAct bd0
| isHasteRound bd0 = pure bd0
| otherwise = do
actors <- lift . shuffle . M.toList $ npcs bd0
foldM npcAct bd0 actors
where
npcAct :: Board -> (P.Pos, Creature) -> WriterT [Alert] (Rand StdGen) Board
npcAct bd (p,c)
| ppos:_ <- playerPoss bd =
(forceRand =<< lift (pathAlg c c bd p ppos)) >>= \case
Just dir ->
mapWriterT (pure . runIdentity . (second alerts <$>)) $ tryMoveCreature dir p bd
_ -> pure bd
| otherwise = pure bd
where
-- Force non-calm monsters to move randomly when next to player
forceRand :: Maybe P.Dir -> WriterT [Alert] (Rand StdGen) (Maybe P.Dir)
forceRand md | c `elem` [ CalmMonster, GhostMonster ] = pure md
forceRand (Just dir) | not (creatureCanMove bd c p dir) = randAvailableDir
forceRand Nothing | c == SmartMonster = randAvailableDir
forceRand md = pure md
randAvailableDir = lift . randElem $
filter (\d -> creatureCanMove bd c p d && creatureCanTryMove bd c p d) P.dirs
pathAlg BasicMonster = chaseDir
pathAlg CalmMonster = chaseDir
pathAlg GhostMonster = chaseDir
pathAlg SmartMonster = findPathDir
pathAlg ChaseMonster = findPathDir
pathAlg _ = \_ _ _ _ -> pure Nothing
tryUseInvItem :: InvItem -> P.Dir -> Board -> WriterT [Alert] Maybe Board
tryUseInvItem e d bd
| p:_ <- playerPoss bd = do
tell [AlertUseItem (ItemInvItem e) p d]
let wp = P.wposInDir p d
lift . guard $ canUseOnWall e || (walls bd M.!? wp) `elem` [Nothing, Just BrokenWindow]
let p' = p +^ P.dirPos d
lift . guard $ inBounds p'
case e of
Cloak -> pure $ modWalls (M.insert wp $ CloakWall d 1) bd
Umbrella -> do
guard . M.notMember p' $ items bd
let wp' = P.wposInDir p' d
guard $ inBoundsW wp'
guard $ (walls bd M.!? wp') `elem` [Nothing, Just BrokenWindow]
let addUmbrellaWall = modWalls (M.insert wp' $ UmbrellaWall d)
if p' `M.member` creatures bd
then do
let (bd', mr) = runWriter $ tryMoveCreature d p' bd
guard . getAny $ someAction mr
pure $ addUmbrellaWall bd'
else pure . modItems (M.insert p' $ UmbrellaHandle d) . addUmbrellaWall $ bd
Balloon charges -> let inflate = modCreatures (M.insert p' (InflatedBalloon $ charges - 1))
in if p' `M.member` creatures bd
then do
let (bd', mr) = runWriter $ tryMoveCreature d p' bd
guard . getAny $ someAction mr
pure $ inflate bd'
else pure $ inflate bd
Orb -> ($ bd) <$> dropItem p' (RollingOrb d True)
Flash ->
let flashPoss = wallCone True p wp
flashWPoss = wp `S.insert` invisibleWPoss S.empty (poss S.\\ flashPoss)
delPoss = flip (foldr M.delete) flashPoss
delWPoss = flip (foldr M.delete) flashWPoss
delWPossS = flip (foldr S.delete) flashWPoss
reveal, destroyTreasure :: Board -> Board
reveal = destroyTreasure
. modUnrevealed (`S.difference` flashPoss)
. modVisible (`S.union` flashPoss)
-- | Pretend we created and immediately destroyed a treasure
destroyTreasure bd' | null (unrevealed bd') = onSpawnTreasure bd'
| otherwise = bd'
in tell [AlertHighlight flashPoss flashWPoss] $>
(reveal . modItems delPoss . modCreatures delPoss . modTagged delWPossS $ modWalls delWPoss bd)
Tent -> do
guard . M.notMember p' $ creatures bd
pure .
modWalls (flip (foldr (M.alter (Just . fromMaybe TentWall))) (filter inBoundsW $ P.wposInDir p' <$> P.dirs))
. modCreatures (M.delete p . M.insert p' Player) $ bd
Spraypaint _ -> do
guard $ wp `M.member` walls bd
pure $ modTagged (S.insert wp) bd
_ -> ($ bd) <$> dropItem p' (ItemInvItem e)
| otherwise = lift Nothing
where
dropItem p i = do
guard . M.notMember p $ items bd
guard . M.notMember p $ creatures bd
pure $ modItems (M.insert p i)
canUseOnWall Flash = True
canUseOnWall (Spraypaint _) = True
canUseOnWall _ = False
doPhysics :: Board -> Writer [Alert] Board
doPhysics bd0
| isHasteRound bd0
= pure $ modStatuses (decayStatuses True) bd0
| otherwise
= modStatuses (decayStatuses False)
. modItems decayCameras
. decayCloaks
. tickTimer
<$> rollOrbs bd0
where
decayCloaks bd = foldr decayCloakWall bd . M.assocs . M.filter isCloakWall $ walls bd
isCloakWall (CloakWall _ _) = True
isCloakWall _ = False
decayCloakWall :: (P.WPos, Wall) -> Board -> Board
decayCloakWall (wp, CloakWall d n)
| n > 0 = modWalls . M.insert wp $ CloakWall d (n-1)
| otherwise = modTagged (S.delete wp)
. modWalls (M.delete wp)
. modItems (M.insert (P.posInDir wp d) $ ItemInvItem Cloak)
decayCloakWall _ = id
decayCameras = M.mapMaybe $ \case
ItemInvItem (Camera n) | n > 0 -> Just . ItemInvItem . Camera $ n-1
| otherwise -> Nothing
i -> Just i
decayStatuses hasteOnly = M.filter (>0) . M.mapWithKey decay where
decay Haste n = n-1
decay _ n | hasteOnly = n
decay _ n = n-1
rollOrbs bd = foldM rollOrb bd (M.assocs $ items bd) where
rollOrb :: Board -> (P.Pos, Item) -> Writer [Alert] Board
rollOrb bd' (p, RollingOrb dir True)
= pure $ modItems (M.insert p (RollingOrb dir False)) bd'
rollOrb bd' (p, orb@(RollingOrb dir False))
| let p' = p +^ P.dirPos dir
, inBounds p'
, p' `M.notMember` items bd'
, P.wposInDir p dir `M.notMember` walls bd'
= tell [AlertMoveItem orb p dir] $> modItems (M.delete p . M.insert p' orb) bd'
| otherwise
= pure $ modItems (M.insert p (ItemInvItem Orb)) bd'
rollOrb bd' _ = pure bd'
tickTimer = rollOver . modTimer (+1) where
rollOver bd | timer bd >= turnsPerSide
= modConf (BC.modRolls decSides decSides) . setTimer 0 $ bd
rollOver bd = bd
decSides = RF.modSides (+ (-1))
turnsPerSide = 8
addPower :: Bool -> Board -> Board
addPower overUsable bd
| p:_ <- playerPoss bd = modPowers (M.alter (add p) p) bd
| otherwise = bd
where
add p Nothing = Just . Pow.upgrade $ Pow.new (typeAt p) overUsable
add _ (Just pow) = Just $ Pow.upgrade pow
typeAt (P.Pos x y)
| 2*x > w = typeAt (P.Pos (w-1-x) y)
| 2*y > h = typeAt (P.Pos x (h-1-y))
typeAt (P.Pos 0 0) = Pow.Smoke
typeAt (P.Pos 0 1) = Pow.Dazzle
typeAt (P.Pos 0 2) = Pow.Teleport
typeAt (P.Pos 1 0) = Pow.Ghost
typeAt (P.Pos 1 1) = Pow.Heal
typeAt (P.Pos 1 2) = Pow.Haste
typeAt (P.Pos 2 0) = Pow.Teleport
typeAt (P.Pos 2 1) = Pow.Foresight
typeAt (P.Pos 2 2) = Pow.Undo
typeAt _ = Pow.Heal -- impossible