Chapter 4
Compound Data: Objects

Note: We’re not sure what this illustration is for, but maybe it will gain some purpose.

The weresquirrel

Numbers, booleans, images, and strings are all really useful, and allow us to write interesting programs! But, alone, they have some limitations. In particular, sometimes we want to talk about different pieces of data together. In the previous chapter, we talked about modelling the shape of the information in our program after what we are trying to represent in the real world. Sometimes, we can represent something as just a number, or just a string, or just an image, but often we want to combine them together.

For example, if I want to write a program that I use to organize books, I will want to have some notion of a book in the program. I could try to figure out a way to represent it with just a string (perhaps an ISBN?), but realistically, I probably want a bit more information. To do this, Javascript allows us to construct objects. An object is a collection of fields, where each field has a name and a value. For example, if I wanted to represent the book "The Left Hand of Darkness", I might write:

var lhod = { title: "The Left Hand of Darkness",
             author: "Ursula K. Le Guin",
             year: 1969
           };

Objects are written with a {, then a series of field, colon, value clauses, each separated from one another with a comma. New lines between the fields are not necessary, but are often used to make it easier to read. We could (but shouldn’t) have written the above as:

var lhod = {title:"The Left Hand of Darkness",author:"Ursula K. Le Guin",year:1969};

When following the design recipe, we write them in signatures in a similar way, with fields, colons, and then the signature for what goes into that field. For example, we might write the above as:

// A book is { title: string, author: string, year: number}

As in the above example, we can store objects in variables, and we can pass them to functions. To get the fields out, we use . followed by the name of the field. For example:

var lhod = { title: "The Left Hand of Darkness",
             author: "Ursula K. Le Guin",
             year: 1969
           };
print(lhod.title);

Since objects are values like strings, numbers, and images, we can put objects into fields of other objects. For example

var lhod = { title: "The Left Hand of Darkness",
             author: {first: "Ursula K.", last: "Le Guin"},
             year: 1969
           };
print("The first name is: " + lhod.author.first);

The signature for this is:

// { title: string,
//   author: { first : string, last: string }
//   year: number
// }

Big Bang

The majority of this chapter is an extended example using objects, the tools we’ve learned so far, and a new, more powerful version of animate that allows us to build interactive games. This function is called bigBang. The idea of big bang is to construct an animation, but to allow you more control over it than you had with animate.

You can try out the finished example here (by the end of the chapter, you will be able to build similar, and better, games!):

http://tlcjs.org/games/flappy.html

So, let’s get into it!

In animate, your function was called with a number (that was the number of ticks since the animation started, and that we sometimes thought of as time, sometimes as a position, etc), and it returns an image. With bigBang, you get to define what the world state is that you get passed each time (in animate, the world state was a number), and you get to define how it changes each time step (with animate, the world state, which is a number, was incremented by one each time step). There is also a way to respond to keyboard input.

bigBang takes four arguments:

To review, we can use bigBang to replicate the way that animate worked:

animate(function (ticks) {
  return placeImage(circle(10, "red"), emptyScene(400,100), ticks, 70);
});
bigBang(0,
        function (ticks) {
          return placeImage(circle(10, "red"), emptyScene(400,100), ticks, 70);
        },
        function (world) { return world + 1; },
        function (world, key) { return world; });

But we can, and will, do so much more with it!

Flappy Bird

First, lets talk a little bit about our Flappy bird: http://tlcjs.org/games/flappy.html

Looking at what is on the screen at a given time, we can see the bird and the two pipes. Since both of those move over time, it seems like we need to be able to keep track of where they are. To start, we won’t worry about the pipes, and will only focus on moving our bird up and down. Since we only go up and down, we really just need to know how high the bird is, which is just a number.

But, since we are going to make this more complicated, and since this is the chapter about objects, our world state will be an object with (for now) one field:

// world state is { bird_position : number }

Now we want to figure out how to draw the scene with the bird on it. In a moment, we’ll make a much better drawing, but for now, we’ll build a simple "bird" and draw it over some ground. To do this, we’ll define our shapes and then create our drawWorld function:

var bird = circle(25, "blue");
var ground = placeImage(rectangle(600, 10, "brown"), emptyScene(600, 400), 0, 390);

// { bird_position : number } -> image
function drawWorld(world) {
  return overlay(character, ground);
}

print(drawWorld({bird_position: 0}));

That worked, and it put our character on the background but, as you may have noticed, it didn’t use the "world" object or its bird_position. As a result, calling it with different positions doesn’t change the image. You can verify this by modifying the line that prints out the world in the above example — nothing will change about the image that is created.

So we should have drawWorld vary where the bird is based on the bird_position field in the world state.

var bird = circle(25, "blue");
var ground = placeImage(rectangle(600, 10, "brown"), emptyScene(600, 400), 0, 390);

// { bird_position : number } -> image
function drawWorld(world) {
  return placeImage(bird, ground, 200, world.bird_position);
}

print(drawWorld({bird_position: 50}));
print(drawWorld({bird_position: 200}));

And now we can see how the image varies (the bird is further from the top, as placeImage takes the distance from the left and distance from the top - see the Reference for more detail).

Looking back at bigBang, we now have a world state definition (and the initial state could be {bird_position: 0}), we have a way of drawing the world, so we need two more things: how the world changes over time, and how it changes based on keyboard input.

To start, lets not worry about gravity, so the only change is based on keyboard input (which means that the function with signature world -> world that controls how the world changes each time tick is just function onTick(world) { return world; } - the function that does nothing and returns its argument). That function takes the world state and a string that represents a key that was pressed. For our purposes, the only one that matters is "Space", which means the spacebar was pressed (note: the cross-browser support for this stuff is terrible - for TLCjs, we chose to use what seems to be the best supported between firefox and chrome, but doesn’t work elsewhere. And there may be differences even between those two browsers - you can see a table of value https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code).

Let’s follow the design recipe for this function.

First, we’ll write a signature. From the documentation of bigBang, we know that it takes two arguments: our world state and a string representing a key that was pressed. And it is supposed to return a new world. So, we write:

// { bird_position : number }, string -> { bird_position : number }

Next, we’ll write a purpose statement. In this case:

// If the second argument is "Space", we return
// a new world, where the bird_position has decreased by 10, which will
// move the bird up by 10 pixels. Otherwise, we return the world unchanged.

Now we need to write out the function header and some tests:

function onKey(world, key) {

}
shouldEqual(onKey({ bird_position: 100 }, "Space"), { bird_position: 90 });
shouldEqual(onKey({ bird_position: 100 }, "Something Else"), { bird_position: 100 });

And in writing those, I wonder whether the bird should be able to go off the top of the screen - ie, should this test pass?

shouldEqual(onKey({ bird_position: 0 }, "Space"), { bird_position: -10 });

Or should this one:

shouldEqual(onKey({ bird_position: 0 }, "Space"), { bird_position: 0 });

For now, we’ll go with the first option (as it’s a little simpler), but as an exercise, you could use the second one instead. Now we fill in the template. For objects, there is one template for each field in the object. So in our case, it looks like:

function onKey(world, key) {
  world.bird_position;
  key;
}
shouldEqual(onKey({ bird_position: 100 }, "Space"), { bird_position: 90 });
shouldEqual(onKey({ bird_position: 100 }, "Something Else"), { bird_position: 100 });
shouldEqual(onKey({ bird_position: 0 }, "Space"), { bird_position: -10 });

If the world had more fields, we would have more parts of the template. Now we’re ready to write the body. Looking at the purpose statement, we see the sentance if the second argument is "Space", which makes it seem like we should have if (key === "Space") somewhere, so let’s add that (note: since we have used key, I removed the template version):

function onKey(world, key) {
  if (key === "Space") {

  } else {

  }
  world.bird_position;
}
shouldEqual(onKey({ bird_position: 100 }, "Space"), { bird_position: 90 });
shouldEqual(onKey({ bird_position: 100 }, "Something Else"), { bird_position: 100 });
shouldEqual(onKey({ bird_position: 0 }, "Space"), { bird_position: -10 });

Now it says that if that’s true, we return a new world where bird_position has decreased by ten. Which we can do with return { bird_position: world.bird_position - 10 }; Otherwise, it says (so, in the else branch), we are supposed to return the unchanged world.

We can put that together to get:

function onKey(world, key) {
  if (key === "Space") {
    return { bird_position: world.bird_position - 10 };
  } else {
    return world;
  }
}
shouldEqual(onKey({ bird_position: 100 }, "Space"), { bird_position: 90 });
shouldEqual(onKey({ bird_position: 100 }, "Something Else"), { bird_position: 100 });
shouldEqual(onKey({ bird_position: 0 }, "Space"), { bird_position: -10 });

And, all our tests pass! So we’re done with onKey.

We said we weren’t going to worry about gravity just yet, which means that nothing is going to change over time, so we can just use function onTick(world) { return word; } as our function that changes the world over time and can put all the pieces together. We get flappy version 1:

var eye = circle(5, "white");
var mouth = placeImage(circle(10, "rebeccapurple"), circle(10, "white"), 0, -5);
var character = placeImage(mouth, placeImage(eye, rectangle(50, 50, "rebeccapurple"), 30, 10), 40, 20);
var ground = placeImage(rectangle(600, 10, "brown"), emptyScene(600, 400), 0, 390);

// { bird_position : number } -> image
function drawWorld(world) {
  return placeImage(character, ground, 200, world.bird_position);
}

// { bird_position : number } -> { bird_position : number }
function onTick(world) {
  return world;
}

shouldEqual(onTick({ bird_position: 100}), { bird_position: 100 });

function onKey(world, key) {
  if (key === "Space") {
    return { bird_position: world.bird_position - 10 };
  } else {
    return world;
  }
}
shouldEqual(onKey({ bird_position: 100 }, "Space"), { bird_position: 90 });
shouldEqual(onKey({ bird_position: 100 }, "Something Else"), { bird_position: 100 });
shouldEqual(onKey({ bird_position: 0 }, "Space"), { bird_position: -10 });

bigBang({bird_position: 100}, drawWorld, onTick, onKey);

Which if you click on the screen (so the Space goes to that, rather than telling the browser to scroll), you can move the bird up (and out of the screen). So the first part is working!

Now let’s revisit gravity. The problem, so far, is that our bird doesn’t fall down. In the game, if you aren’t actively flapping, you are supposed to fall to the ground. This happens automatically, without input, so it has to happen in the function that changes the world each tick (which we named onTick).

Let’s follow the design recipe for it.

We already have the signature:

// { bird_position : number } -> { bird_position : number }

Which says it takes a world and produces a new world (one tick later). Thinking about what we wrote above about gravity, it seems like the purpose statement should be something like:

// This returns a new world where the bird position has increased
// by one (which corresponds to the bird _falling_, since the position
// counts from the top).

We can now write the header and some tests:

function onTick(world) {

}
shouldEqual(onTick({ bird_position: 0 }), {bird_position: 1});
shouldEqual(onTick({ bird_position: 50 }), {bird_position: 51});

Writing that, we might wonder what should happen when the bird hits the ground. In some versions of the game, the game ends, and you could also (as in our example version) just make the bird stop when it hits the ground. Let’s do the latter - as an exercise, you could make the game end (we’ll see, later, how we could accomplish that!).

Making the bird stop amounts to the behavior:

shouldEqual(onTick({ bird_position: 390 }), { bird_position: 390 });

Since when we defined the scene in our drawing function above, we made the ground start at 390 from the top (exercise: is that right? Will the bird actually overlap with the ground?).

The template is similar for the onKey function - mainly it is just world.bird_position, so we’ll go right to writing the body, knowing we should probably use world.bird_position. Based on the purpose statement, we are supposed to return a new world, and the bird_position value is supposed to be one more than the previous:

// { bird_position : number } -> { bird_position : number }
// This returns a new world where the bird position has increased
// by one (which corresponds to the bird _falling_, since the position
// counts from the top).
function onTick(world) {
  return { bird_position: world.bird_position + 1 };
}
shouldEqual(onTick({ bird_position: 0 }), {bird_position: 1});
shouldEqual(onTick({ bird_position: 50 }), {bird_position: 51});
shouldEqual(onTick({ bird_position: 390 }), { bird_position: 390 });

But one of our tests failed! Oops, we said we would stop at the bottom, but we never updated our purpose statement, so, following that, we wrote the wrong function. Let’s fix the purpose statement first:

// This returns a new world where the bird position has increased
// by one (which corresponds to the bird _falling_, since the position
// counts from the top). However, if the position is 390 (ie, bird is
// at bottom) we just return the same world unchanged.

Now we see in the purpose statement if the position is 390, which makes it sound like we should have if (world.bird_position === 390) in our function, so lets fill that in and see what we have:

// { bird_position : number } -> { bird_position : number }
// This returns a new world where the bird position has increased
// by one (which corresponds to the bird _falling_, since the position
// counts from the top). However, if the position is 390 (ie, bird is
// at bottom) we just return the same world unchanged.
function onTick(world) {
  if (world.bird_position === 390) {

  } else {

  }
}
shouldEqual(onTick({ bird_position: 0 }), {bird_position: 1});
shouldEqual(onTick({ bird_position: 50 }), {bird_position: 51});
shouldEqual(onTick({ bird_position: 390 }), { bird_position: 390 });

So it seemed like we were write in the case that the position was not 390. So we can put in our old answer for the else branch. What do we do in the if branch? Well, the purpose statement now says we just return the same world, which we can do!

// { bird_position : number } -> { bird_position : number }
// This returns a new world where the bird position has increased
// by one (which corresponds to the bird _falling_, since the position
// counts from the top). However, if the position is 390 (ie, bird is
// at bottom) we just return the same world unchanged.
function onTick(world) {
  if (world.bird_position === 390) {
     return world;
  } else {
     return { bird_position: world.bird_position + 1 };
  }
}
shouldEqual(onTick({ bird_position: 0 }), {bird_position: 1});
shouldEqual(onTick({ bird_position: 50 }), {bird_position: 51});
shouldEqual(onTick({ bird_position: 390 }), { bird_position: 390 });

And now all our tests pass! Great. So lets take this new version of onTick and put it into our flappy version 1 to get flappy version 2.

var eye = circle(5, "white");
var mouth = placeImage(circle(10, "rebeccapurple"), circle(10, "white"), 0, -5);
var character = placeImage(mouth, placeImage(eye, rectangle(50, 50, "rebeccapurple"), 30, 10), 40, 20);
var ground = placeImage(rectangle(600, 10, "brown"), emptyScene(600, 400), 0, 390);

// { bird_position : number } -> image
function drawWorld(world) {
  return placeImage(character, ground, 200, world.bird_position);
}

// { bird_position : number } -> { bird_position : number }
// This returns a new world where the bird position has increased
// by one (which corresponds to the bird _falling_, since the position
// counts from the top). However, if the position is 390 (ie, bird is
// at bottom) we just return the same world unchanged.
function onTick(world) {
  if (world.bird_position === 390) {
     return world;
  } else {
     return { bird_position: world.bird_position + 1 };
  }
}
shouldEqual(onTick({ bird_position: 0 }), {bird_position: 1});
shouldEqual(onTick({ bird_position: 50 }), {bird_position: 51});
shouldEqual(onTick({ bird_position: 390 }), { bird_position: 390 });

// { bird_position : number }, string -> { bird_position : number }
function onKey(world, key) {
  if (key === "Space") {
    return { bird_position: world.bird_position - 10 };
  } else {
    return world;
  }
}
shouldEqual(onKey({ bird_position: 100 }, "Space"), { bird_position: 90 });
shouldEqual(onKey({ bird_position: 100 }, "Something Else"), { bird_position: 100 });
shouldEqual(onKey({ bird_position: 0 }, "Space"), { bird_position: -10 });

bigBang({bird_position: 100}, drawWorld, onTick, onKey);

Two things to notice - it is way to hard to get into the air! And the answer to the exercise above, it definitely seems like we aren’t stopping soon enough - the bird goes into the ground. As an exercise, figure out how to fix both of these problems.

Once you’ve finished that, let’s move onto the next part - adding pipes.

First, we need to think a little bit about what it means to have pipes. We’ll need to draw them, so we’ll need to change our drawWorld function, but also, they move over time, which means they need to be in our world state, and we’ll need to update them in our onTick function. We won’t worry about collisions / ending the game yet.

Let’s think about how we might represent the pipes. We need to record how far from the left that they are, as that’s what we’ll be changing (so that they move). We also will, eventually, need to allow their height to vary, but we’ll not worry about that for now and just assume that the pipes are always the same height.

So we’ll just add, to our world state, a pipes_position field, which is a number that represents the distance (in pixels) from the left that the pipes are. That means that everywhere above where we wrote { bird_position : number } in signatures for our world, we now will write: { bird_position : number, pipes_position : number } instead.

Let’s modify each function, starting with the drawing.

var eye = circle(5, "white");
var mouth = placeImage(circle(10, "rebeccapurple"), circle(10, "white"), 0, -5);
var character = placeImage(mouth, placeImage(eye, rectangle(50, 50, "rebeccapurple"), 30, 10), 40, 20);
var ground = placeImage(rectangle(600, 10, "brown"), emptyScene(600, 400), 0, 390);
var pipe = rectangle(30, 100, "green");

// { bird_position : number, pipes_position : number } -> image
function drawWorld(world) {
  return placeImage(pipe,
                    placeImage(pipe,
                               placeImage(character, ground, 200, world.bird_position),
                               world.pipes_position,
                               0),
                    world.pipes_position,
                    300);
}

print(drawWorld({bird_position: 0, pipes_position: 300}));

Next, we want to make it so that the pipes move to the left. Let’s follow the design recipe to guide us how we want to change the existing function.

We already know the signature - it is what it was, except the world now has a pipes_position field:

// { bird_position : number, pipes_position : number }
// -> { bird_position : number, pipes_position : number }

We now want to modify the purpose statement. We need to modify it to specify what happens to the pipes_position field. In particular, we want it to decrease until it is 0 (when the pipes will go off the screen to the left) and at that point, for them to re-appear (or, if you will, for new pipes to appear) all the way to the right, which is position 600.

Written out:

// This returns a new world where the bird position has increased
// by one (which corresponds to the bird _falling_, since the position
// counts from the top) and the pipes position has decreased by
// 1. However, if the bird position is 390 (ie, bird is
// at bottom) we just return the same world unchanged, and if the
// pipes position is 0, we instead set it to 600.

Now we can fix the tests and add a new one to cover the pipe behavior:

function onTick(world) {
}
shouldEqual(onTick({ bird_position: 0, pipes_position: 500 }), {bird_position: 1, pipes_position: 499});
shouldEqual(onTick({ bird_position: 50, pipes_position: 500 }), {bird_position: 51, pipes_position: 499});
shouldEqual(onTick({ bird_position: 390, pipes_position: 500 }), {bird_position: 390, pipes_position: 499});
shouldEqual(onTick({ bird_position: 0, pipes_position: 0 }), {bird_position: 1, pipes_position: 600});

Using this, we can rewrite the body to be:

function onTick(world) {
  if (world.bird_position === 390) {
     var newBird = 390;
  } else {
     var newBird = world.bird_position + 1;
  }

  if (world.pipes_position === 0) {
     var newPipes = 600;
  } else {
     var newPipes = world.pipes_position - 1;
  }

  return { bird_position: newBird, pipes_position: newPipes };
}
shouldEqual(onTick({ bird_position: 0, pipes_position: 500 }), {bird_position: 1, pipes_position: 499});
shouldEqual(onTick({ bird_position: 50, pipes_position: 500 }), {bird_position: 51, pipes_position: 499});
shouldEqual(onTick({ bird_position: 390, pipes_position: 500 }), {bird_position: 390, pipes_position: 499});
shouldEqual(onTick({ bird_position: 0, pipes_position: 0 }), {bird_position: 1, pipes_position: 600});

Note that we did something somewhat new here - we defined variables within an if, which allows us to easily handle the multiple conditions that we now have (the bird can get to the bottom of the screen and the pipes can get to the left of the screen). If you do this, be sure you define the same variables in both branches, or else you may have a variable that isn’t defined.

And now we can put this together with flappy version 2 to get flappy version 3 (note: We had to make minor changes to onKey - we just had to make sure when we created a new world, we included our new pipes_position field, and the signature/tests had to correspondingly change):

var eye = circle(5, "white");
var mouth = placeImage(circle(10, "rebeccapurple"), circle(10, "white"), 0, -5);
var character = placeImage(mouth, placeImage(eye, rectangle(50, 50, "rebeccapurple"), 30, 10), 40, 20);
var ground = placeImage(rectangle(600, 10, "brown"), emptyScene(600, 400), 0, 390);
var pipe = rectangle(30, 100, "green");

// { bird_position : number, pipes_position : number } -> image
function drawWorld(world) {
  return placeImage(pipe,
                    placeImage(pipe,
                               placeImage(character, ground, 200, world.bird_position),
                               world.pipes_position,
                               0),
                    world.pipes_position,
                    300);
}


// { bird_position : number, pipes_position : number }
// -> { bird_position : number, pipes_position : number }
// This returns a new world where the bird position has increased
// by one (which corresponds to the bird _falling_, since the position
// counts from the top) and the pipes position has decreased by
// 1. However, if the bird position is 390 (ie, bird is
// at bottom) we just return the same world unchanged, and if the
// pipes position is 0, we instead set it to 600.
function onTick(world) {
  if (world.bird_position === 390) {
     var newBird = 390;
  } else {
     var newBird = world.bird_position + 1;
  }

  if (world.pipes_position === 0) {
     var newPipes = 600;
  } else {
     var newPipes = world.pipes_position - 1;
  }

  return { bird_position: newBird, pipes_position: newPipes };
}
shouldEqual(onTick({ bird_position: 0, pipes_position: 500 }), {bird_position: 1, pipes_position: 499});
shouldEqual(onTick({ bird_position: 50, pipes_position: 500 }), {bird_position: 51, pipes_position: 499});
shouldEqual(onTick({ bird_position: 390, pipes_position: 500 }), {bird_position: 390, pipes_position: 499});
shouldEqual(onTick({ bird_position: 0, pipes_position: 0 }), {bird_position: 1, pipes_position: 600});

// { bird_position : number, pipes_position : number }, string
// -> { bird_position : number, pipes_position : number }
function onKey(world, key) {
  if (key === "Space") {
    return { bird_position: world.bird_position - 10, pipes_position: world.pipes_position };
  } else {
    return world;
  }
}
shouldEqual(onKey({ bird_position: 100, pipes_position: 100 }, "Space"),
            { bird_position: 90, pipes_position: 100 });
shouldEqual(onKey({ bird_position: 100, pipes_position: 100 }, "Something Else"),
            { bird_position: 100, pipes_position: 100 });
shouldEqual(onKey({ bird_position: 0, pipes_position: 100 }, "Space"),
            { bird_position: -10, pipes_position: 100 });

bigBang({bird_position: 100, pipes_position: 600}, drawWorld, onTick, onKey);

Next, we’ll make it so that if you run into the pipes you actually lose!

Ending the game

There are two parts of this:

  1. Figuring out if we have run into a pipe.

  2. Ending the game.

We’ll handle both of these. First, let’s figure out how to detect if we have run into a pipe. When adding things to an existing program, it’s always important to think about where that functionality should go. In our game, we have a few possibilities:

Let’s think about the last one first. If we put this in onKey, then it can only run when someone is pressing a key. But we need to detect collisions even if they aren’t actively hitting a key, so this seems like not an ideal place for it. We could also try to put it in the drawing function, which might work - we could draw something else if they had collided. But, since drawing can’t actually update the world state, once the bird had passed through the wall (when we would be drawing it as collided), it would end up on the other side unchanged!

This leaves onTick as the only place that makes sense. And, there is good reason for this: having lost the game seems like it is part of the important information about the game, which means it should live in the world state, and onTick is how we update the world state as time passes.

We’ll do this in two parts:

  1. We’ll create a new function hasCollided that takes a world state and returns true if the bird in that world has run into the pipe and false otherwise.

  2. We’ll use this function in the body of onTick to change the world state to indicate that the game is over.

But first, what does it mean for the game to be over? How should we represent that? Well, it seems like we could add another field to our world state that is is_game_over that would start as false and then if we ever collide would turn to true. This would allow us to change how we draw the game once it is over.

So our new world state now looks like:

// a world is { bird_position : number,
//              pipes_position : number,
//              is_game_over : boolean
//            }

And we can now use that to write hasCollided:

We’ll do this abbreviated. As an exercise, follow through the design recipe for this function on your own, and then check against what we do here to see where you made different choices (different tests, different names, etc).

// { bird_position : number, pipes_position : number, is_game_over : boolean } -> boolean
// This function calculates whether the world passed
// to it represents a bird that has collided into the pipes.
function hasCollided(world) {
  world.bird_position;
  world.pipes_position;
  return true;
}

shouldEqual(hasCollided({ bird_position: 200, pipes_position: 200 }), false);
shouldEqual(hasCollided({ bird_position: 0, pipes_position: 200 }), true);
shouldEqual(hasCollided({ bird_position: 0, pipes_position: 400 }), false);

Here we’ve combined the first several steps of the design recipe - thinking about data, writing signature, writing header, writing tests. We also added some templates, and eliminated one - world.is_game_over, as probably hasCollided doesn’t have to worry about that - once the game is over, collisions are no longer relevant!

Now let’s fill it is to get the tests to pass:

// { bird_position : number, pipes_position : number, is_game_over : boolean } -> boolean
// This function calculates whether the world passed
// to it represents a bird that has collided into the pipes.
function hasCollided(world) {
  world.bird_position;
  world.pipes_position;
  if (world.pipes_position > 150 && world.pipes_position < 250) {
    if (world.bird_position < 100 || world.bird_position > 250) {
      return true;
    } else {
      return false;
    }
  } else {
    return false;
  }
}

shouldEqual(hasCollided({ bird_position: 200, pipes_position: 200 }), false);
shouldEqual(hasCollided({ bird_position: 0, pipes_position: 200 }), true);
shouldEqual(hasCollided({ bird_position: 0, pipes_position: 400 }), false);

Now let’s change onTick. Here is the old onTick, with the signature changed to include is_game_over. Where should we put this logic?

// { bird_position : number, pipes_position : number, is_game_over : boolean }
// -> { bird_position : number, pipes_position : number, is_game_over : boolean }
function onTick(world) {
  if (world.bird_position === 390) {
     var newBird = 390;
  } else {
     var newBird = world.bird_position + 1;
  }

  if (world.pipes_position === 0) {
     var newPipes = 600;
  } else {
     var newPipes = world.pipes_position - 1;
  }

  return { bird_position: newBird, pipes_position: newPipes };
}

Well, first, if the game is already over, we probably don’t need to change anything about the world, so let’s add a case for that:

// { bird_position : number, pipes_position : number, is_game_over : boolean }
// -> { bird_position : number, pipes_position : number, is_game_over : boolean }
function onTick(world) {
  if (world.game_is_over) {
    return world;
  } else if (/* check for collision */) {

  } else {
    if (world.bird_position === 390) {
       var newBird = 390;
    } else {
       var newBird = world.bird_position + 1;
    }

    if (world.pipes_position === 0) {
       var newPipes = 600;
    } else {
       var newPipes = world.pipes_position - 1;
    }

    return { bird_position: newBird, pipes_position: newPipes };
  }
}

I added an else if branch for what to do if we detect a collision, as it seems like we should do something different. But detecting a collision is a problem we’ve already solved! If we have collided, we should return the same bird/pipes position, but set is_game_over to true. This gets us to:

// { bird_position : number, pipes_position : number, is_game_over : boolean }
// -> { bird_position : number, pipes_position : number, is_game_over : boolean }
function onTick(world) {
  if (world.game_is_over) {
    return world;
  } else if (hasCollided(world)) {
    return { bird_position: world.bird_position, pipes_position: world.pipes_position, is_game_over: true };
  } else {
    if (world.bird_position === 390) {
       var newBird = 390;
    } else {
       var newBird = world.bird_position + 1;
    }

    if (world.pipes_position === 0) {
       var newPipes = 600;
    } else {
       var newPipes = world.pipes_position - 1;
    }

    return { bird_position: newBird, pipes_position: newPipes, is_game_over: false };
  }
}

Now if we put it together (note, we had to add is_game_over to onKey and the initial world), we have a game that detects collisions and stops when we collide! As an exercise, prevent onKey from changing the world once the game is over, and as another exercise, draw something on the world when the game is over, so it is easier to tell what happened!

var eye = circle(5, "white");
var mouth = placeImage(circle(10, "rebeccapurple"), circle(10, "white"), 0, -5);
var character = placeImage(mouth, placeImage(eye, rectangle(50, 50, "rebeccapurple"), 30, 10), 40, 20);
var ground = placeImage(rectangle(600, 10, "brown"), emptyScene(600, 400), 0, 390);
var pipe = rectangle(30, 100, "green");

// { bird_position : number, pipes_position : number, is_game_over : boolean } -> image
function drawWorld(world) {
  return placeImage(pipe,
                    placeImage(pipe,
                               placeImage(character, ground, 200, world.bird_position),
                               world.pipes_position,
                               0),
                    world.pipes_position,
                    300);
}

// { bird_position : number, pipes_position : number, is_game_over : boolean } -> boolean
// This function calculates whether the world passed
// to it represents a bird that has collided into the pipes.
function hasCollided(world) {
  world.bird_position;
  world.pipes_position;
  if (world.pipes_position > 150 && world.pipes_position < 250) {
    if (world.bird_position < 100 || world.bird_position > 250) {
      return true;
    } else {
      return false;
    }
  } else {
    return false;
  }
}

shouldEqual(hasCollided({ bird_position: 200, pipes_position: 200 }), false);
shouldEqual(hasCollided({ bird_position: 0, pipes_position: 200 }), true);
shouldEqual(hasCollided({ bird_position: 0, pipes_position: 400 }), false);


// { bird_position : number, pipes_position : number, is_game_over : boolean }
// -> { bird_position : number, pipes_position : number, is_game_over : boolean }
// This returns a new world where the bird position has increased
// by one (which corresponds to the bird _falling_, since the position
// counts from the top) and the pipes position has decreased by
// 1. However, if the bird position is 390 (ie, bird is
// at bottom) we just return the same world unchanged, and if the
// pipes position is 0, we instead set it to 600.
function onTick(world) {
  if (world.game_is_over) {
    return world;
  } else if (hasCollided(world)) {
    return { bird_position: world.bird_position, pipes_position: world.pipes_position, is_game_over: true };
  } else {
    if (world.bird_position === 390) {
       var newBird = 390;
    } else {
       var newBird = world.bird_position + 1;
    }

    if (world.pipes_position === 0) {
       var newPipes = 600;
    } else {
       var newPipes = world.pipes_position - 1;
    }

    return { bird_position: newBird, pipes_position: newPipes, is_game_over: false };
  }
}
shouldEqual(onTick({ bird_position: 0, pipes_position: 500, is_game_over: false }), {bird_position: 1, pipes_position: 499, is_game_over: false});
shouldEqual(onTick({ bird_position: 50, pipes_position: 500, is_game_over: false }), {bird_position: 51, pipes_position: 499, is_game_over: false});
shouldEqual(onTick({ bird_position: 390, pipes_position: 500, is_game_over: false }), {bird_position: 390, pipes_position: 499, is_game_over: false});
shouldEqual(onTick({ bird_position: 0, pipes_position: 0, is_game_over: false }), {bird_position: 1, pipes_position: 600, is_game_over: false});

// { bird_position : number, pipes_position : number, is_game_over : boolean}, string
// -> { bird_position : number, pipes_position : number, is_game_over : boolean }
function onKey(world, key) {
  if (key === "Space") {
    return { bird_position: world.bird_position - 10, pipes_position: world.pipes_position, is_game_over: world.is_game_over };
  } else {
    return world;
  }
}
shouldEqual(onKey({ bird_position: 100, pipes_position: 100, is_game_over: false }, "Space"),
            { bird_position: 90, pipes_position: 100, is_game_over: false });
shouldEqual(onKey({ bird_position: 100, pipes_position: 100, is_game_over: false }, "Something Else"),
            { bird_position: 100, pipes_position: 100, is_game_over: false });
shouldEqual(onKey({ bird_position: 0, pipes_position: 100, is_game_over: false }, "Space"),
            { bird_position: -10, pipes_position: 100, is_game_over: false });

bigBang({bird_position: 100, pipes_position: 600, is_game_over: false}, drawWorld, onTick, onKey);