scroll-1.20151219: Player/Move.hs
{-# LANGUAGE FlexibleContexts #-}
module Player.Move where
import Control.Monad.State.Strict
import Data.Char
import Data.Maybe
import Control.Applicative
import Prelude
import Types
import World
import Level.Border
import Player
import Status
import Player.Consume
import Unicode
-- If the last but one segment is full, the player is too full to move
-- forward.
checkTailFull :: M Bool
checkTailFull = do
p <- gets player
return $
length (playerBody p) >= playerLen p &&
case ptail p of
[] -> False
(s:_) -> isJust (segmentSwallowed s)
where
ptail = drop 1 . reverse . playerBody
data MoveResult
= CannotMove
| EscapedScroll
| SuccessfulMove (M NextStep -> M NextStep)
| SuccessfulBacktrack
| TooFullToMove
checkedMove :: Direction -> M MoveResult
checkedMove dir = do
p <- gets player
let oldpos@(oldx, oldy) = playerHead p
let newpos = (fx oldx, fy oldy)
case playerBody p of
(s:_) | segmentPos s == newpos && segmentSide s == CurrentSide ->
backTrack p newpos
_ -> go p oldpos newpos =<< checkTailFull
where
(fx, fy) = directionOffset dir
go _ _ _ True = return TooFullToMove
go p oldpos newpos@(x, y) False = do
maxx <- worldWidth
maxy <- worldHeight
if x < 0 || y < 0 || x >= maxx || y >= maxy
then return EscapedScroll
else do
c <- readWorld newpos
if isBoundry c || not (isSpace c || isEdible c)
then return CannotMove
else if playerBerzerk p && not (isSpace c)
then berzerkpush p c oldpos newpos maxx maxy
else if playerEnergized p && isAlphaNum c
then push p c oldpos newpos maxx maxy
else consume p c oldpos newpos
consume p c oldpos newpos = do
let newsegment = Segment oldpos dir CurrentSide (playerSwallowing p) False
let p' = p
{ playerHead = newpos
, playerBody = shiftBody p newsegment
}
modify $ \w -> w { player = p' }
return $ SuccessfulMove $ flip playerConsume c
berzerkpush p c oldpos newpos maxx maxy = do
pushed <- fst <$> tryPushChar True c newpos dir maxx maxy
when pushed $
showMessage $ "You bash at the walls. Move!"
consume p (if pushed then ' ' else c) oldpos newpos
push p c oldpos newpos maxx maxy = do
pushed <- fst <$> tryPushChar False c newpos dir maxx maxy
when pushed $
showMessage $ "You shove the " ++ unicodeCharDesc c ++ "!"
consume p (if pushed then ' ' else c) oldpos newpos
tryPushChar :: Bool -> Char -> Pos -> Direction -> Int -> Int -> M (Bool, Char)
tryPushChar recur c pos@(x, y) dir maxx maxy
| isBoundry c || x2 < 0 || y2 < 0 || x2 >= maxx || y2 >= maxy =
return (False, c)
| otherwise = do
nextc <- readWorld (x2, y2)
let swap swapc = do
writeWorld (x2,y2) c
writeWorld pos swapc
return (True, swapc)
if isSpace nextc
then swap nextc
else if recur
then do
(ok, c') <- tryPushChar True nextc (x2,y2) dir maxx maxy
if ok
then swap c'
else return (False, c)
else return (False, c)
where
(fx, fy) = directionOffset dir
(x2, y2) = (fx x, fy y)
-- A full player can only back up.
-- Doing so deletes the segment they backtrack over,
-- and anything that was in that segment is moved to
-- their mouth.
--
-- However, if the segment is the only segment, anything it
-- contains is ignored. This handles a case where the last but one segment
-- was full, and then the last segment got destroyed. In this case,
-- the player should be able to back up over the new final segment, and then
-- away from it, without getting stuck on that it contained.
backTrack :: Player -> Pos -> M MoveResult
backTrack p newpos = case playerBody p of
(s:rest) | segmentPos s == newpos && segmentSide s == CurrentSide -> do
let p' = p
{ playerHead = newpos
, playerSwallowing =
if null rest
then Nothing
else segmentSwallowed s
, playerBody = rest
}
modify $ \w -> w { player = p' }
return SuccessfulBacktrack
_ -> return TooFullToMove
diveThrough :: M Bool
diveThrough = go =<< checkCanDive
where
go False = unable "No holes here in the scroll."
go True = do
p <- gets player
if toofull p
then unable "You're too full of letters to squeeze through!"
else do
s <- get
let s' = flipOver s
put s' { player = dive (player s') }
return True
toofull p = any (isJust . segmentSwallowed) (playerBody p)
|| isJust (playerSwallowing p)
unable m = do
showMessage m
return False
-- One body segment is used to go through the scroll,
-- unless the most recent segment is already inside,
dive :: Player -> Player
dive p = case playerBody p of
(s:_) | segmentSide s == InSide -> p
([_]) -> p { playerBody = shiftBody p (Segment (0,0) DDive InSide Nothing False) }
_ -> p