8
\$\begingroup\$

I wrote a partial implementation of Snake in Haskell. As of now, it only supports the movement of the snake. However, since the code is getting complex, I'm requesting for a review of the code before I add on the food, growing and scoring functions.

I'd be glad to hear any suggestions on how this code could be written in a more idiomatic and simpler way (namely, stepSnake and step are long and messy).

-- Snake implementation in Haskell.
module Snake (
 newGame
) where
import Control.Lens (set, ix)
import Data.Foldable (toList)
import qualified Data.Sequence as S
-- Size of the grid
n :: Int
n = 5
data Marking = Empty | Food | Snake
 deriving (Show, Read, Eq)
type Grid = [[Marking]]
emptyGrid :: Int -> Grid
emptyGrid n = replicate n $ replicate n Empty
type Coord = (Int, Int)
data Direction = N | E | W | S
 deriving (Show, Read, Eq)
stepCoord :: Coord -> Direction -> Coord
stepCoord (i, j) dir = case dir of
 N -> (i - 1, j)
 E -> (i, j + 1)
 W -> (i, j - 1)
 S -> (i + 1, j)
setCoord :: Marking -> Coord -> Grid -> Grid
setCoord mark (i, j) grid = set (ix i . ix j) mark grid
validCoord :: Coord -> Bool
validCoord (i, j) = i > 0 && j > 0 && i < n && j < n
type Snake = S.Seq Coord
newSnake :: Int -> Snake
newSnake n = S.fromList [c, c', c'']
 where c = (n `div` 2, n `div` 2)
 c' = stepCoord c E
 c'' = stepCoord c' E
stepSnake :: Snake -> Direction -> Maybe (Coord, Snake, Coord)
stepSnake snake dir = if validCoord tail' && not (tail' `elem` toList snake')
 then Just (head, snake'', tail')
 else Nothing
 where (head, snake') = case S.viewl snake of
 S.EmptyL -> error "snake is empty"
 x S.:< xs -> (x, xs)
 tail = case S.viewr snake of
 S.EmptyR -> error "snake is empty"
 _ S.:> x -> x
 tail' = stepCoord tail dir
 snake'' = snake' S.|> tail'
data Game = Game {
 grid :: Grid,
 snake :: Snake
} deriving Show
newGame :: Game 
newGame = Game {
 grid = foldl (flip $ setCoord Snake) (emptyGrid n) (toList snake),
 snake = snake
} where snake = newSnake n
step :: Game -> Direction -> Maybe Game
step (Game grid snake) dir = case stepSnake snake dir of
 Nothing -> Nothing
 Just (head, snake', tail') -> Just $ Game (foldl f grid [(head, Empty), (tail', Snake)]) snake'
 where f grid (coord, mark) = setCoord mark coord grid
Pimgd
22.5k5 gold badges68 silver badges144 bronze badges
asked May 7, 2015 at 13:28
\$\endgroup\$
5
  • \$\begingroup\$ How do I actually make use of this code? \$\endgroup\$ Commented Aug 31, 2020 at 9:57
  • \$\begingroup\$ What do you plan to use it for? \$\endgroup\$ Commented Sep 1, 2020 at 10:33
  • \$\begingroup\$ As a reference for my own exercise of implementing it. Why? \$\endgroup\$ Commented Sep 1, 2020 at 10:52
  • \$\begingroup\$ I think you can start from newGame and see how the grid changes as you run step game dir, hopefully that helps. \$\endgroup\$ Commented Sep 2, 2020 at 13:05
  • \$\begingroup\$ Ok, undestood, thanks. I gave for granted you were presenting a playable game. \$\endgroup\$ Commented Sep 2, 2020 at 13:06

1 Answer 1

5
\$\begingroup\$

It's a little dangerous to define a top-level constant like n, the name isn't descriptive or distinctive and you end up shadowing it quite a bit. A compiling but incorrect typo is almost inevitable. Since you gave it a definitive comment, why not just give it that name instead? Try defaultGridSize.

I can see why you may have used a case statement in stepCoord (to save typing out stepCoord (i, j) repeatedly), but I have a strong preference in the other direction. All pattern-matching which can happen at the top-level generally should.

stepCoord :: Coord -> Direction -> Coord
stepCoord (i, j) N = (i - 1, j)
-- etc...

Or you can write a handy little utility function and reorder your arguments to write a point-free function.

tuply :: (a -> c, b -> d) -> (a, b) -> (c, d) -- tuple, apply, two ply!
tuply (f, g) (a, b) = (f a, g b)
stepCoord :: Direction -> Coord -> Coord
stepCoord N = tuply (subtract 1, id )
stepCoord E = tuply (id , + 1 )
stepCoord W = tuply (id , subtract 1)
stepCoord S = tuply (+ 1 , id )

Generally all your types should be at the top of the file, i.e. Snake and Game.

I might use a list comprehension in writing newSnake to clean it up a little and reduce potential errors from referencing the wrong c('(')) value.

newSnake :: Int -> Snake
newSnake boardSize = S.fromList [(mid, j) | j <- [mid .. mid + 2]]
 where mid = boardSize `div` 2

Right-hand sides composed solely of an if-statement are good candidates for writing guards.

stepSnake snake dir
 | validCoord tail' && not (tail' `elem` toList snake') = Just (head, snake'', tail')
 | otherwise = Nothing

This is a very dense function though, I think it might be easier to write if you wrap the version using a Seq snake around a primitive list version. Your snakes will presumably never grow so long as to actually cause a user noticeable slowdown between steps due to list processing, so you might think about tossing out Seq altogether.

stepSnake :: Snake -> Direction -> Maybe (Coord, Snake, Coord)
stepSnake snake dir = fmap (second fromList) (stepSnakeList (toList snake) dir)
 where second f (a, b, c) = (a, f b, c)
stepSnakeList :: [Coord] -> Direction -> Maybe (Coord, [Coord], Coord)
stepSnakeList snake dir
 | validCoord newHead && not (newHead `elem` snake) = Just (oldTail, newSnake, newHead)
 | otherwise = Nothing
 where
 oldTail = head snake
 body = tail snake
 oldHead = last snake
 newHead = stepCoord oldHead dir
 newSnake = body ++ [newHead]

Use foldl' from Data.List instead of foldl for newGame and step. Why? It's a long story.

answered May 7, 2015 at 17:03
\$\endgroup\$
7
  • 1
    \$\begingroup\$ (Control.Arrow.&&&) can do the job tuply is. \$\endgroup\$ Commented May 7, 2015 at 22:22
  • \$\begingroup\$ Sure, but then you've got to be willing to use Control.Arrow. \$\endgroup\$ Commented May 7, 2015 at 22:39
  • \$\begingroup\$ @bisserlis Thanks for your feedback! I'll follow your suggestions and post a new version for review. @Carl Would you mind giving an example? I'm not familiar with Control.Arrow. \$\endgroup\$ Commented May 8, 2015 at 12:30
  • 1
    \$\begingroup\$ @wei2912 stepCoord N = subtract 1 &&& id would be the first branch if you have Control.Arrow imported. \$\endgroup\$ Commented May 8, 2015 at 12:46
  • \$\begingroup\$ @Carl Alright, giving that a try. \$\endgroup\$ Commented May 8, 2015 at 15:44

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.