packages feed

breakout-0.0.2: src/breakout.hs

{-# LANGUAGE DeriveDataTypeable #-}

module Main where

import qualified Graphics.UI.HaskGame as HaskGame
import qualified Graphics.UI.HaskGame.Vector2 as Vector2
import qualified Graphics.UI.HaskGame.Rect as Rect
import Graphics.UI.HaskGame.Vector2(Vector2(..))
import Graphics.UI.HaskGame.Color(Color(..))
import qualified Graphics.UI.SDL as SDL
import qualified Graphics.UI.SDL.Video as SDL.Video
import Graphics.UI.SDL(Rect(..))
import qualified System.IO as IO
import qualified Control.Exception as Exception
import Control.Exception(Exception)
import Data.Typeable(Typeable)
import Data.Maybe(listToMaybe, isNothing, mapMaybe)
import Control.Monad(forM_, forever, when)
import Control.Monad.State.Strict(evalStateT, modify, get)
import Control.Monad.Trans(liftIO)
import Control.Applicative((<$>))

data Direction = X | Y
  deriving (Eq, Ord, Show, Read)

data QuitException = QuitException
  deriving (Show, Typeable)
instance Exception QuitException

displayWidth :: Int
displayHeight :: Int
displaySize :: Vector2 Int
displaySize@(Vector2 displayWidth displayHeight) = Vector2 640 480
colordepth :: Int
colordepth = 32

capRange :: Ord a => a -> a -> a -> a
capRange bottom top x = (x `max` bottom) `min` top

bgColor :: Color
bgColor = Color 0 0 0

brickColor :: Color
brickColor = Color 255 0 0
brickWidth :: Int
brickWidth = 72
brickHeight :: Int
brickHeight = 30
spaceWidth :: Int
spaceWidth = 8
spaceHeight :: Int
spaceHeight = 5
ballRadius :: Int
ballRadius = 8

playerColor :: Color
playerColor = Color 0 0 255

playerHeight :: Int
playerHeight = 15
initialPlayerWidth :: Int
initialPlayerWidth = 80
initialBallSpeed :: Vector2 Double
initialBallSpeed = Vector2 3 (-2)

data GameState = GameState {
          gsPlayerPos :: Int
        , gsPlayerWidth :: Int
        , gsBall :: Maybe (Vector2 Double, Vector2 Double)
        , gsBrickPositions :: [Vector2 Int]
        }

playerRect :: GameState -> Rect
playerRect GameState{gsPlayerPos=playerPos
                    ,gsPlayerWidth=playerWidth} =
    Rect (playerPos - halfWidth) (displayHeight - playerHeight)
         playerWidth displayHeight
    where
      halfWidth = playerWidth `div` 2

capPlayerRange :: Int -> Int -> Int
capPlayerRange curPlayerWidth = capRange halfWidth (displayWidth - halfWidth)
    where halfWidth = curPlayerWidth `div` 2

initGameState :: GameState
initGameState = GameState 0 initialPlayerWidth Nothing initBrickPositions

-- This boilerplate should really be automatic from records :-(
type Endo a = (a -> a)
type Inside whole part = Endo part -> Endo whole
atGsPlayerPos :: Inside GameState Int
atGsPlayerPos f gs = gs{gsPlayerPos = f (gsPlayerPos gs)}
atGsPlayerWidth :: Inside GameState Int
atGsPlayerWidth f gs = gs{gsPlayerWidth = f (gsPlayerWidth gs)}
atGsBall :: Inside GameState (Maybe (Vector2 Double, Vector2 Double))
atGsBall f gs = gs{gsBall = f (gsBall gs)}
atGsBrickPositions :: Inside GameState [Vector2 Int]
atGsBrickPositions f gs = gs{gsBrickPositions = f (gsBrickPositions gs)}

initBrickPositions :: [Vector2 Int]
initBrickPositions =
    let height = displayHeight * 2 `div` 5
    in [Vector2 x y
       | x <- [0, brickWidth+spaceWidth..displayWidth-brickWidth]
       , y <- [0, brickHeight+spaceHeight..height-brickHeight]]

brickRect :: Vector2 Int -> Rect
brickRect (Vector2 x y) = Rect x y brickWidth brickHeight

handleEvents :: [SDL.Event] -> IO ()
handleEvents events = do
  forM_ events $
    \event -> case event of
       SDL.Quit -> Exception.throwIO QuitException
       _ -> return ()

draw :: SDL.Surface -> GameState -> IO ()
draw display gs = do
    forM_ (gsBrickPositions gs) $ \pos ->
        HaskGame.fillRect display (brickRect pos) brickColor
    let Vector2 ballx bally = ballPosition gs
    HaskGame.fillRect display (playerRect gs) playerColor
    let ballRect = Rect (truncate ballx - ballRadius) (truncate bally - ballRadius) (ballRadius*2) (ballRadius*2)
    HaskGame.fillRect display ballRect playerColor

nextGameState :: GameState -> GameState
nextGameState gs =
    maybe gs newState $ gsBall gs
  where
    Rect px _ pw _ = playerRect gs
    brickPositions = gsBrickPositions gs
    fi = fromIntegral
    newState (ballPos, ballSpeed@(Vector2 speedX speedY)) =
        gs{gsBall=if ballY - fi ballRadius >= fi displayHeight
                          then Nothing
                          else Just (ballPos', ballSpeed')
          ,gsBrickPositions=filter (isNothing . collision) $ brickPositions }
      where
        ballPos'@(Vector2 ballX ballY) = ballPos + ballSpeed
        collision = collideBall (truncate <$> ballPos') . brickRect
        firstCollision = listToMaybe $ mapMaybe collision brickPositions

        hitPlayer = ballY + fi ballRadius >= fi (displayHeight - playerHeight) &&
                    ballX+fi ballRadius >= fi px && ballX-fi ballRadius <= fi (px+pw)
        factor = if hitPlayer
                 then (+) $ 8 * (ballX - fi px - fi pw/2) / fi pw
                 else id
        collideX = maybe False (==X) firstCollision
        collideY = maybe False (==Y) firstCollision

        ballSpeed' =
            Vector2
            (factor $
             if ballX - fi ballRadius > 0 && ballX + fi ballRadius < fi displayWidth && not collideX
             then speedX
             else -speedX)
            (if ballY - fi ballRadius > 0 && not hitPlayer && not collideY
             then speedY
             else -speedY)

collideBall :: Vector2 Int -> Rect -> Maybe Direction
collideBall (Vector2 ballX ballY) r =
    let intersect = r `Rect.intersect` (Rect (ballX - ballRadius) (ballY - ballRadius)
                                         (ballRadius*2) (ballRadius*2))
        Vector2 iwidth iheight = Rect.getSize intersect
    in if iwidth <= 0 || iheight <= 0
       then Nothing
       else Just $
            if iwidth < iheight
            then X
            else Y

ballPosition :: GameState -> Vector2 Double
ballPosition GameState{gsBall=ballPosSpeed
                      ,gsPlayerPos=playerPos} =
    maybe (fromIntegral <$> Vector2 playerPos (displayHeight - playerHeight - ballRadius))
          fst $
    ballPosSpeed

sdlIteration :: SDL.Surface -> GameState -> IO ()
sdlIteration display gs =
  do
    HaskGame.fillSurface display bgColor
    draw display gs
    SDL.flip display
    HaskGame.getEvents >>= handleEvents
    ticks <- SDL.getTicks
    SDL.delay (10 - (ticks `mod` 10))

mainLoop :: SDL.Surface -> IO ()
mainLoop display =
  (`evalStateT` initGameState) . forever $ do
    (mouseX, _, buttons) <- liftIO $ SDL.getMouseState
    let leftPressed = SDL.ButtonLeft `elem` buttons

    gameState <- get
    liftIO $ sdlIteration display gameState
    modify . atGsPlayerPos . const . capPlayerRange (gsPlayerWidth gameState) $ mouseX
    ball <- gsBall `fmap` get
    when (leftPressed && isNothing ball) $ do
      ballPos <- ballPosition `fmap` get
      modify . atGsBall . const $ Just (ballPos, initialBallSpeed)
    modify nextGameState

main :: IO ()
main = do
  HaskGame.withInit $ do
    SDL.Video.showCursor False
    display <- HaskGame.setVideoMode displaySize colordepth
    mainLoop display
      `Exception.catch`
      \QuitException -> return ()