Snake in Haskell with Hamilton Loop

After completing the course on Haskell, I decided to consolidate my knowledge with the first project. We will write a snake for the terminal. To make the game unique, let's add a bot that will go through the game itself.





The project is written on haskell-platform, Ubuntu 20.04.





GitHub project





Game loop

. , . Control.Concurrent. forkIO MVar. , tryInput Maybe Char , . . System.IO - EOL . , hSetBuffering stdin NoBuffering Windows - getChar EOL . System.Console.ANSI .





UPDATE

hReady, @GospodinKolhoznik





import Control.Concurrent
import System.Console.ANSI
import System.IO

gameLoop :: ThreadId -> MVar Char -> IO ()
gameLoop inputThId input = do
  tryInput <- tryTakeMVar input
  gameLoop inputThId input  

inputLoop :: MVar Char -> IO ()
inputLoop input = (putMVar input =<< getChar) >> inputLoop input 

main = do
  hSetBuffering stdin NoBuffering 
  hSetEcho stdin False
  clearScreen
  input <- newEmptyMVar
  inputThId <- forkIO $ inputLoop input
  gameLoop inputThId input
      
      



. 4 : Process - , Bot - , GameOver Quit. data World, - gameLoop. , , . . (0,0) . , 4 .





data StepDirection  =  DirUp
                   	 | DirDown
                     | DirLeft
                   	 | DirRight deriving (Eq)   
type Point = (Int, Int)
type Snake = [Point]
data WorldState = Process 
               	    | GameOver
               	    | Quit 
               	    | Bot deriving (Eq)   
data World = World  { snake :: Snake
                    , direction :: StepDirection
                    , fruit :: Point
                    , worldState :: WorldState
                    }        
gameLoop :: ThreadId -> MVar Char -> World -> IO ()
{-- … --}
      
      



. Data.Time.Clock. 3 : lastUpdateTime - , updateDelay - isUpdateIteration - . timerController. isUpdateIteration, .





import Data.Time.Clock

data World 							 = World  { 
                   	          		{-- … --}
                                  , lastUpdateTime :: UTCTime
                                  , updateDelay :: NominalDiffTime
                                  , isUpdateIteration :: Bool
                                  }  

initWorld :: UTCTime -> World
initWorld timePoint = World { snake = [(10, y) | y <- [3..10]] 
                            , direction = DirRight
                            , fruit = (3, 2)
                            , lastUpdateTime = timePoint
                            , updateDelay = 0.3
                            , isUpdateIteration = True
                            , worldState = Process
                            }  

timerController :: UTCTime -> World -> World
timerController timePoint world
  | isUpdateTime timePoint world = world { lastUpdateTime = timePoint
                                         , isUpdateIteration = True 
                                         }
  | otherwise                    = world where
    isUpdateTime timePoint world = 
    	diffUTCTime timePoint (lastUpdateTime world) >= updateDelay world

gameLoop inputThId input oldWorld = do
 {-- … --}
  timePoint <- getCurrentTime
  let newWorld = timerController timePoint oldWorld
  gameLoop inputThId input newWorld { isUpdateIteration = False }  

main = do
  {-- … --}
  timePoint <- getCurrentTime
  gameLoop inputThId input (initWorld timePoint)
      
      



inputController. WSAD . , , , 1 . , . , , updateDelay. pointStep , , .





pointStep :: StepDirection -> Point -> Point
pointStep direction (x, y) = case direction of
  DirUp -> (x, y - 1) 
  DirDown -> (x, y + 1)
  DirLeft -> (x - 1, y)
  DirRight -> (x + 1, y)

