plum

#treesitter#compiler#wasm

git clone https://git.pyrossh.dev/plum

A statically typed, imperative programming language inspired by rust, python


e465385 — Peter John 2026-11-03T12:10:24+05:30
examples: Orb Run World functions become World methods
Files changed (2) hide show
  1. plum-examples/game.plum +182 -176
  2. plum-examples/game_scene.plum +202 -196
plum-examples/game.plum CHANGED
@@ -3,9 +3,9 @@
3
3
  # the mines. Three lives; Enter or R restarts after the game is over. The music
4
4
  # loops from `init`; orbs, mines and the end of a run each play a sound.
5
5
  #
6
- # All rules are plain functions over `World` that never call the host, so the
6
+ # All rules are methods of `World` (plus a few free helpers) that never call the host, so
7
- # `test` blocks below run under `plum test`; only `init`, `update` and `draw`
7
+ # the `test` blocks below run under `plum test`; only `init`, `update` and `draw`
8
- # talk to the host.
8
+ # (and `startMusic`, `sprite`) talk to the host.
9
9
  module game
10
10
  import std/Str
11
11
  import std/Int
@@ -50,7 +50,163 @@ record World(
50
50
  pickupSound: Int,
51
51
  hitSound: Int,
52
52
  overSound: Int,
53
- musicSound: Int)
53
+ musicSound: Int) =
54
+
55
+ fun create(seed: Int, sheet: Int) -> World =
56
+ rng := Random(seed: seed)
57
+ ship := Body(x: Rules.ArenaW / 2.0, y: Rules.ArenaH / 2.0, vx: 0.0, vy: 0.0, angle: 0.0)
58
+ w := World(
59
+ ship: ship, orbs: List[Body](), mines: List[Body](), rng: rng, score: 0, best: 0,
60
+ lives: Rules.StartLives, hurt: 0.0, clock: 0.0, over: false, sheet: sheet,
61
+ pickupSound: -1, hitSound: -1, overSound: -1, musicSound: -1)
62
+ for i := range Rules.OrbCount
63
+ w.orbs.push(spawn(rng, ship, 120.0, 40.0))
64
+ for i := range mineCount(0)
65
+ w.mines.push(spawn(rng, ship, 220.0, 90.0))
66
+ w
67
+
68
+ # A world with the orbs and mines moved out of the way.
69
+ fun empty() -> World =
70
+ w := World.create(7, -1)
71
+ for i := range w.orbs.length()
72
+ w.orbs.at(i).x = 5000.0
73
+ w.orbs.at(i).vx = 0.0
74
+ w.orbs.at(i).vy = 0.0
75
+ for i := range w.mines.length()
76
+ w.mines.at(i).x = 5000.0
77
+ w.mines.at(i).vx = 0.0
78
+ w.mines.at(i).vy = 0.0
79
+ w
80
+
81
+ # Starts a fresh game, keeping the best score and the random stream.
82
+ fun restart(self) =
83
+ self.best = self.best.max(self.score)
84
+ self.score = 0
85
+ self.lives = Rules.StartLives
86
+ self.hurt = 0.0
87
+ self.over = false
88
+ self.ship.x = Rules.ArenaW / 2.0
89
+ self.ship.y = Rules.ArenaH / 2.0
90
+ self.ship.vx = 0.0
91
+ self.ship.vy = 0.0
92
+ for i := range self.orbs.length()
93
+ moved := spawn(self.rng, self.ship, 120.0, 40.0)
94
+ self.orbs.at(i).x = moved.x
95
+ self.orbs.at(i).y = moved.y
96
+ self.orbs.at(i).vx = moved.vx
97
+ self.orbs.at(i).vy = moved.vy
98
+ while self.mines.length() > mineCount(0)
99
+ _ := self.mines.pop()!
100
+ for i := range self.mines.length()
101
+ moved := spawn(self.rng, self.ship, 220.0, 90.0)
102
+ self.mines.at(i).x = moved.x
103
+ self.mines.at(i).y = moved.y
104
+ self.mines.at(i).vx = moved.vx
105
+ self.mines.at(i).vy = moved.vy
106
+
107
+ # One simulation step. Does nothing once the game is over.
108
+ fun step(self, ax: Float, ay: Float, dt: Float) =
109
+ self.clock = self.clock + dt
110
+ if self.over
111
+ return
112
+ self.hurt = {self.hurt - dt}.max(0.0)
113
+ steer(self.ship, ax, ay, dt)
114
+ for i := range self.orbs.length()
115
+ orb := self.orbs.at(i)
116
+ drift(orb, Rules.OrbRadius, dt)
117
+ if overlaps(self.ship, Rules.ShipRadius, orb, Rules.OrbRadius)
118
+ self.score = self.score + Rules.OrbPoints
119
+ moved := spawn(self.rng, self.ship, 120.0, 40.0)
120
+ orb.x = moved.x
121
+ orb.y = moved.y
122
+ orb.vx = moved.vx
123
+ orb.vy = moved.vy
124
+ while self.mines.length() < mineCount(self.score)
125
+ self.mines.push(spawn(self.rng, self.ship, 220.0, 90.0))
126
+ for i := range self.mines.length()
127
+ mine := self.mines.at(i)
128
+ drift(mine, Rules.MineRadius, dt)
129
+ mine.angle = mine.angle + 2.0 * dt
130
+ if self.hurt <= 0.0 && overlaps(self.ship, Rules.ShipRadius, mine, Rules.MineRadius)
131
+ self.lives = self.lives - 1
132
+ self.hurt = Rules.HurtTime
133
+ if self.lives <= 0
134
+ self.over = true
135
+ self.best = self.best.max(self.score)
136
+
137
+ # Restarts the music loop from the top.
138
+ fun startMusic(self) =
139
+ soundStop(self.musicSound)
140
+ soundLoop(self.musicSound, musicVolume())
141
+
142
+ # Draws frame `frame` of sheet row `row`, centred on (x, y), at `Rules.Zoom` times its size.
143
+ fun sprite(self, row: Int, frame: Int, x: Float, y: Float, angle: Float, tint: Color) =
144
+ size := Rules.Cell * Rules.Zoom
145
+ drawTextureEx(
146
+ self.sheet, Float(frame) * Rules.Cell, Float(row) * Rules.Cell, Rules.Cell, Rules.Cell,
147
+ x - size / 2.0, y - size / 2.0, size, size, angle, false, tint)
148
+
149
+ fun drawStars(self) =
150
+ for i := range 40
151
+ sx := Float({i * 7919} % 800)
152
+ sy := Float({i * 104729} % 600)
153
+ twinkle := 0.25 + 0.2 * Math.sin(self.clock * 2.0 + Float(i))
154
+ drawCircle(sx, sy, 1.0 + Float(i % 3) * 0.5, Color(r: 0.7, g: 0.8, b: 1.0, a: twinkle))
155
+
156
+ fun update(self, dt: Float) =
157
+ if self.over && {isKeyPressed(Key.Enter) || isKeyPressed(Key.KeyR)}
158
+ self.restart()
159
+ self.startMusic()
160
+ left := isKeyDown(Key.Left) || isKeyDown(Key.KeyA)
161
+ right := isKeyDown(Key.Right) || isKeyDown(Key.KeyD)
162
+ up := isKeyDown(Key.Up) || isKeyDown(Key.KeyW)
163
+ down := isKeyDown(Key.Down) || isKeyDown(Key.KeyS)
164
+ score := self.score
165
+ lives := self.lives
166
+ self.step(axis(left, right), axis(up, down), dt)
167
+ if self.score > score
168
+ soundPlay(self.pickupSound, 0.8)
169
+ if self.lives < lives
170
+ if self.over
171
+ soundStop(self.musicSound)
172
+ soundPlay(self.overSound, 1.0)
173
+ else
174
+ soundPlay(self.hitSound, 1.0)
175
+
176
+ fun draw(self) =
177
+ sw := screenWidth()
178
+ sh := screenHeight()
179
+ zoom := {sw / Rules.ArenaW}.min(sh / Rules.ArenaH)
180
+ clear(Color.rgb(0.04, 0.05, 0.1))
181
+ setCamera(0.0 - {sw / zoom - Rules.ArenaW} / 2.0, 0.0 - {sh / zoom - Rules.ArenaH} / 2.0, zoom)
182
+ drawRect(0.0, 0.0, Rules.ArenaW, Rules.ArenaH, Color.rgb(0.07, 0.08, 0.16))
183
+ self.drawStars()
184
+ for i := range self.orbs.length()
185
+ orb := self.orbs.at(i)
186
+ blendMode(Blend.Add)
187
+ drawCircle(orb.x, orb.y, 26.0, Color(r: 0.35, g: 0.22, b: 0.0, a: 0.5))
188
+ drawCircle(orb.x, orb.y, 17.0, Color(r: 0.6, g: 0.4, b: 0.05, a: 0.5))
189
+ blendMode(Blend.Normal)
190
+ self.sprite(1, frameOf(self.clock + Float(i) * 0.1, 4, 8.0), orb.x, orb.y, 0.0, Color.White)
191
+ for i := range self.mines.length()
192
+ mine := self.mines.at(i)
193
+ self.sprite(2, frameOf(self.clock, 4, 4.0), mine.x, mine.y, mine.angle, Color.White)
194
+ blink := self.hurt > 0.0 && frameOf(self.clock, 2, 10.0) == 0
195
+ if !self.over && !blink
196
+ flame := frameOf(self.clock, 4, 14.0)
197
+ drawCircle(self.ship.x, self.ship.y, 17.0, Color(r: 0.4, g: 0.8, b: 1.0, a: 0.12))
198
+ self.sprite(0, flame, self.ship.x, self.ship.y, self.ship.angle, Color.White)
199
+ drawText("Score {self.score}", 16.0, 12.0, 28.0, Color.White)
200
+ drawText("Best {self.best.max(self.score)}", 16.0, 44.0, 20.0, Color.Yellow)
201
+ for i := range self.lives
202
+ drawTextureEx(
203
+ self.sheet, 0.0, 0.0, Rules.Cell, Rules.Cell,
204
+ Rules.ArenaW - 40.0 - Float(i) * 36.0, 12.0, 32.0, 32.0, 0.0, false, Color.White)
205
+ if self.over
206
+ drawRect(0.0, 240.0, Rules.ArenaW, 120.0, Color(r: 0.0, g: 0.0, b: 0.0, a: 0.6))
207
+ drawText("Game over", 300.0, 258.0, 48.0, Color.Red)
208
+ drawText("Press Enter to play again", 270.0, 316.0, 24.0, Color.White)
209
+ resetCamera()
54
210
 
