lens-1.4: examples/Pong.hs
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE NoMonomorphismRestriction #-}
import Control.Applicative ((<$>), (<*>))
import Control.Lens
import Control.Lens.TH
import Control.Monad.State
import Data.Set (Set, member, empty, insert, delete)
import Graphics.Gloss
import Graphics.Gloss.Interface.Pure.Game
-- Some global constants
gameSize = 300
windowSize = 480
ballRadius = 0.02
initialSpeed = (0.8, 0.3)
speedIncrease = 1.1
paddleWidth = 0.02
paddleHeight = 0.3
paddleSpeed = 1
textSize = 0.3 / gameSize
-- Pure data type for representing the game state
data Pong = Pong
{ _ballPos :: Point
, _ballSpeed :: Vector
, _paddle1 :: Float
, _paddle2 :: Float
, _score :: (Int, Int)
-- Since gloss doesn't cover this, we store the set of pressed keys
, _keys :: Set Key
}
initial :: Pong
initial = Pong (0, 0) initialSpeed 0 0 (0, 0) empty
-- Some nice lenses to go with it
makeLenses ''Pong
-- I'm just renaming the tuple lenses for enhanced clarity with points/vectors
_x = _1
_y = _2
-- This will be in Data.Pair.Lens soon
both :: Traversal (a,a) (b,b) a b
both f (x,y) = (,) <$> f x <*> f y
-- Game update logic
update :: Float -> Pong -> Pong
update time = execState $ do
updatePaddles time
updateBall time
checkBounds
-- Move the ball by adding its current speed
updateBall :: Float -> State Pong ()
updateBall time = do
speed <- use ballSpeed
ballPos += speed `mul` time
-- Make sure it doesn't leave the playing area
ballPos.both %= clampPad ballRadius
where
(a,b) `mul` c = (c*a, c*b)
infixl 7 `mul`
-- Update the paddles
updatePaddles :: Float -> State Pong ()
updatePaddles time = do
p <- get
-- Update the player's paddle based on keys
when (SpecialKey KeyUp `isIn` p^.keys) $
paddle1 += paddleSpeed * time
when (SpecialKey KeyDown `isIn` p^.keys) $
paddle1 -= paddleSpeed * time
-- Update the CPU's paddle based on the ball's relative position
case compare (p^.ballPos._y) (p^.paddle2) of
GT -> paddle2 += paddleSpeed * time
LT -> paddle2 -= paddleSpeed * time
_ -> return ()
-- Make sure both paddles don't leave the playing area
paddle1 %= clamp
paddle2 %= clamp
where
clamp = clampPad (paddleHeight/2)
isIn = member
infixl 7 `isIn`
-- Clamp to the region (-1, 1) but with padding
clampPad :: Float -> Float -> Float
clampPad pad = max (pad - 1) . min (1 - pad)
-- Check for collisions and/or scores
checkBounds :: State Pong ()
checkBounds = do
p <- get
let (x,y) = p^.ballPos
-- Check for collisions with the top or bottom
when (abs y >= top) $
ballSpeed._y %= negate
-- Check for collisions with paddles
let check l = y >= p^.l - paddleHeight/2 && y <= p^.l + paddleHeight/2
collide = do
ballSpeed._x %= negate
ballSpeed.both *= speedIncrease
when (x <= left) $
if check paddle1
then collide
else do
score._2 += 1
reset
when (x >= right) $
if check paddle2
then collide
else do
score._1 += 1
reset
where
top = 1 - ballRadius
left = ballRadius + paddleWidth/2 - 1
right = -left
-- Reset the game
reset :: State Pong ()
reset = do
ballPos ^= initial^.ballPos
ballSpeed ^= initial^.ballSpeed
-- Drawing a pong state to the screen
draw :: Pong -> Picture
draw p = scale gameSize gameSize $ Pictures
[ drawBall `at` p^.ballPos
, drawPaddle `at` (-1, p^.paddle1)
, drawPaddle `at` ( 1, p^.paddle2)
-- Score and playing field
, scale textSize textSize (p^.score.pretty.to text) `at` (-0.1, 0.85)
, rectangleWire 2 2
]
where
-- Pretty printing lens
pretty = to (\(x,y) -> show x ++ " " ++ show y)
p `at` (x,y) = translate x y p
infixr 1 `at`
drawPaddle :: Picture
drawPaddle = rectangleSolid paddleWidth paddleHeight
drawBall :: Picture
drawBall = circleSolid ballRadius
-- Handle input by simply updating the keys set
handle :: Event -> Pong -> Pong
handle (EventKey k Down _ _) = keys %~ insert k
handle (EventKey k Up _ _) = keys %~ delete k
handle _ = id
-- The main program action
main = play display backColor fps initial draw handle update
where
-- display = InWindow "Pong!" (windowSize, windowSize) (800, 600)
display = FullScreen (800,600)
backColor = white
fps = 120