My final day with Elm, at least for now started out a bit challenging. As before, the examples in the book are for elm version 0.13 not 0.14 and there has been enough change that porting the example program across was annoying.
But after that, the whole thing became a lot more fun. You see, day 3 of Elm consists of a workthrough of one bigg-ish Elm program. Bruce Tate talks us through how the game, called Language Head
works. And the exercises take us through adding extra features to the game.
The genius of the whole thing is that we are just manipulating streams of data
You can grab a working 0.14 compatible version of Language Head from my Seven More Languages repository if you would just like to get cracking on the fun stuff.
Adding extra features is reasonably painless for much of the time, at least if you have a reasonable grasp of a language like ML or Haskell. The genius of the whole thing is that we are just manipulating streams of data. No 'clever' stuff is going on at all.
Here is how my final version of the game looked; it is just iframed in, click it and press space to start the game. Alternatively, just play on another page. Incidentally, if you use the vimperator firefox extension, you will need to press shift-esc to allow Elm to capture your keyboard input.
Exercises
The final Language Head code is sitting in my Seven More Languages repository if you’d rather read it that way.
Easy exercises
Provide another message that asks the user to press the spacebar to start.
This was a nice straightforward task, I just extended the stateMessage code a little. stateMessage state =
case state of
GameOver -> "Game Over"
Pause -> "Press spacebar to start"
otherwise -> "Language Head"
Make the heads bounce more times as they cross the screen.
There are a couple of approaches that would work for doing this; reducing the x-movement when heads bounce, or increasing the effect of gravity. Both cause more bounces. I implemented both with functions to allow tweaking fo the figures. Here is gravity. gravity = 0.95
bounce head =
{ head | vy <- if head.y > bottom && head.vy > 0
then -head.vy * gravity
else head.vy }
And here is a tweakable x-movement -- Make heads bounce more times, reduce x movement
-- Larger number reduces x movement, 1 is default
xMoveFactor = 1
moveHead ({x, y, vx, vy} as head) =
{ head | x <- x + ((vx * secsPerFrame) / xMoveFactor)
, y <- y + vy * secsPerFrame
, vy <- vy + secsPerFrame * 400 }
Make the road look more like a road and the building look more like a building.
For some reason I wanted to make this look like a depressing UK city, so I went on google images and got hold of a tower block image and an image of a road. I have commented out the original bits of code. drawRoad w h =
-- filled gray (rect (toFloat w) 100)
toForm (image 800 160 "img/road.jpg")
|> moveY (-(half h) + 50)
drawBuilding w h =
--filled red (rect 100 (toFloat h))
toForm (image 139 600 "img/tower-block.jpg")
|> moveX (-(half w) + 50)
Medium exercises
Add some random elements to when the heads get added so that all of the games are no longer the same.
I just tweaked the head spawning code such that about half the time an head would be added and then at a more random interval than before. I also changed the code to make sure that only a maximum number of heads were in play concurrently. -- don't show more than this number of heads at a time
maxConcurrentHeads = 3
-- Probability that a new head will be shown per evaluation
-- of spawnHead all other factors being equal
newHeadProbability = 0.5
-- Witness the silliness of my random seeding strategy.
-- Just use the already calculated rand and do spurious
-- maths with it.
spawnHead score heads rand =
let (addProbability, seed') = generate (float 0 1) (initialSeed (11+rand*36712))
divideScoreBy = ((1+rand) * 1000)
addHead = length heads < (score // divideScoreBy + 1)
&& all (\head -> head.x > 107.0) heads
&& addProbability > newHeadProbability
&& List.length heads < maxConcurrentHeads
in if addHead
then defaultHead rand :: heads
else heads
Make the game choose a random head from a list of graphics.
As I read this it means pick the head from a list instead of using the headImage function. First I made my list, then a small convenience function to tell me its length minus one (that is its max index). Then I implemented the Haskell !! operator (after a fashion) and finally implemented the actual headList' function, which has the same effect of the older function. Though in theory it should be slower. Not that I noticed though. headList = [ "img/brucetate.png"
, "img/davethomas.png"
, "img/evanczaplicki.png"
, "img/joearmstrong.png"
, "img/josevalim.png"
]
headListLen = length headList - 1
-- A partial function, to return the nth element of a list
-- like in Haskell
infixl 9 !!
xs !! n = List.head (List.drop n xs)
headImage' n =
if n > headListLen || n < 0
then headList !! 1
else headList !! n
Don’t allow another head to be added too closely to an existing head.
My first attempt was complicated and wrong. In the spawnHeads function I added a local gap detecting function and checked it when spawning new heads.gapBetweenHeads = 400.0
…
gapOK = biggestHeadDeltaLessThan gapBetweenHeads heads
I then needed these functions for calculating the differences between all the x fields pairwise in my list. biggestHeadDeltaLessThan : Float -> List Head -> Bool
biggestHeadDeltaLessThan n hs =
all (\h -> h < n) (diffHeadXs hs)
diffHeadXs : List Head -> List Float
diffHeadXs hs =
case hs of
[] -> []
[h] ->[]
h1::h2::tail -> abs (h1.x - h2.x) :: diffHeadXs tail
Of course this was not necessary. I could just check whether any of the heads x co-ordinates was less than a number and know that, if it was, that particular head was still near the left hand side and thus too close to allow another to be spawned. gapOK could be redefined thus. gapOK = all (\h -> h.x > gapBetweenHeads) heads
Show a different kind of head when one reaches the bottom.
Obviously this was an excuse to make a skull appear!skullImg = "img/skull.png"
drawHead w h head =
let x = half w - head.x
y = half h - head.y
src = if head.y >= bottom - 10
then skullImg
else head.img
in toForm (image 75 75 src)
|> move (-x, y)
|> rotate (degrees (x * 2 - 100))
Hard exercises
I skipped a few of these exercises. I am fairly confident I could do them but ran out of time.
As written, the game allows heads to be added so that they reach the bottomm at potentially the same time. Prevent this from happening.
You might twiddle the vy of heads whose y was close to one anothers’ I suppose. Didn’t do this one.
Add other features that show up at different score increments. For example, bounce the heads when the user presses a key. This will let the user survive when two heads reach the bottom at the same time.
This feature is what the B figure at the top of my version of Language Head is all about. First, I modified the Player model to have an extra field, extraBounces. And the input to keep an eye out for presses of the b key. Then I had to tweak the stepHeads function to enable the extra bounce functionality, which required an extraHeadBounce function. Next a tweak to the player logic and a function to deal with incrementing and decrementing the extrabounces field. And done. type alias Player = { x:Float, score:Int, lives:Int, extraBounces:Int }
type alias Input = { space:Bool, x:Int, delta:Time, rand:Int, bkey:Bool }
…
input = sampleOn delta (Input <~ Keyboard.space
~ Mouse.x
~ delta
~ (range 0 headListLen) (every secsPerFrame)
~ Keyboard.isDown (Char.toCode 'b') )
…
defaultGame = { state = Pause,
heads = [],
player = {x=0.0, score=0, extraBounces=0} }
…
stepHeads heads delta x player rand state bpress =
spawnHead player.score heads rand
|> bounceHeads
|> extraHeadBounce bpress player
|> removeComplete
|> moveHeads delta
…
extraHeadBounce bpress player heads =
if bpress && player.extraBounces > 0
then List.map (\h -> {h | vy <- 150.0, y <- 100 }) heads
else heads
…
stepPlayer player mouseX bpress heads =
{ player | score <- stepScore player heads
, x <- toFloat mouseX
, extraBounces <- stepExtraBounces player bpress }
…
perhapsAdd : Int -> Int -> Player -> Bool
perhapsAdd min n player =
player.score > min && (player.score % n) == 0
stepExtraBounces player bpress =
let eb = if bpress
then if player.extraBounces > 0
then -1
else 0
else if perhapsAdd 1000 2000 player
then 1
else 0
in player.extraBounces + eb
I also added the logic for displaying the number of extra bouncesfullBounces player =
txt (Text.height 50) ("B:" ++ (toString player.extraBounces))
drawBounces w h player =
toForm (fullBounces player)
|> move (half w - 550, half h - 40)
display ({state, heads, player} as game) =
let (w, h) = (800, 600)
in collage w h
([ drawBuilding w h
, drawRoad w h
, drawPaddle w h player.x
, drawScore w h player
, drawBounces w h player
, drawMessage w h state
] ++ (drawHeads w h heads) )
Give the user three lives. Add additional lives when the user hits a certain score.
This uses similar logic to the previous answer, but is simpler as we only have to worry about the game state and the Player model.
type alias Player = { x:Float, score:Int, lives:Int, extraBounces:Int }
…
defaultGame = { state = Pause,
heads = [],
player = {x=0.0, score=0, lives=3, extraBounces=0} }
…
stepGameOver x heads player =
if (player.lives <= 1) && not (allHeadsSafe (toFloat x) heads)
then GameOver
else if (allHeadsSafe (toFloat x) heads)
then Play
else Pause
…
stepPlayer player mouseX bpress heads =
{ player | score <- stepScore player heads
, x <- toFloat mouseX
, lives <- stepLives player mouseX heads
, extraBounces <- stepExtraBounces player bpress }
stepLives player x heads =
if allHeadsSafe (toFloat x) heads
then if perhapsAdd 1000 1000 player
then player.lives + 1
else player.lives
else player.lives - 1
perhapsAdd : Int -> Int -> Player -> Bool
perhapsAdd min n player =
player.score > min && (player.score % n) == 0
…
drawLives w h player =
toForm (fullLives player)
|> move (half w - 50, half h - 40)
…
fullLives player =
txt (Text.height 50) ("L:" ++ (toString player.lives))
Provide a better formula for when to add additional heads.
Skipped this one.
Add heads at predetermined spacings.
Skipped this one.
Add another paddle users could move with the A and D keys, or arrow keys. Two paddles and more heads!
Skipped this one.
Final thoughts
I have been very impressed by Elm. It is a well though out and, importantly, fun language and I am almost certain to come back to it in the future. There is a lot to recommend Elm:
- Being able to write (almost) Haskell for the browser
- The simplicity of the Functional Reactive Programming model
- Excellent documentation with plenty of examples
- Strong typing to keep you from doing bad with your javascript
- A time travelling debugger and the online editor allow one to play with the language easily.
Of course, as Bruce points out, Elm may be harder to learn than some languages and the type system might be tricky to get your head round.
Bruce observes:
Elm is quite young. It will be some time before we know whether Elm will gather enough critical mass to break out beyond the emerging languages camp.
Which is very true. Indeed the youth of the language led to my biggest woe with it — the breaking changes which necessitated translating all the exercises. This is just how it is with young languages (sometimes older languages too — Python 3 springs to mind).
A couple of other considerations spring to mind too.
- What if you need to support folks with out javascript enabled?
- What if something goes wrong in the compiled javascript? Hint -- it looks sort of tolerably readable to me on a cursory glance, so maybe no biggie.
Still overall there is a lot to recommend Elm, and my excitement on the first day has proved well-founded. As Bruce says in his concluding remarks:
With Elm alone, you can tell a great deal about language evolution. We’re seeing a movement toward reactive concepts in the user interface and we are seeing the Haskell type system have an increasingly profound impact …
Next language: Elixir. Until then, adieu.