packages feed

Haskelloids (empty) → 0.1.0

raw patch · 4 files changed

+444/−0 lines, 4 filesdep +HGLdep +MonadRandomdep +Yampasetup-changed

Dependencies added: HGL, MonadRandom, Yampa, base, containers, haskell98, hmatrix, random

Files

+ Haskelloids.cabal view
@@ -0,0 +1,72 @@+-- Haskelloids-0.1.0.cabal auto-generated by cabal init. For+-- additional options, see+-- http://www.haskell.org/cabal/release/cabal-latest/doc/users-guide/authors.html#pkg-descr.+-- The name of the package.+Name:                Haskelloids++-- The package version. See the Haskell package versioning policy+-- (http://www.haskell.org/haskellwiki/Package_versioning_policy) for+-- standards guiding when and how versions should be incremented.+Version:             0.1.0++-- A short (one-line) description of the package.+Synopsis:            A reproduction of the Atari 1979 classic "Asteroids"++-- A longer description of the package.+-- Description:         ++-- URL for the project homepage or repository.+Homepage:            http://www.matthewhayden.co.uk++-- The license under which the package is released.+License:             BSD3++-- The file containing the license text.+License-file:        LICENSE++-- The package author(s).+Author:              Matthew Hayden++-- An email address to which users can send suggestions, bug reports,+-- and patches.+Maintainer:          mrehayden@googlemail.com++-- A copyright notice.+-- Copyright:           ++-- Stability of the pakcage (experimental, provisional, stable...)+Stability:           Alpha++Category:            Game++Build-type:          Simple++-- Extra files to be distributed with the package, such as examples or+-- a README.+-- Extra-source-files:  ++-- Constraint on the version of Cabal needed to build this package.+Cabal-version:       >=1.6++Executable Haskelloids+  Hs-Source-Dirs:      src+  -- .hs or .lhs file containing the Main module.+  Main-is:             Haskelloids.hs+  +  -- Packages needed in order to build this package.+  Build-depends:+    base        == 4.*,+    haskell98   == 1.*,+    containers  == 0.*,+    random      == 1.*,+    MonadRandom == 0.1.*,+    hmatrix     >= 0.7 && < 1,+    HGL         == 3.*,+    Yampa       >= 0.9 && < 1+    +  -- Modules not exported by this package.+  -- Other-modules:       +  +  -- Extra tools (e.g. alex, hsc2hs, ...) needed to build the source.+  -- Build-tools:         +  
+ LICENSE view
@@ -0,0 +1,25 @@+Copyright 2010 Matthew Hayden. All rights reserved.++Redistribution and use in source and binary forms, with or without modification, are+permitted provided that the following conditions are met:++   1. Redistributions of source code must retain the above copyright notice, this list of+      conditions and the following disclaimer.++   2. Redistributions in binary form must reproduce the above copyright notice, this list+      of conditions and the following disclaimer in the documentation and/or other materials+      provided with the distribution.++THIS SOFTWARE IS PROVIDED BY MATTHEW HAYDEN ``AS IS'' AND ANY EXPRESS OR IMPLIED+WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND+FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL MATTHEW HAYDEN OR+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF+ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.++The views and conclusions contained in the software and documentation are those of the+authors and should not be interpreted as representing official policies, either expressed+or implied, of Matthew Hayden.
+ Setup.hs view
@@ -0,0 +1,3 @@+#!/usr/bin/env runhaskell+import Distribution.Simple+main = defaultMain
+ src/Haskelloids.hs view
@@ -0,0 +1,344 @@+-- #### Haskelloids.hs++-- TODO Implement hyperspace jump+-- TODO Implement extra asteroids+-- TODO Implement bonus scores+-- TODO Implement bonus life++{-# OPTIONS_GHC -XArrows -XScopedTypeVariables #-}+module Main (main) where++-- Import random+import System.Random (newStdGen)++-- Import control structures+import Control.Arrow (returnA, (&&&))+import Control.Monad (replicateM, liftM, liftM2)+import Control.Monad.Random (Rand, RandomGen(..), getRandomR, getRandomRs,+                             evalRand, evalRandIO, runRand)+import Control.Monad.Random.Class (getSplit)++-- Import data structures+import Data.IdentityList (IList, ILKey)+import qualified Data.IdentityList as IL (empty, elems, insert, delete,+                                          insertMany, assocs, fromList,+                                          mapWithKey)+import Data.Version (Version(..), showVersion)++-- Import geometry and graphics+import Graphics.HGL (runGraphics, overGraphics)+import Graphics.HGL.Window (Window, RedrawMode(..), openWindowEx, setGraphic,+                            getWindowTick)+import Graphics.HGL.Draw.Text (text)+import Haskelloids.Geometry (Figure(..), Point, Point2, inCircle, regularPolygon)+import Haskelloids.Graphics (Graphic, drawFigure)+import qualified Haskelloids.Geometry.Text as Text (Alignment(..), fromString)++-- Import Yampa framework+import FRP.Yampa (Event(..), SF, DTime, arr, constant, identity, (>>>), (<<<),+                  (>>^), (^>>), (^<<), (<<^), reactimate, dpSwitch, notYet,+                  accum, accumHold, tag, edge, switch, dSwitch, never, after,+                  delayEvent, mergeBy, lMerge, event, isEvent, tagWith, gate)++-- Import game input+import Haskelloids.Input (UserInput(..), getWindowEvs, procWindowEvs, inputInit)++-- import game objects+import Haskelloids.Object (Object, ObjectClass(..), ObjectInput(..),+                           ObjectOutput(..), hits)+import Haskelloids.Object.Ship (shipSF, shipFigure)+import Haskelloids.Object.Asteroid (RoidSz(..), asteroidSF)++-- Import path info+import qualified Paths_Haskelloids as Path (version)++-- #### Datatype definitions ###################################################++type Score = Int+type Lives = Int++data GameInput = GameInput {+                   giUserInput :: UserInput,+                   giNewRound  :: Event Int,+                   giRespawn   :: Event ()+                 }++data GameOutput = GameOutput {+                    goGraphic  :: Graphic,+                    goGameOver :: Event Score+                  }++-- #### Program constants ######################################################++-- verNo - version number+verNo :: String+verNo = showVersion $ Path.version { versionTags = [] }++-- ## Window constants ##########################++-- wWidth, wHeight - the heigh and width of the game window in Pixels+wWidth, wHeight :: Int+wWidth  = 800+wHeight = 600++-- wDim - window dimensions+wDim :: Point+wDim = (wWidth, wHeight)++-- refreshPeriod' - the refresh period of the graphics in milliseconds+refreshPeriod' :: Integer+refreshPeriod' = 15++-- refreshPeriod - the refresh period of the graphics in seconds+refreshPeriod :: Double+refreshPeriod = fromInteger refreshPeriod' * 10^^(-3)++-- ## Game constants ############################++-- centre - the center of the game window+centre :: Point2+centre = (centreX, centreY)++-- centerX, centerY - the center of the game window+centreX, centreY :: Double+centreX = fromIntegral $ wWidth`div`2+centreY = fromIntegral $ wHeight`div`2++-- safeRadius - the radius of the area from the central point that asteroids cannot spawn in+safeRadius :: Double+safeRadius = 75++-- initAsteroidsCount - the number of initial asteroids+initAsteroidsCount :: Int+initAsteroidsCount = 5++-- initLivesCount - initial number of lives in a game+initLivesCount :: Int+initLivesCount = 3++-- killPoints - points per asteroid kill+killPoints :: Int+killPoints = 100++-- respawnDelay - the delay between player and asteroid respawns+respawnDelay :: DTime+respawnDelay = 2++-- ## Graphics constants ########################++-- lifeFigure - the figure drawn for a life counter+lifeFigure :: Figure+lifeFigure = Rotate (-pi/2) shipFigure++-- #### Function definitions ###################################################++-- ## Main game loop ############################++-- init - called once at the start of program for setting up game environment+--        returns - the input to the game signal function at time zero+initUI :: IO UserInput+initUI = return inputInit++-- sense - captures any game inputs (e.g. user) and returns them along with the time since the last invocation of sense+--         NB: any processing that takes longer than the window tick will cause a frame to skip+--   w      - target window for rendering game objects+--   _      - action may block, we however won't be doing any blocking actions+--   return - #1 time (in seconds) since last invocation, #2 any game input+sense :: Window -> Bool -> IO (DTime, Maybe UserInput)+sense w _ = do+  evs <- getWindowEvs w+  let gi = procWindowEvs inputInit evs -- fold window events over game input+  getWindowTick w+  return (refreshPeriod, Just gi)++-- actuate - renders the collection of signal functions, conforming to the specification of Yampa's "reactimate"+--   w      - window to render to+--   _      - Has the output changed from the last sample, but we will assume it always has+--   out    - Current output sample+--   return - Termination flag+actuate :: Window -> Bool -> Graphic -> IO Bool+actuate w _ out = do+  setGraphic w out+  return False -- never terminate++-- initAsteroids - create initial asteroid objects+initAsteroids :: RandomGen g => Int -> Point2 -> Rand g [Object]+initAsteroids n safe = replicateM n (createAsteroid safe) ++-- initGameObjects - all initial game objects +initGameObjects :: RandomGen g => Rand g (IList Object)+initGameObjects = do+  let ship = shipSF (wWidth, wHeight) centre+  rds <- initAsteroids initAsteroidsCount centre+  return (IL.fromList $ ship : rds)++-- route - routes incoming object input to the correct signal function+route :: forall sf. (GameInput, IList ObjectOutput)+                      -> IList sf+                      -> IList (ObjectInput, sf)+route (gi,oos) sfs = IL.mapWithKey routeAux $ sfs+  where+    routeAux :: ILKey -> sf -> (ObjectInput, sf)+    routeAux k obj = (ObjectInput { oiHit = if k `elem` hs+                                              then Event ()+                                              else NoEvent,+                                    oiUserInput = giUserInput gi },+                      obj)+    +    hs = hits . IL.assocs $ oos++-- killOrSpawn - adds and removes signal functions upon requests from the game objects, NB: we get passed the old [ObjectOuput] and the new [ObjectOutput]+killOrSpawn :: RandomGen g => g -> ((GameInput, IList ObjectOutput), IList ObjectOutput) -> Event (IList Object -> IList Object)+killOrSpawn g ((gi, _), oos) =+  let es = [ mergeBy (.)+               (ooKillReq oo `tag` flip IL.delete k)+               (fmap (flip IL.insertMany) (ooSpawnReq oo))+           | (k,oo) <- IL.assocs oos ]+      safeCentre = case filter ((== Ship) . ooObjClass) . IL.elems $ oos of+                     []  -> centre+                     [s] -> ooPos s+      respawn    = giRespawn gi `tag` flip IL.insert (shipSF wDim centre)+      newRound   = (flip IL.insertMany . flip evalRand g . flip initAsteroids safeCentre) `fmap` giNewRound gi+  in foldl (mergeBy (.)) NoEvent (respawn : newRound : es)++-- createAsteroid - create a randomly generated asteroid outside the "safe area" circle+createAsteroid :: RandomGen g => Point2 -> Rand g Object+createAsteroid (cx, cy) = do+  g <- getSplit+  xs <- getRandomRs (0, fromIntegral wWidth)+  ys <- getRandomRs (0, fromIntegral wHeight)+  let (x,y) = head . dropWhile unsafe . zip xs $ ys+  s <- getRandomR (0, 1)+  o <- getRandomR (0, 2*pi)+  f <- getRandomR (0, 1)++  return (asteroidSF g (wWidth, wHeight) (x,y) s o RdLarge f)+ where+  pair (x:y:xs) = (x,y) : pair xs+  unsafe (x,y)  = inCircle (cx,cy) safeRadius (x,y)++-- asteroidsKilled - returns a function that increments the score for each asteroid killed+asteroidsKilled :: IList ObjectOutput -> Event (Score -> Score)+asteroidsKilled oos = foldr (mergeBy (.)) NoEvent +                        . map (flip tag (+killPoints) . ooKillReq)+                        . filter ((== Asteroid) . ooObjClass) . IL.elems $ oos++drawObjectOutput :: IList ObjectOutput -> Graphic+drawObjectOutput = overGraphics . map ooGraphic . IL.elems++drawScore :: Score -> Graphic+drawScore = overGraphics . map (drawFigure . Translate (50,50))+              . Text.fromString Text.AlignLeft . show++drawLives :: Lives -> Graphic+drawLives = overGraphics . map drawFigure+              . zipWith Translate [(x,125) | x <- [60,83..]]+              . flip replicate lifeFigure++-- ## Signal functions ##########################++-- allAsteroidsKilled - returns an event after all the asteroids were destroyed+allAsteroidsKilled :: SF (IList ObjectOutput) (Event (Int -> Int))+allAsteroidsKilled =+  tagWith (+1) ^<< edge <<^ not . any ((== Asteroid) . ooObjClass) . IL.elems++-- playerCrashed - returns the an event the moment the player crashes, also returns the second event when it is safe to respawn+playerCrashed :: SF (IList ObjectOutput) (Event (), Event ())+playerCrashed = proc oos -> do+  crash <- crashed -< oos+  spawn <- respawn -< oos+  returnA -< (crash, spawn)+ where+  crashed :: SF (IList ObjectOutput) (Event ())+  crashed = any (\s -> (isEvent . ooKillReq $ s) && (ooObjClass s == Ship)) . IL.elems ^>> edge+  respawn :: SF (IList ObjectOutput) (Event ())+  respawn = switch (never &&& (crashed >>> delayEvent respawnDelay)) (const waitUntilSafe)+  waitUntilSafe :: SF (IList ObjectOutput) (Event ())+  waitUntilSafe = dSwitch (safe &&& safe) (const respawn)+  safe :: SF (IList ObjectOutput) (Event ())+  safe = arr (gate (Event ()) . not . any (inCircle centre safeRadius . ooPos) . filter ((== Asteroid) . ooObjClass) . IL.elems)++-- gameCore - the signal function that orchestrates the collection of visible signal function objects, such as removing collided objects and spawning new asteroids and ships+gameCore :: RandomGen g => g -> IList Object -> SF (GameInput, IList ObjectOutput) (IList ObjectOutput)+gameCore g objs =+  let (g',_) = split g+  in dpSwitch route objs (arr (killOrSpawn g) >>> notYet) (\sfs f -> gameCore g' (f sfs))++-- gameRound - the signal function that is responsible for playing a single "round" of asteroids, maintaining the player score and life counter+gameRound :: RandomGen g => g -> IList Object -> SF UserInput GameOutput+gameRound g objs = proc ui -> do+  rec+    (crashed,respawn) <- playerCrashed -< oos+    score <- accumHold 0 -< (crashed `tag` (+0)) `lMerge` (asteroidsKilled oos)+    lives <- accumHold initLivesCount -< mergeBy (.) (NoEvent `tag` (+1)) (respawn `tag` (subtract 1))+    newRound <- accum initAsteroidsCount <<< delayEvent respawnDelay <<< allAsteroidsKilled -< oos+    let gi = GameInput {+               giUserInput = ui,+               giNewRound  = newRound,+               giRespawn   = respawn+             }+    oos <- gameCore g objs -< (gi, oos)+  +  gameOver <- edge -< lives <= 0+  +  returnA -< GameOutput {+               goGraphic  = overGraphics [drawObjectOutput oos,+                                          drawScore score,+                                          drawLives lives      ],+               goGameOver = gameOver `tag` score+             }++-- menu - the signal function that is responsible for displaying menu events+menu :: RandomGen g => g -> SF UserInput Graphic+menu g = proc ui -> do+  rec+    let gi = GameInput {+               giUserInput = ui,+               giNewRound  = NoEvent,+               giRespawn   = NoEvent+             }+    oos <- gameCore g objs -< (gi, oos)+  returnA -< do { drawObjectOutput oos ; menuGraphics }+ where+  objs :: IList Object+  objs = IL.fromList . flip evalRand g . initAsteroids 8 $ (centreX + 100, centreY + 100)+  menuGraphics :: Graphic+  menuGraphics = do+    overGraphics . map (drawFigure . Translate (centreX, centreY))+      . Text.fromString Text.AlignCentre $ "haskelloids"+    overGraphics . map (drawFigure . Translate (centreX, centreY+100) . Scale 0.5)+      . Text.fromString Text.AlignCentre $ "click to start"++-- gameOver - the signal function that displays the score and waits for user click to go back to the menu+gameOver :: Score -> SF UserInput Graphic+gameOver scr = constant graphics+ where+  graphics :: Graphic+  graphics = do+    overGraphics . map (drawFigure . Translate (centreX, centreY))+      . Text.fromString Text.AlignCentre $ "final score " ++ show scr++-- playRound - play a single round of the game+playRound :: RandomGen g => g -> IList Object -> SF UserInput (Graphic, Event Score)+playRound g init = proc ui -> do+  go <- gameRound g init -<  ui+  returnA -< (goGraphic go, goGameOver go)++-- endlessLoop - play endless rounds+endlessLoop :: RandomGen g => g -> SF UserInput Graphic+endlessLoop g = +  let (initObjs, g') = flip runRand g initGameObjects+      (g'', _) = split g+  in switch (menu g &&& arr uiLeftClick)+       (\_ -> switch (playRound g' initObjs)+                (\sc -> switch (gameOver sc &&& arr uiLeftClick) (\_ -> endlessLoop g'')))++-- ## Main function #############################++main :: IO ()+main = do+  g <- newStdGen+  runGraphics $ do+    w <- openWindowEx ("Haskelloids v" ++ verNo) Nothing (wWidth, wHeight)+           DoubleBuffered (Just refreshPeriod')+    reactimate initUI (sense w) (actuate w) (endlessLoop g)