-
Notifications
You must be signed in to change notification settings - Fork 1
/
flappy.eve
236 lines (172 loc) · 9.33 KB
/
flappy.eve
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
# Flappy Eve
When a player starts the game, we commit a `#world`, a `#player`, and some `#obstacles`. These will keep all of the essential state of the game. All of this information could have been stored on the world, but for clarity we break the important bits of state into objects that they effect.
- The `#world` tracks the distance the player has travelled, the current game screen, and the high score.
- The `#player` stores his current y position and (vertical) velocity.
- The `obstacles` have their (horizontal) offset and gap widths. We put distance on the world and only keep two obstacles; rather than moving the player through the world, we keep the player stationary and move the world past the player. When an obstacle goes off screen, we will wrap it around, update the placement of its gap, and continue on.
## Setup
Add a flappy eve and a world for it to flap in:
~~~
commit
[#player #self name: "eve" x: 25 y: 50 velocity: 0]
[#world screen: "menu" frame: 0 distance: 0 best: 0 gravity: -0.061]
[#obstacle gap: 35 offset: 0]
[#obstacle gap: 35 offset: -1]
~~~
Next we draw the backdrop of the world. The player and obstacle will be drawn later based on their current state. Throughout the app we use resources from [@bhauman's flappy bird demo in clojure][1]. Since none of these things change over time, we commit them once when the player starts the game.
### Draw the game world!
~~~
search
world = [#world]
commit @browser
world <- [#div style: [user-select: "none" -webkit-user-select: "none" -moz-user-select: "none"] children:
[#svg #game-window viewBox: "10 0 80 100", width: 480 children:
[#rect x: 0 y: 0 width: 100 height: 53 fill: "rgb(112, 197, 206)" sort: 0]
[#image x: 0 y: 52 width: 100 height: 43 preserveAspectRatio: "xMinYMin slice" href: "https://cdn.rawgit.com/bhauman/flappy-bird-demo/master/resources/public/imgs/background.png" sort: 1]
[#rect x: 0 y: 95 width: 100 height: 5 fill: "rgb(222, 216, 149)" sort: 0]]]
~~~
## Game menus
These following blocks handle drawing the game's other screens (such as the main menu and the game over scene).
The main menu displays a message instructing the player how to start the game.
~~~
search @browser @session
[#world screen: "menu"]
svg = [#game-window]
bind @browser
svg.children += [#text x: 50 y: 45 text-anchor: "middle" font-size: 6 text: "Click the screen to begin!" sort: 10]
~~~
The "game over" screen displays the final score of the last game, the high score of all games, and a message inviting the player to play the game again.
~~~
search @session @browser
[#world screen: "game over" score best]
svg = [#game-window]
bind @browser
svg.children += [#text x: 50 y: 30 text-anchor: "middle" font-size: 6 text: "Game Over :(" sort: 10]
svg.children += [#text x: 50 y: 55 text-anchor: "middle" font-size: 6 text: "Score {{score}}" sort: 10]
svg.children += [#text x: 50 y: 65 text-anchor: "middle" font-size: 6 text: "Best {{best}}" sort: 10]
svg.children += [#text x: 50 y: 85 text-anchor: "middle" font-size: 4 text: "Click to play again!" sort: 10]
~~~
### Score calculation
We haven't calculated the score yet, so let's do that. We calculate the score as the `floor` of the distance, meaning we just round the distance down to the nearest integer. If the distance between pipes is changed, this value can be scaled to search.
~~~
search
world = [#world distance]
bind
world.score := floor[value: distance]
~~~
### Start a new game
When the game is on the "menu" or "game over" screens, a click anywhere in the application will (re)start the game. Additionally, if the current score is better than the current best, we'll swap them out now. Along with starting the game, we make sure to reset the distance and player positions in the came of a restart.
~~~
search @event @session
[#click element: [#world]]
world = if world = [#world screen: "menu"] then world
else [#world screen: "game over"]
new = if world = [#world score best] score > best then score
else if world = [#world best] then best
player = [#player]
commit
world <- [screen: "game" distance: 0 best: new]
player <- [x: 25 y: 50 velocity: 0]
~~~
## Drawing
### Player
Next we draw the `#player` at its (x,y) coordinates. Since the player is stationary in x, setting his x position here dynamically is just a formality, but it allows us to configure his position on the screen when we initialize. We create the sprite first, then set the x and y positions to let us reuse the same element regardless of where the player is.
Draw the player
~~~
search @session @browser
svg = [#game-window]
player = [#player x y]
bind @browser
sprite = [#image player | width: 10 height: 10 href: "http://i.imgur.com/sp68LtM.gif" sort: 8]
sprite.x := x - 5
sprite.y := y - 5
svg.children += sprite
~~~
### Obstacles
Drawing obstacles is much the same process as drawing the player, but we encapsulate the sprites into a nested SVG to group and move them as a unit.
Draw the obstacles
~~~
search @session @browser
svg = [#game-window]
obstacle = [#obstacle x height gap]
bottom-height = height + gap
imgs = "https://cdn.rawgit.com/bhauman/flappy-bird-demo/master/resources/public/imgs"
bind @browser
sprite-group = [#svg #obs-spr obstacle sort: 2 overflow: "visible" children:
[#image y: 0 width: 10 height, preserveAspectRatio: "none" href: "{{imgs}}/pillar-bkg.png" sort: 1]
[#image x: -1 y: height - 5 width: 12 height: 5 href: "{{imgs}}/lower-pillar-head.png" sort: 2]
[#image y: bottom-height width: 10 height: 90 - bottom-height, preserveAspectRatio: "none" href: "{{imgs}}/pillar-bkg.png" sort: 1]
[#image x: -1 y: bottom-height width: 12 height: 5 href: "{{imgs}}/lower-pillar-head.png" sort: 2]]
sprite-group.x := x
svg.children += sprite-group
~~~
## Game Logic
Now we need some logic to actually play the game. We slide obstacles along proportional to the distance travelled, and wrap them around to the beginning once they're entirely off screen. Additionally, we only show obstacles once their distance travelled is positive. This allows us to offset a pipe in the future, without the modulo operator wrapping it around to start off halfway through the screen.
### Obstacles
Every 2 distance a wild obstacle appears
~~~
search
[#world distance]
obstacle = [#obstacle offset]
obstacle-distance = distance + offset
obstacle-distance >= 0
bind
obstacle <- [x: 100 - (50 * mod[value: obstacle-distance, by: 2])]
~~~
When the obstacle is offscreen (`x > 90`), we randomly adjust the height of its gap to ensure the game doesn't play the same way twice. Eve's current random implementation yields a single result per seed per evaluation, so you can ask for `random[seed: "foo"]` in multiple queries and get the same result in that evaluation. In practice, this means that for every unique sample of randomness you care about in a program at a fixed time, you should use a unique seed. In this case, since we want one sample per obstacle, we just use the obstacle UUIDs as our seeds. The magic numbers in the equation just keep the gap from being at the very top of the screen or underground.
Readjust the height of the gap every time the obstacle resets
~~~
search
[#world screen: "game" frame]
obstacle = [#obstacle x > 90]
height = random[seed: frame] * 30 + 5
commit
obstacle.height := height
~~~
### Flapping the player
When a player clicks during gameplay, we give the bird some lift by setting its velocity.
~~~
search @event @session
[#click element: [#world]]
[#world screen: "game"]
player = [#player #self]
commit
player.velocity := 1.17
~~~
### Scroll the world
Next, we scroll the world in time with frame updates. Eve is currently locked to 60fps updates here, but this will probably be configurable in the future. Importantly, we only want to update the world state once per frame, so to ensure that we note the offset of the frame we last computed in `world.frame` and ensure we’re not recomputing for the same offset.
~~~
search @session @event
[#time frames]
world = [#world screen: "game" frame != frames gravity]
player = [#player y velocity]
not([#click])
commit
world.frame := frames
world.distance := world.distance + 1 / 60
player <- [y: y - velocity, velocity: velocity + gravity]
~~~
### Collision
Checking collision with the ground is very simple. Since we know the y height of the ground, we just check if the player's bottom (determined by center + radius) is below that point.
The game is lost if the player hits the ground.
~~~
search
world = [#world screen: "game"]
[#player y > 85] // ground height + player radius
commit
world.screen := "game over"
~~~
Collision with the pipes is only slightly harder. Since they come in pairs, we first determine if the player is horizontally in a slice that may contain pipes and if so, whether we're above or below the gap. If neither, we're in the clear, otherwise we've collided.
The game is lost if the player hits an `#obstacle`
~~~
search
world = [#world screen: "game"]
[#player x y]
[#obstacle x: obstacle-x height gap]
∂x = abs[value: obstacle-x + 5 - x] - 10 // distance between the edges of player and obstacle (offset of 1/2 obstacle width because origin is on the left)
∂x < 0
collision = if y - 5 <= height then true
else if y + 5 >= gap + height then true
commit
world.screen := "game over"
~~~
[1]: https://github.com/bhauman/flappy-bird-demo