55
211
  fun distance(a: Body, b: Body) -> Float =
56
212
  Math.hypot(a.x - b.x, a.y - b.y)
@@ -83,45 +239,6 @@ fun spawn(rng: Random, avoid: Body, gap: Float, speed: Float) -> Body =
83
239
  fun mineCount(score: Int) -> Int =
84
240
  {3 + score / 50}.min(8)
85
241
 
86
- fun newWorld(seed: Int, sheet: Int) -> World =
87
- rng := Random(seed: seed)
88
- ship := Body(x: Rules.ArenaW / 2.0, y: Rules.ArenaH / 2.0, vx: 0.0, vy: 0.0, angle: 0.0)
89
- w := World(
90
- ship: ship, orbs: List[Body](), mines: List[Body](), rng: rng, score: 0, best: 0,
91
- lives: Rules.StartLives, hurt: 0.0, clock: 0.0, over: false, sheet: sheet,
92
- pickupSound: -1, hitSound: -1, overSound: -1, musicSound: -1)
93
- for i := range Rules.OrbCount
94
- w.orbs.push(spawn(rng, ship, 120.0, 40.0))
95
- for i := range mineCount(0)
96
- w.mines.push(spawn(rng, ship, 220.0, 90.0))
97
- w
98
-
99
- # Starts a fresh game, keeping the best score and the random stream.
100
- fun restart(w: World) =
101
- w.best = w.best.max(w.score)
102
- w.score = 0
103
- w.lives = Rules.StartLives
104
- w.hurt = 0.0
105
- w.over = false
106
- w.ship.x = Rules.ArenaW / 2.0
107
- w.ship.y = Rules.ArenaH / 2.0
108
- w.ship.vx = 0.0
109
- w.ship.vy = 0.0
110
- for i := range w.orbs.length()
111
- moved := spawn(w.rng, w.ship, 120.0, 40.0)
112
- w.orbs.at(i).x = moved.x
113
- w.orbs.at(i).y = moved.y
114
- w.orbs.at(i).vx = moved.vx
115
- w.orbs.at(i).vy = moved.vy
116
- while w.mines.length() > mineCount(0)
117
- _ := w.mines.pop()!
118
- for i := range w.mines.length()
119
- moved := spawn(w.rng, w.ship, 220.0, 90.0)
120
- w.mines.at(i).x = moved.x
121
- w.mines.at(i).y = moved.y
122
- w.mines.at(i).vx = moved.vx
123
- w.mines.at(i).vy = moved.vy
124
-
125
242
  # Accelerates the ship by input axes (-1..1), applies drag and keeps it in the arena.
126
243
  fun steer(ship: Body, ax: Float, ay: Float, dt: Float) =
127
244
  ship.vx = ship.vx + ax * Rules.Thrust * dt