inputController :: Maybe Char -> World -> World
inputController command world = let
  boost dir1 dir2 = if dir1 == dir2 then 0.05 else 0.3
  filterSecondSegmentDir (x:[]) dirOld dirNew = dirNew
  filterSecondSegmentDir (x:xs) dirOld dirNew | pointStep dirNew x == head xs = dirOld
                                              | otherwise = dirNew in 
    case command of
    Just 'w' -> world { direction = filterSecondSegmentDir (snake world) (direction world) DirUp 
                      , updateDelay = boost (direction world) DirUp
                      , worldState = Process
                      }
    Just 's' -> world { direction = filterSecondSegmentDir (snake world) (direction world) DirDown
                      , updateDelay = boost (direction world) DirDown
                      , worldState = Process
                      }
    Just 'a' -> world { direction = filterSecondSegmentDir (snake world) (direction world) DirLeft 
                      , updateDelay = boost (direction world) DirLeft
                      , worldState = Process
                      }
    Just 'd' -> world { direction = filterSecondSegmentDir (snake world) (direction world) DirRight 
                      , updateDelay = boost (direction world) DirRight
                      , worldState = Process
                      }
    Just 'q' -> world { worldState = Quit }
    Just 'h' -> world { worldState = Bot }
    _        -> world { updateDelay =  0.3 }
      
      



moveController , isUpdateIteration .





snakeStep :: StepDirection -> Snake ->  Snake
snakeStep direction snake = (pointStep direction $ head snake):(init snake)

moveController :: World -> World
moveController world 
  | not $ isUpdateIteration world = world
  | otherwise = world { snake = snakeStep (direction world) (snake world) }
      
      



. , . , . (1,1) — (20,20) — .





initWalls :: Walls
initWalls = ((1,1),(20,20))
      
      



, . Haskell System.Random, randomR. , , randomR , . . , . .





import System.Random

data World = World  { 
                    {-- … --}
                    , oldLast :: Point
                    , rand :: StdGen
                    }    
initWorld timePoint = World   {
                              {-- … --}
                              , oldLast = (0, 0)
                              , rand = mkStdGen 0
                              } 

{-- … --}
timerController timePoint world
  | isUpdateTime timePoint world  = world { 
                                          {-- … --}
                                          , oldLast = last $ snake world
                                          }
{-- … --}                                       
      
      



.





collisionSnake :: Snake -> Bool
collisionSnake (x:xs) = any (== x)  xs

collisionWall :: Point -> Walls -> Bool
collisionWall (sx,sy) ((wx1,wy1),(wx2,wy2)) = 
	sx <= wx1 || sx >= wx2 || sy <= wy1 || sy >= wy2
      
      



collisionController. GameOver , . , . , 1 , GameOver .





collisionController :: World -> World
collisionController world
  | not $ isUpdateIteration world                = world
  | collisionSnake $ snake world                 = world { worldState = GameOver } 
  | collisionWall (head $ snake world) initWalls = world { worldState = GameOver }
  | checkWin (snake world) initWalls             = world { worldState = GameOver }
  | collisionFruit (snake world) (fruit world)   = world { snake = 
                                                           (snake world) ++ [oldLast world]
                                                         , fruit = newFruit
                                                         , rand = newRand
                                                         }
  | otherwise                                    = world where
    checkWin snake ((x1, y1),(x2, y2)) = (x2 - x1 - 1) * (y2 - y1 - 1) - length snake == 1
    collisionFruit snake fruit = fruit == head snake
    (newFruit, newRand) = freeRandomPoint world (rand world)
    randomPoint ((minX, minY), (maxX, maxY)) g = let
      (x, g1) = randomR (minX + 1, maxX - 1) g
      (y, g2) = randomR (minY + 1, maxY - 1) g1 in 
        ((x, y), g2)
    freeRandomPoint world g | not $ elem point ((fruit world):(snake world)) = 
                                (point, g1)
                            | otherwise = freeRandomPoint world g1 where
                                (point, g1) = randomPoint initWalls g
      
      



. drawPoint . renderWorld . isUpdateIteration, moveController, collisionController renderWorld . , . .





renderWorld :: World -> IO ()
renderWorld world
  | not $ isUpdateIteration world = return ()
  | otherwise = do
    drawPoint '@' (fruit world)  
    drawPoint ' ' (oldLast world)
    mapM_ (drawPoint 'O') (snake world)
    setCursorPosition 0 0 

drawPoint :: Char -> Point -> IO ()
drawPoint char (x, y) = setCursorPosition y x >> putChar char   

drawWalls :: Char -> Walls -> IO ()
drawWalls char ((x1, y1),(x2, y2)) = do
  mapM_ (drawPoint char) [(x1, y)| y <- [y1..y2]]
  mapM_ (drawPoint char) [(x, y1)| x <- [x1..x2]]
  mapM_ (drawPoint char) [(x2, y)| y <- [y1..y2]] 
  mapM_ (drawPoint char) [(x, y2)| x <- [x1..x2]]

main = do
  {-- … --}
  drawWalls '#' initWalls
  {-- … --}
      
      



.





gameLoop inputThId input oldWorld = do
  {-- … --}
  let newWorld = collisionController . moveController $ timerController timePoint (inputController tryInput oldWorld) 
  renderWorld newWorld
  {-- … --}
      
      



. . CodeBullet. @RussianDragon. .





: - , . . : Path - , ClosedPath - .





type Path = [Point]
type ClosedPath = [Point]
      
      



, wallsFirstPoint . . isPathContain , . clockwise . distBetweenPoints - , .





clockwise = [DirUp, DirRight, DirDown, DirLeft]

wallsFirstPoint :: Point
wallsFirstPoint = ((fst $ fst initWalls) + 1, (snd $ fst initWalls) + 1)

isPathContain :: Path -> Point -> Bool
isPathContain path point = any (== point) path

distBetweenPoints :: Point -> Point -> Int
distBetweenPoints (x1, y1) (x2, y2) = abs (x1 - x2) + abs (y1 - y2)
      
      



getHamPath. , , . , , . , nextHamPathPoint. 4 . , . , nextHamPathPoint , . , .





getHamPath :: Point -> ClosedPath -> ClosedPath
getHamPath currentPoint hamPath  | hamPathCapacity initWalls == length (currentPoint:hamPath) 
                                   && distBetweenPoints currentPoint (last hamPath) == 1 
                                     = currentPoint:hamPath
                                 | otherwise = getHamPath newPoint (currentPoint:hamPath) where
                                    newPoint = nextHamPathPoint (currentPoint:hamPath) clockwise
                                    hamPathCapacity ((x1, y1),(x2, y2)) = (x2 - x1 - 1) * (y2 - y1 - 1) 

nextHamPathPoint :: Path -> [StepDirection] -> Point
nextHamPathPoint _       []         = error "incorrect initWalls"
nextHamPathPoint hamPath (dir:dirs) | isPathContain hamPath virtualPoint 
                                      || collisionWall virtualPoint initWalls = 
                                      		nextHamPathPoint hamPath dirs 
                                    | otherwise = virtualPoint where
  virtualPoint = pointStep dir (head hamPath)
      
      



.





data World = World  { 
                    {-- … --} 
                    , hamPath :: ClosedPath
                    }    
initWorld timePoint = World   {
                              {-- … --}
                              , hamPath = getHamPath wallsFirstPoint []
                              } 
      
      



2 . , , . DirFromHead DirFromTail .





data PathDirection = DirFromHead | DirFromTail deriving (Eq)
      
      



nextDirOnPath, . (botStepDir, botPathDir) . . DirFromHead, , .





moveController world 
  {-- … --}
  | worldState world == Process = world {snake = snakeStep (direction world) (snake world)}
  | otherwise                   = world { snake = snakeStep botStepDir (snake world)
                                        , hamPath = if botPathDir == DirFromTail then hamPath world else reverse $ hamPath world  
                                        } where 
                                           (botStepDir, botPathDir) = nextDirOnPath (snake world) (hamPath world) 

nextDirOnPath :: Snake -> ClosedPath -> (StepDirection, PathDirection)
nextDirOnPath = undefined
      
      



: dirBetweenPoints pointNeighborsOnPath .





dirBetweenPoints :: Point -> Point -> StepDirection
dirBetweenPoints (x1, y1) (x2, y2)  | x1 == x2 = if y1 > y2 then DirUp else DirDown
                                    | y1 == y2 = if x1 > x2 then DirLeft else DirRight 
                                    | otherwise = if abs (x1 - x2) < abs (y1 - y2) then 
                                        dirBetweenPoints (x1, 0) (x2, 0) else
                                        dirBetweenPoints (0, y1) (0, y2)  

