I did not plan on participating in The last Ludum Dare #35 which started around three weeks ago. Ludum Dare more often than not comes at a very inconvenient time for me. It usually starts at around 5am on Saturdays and carries over until around that time on Tuesdays, Since I am working on Sundays the only free time I have to work on LD without taking vacation from work is Saturday and Sunday/Monday evenings. This LD was no exception.
Usually, one of the hardest parts about LD for me is thinking about an idea for a game. On LD 34 I spent most of Saturday deciding on an idea. This time, after discovering the theme for LD35 (Shapeshift), An idea for a game that I could implement in a short amount of time came to me almost immediately.
My idea was making a trivia-like game where one needs to choose the right shape to pass through a wall, similarly to a few game shows I once saw on TV. This idea for a game was pretty simple to implement from scratch: No need for fancy graphics or game mechanics - just a few shapes on the screen.
I figured it will take me around 8-10 hours to implement a basic version of the game. Since it was an acceptable amount of time for me, I decided to go for it and participate.
My weapon choice these days for making games for game jams is PureScript. Since PureScript compiles to JavaScript it is simple for others to play without needing to download anything and it's purely functional as well, just the way I like it.
My workflow was basically writing the code in Emacs in one window and running pulp server
in a terminal. pulp server
automatically re-compiles a project when a source file changes and serves it over http. So all I had to do is make some changes, save the file and refresh the browser. If the project failed to compile, pulp
would print the error messages.
The first thing I started with is assembling the game loop using Signals, that basically means declaring some state type, creating an input signal, and making a loop that will update the state according to the input and will render to canvas.
After that, I started creating the "assets" - the shapes I was going to use in the game. I made the few shapes I wanted to use using combinators from purescript-drawing and started playing with them on the screen, seeing how the move and change when I press the buttons on my keyboard and adjusting accordingly.
The next few steps were pretty standard: Add more data to the global state like current shape, the questions, the wall, etc, adding the logic to the state update function and deciding how to draw the state. A few hours spent and I had a working game.
There were, however, a few more features that could make the game a bit more interesting. I could let the users create their own trivias, and I could also add some sounds to the game. To do these I'd first have to learn a bit about fetching data from the outside world and parsing JSON in purescript, I'll also need to learn how to play sounds (which actually led me to learn how to wrap an existing javascript library and use it from purescript).
I had one evening for each problem, I'd need to learn these features and use them. I'd like to share with you the few things I learnt :)
How to make user questions?
My approach was:
- Get a url to a JSON defining the questions as a query parameter
- Retrieve the data from that url
- parse it to a purescript data structure
Let's go over the stages one by one.
1. Get a url to a JSON defining the questions as a query parameter
To do this I needed to use the following packages:
- purescript-dom - getting the query parameters string
- purescript-uri - parser for the query string
- purescript-string-parsers - actually running the parser
Here is a snippet that gets the query string parameters as a mapping from query name to value.
module QueryParams where
import Prelude
import Data.Either (Either)
import Control.Monad.Eff (Eff)
import DOM (DOM)
import DOM.HTML (window)
import DOM.HTML.Window (location)
import DOM.HTML.Location (search)
import Data.URI.Query (parseQuery, Query)
import Text.Parsing.StringParser (runParser, ParseError)
queryParams :: forall e. Eff ( dom :: DOM | e ) (Either ParseError Query)
queryParams = map (runParser parseQuery) (window >>= location >>= search)
2. Retrieve the data from a Url
This is dead simple using purescript-affjax. All that is needed is to write get url
and affjax will fetch it for you.
3. Parse the data
So I (maybe) got a JSON containing questions and answers, how do I parse it?
First, let's see the model for the Q/As:
newtype QA = QA
{ question :: String
, answers :: Array Ans
}
newtype Ans = Ans (Tuple Boolean String)
type QAs = Zipper QA
where Zipper is a common zipper data structure. You can see the implementation of it here.
Let's look at an example input we are planning to parse:
[{
"question": "1 + 1 =",
"answers": [{
"correct": true,
"value": "2"
}, {
"correct": false,
"value": "3"
}]
}]
This is already pretty simillar to QA
, the only difference is that QAs
is using a Zipper
and not an Array
as the JSON. we can fix this easily:
toQAs :: Array QA -> Maybe (Zipper QA)
toQAs as = case uncons as of
Just {head: x, tail: xs} -> Just $ fromArray x xs
Nothing -> Nothing
But first, we need to parse the data. The purescript-foreign
package
contains a function readJSON
which can try to parse this object as a type that implements the IsForeign
typeclass.
Because we need it to parse Array QA
, we need Array QA
to have an instance of
IsForeign
. Fortunately, most basic types in purescript already have an instance for that class,
such as Boolean
, String
and IsForeign a => Array a
.
So all that is needed is to implement an IsForeign
instance for the types QA
and Ans
.
instance ansIsForeign :: IsForeign Ans where
read value = do
q <- readProp "correct" value
v <- readProp "value" value
pure $ Ans (Tuple q v)
instance qaIsForeign :: IsForeign QA where
read value = do
q <- readProp "question" value
as <- readProp "answers" value
pure $ QA { question: q, answers: as }
Now we can just use readJSON
to try and parse the result we get from affjax's get
, and that's it!
Playing Audio
For the second interesting part, we'll look at how to wrap a function or two from a javascript library.
To play audio in the browser I have chosen to use howler.js, mainly for it's ease of use. Fortunately at the time bindings for such library were not available in purescript so I got to learn how to wrap it myself.
Looking at the documentation of howler.js, I basically needed only two things, how to create a sound from url and how to play it. Let's form this API in purescript before continuing.
Creating and playing sounds are basically effects, so we would like to tag them as such. to define and effect
we give it the kind !
foreign import data SOUND :: !
We would also like a type to represent the sound object we create in javascript:
foreign import data Sound :: *
Next, let's create the signature for the javascript functions we are going to use. We'll declare the effects we are going to use, the inputs and the output.
foreign import newSound :: forall e. String -> Eff (sound :: SOUND | e) Sound
foreign import playSound :: forall e. Sound -> Eff (sound :: SOUND | e) Unit
This is basically all we need from the purescript front!
module Sound
( Sound
, SOUND
, newSound
, playSound
) where
import Prelude (Unit)
import Control.Monad.Eff (Eff)
foreign import data SOUND :: !
foreign import data Sound :: *
foreign import newSound :: forall e. String -> Eff (sound :: SOUND | e) Sound
foreign import playSound :: forall e. Sound -> Eff (sound :: SOUND | e) Unit
Now for the javascript front:
"use strict";
// module Sound
exports.newSound = function(url) {
return function() {
return new Howl({ urls: [url], volume: 0.15 });
};
};
exports.playSound = function(soundObj) {
return function() {
return soundObj.play();
};
};
A few things to note:
- The
// module Sound
is important, it declares the name of the module for purescript [Update: No longer needed in 0.9] exports.<function-name>
exposes that function in the module so we can import it from purescript- When representing effectful functions, we need to wrap the body of the function in
return function() { }
(more about that here)
Now we can use our new library and play music from purescript!
This eventually lead me wrap a little more of howler.js for your convenience. You can find the bindings here.
Conclusion
I had a very pleasant experience using purescript for Ludum Dare this time, and I would like to thank to the people at #purescript in freenode who helped me a lot during the jam. If you ever get stuck in something purescript related, you should definitely consider seeking help in the channel. There are a lot of really nice people always willing to help :)
And, of course, I almost forgot! Here is a link to my game. You can also view the source at the github repo.