# Snake in Purescript

How to program a simple game in Purescript

Posted: 2016-11-06 , Modified: 2016-11-06

## Introduction

• Play the game here.
• Check out the code on github.
• The code for Snake.purs is here.

Purescript is a functional programming language that compiles down to Javascript. In this post, I describe the process of building a simple game, Snake, in PureScript. Along the way, we’ll see how input/signals work, how to render to the screen, and how to work with arrays. I assume familiarity with functional programming. I’ll make some comparisons with Haskell and Elm.

I like Purescript because it has the functional power of Haskell, while fitting very well into the web ecosystem (e.g. interops with Javascript), like Elm. Think of it as Elm with all the features that come with Haskell, such as monads.

Some resources:

• Reference
• Purescript for Haskell/Elm users
• Community: The folks on Slack and IRC have been very helpful in helping me get set up and debugging my code.
• Other things written using purescript-signal

## Getting started

First follow the instructions here to install Purescript.

Start a new project by

mkdir basic-games
cd basic-games
pulp init

Create a file Snake.purs in src/.

## Imports

First, the imports.

module Snake where

import Prelude -- must be explicitly imported
import Control.Monad.Eff.Console (CONSOLE, log)
import Control.Monad.Eff.Random (RANDOM, randomInt)
import Data.Array (length, uncons, slice, (:), last)
import Data.Functor
import Data.Generic
import Data.Int
import Data.Maybe
import Data.Traversable
import Data.Tuple
import Graphics.Canvas (CANVAS, closePath, lineTo, moveTo, fillPath,
setFillStyle, arc, rect, getContext2D,
getCanvasElementById, Context2D, Rectangle, clearRect)
import Partial.Unsafe (unsafePartial)
import Signal (Signal, runSignal, foldp, sampleOn, map4)
import Signal.DOM (keyPressed)
import Signal.Time (Time, second, every)
import Test.QuickCheck.Gen -- for randomness

You will need to install all these packages. You can install them one at a time via

bower install <package-name> --save

or copy the bower.json file here and do bower install to install all of them at once.

## The Model-Update-View-Signal Architecture

We break our program into the following parts. (For more information, see the Elm architecture tutorial.)

• Model: This is a complete description of the state of the program at any time. For Snake, this would include the array containing all points of the snake, the direction it is going, and the location of the mouse.
• Update: Define a step function which given an input and the current model, returns the updated model. For Snake, an update would be the direction, and the step function would move the snake one unit in that direction, and check for things like whether the snake ate the mouse or bumped into the wall.
• View: Given a model, describe how to render it on the screen.
• Signal: Specify the input signals. A signal of type a is a time-varying value of type a. For a primer on signals see here.1

To put these components together we use:

• foldp, the magic function that “folds” the input signals into the model via the step function. Note the similarity to foldl both in the type signature and the picture. In the picture a corresponds to Input and b corresponds to Model. So given time-varying inputs and a starting model, foldp produces a time-varying model.

foldp :: forall a b. (a -> b -> b) -> b -> (Signal a) -> (Signal b)
• runSignal: Given a signal encapsulating an effect, makes the effect happen.

runSignal :: forall e. Signal (Eff e Unit) -> Eff e Unit

(In the old version of Elm, main is simply a Signal, but here, the type of main is an analogue of IO in Haskell.)

The skeleton of the program looks like this.

-- Model
type Model = ???

start :: Model
start = ???

-- Update
type Input = ???

step :: Input -> Model -> Model
step inp m = ???

-- View
render :: Model -> Eff _ Unit
render m = ???

-- Signal
input :: Eff _ (Signal Input)
input = ???

-- Putting it all together
main :: Eff _ Unit
main =
void do
signal <- input
-- game :: Signal Model
game <- foldp step start signal
-- map :: forall a b. (a -> b) -> f a -> f b
--        (Model -> Eff _ Unit) -> Signal Model -> Signal (Eff _ Unit)
runSignal (map render game)

The Eff monad corresponds to Haskell’s IO monad, but uses extensible effects: it lists out explicitly all the effects it has (access to randomness, DOM, drawing canvas, console, etc.). For example, Eff (random :: RANDOM, canvas :: CANVAS | eff) Unit means having effects that include RANDOM and CANVAS). PureScript will infer all effects if you put _. See Chapter 8.10 of the PureScript book.

For a warm-up, here is a basic example following this paradigm. Here, the model is an integer (the location on a single axis), the update is moving -1, 0, or 1 units, the view is simply writing the location to the console, and the signal comes from the arrow keys once every second.

## Model

For Snake, the model is a record containing

• the location of the mouse
• snake as an array of points
• whether the snake is alive

We also include some the dimensions xd, yd of the game board, the size of each square (alternatively, these can be hard-coded in), and the prev square (this is optional—it gives us an easy way to erase the tail of the snake when it moves). Valid positions are in $$[1,xd]\times [1,yd]$$.

We want the mouse location to be generated at random. How to do this? We’ll fill in that part later. (For now, you can put in an arbitrary point.)

Note also that the y-coordinate is 0 at the top of the screen and increases going down. The snake starts at the upper-left corner moving right.

type Point = Tuple Int Int

--MODEL
type Snake = Array Point

type Model = {xd :: Int, yd :: Int, size :: Int, mouse:: Point, snake :: Snake, dir :: Point, alive :: Boolean, prev :: Maybe Point}

start :: Model
start =
{xd : 25, yd : 25, size : 10, mouse : ??, snake : [Tuple 1 1], dir: Tuple 1 0, alive : true, prev : Nothing}

• there is no syntactic sugar for Tuple.
• the basic list type is Array.
• booleans are Boolean and have values true or false.

## Update

First we need two helper function: check to see if a point is in bounds, and check to see if the snake is OK (given the point where the head of the snake moves, check to see that it is in bounds and not part of the snake’s body).

inBounds :: Point -> Model -> Boolean
inBounds (Tuple x y) m =
(x > 0) && (y > 0) && (x <= m.xd) && (y <= m.yd)

checkOK :: Point -> Model -> Boolean
checkOK pt m =
let
s = m.snake
in
m.alive && (inBounds pt m) && not (pt elem (body s))

body :: forall a. Array a -> Array a
body li = slice 0 ((length li) - 1) li

For the step function, there are 3 cases:

• The snake is OK:
• The snake eats the mouse. The snake grows, and regenerate the mouse on a random square. (We omit the randomness for now.)
• The snake doesn’t eat the mouse, in which case it moves. Here body is all of the snake except the last point.
• The snake is not OK: alive = false and nothing else happens.
step :: Partial => Point -> Model -> Model
step dir m =
let
-- override the direction with the input, unless there is no input (corresponding to (0,0))
d = if dir /= Tuple 0 0
then dir
else m.dir
s = m.snake
let hd = (head s + d)
if checkOK hd m
then
if (hd == m.mouse)
then m { snake = hd : s
, mouse = ???
, dir = d
, prev = Nothing -- snake grows; nothing is deleted
}
else m { snake = hd : (body s)
, dir = d
, prev = last s -- snake moves; the last pixel is deleted
}
else m { alive = false, prev = Nothing}

Some differences from Haskell:

• Note that all partial functions must be notated with the empty type constraint Partial. head as imported from Data.Array.Partial is a partial function because it is undefined on []. (We need not worry because the snake will never be empty.)
• Polymorphic functions must have type variables listed out at the beginning after forall.

Note that we can add points! (Note the line head s + d.) This is because Data.Tuple has the instance (Ring a, Ring b) => Ring (Tuple a b). If both components of a tuple can be added, then addition is automatically defined for the tuple.

## View

Given the model, we have 4 things to render: the walls (a $$27\times 27$$ rectangle here), the background (a $$25\times 25$$ rectangle), the snake and the mouse. For instructions on using purescript-canvas see Chapter 9 of the PureScript book. Change the colors as you wish.