pointNeighborsOnPath :: Point -> ClosedPath -> (Point, Point)
pointNeighborsOnPath point path | not $ isPathContain path point || length path < 4 = error "incorrect initWalls"
                                | point == head path = (last path, head $ tail path)
                                | point == last path = (last $ init path, head path)
                                | otherwise = _pointNeighborsOnPath point path where
                                  _pointNeighborsOnPath point (a:b:c:xs) = if point == b then (a,c) else _pointNeighborsOnPath point (b:c:xs)
      
      



, , .





nextDirOnPath :: Snake -> ClosedPath -> (StepDirection, PathDirection)
nextDirOnPath (snakeHead:snakeTail)  path   | snakeTail == [] = (dirBetweenPoints snakeHead point1, DirFromTail) 
                                            | point1 == head snakeTail = (dirBetweenPoints snakeHead point2, DirFromHead)
                                            | otherwise = (dirBetweenPoints snakeHead point1, DirFromTail) where 
                                                (point1, point2) = pointNeighborsOnPath snakeHead path
      
      



, , .





, : collisionSnakeOnPath , distBetweenPointsOnPath . DirFromTail , DirFromHead.





collisionSnakeOnPath :: Snake -> Point -> ClosedPath -> PathDirection -> Bool
collisionSnakeOnPath snake point path pathDir | null $ common snake pathPart = False
                                              | otherwise = True where
  pathPart = takePathPart point (if pathDir == DirFromHead then path else reverse path) (length snake)
  common xs ys = [ x | x <- xs , y <- ys, x == y]
  takePathPart point path len = _takePathPart point (path ++ (take len path)) len where
    _takePathPart _     []     _    = []
    _takePathPart point (x:xs) len  | x == point = x:(take (len - 1) xs)
                                    | otherwise = _takePathPart point xs len

distBetweenPointsOnPath :: Point -> Point -> ClosedPath -> (Int, Int)
distBetweenPointsOnPath point1 point2 path  | point1 == point2 = (0, 0)
                                            | id1 < id2 = (length path - id2 + id1,id2 - id1)
                                            | otherwise = (id1 - id2, length path - id1 + id2) where
  (id1,id2) = pointIndexOnPath (point1,point2) path 0 (0,0)
  pointIndexOnPath _               []     _   ids                     = ids
  pointIndexOnPath (point1,point2) (x:xs) acc (id1,id2) | x == point1 = pointIndexOnPath (point1,point2) xs (acc+1) (acc,id2)
                                                        | x == point2 = pointIndexOnPath (point1,point2) xs (acc+1) (id1,acc) 
                                                        | otherwise   = pointIndexOnPath (point1,point2) xs (acc+1) (id1,id2)  
      
      



. enterPointBypass , . , nextDirOnPath.





nextDirBot :: Snake -> Point -> ClosedPath -> (StepDirection, PathDirection)
nextDirBot snake fruit path | distBypass1 < distBypass2 && distBypass1 < distToFruit1 
                              && not (collisionSnakeOnPath snake enterPointBypass path DirFromTail) 
                                = (dirBetweenPoints (head snake) enterPointBypass, DirFromTail)
                            | distBypass2 < distToFruit1 
                              && not (collisionSnakeOnPath snake enterPointBypass path DirFromHead) 
                                = (dirBetweenPoints (head snake) enterPointBypass, DirFromHead) 
                            | otherwise = nextDirOnPath snake path where
  dirBypass = dirBetweenPoints (head snake) fruit
  enterPointBypass = pointStep dirBypass (head snake)
  (distBypass1, distBypass2) = distBetweenPointsOnPath enterPointBypass fruit path
  (distToFruit1, _) = distBetweenPointsOnPath (head snake) fruit path
      
      



2 . enterPointBypass , , . , , , , , .





Let's connect our nextDirBot to the snake motion controller, add a menu and look at the result.








All Articles