packages feed

dual-game-0.1.0.0: dual.hs

{-# LANGUAGE DeriveGeneric #-}

import Network.WebSockets hiding (Message)
import Network.Socket (withSocketsDo)
import Graphics.Gloss.Interface.IO.Game
import Data.Bifunctor
import Data.Biapplicative
import Data.Monoid
import Data.Maybe (catMaybes)
import Data.List (partition)
import Control.Concurrent.Chan
import Control.Concurrent.MVar
import Control.Concurrent
import Control.Monad (forever)
import GHC.Generics
import Data.Serialize (Serialize, encodeLazy, decodeLazy)
import Control.Monad
import System.Environment
import System.Exit


type Time = Float
type Hp = Float
type Position = (Float, Float)
type Velocity = (Float, Float)
type BSize = Float
type OtherLost = Bool

data Bullet = Bullet BSize Position Velocity deriving (Show, Read, Generic)

data GameState = Playing | Won | Lost | Even deriving (Show, Eq, Read, Generic)

data Ammo = Ammo { availAmmo :: Float
                 , charging :: Bool
                 , charged :: Float }
                 deriving (Show, Read, Generic)

data World = World { pos :: Position
                   , hp :: Hp
                   , ammo :: Ammo
                   , velocity :: Velocity
                   , gameState :: GameState
                   , ownBullets :: [Bullet]
                   , othBullets :: [Bullet] }
                   deriving (Show, Read, Generic)

data Role = Server Int | Client String Int deriving (Show, Read)

data Message = ILost | IShot Bullet deriving (Show, Read, Generic)

instance Serialize Bullet
instance Serialize GameState
instance Serialize Ammo
instance Serialize World
instance Serialize Message

fromRight (Right a) = a --FIXME ignore, report or something
fromRight _ = error "deserialization error. sorry :("

instance WebSocketsData Message where
  fromLazyByteString = fromRight . decodeLazy
  toLazyByteString = encodeLazy
  fromDataMessage (Binary bs) = fromLazyByteString bs
  fromDataMessage _ = error "Invalid websocket message"

instance WebSocketsData Bullet where
  fromLazyByteString = fromRight . decodeLazy
  toLazyByteString = encodeLazy
  fromDataMessage (Binary bs) = fromLazyByteString bs
  fromDataMessage _ = error "Invalid websocket message"

maxAmmo = 6
chargingSpeed = 2
rechargingSpeed = 2
minCharge = 1.0
addedBulletVelocity = 2
bulletVelocity = 200
playerVelocity = 200
maxHp = 100
damageMultiplier = 10
playerHitBox = (playerWidth/2, playerHeight/2) -- half the size of the hitbox side
playerWidth = 80
playerHeight = 80

screenW = 800
screenH = 600

ownColor = magenta
othColor = yellow
lifeColor = green
ammoColor = red

fps = 60

main :: IO ()
main = do
  args <- getArgs
  role <- case args of ip:port:[] -> return $ Client ip (read port)
                       port:[] -> return $ Server $ read port
                       _ -> do
                               putStrLn "Invalid arguments."
                               putStrLn "Usage:"
                               putStrLn "  dual address port      - connects to an existing server"
                               putStrLn "  dual port              - creates a server"
                               exitFailure
  sendChan <- newChan
  recvMVar <- newMVar []
  otherLostMVar <- newMVar False
  _ <- forkIO $ case role of
    Server port -> do
      runServer "0.0.0.0" port $ ws otherLostMVar recvMVar sendChan <=< acceptRequest
    Client ip port -> do
      withSocketsDo $ runClient ip port "/" $ ws otherLostMVar recvMVar sendChan
  playIO (InWindow "DUAL" (screenW, screenH) (0, 0))
    black
    fps
    defaultWorld
    render
    handleInput
    (step otherLostMVar recvMVar sendChan)
  --TODO clean up the socket properly

ws :: MVar OtherLost -> MVar [Bullet] -> Chan Message -> Connection -> IO ()
ws gameStateM rM sC conn = do
  _ <- forkIO $ recvData conn gameStateM rM
  sendData conn sC

sendData :: Connection -> Chan Message -> IO ()
sendData conn ch = do
  forever $ do
    m <- readChan ch
    sendBinaryData conn m
  --hClose hdl

recvData :: Connection -> MVar OtherLost -> MVar [Bullet] -> IO ()
recvData conn gsMV bMV = do
  forever $ do
    msg <- receiveData conn
    case msg of IShot b -> modifyMVar_ bMV  (\bs -> return (b:bs))
                ILost   -> modifyMVar_ gsMV (return . const True)
  --hClose hdl

defaultAmmo :: Ammo
defaultAmmo = Ammo { availAmmo = maxAmmo
                   , charging = False
                   , charged = 0 }

defaultWorld :: World
defaultWorld = World { pos = (0,0)
                     , hp = maxHp
                     , ammo = defaultAmmo
                     , velocity = (0,0) 
                     , gameState = Playing
                     , ownBullets = []
                     , othBullets = [] }

render :: Monad m => World -> m Picture
render world = return $ renderBullets world
                      <> statePicture (gameState world)
                      <> borders
                      <> translate x y (color ownColor $ rectangleSolid playerWidth playerHeight)
                      <> translate x (y - playerHeight / 2 - 5) (color ammoColor $ rectangleSolid (10* availAmmo (ammo world)) 5)
                      <> if hp world > 0
                           then translate x (y - playerHeight / 2 - 13) $ color lifeColor $ rectangleSolid (1* hp world) 5
                           else Blank
  where x = fst $ pos world
        y = snd $ pos world

statePicture :: GameState -> Picture
statePicture Won  = Translate (-350) 0 $ Color red $ Graphics.Gloss.Interface.IO.Game.Text "YOU WON"
statePicture Lost = Translate (-350) 0 $ Color red $ Graphics.Gloss.Interface.IO.Game.Text "YOU LOST"
statePicture Even = Translate (-350) 0 $ Color red $ Graphics.Gloss.Interface.IO.Game.Text "EVEN"
statePicture Playing = Blank

borders :: Picture
borders = color white $ rectangleWire (fromIntegral screenW - 2) (fromIntegral screenH - 2)

renderBullets :: World -> Picture
renderBullets world = Pictures (fmap (renderBullet ownColor) (ownBullets world) ++ fmap (renderBullet othColor) (othBullets world))

renderBullet :: Color -> Bullet -> Picture
renderBullet c (Bullet size (x,y) _) = translate x y $ color c $ rectangleSolid (10*size) (10*size)

handleInput :: Monad m => Event -> World -> m World
handleInput (EventKey (SpecialKey KeyDown)  Down _ _) world = return world { velocity = second (const (-1)) $ velocity world }
handleInput (EventKey (SpecialKey KeyDown)  Up   _ _) world = return world { velocity = second (const    0) $ velocity world }
handleInput (EventKey (SpecialKey KeyUp)    Down _ _) world = return world { velocity = second (const    1) $ velocity world }
handleInput (EventKey (SpecialKey KeyUp)    Up   _ _) world = return world { velocity = second (const    0) $ velocity world }
handleInput (EventKey (SpecialKey KeyLeft)  Down _ _) world = return world { velocity = first  (const (-1)) $ velocity world }
handleInput (EventKey (SpecialKey KeyLeft)  Up   _ _) world = return world { velocity = first  (const    0) $ velocity world }
handleInput (EventKey (SpecialKey KeyRight) Down _ _) world = return world { velocity = first  (const    1) $ velocity world }
handleInput (EventKey (SpecialKey KeyRight) Up   _ _) world = return world { velocity = first  (const    0) $ velocity world }
handleInput (EventKey (SpecialKey KeySpace) Down _ _) world = return world { ammo = (ammo world) { charging = True } }
handleInput (EventKey (SpecialKey KeySpace) Up   _ _) world = return world { ammo = (ammo world) { charging = False } }
handleInput _ world = return world

-- Just a bottom-to-top chain of world modifications.
-- The effectful ones are marked with =<<, which is nice.
step :: MVar OtherLost -> MVar [Bullet] -> Chan Message -> Time -> World -> IO World
step stateMVar rCh sCh dt world =
  lose sCh =<<
  sendBullets sCh =<<
  ((destroyOthBullets .
  getHit .
  moveBullets dt .
  movePlayer dt .
  shoot .
  charge dt .
  recharge dt) <$>
  (receiveBullets rCh =<<
  receiveVictory stateMVar world))

receiveBullets :: MVar [Bullet] -> World -> IO World
receiveBullets mv world = do
  bs <- swapMVar mv [] --XXX FIXME not atomic. -> STM/synchronous-channels?
  let bs' = flipVelocity <$> bs
  return world { othBullets = othBullets world ++ bs' }

receiveVictory :: MVar OtherLost -> World -> IO World
receiveVictory otherLostMVar world = do
  won' <- tryReadMVar otherLostMVar --TODO avoid if already won? or laziness already does it?
  let won = case won' of Just a -> a
                         Nothing -> error "Logic error: the game state MVar should always be full!" --TODO replace False with emptyness? use a lock?
  return $ if not won then world
           else world { gameState = f $ gameState world }
             where f Playing = Won
                   f Lost = Even
                   f Even = Even
                   f Won = Won

flipVelocity :: Bullet -> Bullet
flipVelocity (Bullet s p (vx, vy)) = Bullet s p (vx, -vy)

sendBullets :: Chan Message -> World -> IO World
sendBullets ch world = do
  let (onScreen, offScreen) = partition (\(Bullet _ (_, y) _) -> y<=fromIntegral screenH/2) $ ownBullets world
  let reallyOnScreen = filter (\(Bullet _ (x, _) _) -> x<=fromIntegral screenW/2 && x>=fromIntegral screenW/(-2)) onScreen
  writeList2Chan ch $ fmap IShot offScreen
  return world { ownBullets = reallyOnScreen }

lose :: Chan Message -> World -> IO World
lose ch world | gameState world /= Playing = return world
              | hp world <= 0 = do
                                   writeChan ch ILost
                                   return $ world { gameState = Lost }
              | otherwise = return world

getHit :: World -> World
getHit world = world'
  where world' = world { hp = hp' }
        bs = othBullets world
        hp' = hp world - sum (catMaybes $ bulletDamage world <$> bs)

bulletDamage :: World -> Bullet -> Maybe Hp
bulletDamage world (Bullet s p _) = if inHitBox playerHitBox (pos world) p
                                      then Just (s*damageMultiplier)
                                      else Nothing

-- | if the two positions are within the given range
inHitBox :: (Float, Float) -> Position -> Position -> Bool
inHitBox (hx, hy) (ax, ay) (bx, by) = x && y
  where x = abs (ax-bx) < hx
        y = abs (ay-by) < hy

destroyOthBullets :: World -> World
destroyOthBullets world = world { othBullets = filter (\(Bullet _ p _) -> not (hit p || outside p)) $ othBullets world }
  where hit p = inHitBox playerHitBox p $ pos world
        outside (x,y) = x >  (fromIntegral screenW)/2
                     || x < -(fromIntegral screenW)/2
                     || y < -(fromIntegral screenH)/2
--TODO add 'life' property to Bullet
--TODO or delete off-screen

movePlayer :: Time -> World -> World
movePlayer dt world = world { pos = checkBounds $ bimap (+) (+) (bimap (*(playerVelocity*dt)) (*(playerVelocity*dt)) $ velocity world) <<*>> pos world }

checkBounds :: Position -> Position
checkBounds (x,y) = (max (-halfW) $ min halfW x, max (-halfH) $ min halfH y)
  where halfW = fromIntegral screenW / 2
        halfH = fromIntegral screenH / 2

recharge :: Time -> World -> World
recharge dt world = world { ammo = a' }
  where a' = if charging a
               then a
               else a { availAmmo = min maxAmmo (availAmmo a + dt*rechargingSpeed) }
        a = ammo world

charge :: Time -> World -> World
charge dt world = world { ammo = a' }
  where a = ammo world
        a' = a { availAmmo = availAmmo', charged = charged' }
        availAmmo' = if charging a
                       then max 0 $ availAmmo a - chargeAmount
                       else availAmmo a
        charged' = if charging a && availAmmo a - chargeAmount > 0
                     then min maxAmmo $ charged a + chargeAmount
                     else charged a
        chargeAmount = if charged a > 0
                         then dt*chargingSpeed
                         else minCharge

shoot :: World -> World
shoot world = world { ammo = a', ownBullets = bs' }
  where a = ammo world
        bs = ownBullets world
        a' = if shooting then a { charged = 0 } else a
        bs' = if shooting then b:bs else bs
        b = mkBullet world
        shooting = not (charging a) && charged a > 0 && hp world > 0

mkBullet :: World -> Bullet
mkBullet world = Bullet s p v
  where s = charged $ ammo world
        p = pos world
        v = second (+addedBulletVelocity* sqrt s) $ velocity world

moveBullets :: Time -> World -> World
moveBullets dt world = world { ownBullets = moveBullet dt <$> ownBullets world
                             , othBullets = moveBullet dt <$> othBullets world }

moveBullet :: Time -> Bullet -> Bullet
moveBullet dt (Bullet s p v) = Bullet s (bimap (+) (+) (bimap (*(bulletVelocity*dt)) (*(bulletVelocity*dt)) v) <<*>> p) v