@@ -158,53 +275,18 @@ fun drift(b: Body, r: Float, dt: Float) =
158
275
  b.vy = 0.0 - b.vy
159
276
  b.y = b.y.max(r).min(Rules.ArenaH - r)
160
277
 
161
- # One simulation step. Does nothing once the game is over.
162
- fun step(w: World, ax: Float, ay: Float, dt: Float) =
163
- w.clock = w.clock + dt
164
- if w.over
165
- return
166
- w.hurt = {w.hurt - dt}.max(0.0)
167
- steer(w.ship, ax, ay, dt)
168
- for i := range w.orbs.length()
169
- orb := w.orbs.at(i)
170
- drift(orb, Rules.OrbRadius, dt)
171
- if overlaps(w.ship, Rules.ShipRadius, orb, Rules.OrbRadius)
172
- w.score = w.score + Rules.OrbPoints
173
- moved := spawn(w.rng, w.ship, 120.0, 40.0)
174
- orb.x = moved.x
175
- orb.y = moved.y
176
- orb.vx = moved.vx
177
- orb.vy = moved.vy
178
- while w.mines.length() < mineCount(w.score)
179
- w.mines.push(spawn(w.rng, w.ship, 220.0, 90.0))
180
- for i := range w.mines.length()
181
- mine := w.mines.at(i)
182
- drift(mine, Rules.MineRadius, dt)
183
- mine.angle = mine.angle + 2.0 * dt
184
- if w.hurt <= 0.0 && overlaps(w.ship, Rules.ShipRadius, mine, Rules.MineRadius)
185
- w.lives = w.lives - 1
186
- w.hurt = Rules.HurtTime
187
- if w.lives <= 0
188
- w.over = true
189
- w.best = w.best.max(w.score)
190
-
191
278
  # --- host glue: input, drawing ---
192
279
 
193
280
  fun musicVolume() -> Float =
194
281
  0.3
195
282
 
196
- # Restarts the music loop from the top.
197
- fun startMusic(w: World) =
198
- soundStop(w.musicSound)
199
- soundLoop(w.musicSound, musicVolume())
200
-
201
283
  export fun init() -> World =
202
- w := newWorld(2024, textureLoad("sprites.png"))
284
+ w := World.create(2024, textureLoad("sprites.png"))
203
285
  w.pickupSound = soundLoad("pickup.wav")
204
286
  w.hitSound = soundLoad("hit.wav")
205
287
  w.overSound = soundLoad("gameover.wav")
206
288
  w.musicSound = soundLoad("music.wav")
207
- startMusic(w)
289
+ w.startMusic()
208
290
  w
209
291
 
210
292
  fun axis(negative: Bool, positive: Bool) -> Float =
@@ -216,92 +298,16 @@ fun axis(negative: Bool, positive: Bool) -> Float =
216
298
  0.0
217
299
 
218
300
  export fun update(w: World, dt: Float) =
219
- if w.over && {isKeyPressed(Key.Enter) || isKeyPressed(Key.KeyR)}
220
- restart(w)
301
+ w.update(dt)
221
- startMusic(w)
222
- left := isKeyDown(Key.Left) || isKeyDown(Key.KeyA)
223
- right := isKeyDown(Key.Right) || isKeyDown(Key.KeyD)
224
- up := isKeyDown(Key.Up) || isKeyDown(Key.KeyW)
225
- down := isKeyDown(Key.Down) || isKeyDown(Key.KeyS)
226
- score := w.score
227
- lives := w.lives
228
- step(w, axis(left, right), axis(up, down), dt)
229
- if w.score > score
230
- soundPlay(w.pickupSound, 0.8)
231
- if w.lives < lives
232
- if w.over
233
- soundStop(w.musicSound)
234
- soundPlay(w.overSound, 1.0)
235
- else
236
- soundPlay(w.hitSound, 1.0)
237
-
238
- # Draws frame `frame` of sheet row `row`, centred on (x, y), at `Rules.Zoom` times its size.
239
- fun sprite(w: World, row: Int, frame: Int, x: Float, y: Float, angle: Float, tint: Color) =
240
- size := Rules.Cell * Rules.Zoom
241
- drawTextureEx(
242
- w.sheet, Float(frame) * Rules.Cell, Float(row) * Rules.Cell, Rules.Cell, Rules.Cell,
243
- x - size / 2.0, y - size / 2.0, size, size, angle, false, tint)
244
-
245
- fun drawStars(w: World) =
246
- for i := range 40
247
- sx := Float({i * 7919} % 800)
248
- sy := Float({i * 104729} % 600)
249
- twinkle := 0.25 + 0.2 * Math.sin(w.clock * 2.0 + Float(i))
250
- drawCircle(sx, sy, 1.0 + Float(i % 3) * 0.5, Color(r: 0.7, g: 0.8, b: 1.0, a: twinkle))
251
302
 
252
303
  export fun draw(w: World) =
253
- sw := screenWidth()
254
- sh := screenHeight()
255
- zoom := {sw / Rules.ArenaW}.min(sh / Rules.ArenaH)
256
- clear(Color.rgb(0.04, 0.05, 0.1))
257
- setCamera(0.0 - {sw / zoom - Rules.ArenaW} / 2.0, 0.0 - {sh / zoom - Rules.ArenaH} / 2.0, zoom)
258
- drawRect(0.0, 0.0, Rules.ArenaW, Rules.ArenaH, Color.rgb(0.07, 0.08, 0.16))
259
- drawStars(w)
304
+ w.draw()
260
- for i := range w.orbs.length()
261
- orb := w.orbs.at(i)
262
- blendMode(Blend.Add)
263
- drawCircle(orb.x, orb.y, 26.0, Color(r: 0.35, g: 0.22, b: 0.0, a: 0.5))
264
- drawCircle(orb.x, orb.y, 17.0, Color(r: 0.6, g: 0.4, b: 0.05, a: 0.5))
265
- blendMode(Blend.Normal)
266
- sprite(w, 1, frameOf(w.clock + Float(i) * 0.1, 4, 8.0), orb.x, orb.y, 0.0, Color.White)
267
- for i := range w.mines.length()
268
- mine := w.mines.at(i)
269
- sprite(w, 2, frameOf(w.clock, 4, 4.0), mine.x, mine.y, mine.angle, Color.White)
270
- blink := w.hurt > 0.0 && frameOf(w.clock, 2, 10.0) == 0
271
- if !w.over && !blink
272
- flame := frameOf(w.clock, 4, 14.0)
273
- drawCircle(w.ship.x, w.ship.y, 17.0, Color(r: 0.4, g: 0.8, b: 1.0, a: 0.12))
274
- sprite(w, 0, flame, w.ship.x, w.ship.y, w.ship.angle, Color.White)
275
- drawText("Score {w.score}", 16.0, 12.0, 28.0, Color.White)
276
- drawText("Best {w.best.max(w.score)}", 16.0, 44.0, 20.0, Color.Yellow)
277
- for i := range w.lives
278
- drawTextureEx(
279
- w.sheet, 0.0, 0.0, Rules.Cell, Rules.Cell,
280
- Rules.ArenaW - 40.0 - Float(i) * 36.0, 12.0, 32.0, 32.0, 0.0, false, Color.White)
281
- if w.over
282
- drawRect(0.0, 240.0, Rules.ArenaW, 120.0, Color(r: 0.0, g: 0.0, b: 0.0, a: 0.6))
283
- drawText("Game over", 300.0, 258.0, 48.0, Color.Red)
284
- drawText("Press Enter to play again", 270.0, 316.0, 24.0, Color.White)
285
- resetCamera()
286
305
 
287
306
  # --- tests ---
288
307
 
289
308
  fun still(x: Float, y: Float) -> Body =
290
309
  Body(x: x, y: y, vx: 0.0, vy: 0.0, angle: 0.0)
291
310
 
292
- # A world with the orbs and mines moved out of the way.
293
- fun emptyWorld() -> World =
294
- w := newWorld(7, -1)
295
- for i := range w.orbs.length()
296
- w.orbs.at(i).x = 5000.0
297
- w.orbs.at(i).vx = 0.0
298
- w.orbs.at(i).vy = 0.0
299
- for i := range w.mines.length()
300
- w.mines.at(i).x = 5000.0
301
- w.mines.at(i).vx = 0.0
302
- w.mines.at(i).vy = 0.0
303
- w
304
-
305
311
  test "steering accelerates and drag slows the ship"
306
312
  ship := still(400.0, 300.0)
307
313
  steer(ship, 1.0, 0.0, 0.1)
@@ -327,51 +333,51 @@ test "drifters bounce off the walls"
327
333
  assert b.x == Rules.ArenaW - 10.0
328
334
 
329
335
  test "collecting an orb scores and respawns it elsewhere"
330
- w := emptyWorld()
336
+ w := World.empty()
331
337
  orb := w.orbs.at(0)
332
338
  orb.x = w.ship.x
333
339
  orb.y = w.ship.y
334
- step(w, 0.0, 0.0, 0.016)
340
+ w.step(0.0, 0.0, 0.016)
335
341
  assert w.score == Rules.OrbPoints
336
342
  assert distance(orb, w.ship) >= 120.0
337
343
  assert orb.x < 5000.0
338
344
 
339
345
  test "a mine costs one life, then the ship is briefly safe"
340
- w := emptyWorld()
346
+ w := World.empty()
341
347
  mine := w.mines.at(0)
342
348
  mine.x = w.ship.x
343
349
  mine.y = w.ship.y
344
- step(w, 0.0, 0.0, 0.016)
350
+ w.step(0.0, 0.0, 0.016)
345
351
  assert w.lives == Rules.StartLives - 1
346
352
  assert w.hurt > 0.0
347
- step(w, 0.0, 0.0, 0.016)
353
+ w.step(0.0, 0.0, 0.016)
348
354
  assert w.lives == Rules.StartLives - 1
349
355
  mine.x = 5000.0
350
356
  for i := range 100
351
- step(w, 0.0, 0.0, 0.016)
357
+ w.step(0.0, 0.0, 0.016)
352
358
  assert w.hurt == 0.0
353
359
 
354
360
  test "the game ends at zero lives and then ignores steps"
355
- w := emptyWorld()
361
+ w := World.empty()
356
362
  w.lives = 1
357
363
  w.score = 40
358
364
  mine := w.mines.at(0)
359
365
  mine.x = w.ship.x
360
366
  mine.y = w.ship.y
361
- step(w, 0.0, 0.0, 0.016)
367
+ w.step(0.0, 0.0, 0.016)
362
368
  assert w.over
363
369
  assert w.lives == 0
364
370
  assert w.best == 40
365
371
  x := w.ship.x
366
- step(w, 1.0, 0.0, 0.5)
372
+ w.step(1.0, 0.0, 0.5)
367
373
  assert w.ship.x == x
368
374
 
369
375
  test "restart resets the run and keeps the best score"
370
- w := emptyWorld()
376
+ w := World.empty()
371
377
  w.score = 90
372
378
  w.lives = 0
373
379
  w.over = true
374
- restart(w)
380
+ w.restart()
375
381
  assert !w.over
376
382
  assert w.score == 0
377
383
  assert w.best == 90
@@ -383,21 +389,21 @@ test "more mines appear as the score grows"
383
389
  assert mineCount(0) == 3
384
390
  assert mineCount(100) == 5
385
391
  assert mineCount(10000) == 8
386
- w := emptyWorld()
392
+ w := World.empty()
387
393
  w.score = 100
388
- step(w, 0.0, 0.0, 0.016)
394
+ w.step(0.0, 0.0, 0.016)
389
395
  assert w.mines.length() == 5
390
396
 
391
397
  test "the same seed plays out the same way"
392
- a := newWorld(11, -1)
398
+ a := World.create(11, -1)
393
- b := newWorld(11, -1)
399
+ b := World.create(11, -1)
394
400
  for i := range 120
395
- step(a, 1.0, -1.0, 0.016)
401
+ a.step(1.0, -1.0, 0.016)
396
- step(b, 1.0, -1.0, 0.016)
402
+ b.step(1.0, -1.0, 0.016)
397
403
  assert a.ship.x == b.ship.x
398
404
  assert a.score == b.score
399
405
  assert a.mines.at(0).x == b.mines.at(0).x
400
- assert newWorld(12, -1).orbs.at(0).x != a.orbs.at(0).x
406
+ assert World.create(12, -1).orbs.at(0).x != a.orbs.at(0).x
401
407
 
402
408
  test "animation frames cycle"
403
409
  assert frameOf(0.0, 4, 8.0) == 0
plum-examples/game_scene.plum CHANGED
@@ -11,7 +11,7 @@
11
11
  # (its sprite frames), and the world is a tree: layer Groups under `stage.root`
12
12
  # (backdrop, stars, mines, orbs, ship, HUD), so `stage.act(dt)` and `stage.draw()` do
13
13
  # the per-kind work and z-order is the order of the layers. Collisions, scoring,
14
- # spawning, lives and restart stay in plain functions over `World`.
14
+ # spawning, lives and restart are methods of `World`.
15
15
  #
16
16
  # Prefer game.plum's immediate mode for a flat arcade game with a handful of kinds
17
17
  # where one state record and one pure `update` are easiest to test. Prefer this
@@ -262,158 +262,192 @@ record World(
262
262
  pickupSound: Int,
263
263
  hitSound: Int,
264
264
  overSound: Int,
265
- musicSound: Int)
266
-
267
- fun newWorld(seed: Int, sheet: Int) -> World =
268
- rng := Random(seed: seed)
269
- stage := Stage.create()
270
- ship := Ship(x: Rules.ArenaW / 2.0, y: Rules.ArenaH / 2.0, tex: sheet)
271
- stars := Group()
272
- for i := range 40
273
- stars.add(Star.create(i))
274
- mines := Group()
275
- orbs := Group()
276
- hud := Group()
277
- scoreText := Text.create("Score 0", 28.0, Color.White)
278
- scoreText.x = 16.0
279
- scoreText.y = 12.0
280
- bestText := Text.create("Best 0", 20.0, Color.Yellow)
281
- bestText.x = 16.0
282
- bestText.y = 44.0
283
- hud.add(scoreText)
284
- hud.add(bestText)
285
- icons := List[Sprite]()
286
- for i := range Rules.StartLives
287
- icon := Sprite.create(sheet, 0.0, 0.0, Rules.Cell, Rules.Cell, 32.0, 32.0)
288
- icon.x = Rules.ArenaW - 40.0 - Float(i) * 36.0
289
- icon.y = 12.0
290
- hud.add(icon)
291
- icons.push(icon)
292
- overlay := Group()
293
- overlay.visible = false
294
- overlay.add(Rect.create(Rules.ArenaW, 120.0, Color(r: 0.0, g: 0.0, b: 0.0, a: 0.6)).at(0.0, 240.0))
295
- overlay.add(Text.create("Game over", 48.0, Color.Red).at(300.0, 258.0))
296
- overlay.add(Text.create("Press Enter to play again", 24.0, Color.White).at(270.0, 316.0))
297
- hud.add(overlay)
298
- stage.root.add(Rect.create(Rules.ArenaW, Rules.ArenaH, Color.rgb(0.07, 0.08, 0.16)))
299
- stage.root.add(stars)
300
- stage.root.add(mines)
301
- stage.root.add(orbs)
302
- stage.root.add(ship)
303
- stage.root.add(hud)
304
- w := World(
305
- stage: stage, ship: ship, mines: mines, orbs: orbs, mineList: List[Mine](), orbList: List[Orb](),
306
- overlay: overlay, scoreText: scoreText, bestText: bestText, lifeIcons: icons,
307
- rng: rng, score: 0, best: 0, lives: Rules.StartLives, over: false, clock: 0.0, sheet: sheet,
308
- pickupSound: -1, hitSound: -1, overSound: -1, musicSound: -1)
309
- for i := range Rules.OrbCount
310
- orb := Orb.create(sheet, Float(i) * 0.1, spawn(rng, ship.x, ship.y, 120.0, 40.0))
311
- orbs.add(orb)
312
- w.orbList.push(orb)
313
- for i := range mineCount(0)
314
- addMine(w)
315
- w
316
-
317
- # Adds one mine, far from the ship, to the mines layer.
318
- fun addMine(w: World) =
319
- mine := Mine.create(w.sheet, spawn(w.rng, w.ship.x, w.ship.y, 220.0, 90.0))
320
- w.mines.add(mine)
321
- w.mineList.push(mine)
322
-
323
- # Starts a fresh game, keeping the best score and the random stream.
324
- fun restart(w: World) =
325
- w.best = w.best.max(w.score)
326
- w.score = 0
327
- w.lives = Rules.StartLives
328
- w.over = false
329
- ship := w.ship
330
- ship.hurt = 0.0
331
- ship.visible = true
332
- ship.x = Rules.ArenaW / 2.0
333
- ship.y = Rules.ArenaH / 2.0
334
- ship.vx = 0.0
335
- ship.vy = 0.0
336
- for i := range w.orbList.length()
337
- w.orbList.at(i).place(spawn(w.rng, ship.x, ship.y, 120.0, 40.0))
338
- while w.mineList.length() > mineCount(0)
339
- gone := w.mineList.pop()!
340
- w.mines.remove(gone)
341
- for i := range w.mineList.length()
342
- w.mineList.at(i).place(spawn(w.rng, ship.x, ship.y, 220.0, 90.0))
343
- sync(w)
344
-
345
- # Copies the score, best score, lives and game-over panel into the HUD actors.
346
- fun sync(w: World) =
347
- w.scoreText.text = "Score {w.score}"
348
- w.bestText.text = "Best {w.best.max(w.score)}"
349
- for i := range w.lifeIcons.length()
350
- w.lifeIcons.at(i).visible = i < w.lives
351
- w.overlay.visible = w.over
352
-
353
- # Lets the actors move (while the game runs) or only animate (once it is over).
354
- fun setLive(w: World, live: Bool) =
355
- w.ship.live = live
356
- for i := range w.orbList.length()
357
- w.orbList.at(i).live = live
358
- for i := range w.mineList.length()
359
- w.mineList.at(i).live = live
265
+ musicSound: Int) =
266
+
267
+ fun create(seed: Int, sheet: Int) -> World =
268
+ rng := Random(seed: seed)
269
+ stage := Stage.create()
270
+ ship := Ship(x: Rules.ArenaW / 2.0, y: Rules.ArenaH / 2.0, tex: sheet)
271
+ stars := Group()
272
+ for i := range 40
273
+ stars.add(Star.create(i))
274
+ mines := Group()
275
+ orbs := Group()
276
+ hud := Group()
277
+ scoreText := Text.create("Score 0", 28.0, Color.White)
278
+ scoreText.x = 16.0
279
+ scoreText.y = 12.0
280
+ bestText := Text.create("Best 0", 20.0, Color.Yellow)
281
+ bestText.x = 16.0
282
+ bestText.y = 44.0
283
+ hud.add(scoreText)
284
+ hud.add(bestText)
285
+ icons := List[Sprite]()
286
+ for i := range Rules.StartLives
287
+ icon := Sprite.create(sheet, 0.0, 0.0, Rules.Cell, Rules.Cell, 32.0, 32.0)
288
+ icon.x = Rules.ArenaW - 40.0 - Float(i) * 36.0
289
+ icon.y = 12.0
290
+ hud.add(icon)
291
+ icons.push(icon)
292
+ overlay := Group()
293
+ overlay.visible = false
294
+ overlay.add(Rect.create(Rules.ArenaW, 120.0, Color(r: 0.0, g: 0.0, b: 0.0, a: 0.6)).at(0.0, 240.0))
295
+ overlay.add(Text.create("Game over", 48.0, Color.Red).at(300.0, 258.0))
296
+ overlay.add(Text.create("Press Enter to play again", 24.0, Color.White).at(270.0, 316.0))
297
+ hud.add(overlay)
298
+ stage.root.add(Rect.create(Rules.ArenaW, Rules.ArenaH, Color.rgb(0.07, 0.08, 0.16)))
299
+ stage.root.add(stars)
300
+ stage.root.add(mines)
301
+ stage.root.add(orbs)
302
+ stage.root.add(ship)
303
+ stage.root.add(hud)
304
+ w := World(
305
+ stage: stage, ship: ship, mines: mines, orbs: orbs, mineList: List[Mine](), orbList: List[Orb](),
306
+ overlay: overlay, scoreText: scoreText, bestText: bestText, lifeIcons: icons,
307
+ rng: rng, score: 0, best: 0, lives: Rules.StartLives, over: false, clock: 0.0, sheet: sheet,
308
+ pickupSound: -1, hitSound: -1, overSound: -1, musicSound: -1)
309
+ for i := range Rules.OrbCount
310
+ orb := Orb.create(sheet, Float(i) * 0.1, spawn(rng, ship.x, ship.y, 120.0, 40.0))
311
+ orbs.add(orb)
312
+ w.orbList.push(orb)
313
+ for i := range mineCount(0)
314
+ w.addMine()
315
+ w
316
+
317
+ # A world with the orbs and mines moved out of the way.
318
+ fun empty() -> World =
319
+ w := World.create(7, -1)
320
+ for i := range w.orbList.length()
321
+ w.orbList.at(i).place(Motion(x: 5000.0, y: 0.0, vx: 0.0, vy: 0.0))
322
+ for i := range w.mineList.length()
323
+ w.mineList.at(i).place(Motion(x: 5000.0, y: 0.0, vx: 0.0, vy: 0.0))
324
+ w
325
+
326
+ # Adds one mine, far from the ship, to the mines layer.
327
+ fun addMine(self) =
328
+ mine := Mine.create(self.sheet, spawn(self.rng, self.ship.x, self.ship.y, 220.0, 90.0))
329
+ self.mines.add(mine)
330
+ self.mineList.push(mine)
331
+
332
+ # Starts a fresh game, keeping the best score and the random stream.
333
+ fun restart(self) =
334
+ self.best = self.best.max(self.score)
335
+ self.score = 0
336
+ self.lives = Rules.StartLives
337
+ self.over = false
338
+ ship := self.ship
339
+ ship.hurt = 0.0
340
+ ship.visible = true
341
+ ship.x = Rules.ArenaW / 2.0
342
+ ship.y = Rules.ArenaH / 2.0
343
+ ship.vx = 0.0
344
+ ship.vy = 0.0
345
+ for i := range self.orbList.length()
346
+ self.orbList.at(i).place(spawn(self.rng, ship.x, ship.y, 120.0, 40.0))
347
+ while self.mineList.length() > mineCount(0)
348
+ gone := self.mineList.pop()!
349
+ self.mines.remove(gone)
350
+ for i := range self.mineList.length()
351
+ self.mineList.at(i).place(spawn(self.rng, ship.x, ship.y, 220.0, 90.0))
352
+ self.sync()
353
+
354
+ # Copies the score, best score, lives and game-over panel into the HUD actors.
355
+ fun sync(self) =
356
+ self.scoreText.text = "Score {self.score}"
357
+ self.bestText.text = "Best {self.best.max(self.score)}"
358
+ for i := range self.lifeIcons.length()
359
+ self.lifeIcons.at(i).visible = i < self.lives
360
+ self.overlay.visible = self.over
361
+
362
+ # Lets the actors move (while the game runs) or only animate (once it is over).
363
+ fun setLive(self, live: Bool) =
364
+ self.ship.live = live
365
+ for i := range self.orbList.length()
366
+ self.orbList.at(i).live = live
367
+ for i := range self.mineList.length()
368
+ self.mineList.at(i).live = live
369
+
370
+ # One simulation step: every actor acts through the stage, then the rules look at
371
+ # the result. Once the game is over the actors only animate; nothing moves or scores.
372
+ fun step(self, ax: Float, ay: Float, dt: Float) =
373
+ self.clock = self.clock + dt
374
+ self.setLive(!self.over)
375
+ self.ship.ax = ax
376
+ self.ship.ay = ay
377
+ self.stage.act(dt)
378
+ if self.over
379
+ return
380
+ ship := self.ship
381
+ for i := range self.orbList.length()
382
+ orb := self.orbList.at(i)
383
+ if overlaps(ship.x, ship.y, Rules.ShipRadius, orb.x, orb.y, Rules.OrbRadius)
384
+ self.score = self.score + Rules.OrbPoints
385
+ orb.place(spawn(self.rng, ship.x, ship.y, 120.0, 40.0))
386
+ while self.mineList.length() < mineCount(self.score)
387
+ self.addMine()
388
+ for i := range self.mineList.length()
389
+ mine := self.mineList.at(i)
390
+ if ship.hurt <= 0.0 && overlaps(ship.x, ship.y, Rules.ShipRadius, mine.x, mine.y, Rules.MineRadius)
391
+ self.lives = self.lives - 1
392
+ ship.hurt = Rules.HurtTime
393
+ if self.lives <= 0
394
+ self.over = true
395
+ self.best = self.best.max(self.score)
396
+ ship.visible = false
397
+ self.sync()
398
+
399
+ # Fits the arena into a screen of `sw` by `sh` pixels, centred.
400
+ fun fit(self, sw: Float, sh: Float) =
401
+ zoom := {sw / Rules.ArenaW}.min(sh / Rules.ArenaH)
402
+ self.stage.camScale = zoom
403
+ self.stage.camX = 0.0 - {sw / zoom - Rules.ArenaW} / 2.0
404
+ self.stage.camY = 0.0 - {sh / zoom - Rules.ArenaH} / 2.0
405
+
406
+ # Restarts the music loop from the top.
407
+ fun startMusic(self) =
408
+ soundStop(self.musicSound)
409
+ soundLoop(self.musicSound, musicVolume())
410
+
411
+ fun update(self, dt: Float) =
412
+ if self.over && {isKeyPressed(Key.Enter) || isKeyPressed(Key.KeyR)}
413
+ self.restart()
414
+ self.startMusic()
415
+ left := isKeyDown(Key.Left) || isKeyDown(Key.KeyA)
416
+ right := isKeyDown(Key.Right) || isKeyDown(Key.KeyD)
417
+ up := isKeyDown(Key.Up) || isKeyDown(Key.KeyW)
418
+ down := isKeyDown(Key.Down) || isKeyDown(Key.KeyS)
419
+ score := self.score
420
+ lives := self.lives
421
+ self.step(axis(left, right), axis(up, down), dt)
422
+ if self.score > score
423
+ soundPlay(self.pickupSound, 0.8)
424
+ if self.lives < lives
425
+ if self.over
426
+ soundStop(self.musicSound)
427
+ soundPlay(self.overSound, 1.0)
428
+ else
429
+ soundPlay(self.hitSound, 1.0)
430
+
431
+ fun draw(self) =
432
+ self.fit(screenWidth(), screenHeight())
433
+ clear(Color.rgb(0.04, 0.05, 0.1))
434
+ self.stage.draw()
360
435
 
361
436
  fun overlaps(ax: Float, ay: Float, ra: Float, bx: Float, by: Float, rb: Float) -> Bool =
362
437
  Math.hypot(ax - bx, ay - by) < ra + rb
363
438
 
364
- # One simulation step: every actor acts through the stage, then the rules look at
365
- # the result. Once the game is over the actors only animate; nothing moves or scores.
366
- fun step(w: World, ax: Float, ay: Float, dt: Float) =
367
- w.clock = w.clock + dt
368
- setLive(w, !w.over)
369
- w.ship.ax = ax
370
- w.ship.ay = ay
371
- w.stage.act(dt)
372
- if w.over
373
- return
374
- ship := w.ship
375
- for i := range w.orbList.length()
376
- orb := w.orbList.at(i)
377
- if overlaps(ship.x, ship.y, Rules.ShipRadius, orb.x, orb.y, Rules.OrbRadius)
378
- w.score = w.score + Rules.OrbPoints
379
- orb.place(spawn(w.rng, ship.x, ship.y, 120.0, 40.0))
380
- while w.mineList.length() < mineCount(w.score)
381
- addMine(w)
382
- for i := range w.mineList.length()
383
- mine := w.mineList.at(i)
384
- if ship.hurt <= 0.0 && overlaps(ship.x, ship.y, Rules.ShipRadius, mine.x, mine.y, Rules.MineRadius)
385
- w.lives = w.lives - 1
386
- ship.hurt = Rules.HurtTime
387
- if w.lives <= 0
388
- w.over = true
389
- w.best = w.best.max(w.score)
390
- ship.visible = false
391
- sync(w)
392
-
393
- # Fits the arena into a screen of `sw` by `sh` pixels, centred.
394
- fun fit(w: World, sw: Float, sh: Float) =
395
- zoom := {sw / Rules.ArenaW}.min(sh / Rules.ArenaH)
396
- w.stage.camScale = zoom
397
- w.stage.camX = 0.0 - {sw / zoom - Rules.ArenaW} / 2.0
398
- w.stage.camY = 0.0 - {sh / zoom - Rules.ArenaH} / 2.0
399
-
400
439
  # --- host glue: input, sound, drawing ---
401
440
 
402
441
  fun musicVolume() -> Float =
403
442
  0.3
404
443
 
405
- # Restarts the music loop from the top.
406
- fun startMusic(w: World) =
407
- soundStop(w.musicSound)
408
- soundLoop(w.musicSound, musicVolume())
409
-
410
444
  export fun init() -> World =
411
- w := newWorld(2024, textureLoad("sprites.png"))
445
+ w := World.create(2024, textureLoad("sprites.png"))
412
446
  w.pickupSound = soundLoad("pickup.wav")
413
447
  w.hitSound = soundLoad("hit.wav")
414
448
  w.overSound = soundLoad("gameover.wav")
415
449
  w.musicSound = soundLoad("music.wav")
416
- startMusic(w)
450
+ w.startMusic()
417
451
  w
418
452
 
419
453
  fun axis(negative: Bool, positive: Bool) -> Float =
@@ -425,44 +459,16 @@ fun axis(negative: Bool, positive: Bool) -> Float =
425
459
  0.0
426
460
 
427
461
  export fun update(w: World, dt: Float) =
428
- if w.over && {isKeyPressed(Key.Enter) || isKeyPressed(Key.KeyR)}
429
- restart(w)
462
+ w.update(dt)
430
- startMusic(w)
431
- left := isKeyDown(Key.Left) || isKeyDown(Key.KeyA)
432
- right := isKeyDown(Key.Right) || isKeyDown(Key.KeyD)
433
- up := isKeyDown(Key.Up) || isKeyDown(Key.KeyW)
434
- down := isKeyDown(Key.Down) || isKeyDown(Key.KeyS)
435
- score := w.score
436
- lives := w.lives
437
- step(w, axis(left, right), axis(up, down), dt)
438
- if w.score > score
439
- soundPlay(w.pickupSound, 0.8)
440
- if w.lives < lives
441
- if w.over
442
- soundStop(w.musicSound)
443
- soundPlay(w.overSound, 1.0)
444
- else
445
- soundPlay(w.hitSound, 1.0)
446
463
 
447
464
  export fun draw(w: World) =
448
- fit(w, screenWidth(), screenHeight())
449
- clear(Color.rgb(0.04, 0.05, 0.1))
450
- w.stage.draw()
465
+ w.draw()
451
466
 
452
467
  # --- tests ---
453
468
 
454
469
  fun near(a: Float, b: Float) -> Bool =
455
470
  {a - b}.abs() < 0.01
456
471
 
457
- # A world with the orbs and mines moved out of the way.
458
- fun emptyWorld() -> World =
459
- w := newWorld(7, -1)
460
- for i := range w.orbList.length()
461
- w.orbList.at(i).place(Motion(x: 5000.0, y: 0.0, vx: 0.0, vy: 0.0))
462
- for i := range w.mineList.length()
463
- w.mineList.at(i).place(Motion(x: 5000.0, y: 0.0, vx: 0.0, vy: 0.0))
464
- w
465
-
466
472
  test "steering accelerates and drag slows the ship"
467
473
  ship := Ship(x: 400.0, y: 300.0, tex: -1)
468
474
  ship.ax = 1.0
@@ -498,7 +504,7 @@ test "a mine spins as it drifts"
498
504
  assert near(mine.x, 405.0)
499
505
 
500
506
  test "the scene has a backdrop, stars, mines, orbs, ship and HUD layers"
501
- w := newWorld(7, -1)
507
+ w := World.create(7, -1)
502
508
  assert w.stage.root.children.length() == 6
503
509
  assert w.stage.root.children.at(1).children.length() == 40
504
510
  assert w.mines.children.length() == mineCount(0)
@@ -508,10 +514,10 @@ test "the scene has a backdrop, stars, mines, orbs, ship and HUD layers"
508
514
  assert same(w.stage.root.children.at(3), w.orbs)
509
515
 
510
516
  test "collecting an orb scores and respawns it elsewhere, still in the orbs layer"
511
- w := emptyWorld()
517
+ w := World.empty()
512
518
  orb := w.orbList.at(0)
513
519
  orb.place(Motion(x: w.ship.x, y: w.ship.y, vx: 0.0, vy: 0.0))
514
- step(w, 0.0, 0.0, 0.016)
520
+ w.step(0.0, 0.0, 0.016)
515
521
  assert w.score == Rules.OrbPoints
516
522
  assert Math.hypot(orb.x - w.ship.x, orb.y - w.ship.y) >= 120.0
517
523
  assert orb.x < 5000.0
@@ -520,10 +526,10 @@ test "collecting an orb scores and respawns it elsewhere, still in the orbs laye
520
526
  assert w.scoreText.text == "Score 10"
521
527
 
522
528
  test "a mine costs one life and makes the ship flash through its own act"
523
- w := emptyWorld()
529
+ w := World.empty()
524
530
  mine := w.mineList.at(0)
525
531
  mine.place(Motion(x: w.ship.x, y: w.ship.y, vx: 0.0, vy: 0.0))
526
- step(w, 0.0, 0.0, 0.016)
532
+ w.step(0.0, 0.0, 0.016)
527
533
  assert w.lives == Rules.StartLives - 1
528
534
  assert w.ship.hurt > 0.0
529
535
  assert !w.lifeIcons.at(2).visible
@@ -532,7 +538,7 @@ test "a mine costs one life and makes the ship flash through its own act"
532
538
  hidden := 0
533
539
  shown := 0
534
540
  for i := range 100
535
- step(w, 0.0, 0.0, 0.016)
541
+ w.step(0.0, 0.0, 0.016)
536
542
  if w.ship.visible
537
543
  shown = shown + 1
538
544
  else
@@ -544,35 +550,35 @@ test "a mine costs one life and makes the ship flash through its own act"
544
550
  assert w.lives == Rules.StartLives - 1
545
551
 
546
552
  test "the ship is briefly safe after a hit"
547
- w := emptyWorld()
553
+ w := World.empty()
548
554
  mine := w.mineList.at(0)
549
555
  mine.place(Motion(x: w.ship.x, y: w.ship.y, vx: 0.0, vy: 0.0))
550
- step(w, 0.0, 0.0, 0.016)
556
+ w.step(0.0, 0.0, 0.016)
551
- step(w, 0.0, 0.0, 0.016)
557
+ w.step(0.0, 0.0, 0.016)
552
558
  assert w.lives == Rules.StartLives - 1
553
559
 
554
560
  test "the game ends at zero lives and then ignores steps"
555
- w := emptyWorld()
561
+ w := World.empty()
556
562
  w.lives = 1
557
563
  w.score = 40
558
564
  mine := w.mineList.at(0)
559
565
  mine.place(Motion(x: w.ship.x, y: w.ship.y, vx: 0.0, vy: 0.0))
560
- step(w, 0.0, 0.0, 0.016)
566
+ w.step(0.0, 0.0, 0.016)
561
567
  assert w.over
562
568
  assert w.lives == 0
563
569
  assert w.best == 40
564
570
  assert w.overlay.visible
565
571
  assert !w.ship.visible
566
572
  x := w.ship.x
567
- step(w, 1.0, 0.0, 0.5)
573
+ w.step(1.0, 0.0, 0.5)
568
574
  assert w.ship.x == x
569
575
 
570
576
  test "after game over nothing moves but the animations keep running"
571
- w := emptyWorld()
577
+ w := World.empty()
572
578
  w.lives = 1
573
579
  mine := w.mineList.at(0)
574
580
  mine.place(Motion(x: w.ship.x, y: w.ship.y, vx: 10.0, vy: 0.0))
575
- step(w, 0.0, 0.0, 0.016)
581
+ w.step(0.0, 0.0, 0.016)
576
582
  assert w.over
577
583
  x := mine.x
578
584
  rot := mine.rotation
@@ -581,7 +587,7 @@ test "after game over nothing moves but the animations keep running"
581
587
  star := Star.create(0)
582
588
  w.stage.root.children.at(1).add(star)
583
589
  for i := range 10
584
- step(w, 1.0, 0.0, 0.1)
590
+ w.step(1.0, 0.0, 0.1)
585
591
  assert mine.x == x
586
592
  assert mine.rotation == rot
587
593
  assert w.ship.x == Rules.ArenaW / 2.0
@@ -592,12 +598,12 @@ test "after game over nothing moves but the animations keep running"
592
598
  assert w.score == 0
593
599
 
594
600
  test "restart resets the run and keeps the best score"
595
- w := emptyWorld()
601
+ w := World.empty()
596
602
  w.score = 90
597
603
  w.lives = 0
598
604
  w.over = true
599
605
  w.overlay.visible = true
600
- restart(w)
606
+ w.restart()
601
607
  assert !w.over
602
608
  assert !w.overlay.visible
603
609
  assert w.ship.visible
@@ -612,29 +618,29 @@ test "more mines appear as the score grows and restart removes them from the lay
612
618
  assert mineCount(0) == 3
613
619
  assert mineCount(100) == 5
614
620
  assert mineCount(10000) == 8
615
- w := emptyWorld()
621
+ w := World.empty()
616
622
  w.score = 100
617
- step(w, 0.0, 0.0, 0.016)
623
+ w.step(0.0, 0.0, 0.016)
618
624
  assert w.mineList.length() == 5
619
625
  assert w.mines.children.length() == 5
620
- restart(w)
626
+ w.restart()
621
627
  assert w.mineList.length() == 3
622
628
  assert w.mines.children.length() == 3
623
629
 
624
630
  test "the same seed plays out the same way"
625
- a := newWorld(11, -1)
631
+ a := World.create(11, -1)
626
- b := newWorld(11, -1)
632
+ b := World.create(11, -1)
627
633
  for i := range 120
628
- step(a, 1.0, -1.0, 0.016)
634
+ a.step(1.0, -1.0, 0.016)
629
- step(b, 1.0, -1.0, 0.016)
635
+ b.step(1.0, -1.0, 0.016)
630
636
  assert a.ship.x == b.ship.x
631
637
  assert a.score == b.score
632
638
  assert a.mineList.at(0).x == b.mineList.at(0).x
633
- assert newWorld(12, -1).orbList.at(0).x != a.orbList.at(0).x
639
+ assert World.create(12, -1).orbList.at(0).x != a.orbList.at(0).x
634
640
 
635
641
  test "fit centres the arena on the screen"
636
- w := newWorld(7, -1)
642
+ w := World.create(7, -1)
637
- fit(w, 1600.0, 600.0)
643
+ w.fit(1600.0, 600.0)
638
644
  assert near(w.stage.camScale, 1.0)
639
645
  assert near(w.stage.camX, -400.0)
640
646
  assert near(w.stage.camY, 0.0)