packages feed

snake (empty) → 0.1.0.0

raw patch · 9 files changed

+264/−0 lines, 9 filesdep +basedep +randomdep +splitsetup-changed

Dependencies added: base, random, split, terminal-size

Files

+ LICENSE view
@@ -0,0 +1,20 @@+Copyright (c) 2016 Habib Alamin++Permission is hereby granted, free of charge, to any person obtaining+a copy of this software and associated documentation files (the+"Software"), to deal in the Software without restriction, including+without limitation the rights to use, copy, modify, merge, publish,+distribute, sublicense, and/or sell copies of the Software, and to+permit persons to whom the Software is furnished to do so, subject to+the following conditions:++The above copyright notice and this permission notice shall be included+in all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ Main.hs view
@@ -0,0 +1,29 @@+module Main where++import System.Exit (exitSuccess)++import Terminal+import World+import Movement++main :: IO ()+main = catchInput $ randomWorld >>= snake++snake :: World -> IO ()+snake world = if gameOver world+    then printWorld world >>+        getChar >> clearScreenWorldPassthru world >>+        exitSuccess+    else printWorld world >>+        getNextMove >>= updateWorldIO world >>=+        clearScreenWorldPassthru >>= snake where+            printWorld = putStr . show+            updateWorldIO world move = updatePelletCoords world >>=+                \coords -> return $ updateWorld move coords world+            changeMoveArgumentToLast fn m p w = fn p w m+            clearScreenWorldPassthru world = clearScreen world >> return world where+                -- We add 1 to the height to account for the top border. We+                -- are already on the same line as the bottom border (as we+                -- use putStr to avoid printing a newline after the world),+                -- so no need to need to account for that.+                clearScreen world = putStr $ "\r\ESC[" ++ show (worldHeight world + 1) ++ "A"
+ Movement.hs view
@@ -0,0 +1,45 @@+module Movement where++import System.IO (hReady, stdin)+import Control.Concurrent (threadDelay)+import Control.Monad (replicateM)++data Move = Up | Down | Left | Right | Noop deriving (Show, Eq, Enum)++data Direction = North | East | South | West deriving (Show, Eq, Enum)+data Coords = Coords Integer Integer deriving Eq++instance Show Coords where+    show (Coords x y) = "(x: " ++ show x ++ ", y: " ++ show y ++ ")"++instance Ord Coords where+    (Coords x y) `compare` (Coords a b) = let yAxisOrdering = y `compare` b+        in if yAxisOrdering == EQ then x `compare` a else yAxisOrdering++mkMove :: String -> Move+mkMove "\ESC[A" = Up+mkMove "\ESC[B" = Down+mkMove "\ESC[C" = Movement.Right+mkMove "\ESC[D" = Movement.Left+mkMove _        = Noop++getNextMove :: IO Move+getNextMove = mkMove <$> keys where+    -- MAGICNUMBER: this feels like a good pace when repeated, but it+    -- should probably be customisable & labelled for why I chose it?+    -- SRP: I think this function does too much, and assumes too much+    -- about its callers.+    keys = threadDelay 100000 >> hReady stdin >>= getFirstChar >>= getRestChars >>= flushInput where+        getFirstChar ready = if ready then sequence [getChar] else return "\NUL"+        getRestChars firstChar = if firstChar == "\ESC" then+            (firstChar ++) <$> replicateM 2 getChar else+            return firstChar+        flushInput caughtChars = hReady stdin >>=+            \ready -> if ready then getChar >> flushInput caughtChars else return caughtChars++indexToCoords :: Integral a => a -> a -> Coords+indexToCoords width index = tupleToCoords $ swap $ index `divMod` width where+    swap (x,y) = (y,x)++tupleToCoords :: Integral a => (a, a) -> Coords+tupleToCoords (x,y) = Coords (fromIntegral x) (fromIntegral y)
+ README.md view
@@ -0,0 +1,4 @@+Snake+=====++A basic console snake game.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ Snake.hs view
@@ -0,0 +1,11 @@+module Snake where++import Movement++data Snake = Snake { getDirection :: Direction+                   , getCoords :: [Coords] } deriving Eq++instance Show Snake where+    show (Snake direction []) = "Snake " ++ show direction ++ " []"+    show (Snake direction coords) = "Snake " ++ show direction ++ " [" ++ unwords (map showSnakeCoord coords) ++ "]" where+        showSnakeCoord (Coords x y) = "(" ++ show x ++ " " ++ show y ++ ")"
+ Terminal.hs view
@@ -0,0 +1,21 @@+module Terminal where++import System.IO (stdin+                , hGetEcho+                , hGetBuffering+                , hSetEcho+                , hSetBuffering+                , BufferMode(..))+import Data.Maybe (fromMaybe)++import qualified System.Console.Terminal.Size as T++catchInput :: IO a -> IO a+catchInput action = (,) <$> hGetEcho stdin <*> hGetBuffering stdin >>=+    \(echo, buffering) -> hSetEcho stdin False >> hSetBuffering stdin NoBuffering >> action >>=+    \result -> hSetEcho stdin echo >> hSetBuffering stdin buffering >> return result++getTerminalWidthHeight :: Integral a => IO (a, a)+getTerminalWidthHeight = maybeWindowToWidthHeight <$> T.size where+    maybeWindowToWidthHeight maybeWindow = (T.width window, T.height window) where+        window = fromMaybe T.Window { T.width = 0, T.height = 0 } maybeWindow
+ World.hs view
@@ -0,0 +1,105 @@+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
+ snake.cabal view
@@ -0,0 +1,27 @@+--  See http://haskell.org/cabal/users-guide++name:                snake+version:             0.1.0.0+synopsis:            A basic console snake game.+description:         A basic console snake game with rudimentary ASCII-based graphics and minimal dependencies.+homepage:            http://code.alaminium.me/habibalamin/snake+license:             MIT+license-file:        LICENSE+author:              Habib Alamin+maintainer:          ha.alamin@gmail.com+category:            Game+build-type:          Simple+extra-source-files:  README.md+cabal-version:       >=1.10++executable snake+  main-is:             Main.hs+  build-depends:       base >=4.8 && <4.9,+                       terminal-size >=0.3.2.1 && <0.3.3,+                       split >=0.2.3 && <0.2.4,+                       random >=1.1 && <1.1.1+  other-modules:       Terminal,+                       World,+                       Snake,+                       Movement+  default-language:    Haskell2010