Posted by: holdenlee | July 12, 2015

Namba.elm: A fast-paced arithmetic game


Play me at http://bit.ly/playnamba.

Source code is available at http://www.github.com/holdenlee/Namba. This is the minimal playable version (v0.1) – leave me some comments so I can make the next version better.

Detailed instructions (plus thoughts on making the game, and directions to go from here) follow. But see if you can figure it out by yourself first.

If you get a score >10, let me know in the comments:)

Instructions

Move Pete (the penguin) by using the LEFT and RIGHT arrow keys (note the field wraps around). Press UP to pick up a block and DOWN to drop the block Pete is currently holding.

The goal is to make the numbers on the right-hand stack, top one first. If the stack overflows, you lose.

Your raw ingredients are the numbers on the left-hand stack. Press SPACE to drop the topmost block from that stack at the current position.

You have an unlimited supply of +, -, *, and / blocks. Press 1, 2, 3, 4, respectively, to drop those blocks.

Operations (functions) must be activated in order to act on blocks. They act on the numbers below them, so if you drop an activated “+” on blocks 3, 4, then the blocks turn into 7. To activate a block, press SHIFT in addition to what you’d normally press (DOWN, 1, 2, 3, 4, or SPACE). (Note that in this version there is no reason to press 1, 2, 3, 4 without pressing SHIFT.) The “copy” block will copy the block below it, and the “clear” block clears the whole stack. They are activated with SHIFT as well.

Once you make the topmost number on the RHS, you get a point. The speed at which the blocks come will increase.

Note that currying happens. If you drop an activated “+” on a single number, say 2, you get (+2). You can then pick up this block, activate it, and drop it on another number. How is this useful?

Design

I wanted to make a game with a “programming” flavor to it. Stack programming seems the most concrete and game-able (and there are lots of games involving stacks, ex. stacks of colored blocks you move around to match colors), and numbers are the natural objects to make. (Though this is the inspiration it’s not clear to me that the game teaches anything about programming at all.)

When I first made the game, operation blocks came randomly with the numbers, and moreover the blocks bubbled up from the bottom of the stacks rather than coming on a separate stack. This turned out to be a very bad idea – there was no way to access the new blocks coming in, and it was too hard to make anything when the operations were random. Hence I made operations free, and new blocks come in separately.

(Note: Namba comes from Number + Lambda.)

Programming

I wrote Namba using Elm, a functional reactive programming language. Elm has all the advantages of a typical functional programming language (clean, pure, runtime error-free), and additionally makes it easy to deal with input as “signals” (hence the “reactive” part).

I’ve been playing with Elm for the past year though this is the first original game I’ve coded up. (I taught a class on Elm for Princeton Splash. See my lecture notes and examples, including a Snake clone.)

Elm code is modular, as long as one follows the “Model-update-view-(signal)” paradigm. (Basic template here.) This also tends the make the logic transparent. I can focus on the logic, because Elm does all the heavy “lift”ing with the signals.

The most involved part was creating a “typing” system. Every block has a “type” associated with it, for example, each operation is Int -> Int -> Int, and when an activated block is put on another one, the program matches the “types”, computes the “type” of the resulting block, and evaluate the expression if the “type” an Int. Since the only types that come up in this version are Int, Int -> Int, and Int -> Int -> Int, this is overkill, but it’ll be easy to extend when I put in more complicated blocks.

Here are the core parts of the model, update, view, and signal components for Namba:

The model is just all the information about the current state of the game in one place:

type alias Game = {w: Int,
                   h: Int,
                   stacks : A.Array (List Block),
                   x: Int,
                   curBlock : Maybe Block,
                   activated: Bool,
                   seed: Seed,
                   inputs : List Block,
                   nextInput : Seed -> (Block, Seed),
                   nextInts : List Int,
                   spawnTime : Time,
                   spawnInts : Seed -> (List Int, Seed), 
                   score : Int,
                   timeSinceSpawn : Time,
                   gameOver : Bool
                  }

type Model = Waiting | Ingame Game

Update encodes the core logic: how the state changes upon various inputs (activate block, go left, go right, drop, pickup, use a “spell” (i.e., an operation +, -, *, /), drop a block from the incoming stack, or the passage of time). (I actually need a function “step : Input -> Model -> Model”, but this is not hard to make from stepGame. Basically, step builds on stepGame by saying how to start/end the game. The template is basically this.)

stepGame : Input -> Game -> Game
stepGame inp m = 
    case inp of
      Activate b -> 
          {m | activated <- b}
      Left ->
          {m | x <- (m.x-1)%(m.w)}
      Right -> 
          {m | x <- (m.x+1)%(m.w)}
      Pickup ->
          case (A.get m.x m.stacks, m.curBlock) of
            (Just [],_) ->  m
            (_, Just _) ->  m
                --if either the stack is empty or already have a block, do not pick up one.
            (Just (top::_), _) ->  {m | curBlock <- Just top}
                                         |> updateCurStack tail'
                                         |> resolveSuccesses
                --pick up a block.
      Spell i -> 
          --look up the ith operation and apply it to the stack.
          let
              b = M.withDefault (numBlock 0) <| D.get i spellDict
          in
            if L.length (curStack m) < m.h
            then m |> updateCurStack (if m.activated then applyStack b else (\x -> b::x))
                   |> resolveSuccesses
            else m      
      Drop -> 
          case m.curBlock of
            Nothing ->  m 
                --no block to drop!
            Just b -> 
                if L.length (curStack m) < m.h
                then {m | curBlock <- Nothing} 
                          |> updateCurStack (if m.activated then applyStack b else (\x -> b::x))
                          |> resolveSuccesses
                else m
      TimeDelta td ->
          let
              newT = m.timeSinceSpawn + td
          in
            --assume there isn't so much lag that two time intervals pass between updates
            if newT > m.spawnTime 
            then 
                --add a new block to the right-hand stack (using the spawnInts function), and reset timeSinceSpawn.
                let
                    (li, s') = m.spawnInts m.seed
                in
                   {m | seed <- s',
                        timeSinceSpawn <- newT - m.spawnTime,
                        nextInts <- m.nextInts ++ li,
                        spawnTime <- 200*second/((toFloat m.score) + 10)
                        }
            else  {m | timeSinceSpawn <- newT}
      BlockFromInput -> 
          --drop a block from input stack.
          case m.inputs of
            [] ->  m 
                  --shouldn't happen
            h::rest -> 
               if L.length (curStack m) < m.h
               then
                   let
                       (b, s) = m.nextInput m.seed
                   in
                      {m | seed <- s,
                           inputs <- rest ++ [b]} 
                       |> updateCurStack (if m.activated then applyStack h else (\x -> h::x))
                       |> resolveSuccesses
               else 
                    m

For the view part, I don’t have to do anything using absolute coordinates. Aside from putting things in containers for consistent spacing, I just have to specify how to put the different components relative to one another. Once I’ve written renderBlock, I can renderStack simply by “mapping” renderBlock over the list of blocks and “flow”ing down, and then “flow” right to make renderStacks. The whole display comes from putting together the display from left to right, of (1) the inputs, (2) the top panel above the stacks, (3) the stack of integers to make, and (4) the score panel.

renderStack : Int -> List Block -> Element 
renderStack h li = container bwidth (h*bheight) midBottom <| flow down (L.map renderBlock li)

renderStacks : Int -> A.Array (List Block) -> Element
renderStacks n = (flow right) << (L.map (renderStack n)) << A.toList

renderGame' : Game -> Element
renderGame' m = 
    flow right
         [renderInputs m.h m.inputs,
          renderTop m `above` renderStacks m.h m.stacks,
          renderNextInts m.h m.nextInts,
          renderInfoPanel m]

Finally, the signal part says how to convert key presses into the types of inputs you see above.

type Input = Activate Bool | Drop | Pickup | Left | Right | BlockFromInput | StartGame Time | TimeDelta Time | Spell Int

{-| Convert a digit press to the signal of the corresponding "spell". Digit i is 48+i. -}
spell : Signal Input
spell = mergeMany (L.map (\i -> keyCodeToState (48+i) (Spell i)) [1..4])

input : Signal Input 
input = mergeMany [map Activate shift,
                   keyCodeToState 40 Drop,
                   --down
                   keyCodeToState 38 Pickup,
                   --up
                   keyCodeToState 37 Left,
                   keyCodeToState 39 Right,
                   keyCodeToState 32 BlockFromInput,
                   --space
                   map (\(i,_) -> StartGame i) (timestamp <| whenPress (isDown 13)),
                   --enter
                   map TimeDelta (fps 20),
                   spell
                  ]

Future plans

Here are some directions I’m thinking of going from here. Ideas are welcome.

  • Zen mode – puzzle mode, where you have a limited number of pre-set blocks and you try to make the given numbers. (Think of this as an extension of Numbo, the game where you try to make a certain number from using a given set of numbers and the basic operations.)
  • Make different levels. At the end of each level there are bonuses for various things (ex. for “laziness”, if you have few total moves).
  • Buy new functions (“spells”) that can be used using keys 5-0. More of these slots open up later in the game. The functions can have various “speeds” – how often you can use them. I want to include some “higher-order” functions – the problem is how to make these functions useful, and not make the game too complicated. In order for these functions to be useful, I could make the new integers not completely random, but obey some patterns…
Advertisements

Responses

  1. For me, most of the game’s difficulty stemmed from the fact that I couldn’t mentally correspond the number keys to the operation I wanted to perform quickly and accurately. I tried cloned the source and changing the keys to the actual +-*/ keys and found it more intuitive even though they were far apart, but then it turns out / also calls up Firefox’s search bar and I decided it was too much work to work around that. Eventually I just played with the original number keys until the correspondence became more familiar.

    I haven’t worked out my strategy completely yet, but it seems a very important part is sometimes randomly multiplying numbers to keep a diverse set of two-digit numbers in store. My best score so far is 20.

  2. Nice! Getting a score of 10 was quite a challenge. It took some practice to be able to enter commands with enough speed and accuracy to progress beyond a few points.

    It seems there are 2 strategies which may be used in tandem:
    1)Grind through inputs quickly if they are not immediately relevant to the top of the target stack. The danger here is finite space for discarded inputs: if you fill it up this strategy grinds to a halt:) The “clear” input is useful for this strategy because it erases a stack of discards.

    2)Evaluate each input carefully and plan to use it for targets below the top one. However, this takes time and must result in top targets being cleared faster than the growth of the target stack in order to prevent target stack overflow from triggering the game to end. Some inputs seem to be more “valuable” than others.

    Great game! It would be nice to be able to configure the initial game speed. Also, it might be fun to have the target stack growth speed increase over time a la tetris.

  3. After going through a few iterations of polishing (although I like it as is) and maybe adding “bonuses”/”achievements”, you should consider submitting this to an online game hub such as Kongregate. I’m not sure how much work/risk would be involved, but it could result in lots of gameplay.

    I like the idea for a “laziness” achievement: Larry Wall would be proud:)

  4. Thanks for the feedback! It’s interesting to hear the different strategies. The target stack does in fact grow quicker over time. I’m curious as to how you’re using the “Copy” blocks. One way I use them is to keep a few (*-1)’s lying around (for quick negation).

    I’ve fixed a bug so that game can now clear multiple blocks at the same time.

  5. Yes, I frequently used copy on -1 to have a negation supply. Also, if I built up a block to large (absolute) value, I would copy it if there were other large targets in the target stack.

  6. Consider the case where 1 action generates a chain of n matches. Namely, an action results in a match between the tops of the working and target stacks and another then match is “revealed” because the new tops match.

    Congratulations: your code automatically detects the revealed match. This situation is not common and is tough to setup intentionally. Therefore it might be fun to reward such an event: provide n^2 points for a chain of length n.


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Categories

%d bloggers like this: