• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

bcpearce / asteroids-rs / 23280422597

19 Mar 2026 04:53AM UTC coverage: 91.899% (-0.1%) from 92.027%
23280422597

Pull #8

github

web-flow
Merge 2579c440e into 0d986657b
Pull Request #8: Added UFO, improved collisions

312 of 335 new or added lines in 7 files covered. (93.13%)

6 existing lines in 1 file now uncovered.

1055 of 1148 relevant lines covered (91.9%)

538359.55 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.52
/src/engine.rs
1
use crate::asteroid::Asteroid;
2
use crate::asteroid::Size as AsteroidSize;
3
use crate::collisions::ShipCollidable;
4
use crate::collisions::ShotCollidable;
5
use crate::debris::{Debris, LineDebris};
6
use crate::ship::Ship;
7
use crate::shot::Shot;
8
use crate::ufo::Ufo;
9
use gloo::events::{EventListener, EventListenerOptions};
10
use gloo::timers::callback::Interval;
11
use std::collections::HashMap;
12
#[cfg(test)]
13
use std::sync::atomic::{AtomicUsize, Ordering};
14
use web_sys::KeyboardEvent;
15
use web_sys::wasm_bindgen::JsCast;
16
use yew::{Component, Context, Html, html};
17

18
#[cfg(test)]
19
static QUICKCHECK_RUN_COUNT: AtomicUsize = AtomicUsize::new(0);
20

21
const INTERVAL_DURATION_MILLIS: u32 = 10;
22
const WIDTH: u32 = 480;
23
const HEIGHT: u32 = 480;
24
const BASE_DIFFICULTY: u32 = 10;
25
const MAX_ASTEROIDS: usize = 10;
26

27
const A_CODE_U: u32 = b'A' as u32;
28
const A_CODE_L: u32 = b'a' as u32;
29
const D_CODE_U: u32 = b'D' as u32;
30
const D_CODE_L: u32 = b'd' as u32;
31
#[cfg(test)]
32
const W_CODE_U: u32 = b'W' as u32;
33
const W_CODE_L: u32 = b'w' as u32;
34
const H_CODE_U: u32 = b'H' as u32;
35
const H_CODE_L: u32 = b'h' as u32;
36
const SPACE_CODE: u32 = b' ' as u32;
37

38
pub enum Msg {
39
    Tick,
40
    Keydown(u32),
41
    Keyup(u32),
42
}
43

44
#[derive(Clone, Debug)]
45
pub enum KeyAction {
46
    Up,
47
    Down,
48
}
49

50
pub struct GameContext {
51
    pub w: f32,
52
    pub h: f32,
53
    pub t: f32,
54
}
55

56
pub trait GameElement {
57
    fn update(&mut self, ctx: &GameContext);
58
    fn alive(&self) -> bool;
59
    fn render(&self) -> Html;
60
    fn destroy(&mut self);
61
}
62

63
type KeyMap = HashMap<u32, KeyAction>;
64

65
struct WindowEventHandler {
66
    _interval: Interval,
67
    _keydown: EventListener,
68
    _keyup: EventListener,
69
}
70
pub struct Engine {
71
    pub w: u32,
72
    pub h: u32,
73
    pub t: f32,
74
    ship: Ship,
75
    shots: Vec<Shot>,
76
    asteroids: Vec<Asteroid>,
77
    ufo: Ufo,
78
    debris: Vec<Debris>,
79
    line_debris: Vec<LineDebris>,
80
    difficulty: u32,
81
    score: i32,
82
    maybe_seed: Option<u64>,
83
    keymap: KeyMap,
84
    _maybe_window_event_handler: Option<WindowEventHandler>,
85
}
86
impl Engine {
87
    fn create_impl(
207✔
88
        maybe_ctx: Option<&Context<Self>>,
207✔
89
        difficulty: u32,
207✔
90
        maybe_engine_seed: Option<u64>,
207✔
91
        maybe_ship_seed: Option<u64>,
207✔
92
    ) -> Self {
207✔
93
        let maybe_window_event_handler = {
207✔
94
            if let Some(ctx) = maybe_ctx {
207✔
95
                let link = ctx.link().clone();
×
96
                let interval = Interval::new(INTERVAL_DURATION_MILLIS, move || {
×
97
                    link.send_message(Msg::Tick);
×
98
                });
×
99
                let window = gloo::utils::window();
×
100
                let options = EventListenerOptions::enable_prevent_default();
×
101
                let keydown = {
×
102
                    let link = ctx.link().clone();
×
103
                    EventListener::new_with_options(&window, "keydown", options, move |e| {
×
104
                        let event = e.dyn_ref::<KeyboardEvent>().unwrap().clone();
×
105
                        link.send_message(Msg::Keydown(event.key_code()));
×
106
                    })
×
107
                };
108
                let keyup = {
×
109
                    let link = ctx.link().clone();
×
110
                    EventListener::new_with_options(&window, "keyup", options, move |e| {
×
111
                        let event = e.dyn_ref::<KeyboardEvent>().unwrap().clone();
×
112
                        link.send_message(Msg::Keyup(event.key_code()));
×
113
                    })
×
114
                };
115
                Some(WindowEventHandler {
×
116
                    _interval: interval,
×
117
                    _keydown: keydown,
×
118
                    _keyup: keyup,
×
119
                })
×
120
            } else {
121
                None
207✔
122
            }
123
        };
124
        Self {
207✔
125
            w: WIDTH,
207✔
126
            h: HEIGHT,
207✔
127
            t: INTERVAL_DURATION_MILLIS as f32,
207✔
128
            ship: Ship::create(WIDTH as f32, HEIGHT as f32, maybe_ship_seed),
207✔
129
            shots: Vec::new(),
207✔
130
            asteroids: Vec::new(),
207✔
131
            ufo: Ufo::create(),
207✔
132
            debris: Vec::new(),
207✔
133
            line_debris: Vec::new(),
207✔
134
            difficulty,
207✔
135
            score: 0,
207✔
136
            maybe_seed: maybe_engine_seed,
207✔
137
            keymap: HashMap::new(),
207✔
138
            _maybe_window_event_handler: maybe_window_event_handler,
207✔
139
        }
207✔
140
    }
207✔
141

142
    fn update_impl(&mut self, msg: Msg) -> bool {
68,151✔
143
        match msg {
68,151✔
144
            Msg::Tick => {
145
                self.handle_loop_update();
47,045✔
146
                while self.asteroids.len() < MAX_ASTEROIDS && self.difficulty > 0 {
47,528✔
147
                    self.spawn_asteroid();
483✔
148
                }
483✔
149
                self.spawn_ufo();
47,045✔
150
                true
47,045✔
151
            }
152
            Msg::Keydown(key) => {
10,559✔
153
                self.handle_keydown(key);
10,559✔
154
                false
10,559✔
155
            }
156
            Msg::Keyup(key) => {
10,547✔
157
                self.handle_keyup(key);
10,547✔
158
                false
10,547✔
159
            }
160
        }
161
    }
68,151✔
162

163
    fn get_context(&self) -> GameContext {
94,673✔
164
        GameContext {
94,673✔
165
            w: self.w as f32,
94,673✔
166
            h: self.h as f32,
94,673✔
167
            t: self.t,
94,673✔
168
        }
94,673✔
169
    }
94,673✔
170

171
    fn handle_loop_update(&mut self) {
47,045✔
172
        let ctx = self.get_context();
47,045✔
173
        if let Some(thrust) = self.keymap.get(&W_CODE_L) {
47,045✔
174
            match thrust {
46,116✔
175
                KeyAction::Down => self.ship.thrust(),
24,039✔
176
                KeyAction::Up => (),
22,077✔
177
            }
178
        }
929✔
179
        let mut game_elements: Vec<&mut dyn GameElement> = Vec::new();
47,045✔
180
        game_elements.push(&mut self.ship);
47,045✔
181
        game_elements.push(&mut self.ufo);
47,045✔
182
        game_elements.extend(self.debris.iter_mut().map(|d| d as &mut dyn GameElement));
47,045✔
183
        game_elements.extend(
47,045✔
184
            self.line_debris
47,045✔
185
                .iter_mut()
47,045✔
186
                .map(|d| d as &mut dyn GameElement),
47,045✔
187
        );
188
        game_elements.extend(self.asteroids.iter_mut().map(|a| a as &mut dyn GameElement));
226,837✔
189
        game_elements.extend(self.shots.iter_mut().map(|s| s as &mut dyn GameElement));
108,250✔
190

191
        for ge in game_elements.iter_mut() {
445,671✔
192
            ge.update(&ctx);
445,671✔
193
        }
445,671✔
194
        self.handle_shot_collision();
47,045✔
195
        self.handle_ship_collision();
47,045✔
196
        self.shots.retain(|s| s.alive());
108,250✔
197
        self.debris.extend(
47,045✔
198
            self.asteroids
47,045✔
199
                .iter()
47,045✔
200
                .filter(|a| !a.alive())
226,855✔
201
                .map(|a| a.make_debris()),
47,045✔
202
        );
203
        self.asteroids.retain(|a| a.alive());
226,855✔
204
    }
47,045✔
205

206
    fn spawn_asteroid(&mut self) {
583✔
207
        self.asteroids
583✔
208
            .push(Asteroid::spawn(&self.get_context(), self.maybe_seed));
583✔
209
        self.difficulty -= 1;
583✔
210
    }
583✔
211

212
    fn spawn_ufo(&mut self) {
47,045✔
213
        if let Some(ufo) = self.ufo.maybe_spawn(self.get_context(), self.maybe_seed) {
47,045✔
214
            self.ufo = ufo;
11✔
215
        }
47,034✔
216
    }
47,045✔
217

218
    fn handle_shot_collision(&mut self) {
47,048✔
219
        let mut new_asteroids: Vec<Asteroid> = Vec::new();
47,048✔
220
        for shot in self.shots.iter_mut().filter(|s| s.alive()) {
108,253✔
221
            let maybe_hit_index: Option<usize> = (|| {
107,611✔
222
                let mut shot_collidables: Vec<&mut dyn ShotCollidable> = Vec::new();
107,611✔
223
                shot_collidables.extend(
107,611✔
224
                    self.asteroids
107,611✔
225
                        .iter_mut()
107,611✔
226
                        .map(|a| a as &mut dyn ShotCollidable),
496,617✔
227
                );
228
                shot_collidables.push(&mut self.ufo);
107,611✔
229
                for (i, collidable) in shot_collidables.iter().enumerate() {
604,186✔
230
                    if collidable.did_collide(shot) {
604,186✔
231
                        self.score += collidable.score();
11✔
232
                        shot.destroy();
11✔
233
                        return Some(i);
11✔
234
                    }
604,175✔
235
                }
236
                None
107,600✔
237
            })();
238
            if let Some(hit_index) = maybe_hit_index {
107,611✔
239
                if hit_index < self.asteroids.len() {
11✔
240
                    let maybe_asteroids = self.asteroids[hit_index].split();
10✔
241
                    if let Some(asteroids) = maybe_asteroids {
10✔
242
                        new_asteroids.extend(asteroids);
10✔
243
                    }
10✔
244
                    self.asteroids[hit_index].destroy();
10✔
245
                } else {
1✔
246
                    self.ufo.destroy();
1✔
247
                    self.debris.extend(self.ufo.get_debris());
1✔
248
                }
1✔
249
            }
107,600✔
250
        }
251
        self.asteroids.extend(new_asteroids);
47,048✔
252
    }
47,048✔
253

254
    fn handle_ship_collision(&mut self) {
47,148✔
255
        if !self.ship.alive() {
47,148✔
256
            return;
4,204✔
257
        }
42,944✔
258
        let mut ship_collidables: Vec<&mut dyn ShipCollidable> = Vec::new();
42,944✔
259
        ship_collidables.extend(self.asteroids.iter_mut().filter_map(|a| match a.sz {
204,191✔
260
            AsteroidSize::Destroyed => None,
12✔
261
            _ => Some(a as &mut dyn ShipCollidable),
204,179✔
262
        }));
204,191✔
263
        for collidable in ship_collidables {
204,129✔
264
            if collidable.did_collide(&self.ship) {
204,129✔
265
                self.ship.destroy();
12✔
266
                self.line_debris
12✔
267
                    .extend(self.ship.spawn_debris(collidable.v()));
12✔
268
                break;
12✔
269
            }
204,117✔
270
        }
271
    }
47,148✔
272

273
    fn add_shot(&mut self) {
1,172✔
274
        let maybe_shot = self.ship.shoot();
1,172✔
275
        if let Some(shot) = maybe_shot {
1,172✔
276
            self.shots.push(shot)
906✔
277
        }
266✔
278
    }
1,172✔
279

280
    fn handle_keydown(&mut self, key_code: u32) {
10,559✔
281
        match key_code {
10,559✔
282
            A_CODE_L | A_CODE_U => self.ship.rotate_left(),
2,319✔
283
            D_CODE_L | D_CODE_U => self.ship.rotate_right(),
2,281✔
284
            H_CODE_L | H_CODE_U => self.ship.hyperspace(),
2,347✔
285
            SPACE_CODE => self.add_shot(),
1,172✔
286
            65..=90 => {
2,440✔
287
                // Force lowercase entry
1,233✔
288
                self.keymap.insert(key_code | 0x20, KeyAction::Down);
1,233✔
289
            }
1,233✔
290
            _ => {
1,207✔
291
                self.keymap.insert(key_code, KeyAction::Down);
1,207✔
292
            }
1,207✔
293
        };
294
    }
10,559✔
295

296
    fn handle_keyup(&mut self, key_code: u32) {
10,547✔
297
        match key_code {
10,547✔
298
            A_CODE_L | A_CODE_U | D_CODE_L | D_CODE_U => self.ship.stop_rotate(),
4,728✔
299
            65..=90 => {
4,645✔
300
                // Force lowercase entry
2,346✔
301
                self.keymap.insert(key_code + 32, KeyAction::Up);
2,346✔
302
            }
2,346✔
303
            _ => {
3,473✔
304
                self.keymap.insert(key_code, KeyAction::Up);
3,473✔
305
            }
3,473✔
306
        }
307
    }
10,547✔
308

309
    fn render(&self) -> Html {
1✔
310
        html! {
1✔
311
            <>
1✔
312
                {self.debris.iter().map(|d| d.render()).collect::<Html>()}
1✔
313
                {self.line_debris.iter().map(|d| d.render()).collect::<Html>()}
1✔
314
                {self.ship.render()}
1✔
315
                {self.ufo.render()}
1✔
316
                {self.shots.iter().map(|s| s.render()).collect::<Html>()}
1✔
317
                {self.asteroids.iter().map(|a| a.render()).collect::<Html>()}
1✔
318
                <text
1✔
319
                    x={(self.w as f32 * 0.1).to_string()}
1✔
320
                    y={(self.h as f32 * 0.1).to_string()}
1✔
321
                    fill="#FFFFFF"
1✔
322
                    stroke="#000000"
1✔
323
                    stroke-width="0.3"
1✔
324
                    font-size="25"
1✔
325
                    font-family="monospace">
1✔
326
                    {self.score}
1✔
327
                </text>
328
            </>
329
        }
330
    }
1✔
331
}
332

333
impl Component for Engine {
334
    type Message = Msg;
335
    type Properties = ();
336

UNCOV
337
    fn create(ctx: &Context<Self>) -> Self {
×
338
        Self::create_impl(Some(ctx), BASE_DIFFICULTY, None, None)
×
339
    }
×
340

UNCOV
341
    fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
×
342
        self.update_impl(msg)
×
343
    }
×
344

UNCOV
345
    fn view(&self, _ctx: &Context<Self>) -> Html {
×
346
        let view_box = format!("0 0 {} {}", self.w, self.h);
×
347
        html! {
×
348
        <svg class="svg-container" viewBox={view_box}>
×
349
            {self.render()}
×
350
        </svg>
351
        }
UNCOV
352
    }
×
353
}
354

355
#[cfg(test)]
356
mod tests {
357
    use super::*;
358
    use crate::math::Point;
359
    use crate::math::{point, polar_point};
360
    use core::f32;
361
    use googletest::prelude::*;
362
    use indoc::indoc;
363
    use is_svg::is_svg_string;
364
    use p_test::p_test;
365
    use quickcheck::{Arbitrary, Gen, QuickCheck};
366
    use quickcheck_macros::quickcheck;
367
    use std::collections::HashMap;
368
    use strum::IntoEnumIterator;
369

370
    fn create_test_engine(difficulty: u32, engine_seed: u64, ship_seed: u64) -> Engine {
207✔
371
        Engine::create_impl(None, difficulty, Some(engine_seed), Some(ship_seed))
207✔
372
    }
207✔
373

374
    #[gtest]
375
    fn it_renders() {
376
        let engine = create_test_engine(BASE_DIFFICULTY, 42, 42);
377
        let svg_string = format!("<svg>{:?}</svg>", engine.render());
378
        assert_that!(is_svg_string(&svg_string), is_true(), "{:?}", &svg_string)
379
    }
380

381
    fn create_edge_points(edge_point_count: u32) -> Vec<Point> {
3✔
382
        (0..=edge_point_count)
3✔
383
            .map(|i| Point::from_polar(1.0, f32::consts::PI * i as f32 / edge_point_count as f32))
27✔
384
            .collect()
3✔
385
    }
3✔
386

387
    #[p_test(
388
        (point!(10, 10), false, AsteroidSize::Large),
389
        (point!(10, 10), true, AsteroidSize::Destroyed),
390
        (point!(100, 100), true, AsteroidSize::Large),
391
    )]
392
    fn it_handles_ship_collisions(
393
        ship_point: Point,
394
        expect_alive: bool,
395
        asteroid_size: AsteroidSize,
396
    ) {
397
        let mut engine = create_test_engine(BASE_DIFFICULTY, 42, 42);
398
        let p = point!(10, 10);
399
        let v = point!(10, 10);
400
        let edge_points = create_edge_points(8);
401
        engine
402
            .asteroids
403
            .push(Asteroid::create(p, v, edge_points, asteroid_size));
404
        engine.ship = Ship::create_for_test(ship_point);
405
        engine.handle_ship_collision();
406
        assert_that!(engine.ship.alive(), eq(expect_alive));
407
    }
408

409
    #[p_test(
410
        (point!(10, 10), true, AsteroidSize::Large),
411
        (point!(10, 10), false, AsteroidSize::Destroyed),
412
        (point!(100, 100), false, AsteroidSize::Large),
413
    )]
414
    fn it_handles_shot_collisions(
415
        shot_point: Point,
416
        expect_split: bool,
417
        asteroid_size: AsteroidSize,
418
    ) {
419
        let mut engine = create_test_engine(BASE_DIFFICULTY, 42, 42);
420
        let p = point!(10, 10);
421
        let v = point!(10, 10);
422
        let edge_points_sz = 8;
423
        let edge_points = (0..=edge_points_sz)
424
            .map(|i| polar_point!(1.0, f32::consts::PI * i as f32 / edge_points_sz as f32))
27✔
425
            .collect();
426
        engine
427
            .asteroids
428
            .push(Asteroid::create(p, v, edge_points, asteroid_size));
429
        engine.shots.push(Shot::create(shot_point, v, 0.0));
430
        engine.handle_shot_collision();
431
        let shot_count = engine.shots.iter().filter(|&s| s.alive()).count();
3✔
432
        let asteroid_count = engine.asteroids.len(); // include destroyed
433
        if expect_split {
434
            assert_that!(asteroid_count, eq(3));
435
            assert_that!(shot_count, eq(0));
436
        } else {
437
            assert_that!(asteroid_count, eq(1));
438
            assert_that!(shot_count, eq(1));
439
        }
440
    }
441

442
    #[derive(Clone, Debug)]
443
    pub struct GameKeyInput(pub Option<(u32, KeyAction)>);
444

445
    impl Arbitrary for GameKeyInput {
446
        fn arbitrary(g: &mut Gen) -> Self {
46,945✔
447
            let keys = [
46,945✔
448
                Some(W_CODE_L),
46,945✔
449
                Some(W_CODE_U),
46,945✔
450
                Some(A_CODE_L),
46,945✔
451
                Some(A_CODE_U),
46,945✔
452
                Some(D_CODE_L),
46,945✔
453
                Some(D_CODE_U),
46,945✔
454
                Some(H_CODE_L),
46,945✔
455
                Some(H_CODE_U),
46,945✔
456
                Some(SPACE_CODE),
46,945✔
457
                None,
46,945✔
458
                None,
46,945✔
459
                None,
46,945✔
460
                None,
46,945✔
461
                None,
46,945✔
462
                None,
46,945✔
463
                None,
46,945✔
464
                None,
46,945✔
465
                None,
46,945✔
466
                None,
46,945✔
467
                None,
46,945✔
468
            ];
46,945✔
469
            if let Some(key) = g.choose(&keys).unwrap() {
46,945✔
470
                let key_action_opts = &[KeyAction::Up, KeyAction::Down];
21,106✔
471
                GameKeyInput(Some((*key, g.choose(key_action_opts).unwrap().clone())))
21,106✔
472
            } else {
473
                GameKeyInput(None)
25,839✔
474
            }
475
        }
46,945✔
476
    }
477

478
    fn quickcheck_params() -> (usize, u64) {
1✔
479
        let mut arbitrary_size: usize = 1000;
1✔
480
        let mut tests: u64 = 100;
1✔
481

482
        if let Ok(v) = std::env::var("QUICKCHECK_SIZE") {
1✔
UNCOV
483
            if let Ok(parsed) = v.parse::<usize>() {
×
484
                arbitrary_size = parsed;
×
485
            }
×
486
        }
1✔
487
        if let Ok(v) = std::env::var("QUICKCHECK_TESTS") {
1✔
UNCOV
488
            if let Ok(parsed) = v.parse::<u64>() {
×
489
                tests = parsed;
×
490
            }
×
491
        }
1✔
492

493
        (arbitrary_size, tests)
1✔
494
    }
1✔
495

496
    #[gtest]
497
    fn it_keeps_score_as_engine_runs() {
498
        fn run_engine(
100✔
499
            actions: Vec<GameKeyInput>,
100✔
500
            difficulty: u32,
100✔
501
            engine_seed: u64,
100✔
502
            ship_seed: u64,
100✔
503
        ) -> bool {
100✔
504
            let difficulty = difficulty % 10;
100✔
505
            let mut engine = create_test_engine(difficulty, engine_seed, ship_seed);
100✔
506
            let run_count = QUICKCHECK_RUN_COUNT.fetch_add(1, Ordering::SeqCst) + 1;
100✔
507
            println!(
100✔
508
                "{:>6}: Running Engine PBT for {} iterations, difficulty={}",
509
                run_count,
510
                actions.len(),
100✔
511
                difficulty
512
            );
513
            engine.update_impl(Msg::Tick);
100✔
514
            for game_key_input in actions {
46,945✔
515
                if let Some((key, is_down_action)) = game_key_input.0 {
46,945✔
516
                    match is_down_action {
21,106✔
517
                        KeyAction::Down => engine.update_impl(Msg::Keydown(key)),
10,559✔
518
                        KeyAction::Up => engine.update_impl(Msg::Keyup(key)),
10,547✔
519
                    };
520
                }
25,839✔
521

522
                struct AsteroidMap(HashMap<AsteroidSize, i32>);
523
                impl AsteroidMap {
524
                    fn create() -> AsteroidMap {
140,835✔
525
                        let mut res: AsteroidMap = AsteroidMap(HashMap::new());
140,835✔
526
                        for sz in AsteroidSize::iter() {
563,340✔
527
                            res.0.insert(sz, 0);
563,340✔
528
                        }
563,340✔
529
                        res
140,835✔
530
                    }
140,835✔
531
                }
532
                fn count_asteroids(asteroids: &[Asteroid]) -> AsteroidMap {
93,890✔
533
                    let mut counts = AsteroidMap::create();
93,890✔
534
                    for a in asteroids.iter() {
453,681✔
535
                        *counts.0.entry(a.sz).or_insert(0) += 1;
453,681✔
536
                    }
453,681✔
537
                    counts
93,890✔
538
                }
93,890✔
539

540
                let score_before_loop = engine.score;
46,945✔
541
                let shots_before_loop = engine.shots.len();
46,945✔
542
                let asteroids_before_loop = count_asteroids(&engine.asteroids);
46,945✔
543
                let ufo_score = engine.ufo.score();
46,945✔
544

545
                engine.update_impl(Msg::Tick);
46,945✔
546

547
                let score_after_loop = engine.score;
46,945✔
548
                let shots_after_loop = engine.shots.len();
46,945✔
549
                let asteroids_after_loop = count_asteroids(&engine.asteroids);
46,945✔
550

551
                if score_after_loop > score_before_loop {
46,945✔
552
                    assert_that!(
10✔
553
                        shots_after_loop,
10✔
554
                        lt(shots_before_loop),
10✔
555
                        "if the score went up, a shot must have contacted a target"
556
                    );
557
                }
46,935✔
558

559
                match engine.ship.alive() {
46,945✔
560
                    true => assert_that!(
42,730✔
561
                        engine.line_debris.len(),
42,730✔
562
                        eq(0),
42,730✔
563
                        "if the ship is alive, no debris from it"
564
                    ),
565
                    false => {
566
                        assert_that!(
4,215✔
567
                            engine.line_debris.len(),
4,215✔
568
                            eq(engine.ship.polygon().len()),
4,215✔
569
                            "if the ship is destroyed, expect the polygon to create LineDebris"
570
                        )
571
                    }
572
                }
573

574
                let destroyed_asteroids = {
46,945✔
575
                    let mut map = AsteroidMap::create();
46,945✔
576

577
                    // Destroyed large are direct
578
                    let destroyed_large = asteroids_before_loop.0[&AsteroidSize::Large]
46,945✔
579
                        - asteroids_after_loop.0[&AsteroidSize::Large];
46,945✔
580
                    map.0.insert(AsteroidSize::Large, destroyed_large);
46,945✔
581

582
                    // Destroyed medium must account for 2 new ones from destroyed large
583
                    let destroyed_medium = asteroids_before_loop.0[&AsteroidSize::Medium]
46,945✔
584
                        - asteroids_after_loop.0[&AsteroidSize::Medium]
46,945✔
585
                        + destroyed_large * 2;
46,945✔
586
                    map.0.insert(AsteroidSize::Medium, destroyed_medium);
46,945✔
587

588
                    // Destroyed small must account for 2 new ones from destroyed medium
589
                    let destroyed_small = asteroids_before_loop.0[&AsteroidSize::Small]
46,945✔
590
                        - asteroids_after_loop.0[&AsteroidSize::Small]
46,945✔
591
                        + destroyed_medium * 2;
46,945✔
592
                    map.0.insert(AsteroidSize::Small, destroyed_small);
46,945✔
593

594
                    map
46,945✔
595
                };
596

597
                let score_from_destroyed = AsteroidSize::iter()
46,945✔
598
                    .map(|sz| destroyed_asteroids.0[&sz] * Asteroid::score_from_size(&sz))
187,780✔
599
                    .reduce(|acc, val| acc + val)
140,835✔
600
                    .expect("Score was not provided by reducer");
46,945✔
601
                let score_from_destroyed =
46,945✔
602
                    score_from_destroyed + if !engine.ufo.alive() { ufo_score } else { 0 };
46,945✔
603

604
                assert_that!(
46,945✔
605
                    score_after_loop,
46,945✔
606
                    eq(score_from_destroyed + score_before_loop),
46,945✔
607
                    indoc! {"
608
                    Expected score to increase from {} to {},
609
                    asteroids_before={:?}, asteroids_after={:?},
610
                    destroyed={:?}"},
611
                    score_before_loop,
612
                    score_after_loop,
613
                    asteroids_before_loop.0,
614
                    asteroids_after_loop.0,
615
                    destroyed_asteroids.0
616
                );
617
            }
618
            true
100✔
619
        }
100✔
620

621
        let (arbitrary_size, tests) = quickcheck_params();
622
        println!(
623
            "Running with arbitraries of size {}, {} tests",
624
            arbitrary_size, tests
625
        );
626
        QuickCheck::new()
627
            .rng(Gen::new(arbitrary_size))
628
            .tests(tests)
629
            .quickcheck(run_engine as fn(Vec<GameKeyInput>, u32, u64, u64) -> bool)
630
    }
631

632
    #[quickcheck]
633
    fn it_never_spawns_an_asteroid_on_the_ship(engine_seed: u64) {
100✔
634
        let mut engine = create_test_engine(BASE_DIFFICULTY, engine_seed, 0);
100✔
635
        engine.spawn_asteroid();
100✔
636
        engine.handle_ship_collision();
100✔
637
        assert_that!(engine.ship.alive(), is_true());
100✔
638
    }
100✔
639
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc