packages feed

snake-0.1.0.0: World.hs

module World where

import System.Random (randomRIO, Random)

import Data.List.Split (chunksOf)

import Terminal
import Movement
import Snake

data World = World { worldWidth :: Integer
                   , worldHeight :: Integer
                   , worldSnake :: Snake
                   , pellet :: Coords
                   , gameOver :: Bool } deriving Eq

instance Show World where
    -- OPTIMIZE: this is a hella long function.
    show World { worldWidth = width 
               , worldHeight = height
               , worldSnake = snake
               , pellet = pellet
               , gameOver = gameOver } = if gameOver
    then "Game Over!\n"
    else showChunked $ map drawPoint rowReverseList where
        rowReverseList = concat $ reverse $ chunksOf (fromIntegral width) [0..(size - 1)]
        size = width * height

        drawPoint index = if snakeBodyIsAt index snake || pelletIsAt index pellet then 'o' else ' '
        snakeBodyIsAt index snake = indexToCoords width index `elem` getCoords snake
        pelletIsAt index pellet = indexToCoords width index == pellet

        showChunked list = boxWorld $ chunksOf (fromIntegral width) list
        boxWorld lineList = "/" ++ horizontalEdges ++ "\\\n" ++
            concatMap (\line -> "|" ++ line ++ "|\n") lineList ++
            "\\" ++ horizontalEdges ++ "/"
        horizontalEdges = replicate (fromIntegral width) '-'

randomWorld :: IO World
randomWorld = getTerminalWidthHeight >>= (\(x,y) -> return (x - 2, y - 2)) >>=
    return . newWorld >>= seedWorld

newWorld :: Integral a => (a, a) -> World
newWorld (width, height) = World { worldWidth = fromIntegral width
                                 , worldHeight = fromIntegral height
                                 , worldSnake = Snake North []
                                 , pellet = Coords pelletX pelletY
                                 , gameOver = False } where
    pelletX = fromIntegral $ width `div` 2
    pelletY = fromIntegral $ height `div` 2

-- OPTIMIZE: this should really be done with a Random instance for the
-- Snake, Coords, World, and Direction types.
seedWorld :: World -> IO World
seedWorld world = randomCoords (worldWidth world) (worldHeight world) >>=
    \coords -> return world { worldSnake = Snake North [coords] }

randomCoords :: (Integral a, Random a) => a -> a -> IO Coords
randomCoords width height = (,) <$> randomRIO (0, width) <*> randomRIO (0, height) >>=
    \(x, y) -> return $ Coords (fromIntegral x) (fromIntegral y)

getSnakeCoords :: World -> [Coords]
getSnakeCoords = getCoords . worldSnake

updatePelletCoords :: World -> IO Coords
updatePelletCoords world = if pellet world `elem` getSnakeCoords world
    then randomCoords (worldWidth world) (worldHeight world)
    else return $ pellet world

-- OPTIMIE: this is a very large function (albeit mainly pattern
-- matching and other simple constructs). It seems to be working
-- well, however, it is not unlikely that I've missed something.
updateWorld :: Move -> Coords -> World -> World
updateWorld move pelletCoords world@World { worldWidth = width
                                          , worldHeight = height
                                          , worldSnake = snake } = if newWorld == world
    then world { gameOver = True }
    else newWorld where
        newWorld = world { pellet = pelletCoords
                         , worldSnake = updateSnake move width height snake } where
            updateSnake m w h Snake { getDirection = d
                                    , getCoords = c } = Snake { getDirection = newDirection m d
                                                              , getCoords = newCoords m w h d c } where
                newDirection Noop           d = d
                newDirection Up             d = if d == South then d else North
                newDirection Down           d = if d == North then d else South
                newDirection Movement.Left  d = if d == East then d else West
                newDirection Movement.Right d = if d == West then d else East
                newCoords m w h d coords = let newHeadCoords = updateHead w h d (head coords) in
                    newHeadCoords : updateTail newHeadCoords coords where
                        updateTail nhc cs = if nhc == pelletCoords then cs
                            else if nhc == head cs then tail cs else init cs
                        updateHead w h d head = progressHead w h d head where
                            progressHead w h d head = if outOfBounds w h $ newHead d head
                                then head
                                else newHead d head where
                                outOfBounds w h coords@(Coords headX headY) = headX > (w - 1) ||
                                                                            headY > (h - 1) ||
                                                                            headX < 0 ||
                                                                            headY < 0 ||
                                                                            coords `elem` c
                                newHead North (Coords x y) = Coords x (y + 1)
                                newHead East  (Coords x y) = Coords (x + 1) y
                                newHead South (Coords x y) = Coords x (y - 1)
                                newHead West  (Coords x y) = Coords (x - 1) y