-
Notifications
You must be signed in to change notification settings - Fork 4
/
04_data.txt
1089 lines (890 loc) · 39.6 KB
/
04_data.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
:chap_num: 4
:prev_link: 03_functions
:next_link: 05_higher_order
= Compound Data: Objects =
////
Outline:
- introduce flat objects as way to collect different data together.
- match this against domain modelling. ie, if we have a book, we may
have a title, author, pub year, which naturally models as
{ title : string, author : string, year: number }
- some examples using that (and the design recipe!)
- introduce bigBang, showing how you can use it, and images, to make
games!
- extended examples and then extended exercise building a real game!
////
Note: We're not sure what this illustration is for, but maybe it will gain some purpose.
image::img/weresquirrel.png[alt="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:
[source,javascript]
----
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:
[source,javascript]
----
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:
[source,javascript]
----
// 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:
[source,javascript]
----
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
[source,javascript]
----
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:
[source,javascript]
----
// { 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[_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:
- The first argument is the initial world state which, if you were
replicating `animate`, would be the number 0.
- The second argument is the same as `animate` - it is a
function that takes your world state (in `animate`, a number) and produces
as output an image which will get drawn.
- The third argument is a function that takes the current world state
as input and produces the new world state as output (in `animate`, this
function just adds `1` to its input).
- The last argument is completely new: it is a function
that takes as input the current world state and a string that
represents a key that was pressed on the keyboard, and produces a new
world state. This makes it so that your "animations" can respond to
keyboard input (which makes them interactive! And can be games).
To review, we can use `bigBang` to replicate the way that animate worked:
[source,javascript]
----
animate(function (ticks) {
return placeImage(circle(10, "red"), emptyScene(400,100), ticks, 70);
});
----
[source,javascript]
----
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[_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:
[source,javascript]
----
// 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:
[source,javascript]
----
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.
[source,javascript]
----
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 link:00_tlcjs_reference.html[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:
[source,javascript]
----
// { bird_position : number }, string -> { bird_position : number }
----
Next, we'll write a purpose statement. In this case:
[source,javascript]
----
// 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:
[source,javascript]
----
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?
[source,javascript]
----
shouldEqual(onKey({ bird_position: 0 }, "Space"), { bird_position: -10 });
----
Or should this one:
[source,javascript]
----
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:
[source,javascript]
----
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):
[source,javascript]
----
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:
[source,javascript]
----
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:
[source,javascript]
----
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:
[source,javascript]
----
// { 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:
[source,javascript]
----
// 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:
[source,javascript]
----
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:
[source,javascript]
----
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:
[source,javascript]
----
// { 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:
[source,javascript]
----
// 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:
[source,javascript]
----
// { 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!
[source,javascript]
----
// { 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.
[source,javascript]
----
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.
[source,javascript]
----
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:
[source,javascript]
----
// { 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:
[source,javascript]
----
// 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:
[source,javascript]
----
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:
[source,javascript]
----
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):
[source,javascript]
----
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:
- In our drawing function.
- In our `onTick` function.
- In our `onKey` function.
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:
[source,javascript]
----
// 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).
[source,javascript]
----
// { 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:
[source,javascript]
----
// { 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?
[source,javascript]
----
// { 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:
[source,javascript]
----
// { 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:
[source,javascript]
----
// { 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!
[source,javascript]
----
var eye = circle(5, "white");
var mouth = placeImage(circle(10, "rebeccapurple"), circle(10, "white"), 0, -5);