render :: forall eff. Partial => Model -> (Eff _ Unit)
render m =
void do
let s = m.snake
let size = m.size
Just canvas <- getCanvasElementById "canvas"
ctx <- getContext2D canvas
--walls
setFillStyle wallColor ctx
fillPath ctx $rect ctx { x: 0.0 , y: 0.0 , w: toNumber$ size*(m.xd + 2)
, h: toNumber $size*(m.yd + 2) } --interior setFillStyle bgColor ctx fillPath ctx$ rect ctx
{ x: toNumber $size , y: toNumber$ size
, w: toNumber $size*(m.xd) , h: toNumber$ size*(m.yd)
}
--snake
for s (\x -> colorSquare m.size x snakeColor ctx)
--mouse
colorSquare m.size (m.mouse) mouseColor ctx

colorSquare :: forall eff. Int -> Point -> String -> Context2D -> Eff (canvas :: CANVAS | eff) Context2D
colorSquare size (Tuple x y) color ctx = do
setFillStyle color ctx
fillPath ctx $rect ctx$ square size x y

square :: Int -> Int -> Int -> Rectangle
square size x y = { x: toNumber $size*x , y: toNumber$ size*y
, w: toNumber $size , h: toNumber$ size
}

white = "#FFFFFF"
black = "#000000"
red = "#FF0000"
yellow = "#FFFF00"
green = "#008000"
blue = "#0000FF"
purple = "800080"

snakeColor = white
bgColor = black
mouseColor = red
wallColor = green

The render function is a bit inefficient since it is redrawing the entire canvas every step (it doesn’t make much difference for such a simple game though). We can replace later calls to render with renderStep which only changes the squares that need to be changed at each time step. (There is much more freedom than Elm to draw specify what you want to draw and redraw.)

renderStep :: forall eff. Partial => Model -> Eff (canvas :: CANVAS | eff) Unit
renderStep m =
void do
let s=m.snake
Just canvas <- getCanvasElementById "canvas"
ctx <- getContext2D canvas
colorSquare m.size (head s) snakeColor ctx
case m.prev of
Nothing -> colorSquare m.size (m.mouse) mouseColor ctx
Just pt -> colorSquare m.size pt bgColor ctx
--make use of the fact: either we draw the mouse or erase the tail, not both, at any one step


## Signal

The step function takes an update of type Point, so we need to produce a Signal Point.

purescript-signal contains signals from various sources, e.g. which keys are pressed and the time. We use the following functions from there:

• keyPressed :: forall e. Int -> Eff (dom :: DOM | e) (Signal Boolean). (Note we need access to the DOM to get a keybord signal.) The key codes for L/U/D/R are 37, 38, 40, 39. We map them to $$(-1,0), (0,-1), (0,1), (1,0)$$, respectively.
• every :: Time -> Signal Time periodically signals the time.
• sampleOn :: forall a b. (Signal a) -> (Signal b) -> (Signal b) creates a signal which yields the current value of the second signal every time the first signal yields. Usually the first signal is a periodic time signal (e.g. every second).

Below, input gives a direction corresponding to the arrow key pressed every 1/20 second.

--SIGNALS
inputDir :: Eff _ (Signal Point)
inputDir =
let
f = \l u d r -> ifs [Tuple l $Tuple (-1) 0, Tuple u$ Tuple 0 (-1), Tuple d $Tuple 0 1, Tuple r$ Tuple 1 0] $Tuple 0 0 --note y goes DOWN in map4 f <$> (keyPressed 37) <*> (keyPressed 38) <*> (keyPressed 40) <*> (keyPressed 39)

input :: Eff _ (Signal Point)
input = sampleOn (fps 20.0) <$> inputDir fps :: Time -> Signal Time fps x = every (second/x) ifs:: forall a. Array (Tuple Boolean a) -> a -> a ifs li z = case uncons li of Just {head : Tuple b y, tail : tl} -> if b then y else ifs tl z Nothing -> z  At this point, we can slap in a main and then compile. main :: Eff _ Unit main = void$ unsafePartial do
-- create the signal
dirSignal <- input
game <- foldp step start dirSignal
runSignal (map render game)

But we don’t have randomness yet.

How do we model randomness in a purely functional program?

We need to add randomness as an effect.

### Something that doesn’t work

The initialization and step functions need randomness, so we can try to rewrite the functions so they have the following type signatures:

init :: Eff (random:: RANDOM) Model

step :: forall e. Point -> Eff (random::RANDOM | e) Model -> Eff (random::RANDOM | e) Model

render :: Eff (random:: RANDOM) Model -> Eff _ Unit

input :: Eff _ (Signal Update)

-- Putting it all together
main :: Eff _ Unit
main =
void do
startGame <- init
signal <- input
-- game :: Signal Model
game <- foldp step startGame signal
-- map :: (a -> b) -> f a -> f b
--        (Model -> Eff _ Unit) -> Signal Model -> Signal (Eff _ Unit)
runSignal (map render game)

If you try this you will get very weird behavior: the mouse will jump all over the place! My best explanation for this is that the effects are compiled in un-executed form in the signal, and at each step, the effects are executed starting from the beginning. Not only does this mean that the random numbers generated are different, it means that the the program will run slower and slower. See a discussion here.

### Using a generator

The standard way to do this is to include a Seed in the state, and whenever we need randomness, use the seed to generate a random number and a new seed, and update the seed. We can add a field for seed in Model

But this is clunky, and just the kind of thing that monads make easier to express! This sounds like a State Seed and in fact, the Gen monad is (basically) just this:

type GenState = { newSeed :: Seed, size :: Size }

newtype Gen a = Gen (StateT GenState Identity a)

Our step function will no longer be a -> b -> b, but will draw on randomness, so will take the form a -> b -> Gen b. We now replace

foldp :: forall a b. (a -> b -> b) -> b -> (Signal a) -> (Signal b)

with the function

foldpR :: forall a b e. (a -> b -> Gen b) -> b -> (Signal a) -> Eff (random :: RANDOM | e) (Signal b)

This is very reusable, and will make anything else we write with randomness painless.

For this, we need a bit of footwork. Behind the scenes, we do as we said before: a function f :: a -> b -> Gen b is the same as a function f' :: a -> (b, GenState) -> (b, GenState). We unravel f, and foldp using this f' as the step function and an initial seed. For sake of reusability, I’ve put foldpR in a separate module, in more generality than necessary, here; add import SignalM to Snake.

The function step :: Partial => Point -> Model -> Model is now step :: Partial => Point -> Model -> Gen Model. In the branch of the if statement where the snake eats the mouse, we keep generating points within the dimensions until the point is not part of the snake. Note that PureScript uses pure instead of return. We add pure to the other branches (not shown).

  do
newMouse <- untilM (\pt -> not (pt elem s || pt == hd)) (randomPoint m.xd m.yd)
pure $m { snake = hd : s , mouse = newMouse , dir = d , prev = Nothing -- snake grows; nothing is deleted } The auxiliary functions here are (untilM being the monadic analogue of “repeat until”) untilM :: forall m a. (Monad m) => (a -> Boolean) -> m a -> m a untilM cond ma = do x <- ma if cond x then pure x else untilM cond ma randomPoint :: Int -> Int -> Gen Point randomPoint xmax ymax = do x <- chooseInt 1 xmax y <- chooseInt 1 ymax pure$ Tuple x y

We also similarly incorporate randomness in the starting model by replacing start :: Model with init :: forall e. Eff (random::RANDOM | e) Model. The final main looks like

main :: Eff _ Unit
main =
void \$ unsafePartial do
--draw the board
gameStart <- init
render gameStart
-- create the signals
dirSignal <- input
-- need to be in effect monad in order to get a keyboard signal
game <- foldpR step gameStart dirSignal
runSignal (map renderStep game)

See all the code here.

The version I linked to has a few extra lines of code so that pressing SPACE restarts the game; the additions are in SnakeS.purs, at the bottom.

## Building

To build the project, run

mkdir dist
pulp build
pulp browserify -m Snake/index.js >> dist/Snake.js

Browserify creates a javascript file with all the javascript libraries included. We need a html file to host the javascript. Create html/index.html with a canvas of the appropriate dimensions:

<!DOCTYPE html>
<html>
</html>