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
1 Answer 1
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.
-
1\$\begingroup\$
(Control.Arrow.&&&)
can do the jobtuply
is. \$\endgroup\$Carl– Carl2015年05月07日 22:22:34 +00:00Commented May 7, 2015 at 22:22 -
\$\begingroup\$ Sure, but then you've got to be willing to use
Control.Arrow
. \$\endgroup\$bisserlis– bisserlis2015年05月07日 22:39:25 +00:00Commented 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\$wei2912– wei29122015年05月08日 12:30:04 +00:00Commented May 8, 2015 at 12:30 -
1\$\begingroup\$ @wei2912
stepCoord N = subtract 1 &&& id
would be the first branch if you haveControl.Arrow
imported. \$\endgroup\$Carl– Carl2015年05月08日 12:46:54 +00:00Commented May 8, 2015 at 12:46 -
\$\begingroup\$ @Carl Alright, giving that a try. \$\endgroup\$wei2912– wei29122015年05月08日 15:44:13 +00:00Commented May 8, 2015 at 15:44
newGame
and see how the grid changes as you runstep game dir
, hopefully that helps. \$\endgroup\$