Skip to content

Instantly share code, notes, and snippets.

@raimohanska
Created January 26, 2014 20:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save raimohanska/8639343 to your computer and use it in GitHub Desktop.
Save raimohanska/8639343 to your computer and use it in GitHub Desktop.
Sketch for a Lion Chases Princess Game
module PrincessVsLion where
import Keyboard
-- character positions
princess = foldp inc 4 (pressesOf Keyboard.space)
lion = foldp inc 0 (fps 2)
-- combined game state
gameState = lift2 makeState princess lion
-- main function
main = lift asText gameState
-- helpers
pressesOf key = keepIf id False key
inc _ prev = prev + 1
makeState p l = { princess= p, lion= l }
@raimohanska
Copy link
Author

Trying to model a simple game for my daugher in Elm.

The idea is that Lion is chasing Princess in a 1-dimensional world. Lion starts at position 0 and Princess at position 4. Lion wins if he catches Princess. Princess wins if she reaches position 13. Lion moves twice a second. Princess moves when spacebar is pressed. Simple!

In the above snippet, I've declared the positions of the characters as Signals in a nice FRP way. I could probably define the end result of the game based on the two signals. Except you cannot have a Signal with undefined initial value, but that should be manageable. The real problem is that I cannot think of a way to cleanly restart the game while re-using the code above.

All the examples I've seen use a single update function that updates the whole game state. Is there a way to re-use simple Signals to compose more complex ones in Elm?

The fact that there's no flatMap makes me a bit pessimistic. You just cannot restart a stream, can you?

@raimohanska
Copy link
Author

In Bacon.js you could

gameResults = spacePress.flatMapFirst ->
  princessPos = spacePress.scan 4, inc
  lionPos = Bacon.interval(500).scan 0, inc
  both = Bacon.combineTemplate { princessPos, lionPos }
  lionWon = both.filter ({princessPos, lionPos}) -> princessPos == lionPos
  princessWon = princessPos.filter (pos) -> pos >= 13
  endResult = lionWon.map("Lion won").merge(princessWon.map("Princess won"))
  princessPos.takeUntil(endResult).onValue(drawPrincess)
  lionPos.takeUntil(endResult).onValue(drawLion)
  return endResult

gameResults.log()

The stuff inside the function passed to flatMapFirst is probably doable in Elm. Except you cannot have signals without initial value like the lionWon and princessWon signals above.

But restarting is a no-do, right? To me it seems it makes it much harder to compose stuff cleanly in Elm.

Please prove me wrong, I'll be mega happy!

@phadej
Copy link

phadej commented Jan 27, 2014

If I understand Elm right, you just end up having single "whole world state", if you want to be able to alter it as single entity (e.g. reset to defaults). Using Bacon.js vocabulary: you work (almost) only with Properties. You don't really have flatMap for Properties there either.

And you can accumulate state with Applicative instance of Signal only.

import Keyboard

pressesOf key = keepIf id False (Keyboard.isDown key)

spaces = lift (\_ -> Just 10) (pressesOf 32) -- Space
resets = lift (\_ -> Nothing) (pressesOf 82) -- R
incs   = lift (\_ -> Just 1) (pressesOf 65) -- A

events : Signal (Maybe number)
events = merges [spaces, incs, resets]

count : Signal number
count = foldp countUpdate 0 events

countUpdate : Maybe number -> number -> number
countUpdate next prev = 
  let c = maybe 0 ((+) prev) next
   in if c > 100 then 0 else c -- over 100, reset as well

main = lift asText count

I guess monadic bind would make sense in use cases if eg HTTP.sendGet would be of type string -> Signal (Response String), but it's cleverly made into Signal string -> Signal (Response String). Applicative is enough there as well.

@raimohanska
Copy link
Author

Now it works!

@phadej you're right, there has to be a function proceeding the "whole world state". And you have to forget almost everything you've learn about Rx/Bacon.js conventions. There's no temporal composition. Just a big state machine. Yet, the end result is quite nice. Except it's still not restartable.

module PrincessVsLion where
import Keyboard

data GameState = Ongoing Int Int | LionWon | PrincessWon

proceedGame keyPresses state = case state of
  LionWon -> LionWon
  PrincessWon -> PrincessWon
  Ongoing princess lion ->
      check ((keyPresses `div` 2) + 4) (lion + 1)

check princess lion =
  if | lion == princess -> LionWon
     | princess >= 13 -> PrincessWon
     | otherwise -> Ongoing princess lion


input = sampleOn (fps 2) (count Keyboard.space)
initState = Ongoing 4 0

gameState = foldp proceedGame initState input

main = lift asText gameState

@raimohanska
Copy link
Author

Pow pow pow, now it's restartable.

https://gist.github.com/raimohanska/8653902

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment