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

bcpearce / asteroids-rs / 23176735321

17 Mar 2026 03:15AM UTC coverage: 88.039% (-4.0%) from 92.027%
23176735321

Pull #8

github

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

102 of 154 new or added lines in 6 files covered. (66.23%)

30 existing lines in 3 files now uncovered.

898 of 1020 relevant lines covered (88.04%)

2036121.93 hits per line

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

88.08
/src/engine.rs
1
use crate::asteroid::Asteroid;
2
use crate::asteroid::Size as AsteroidSize;
3
use crate::collisions::asteroid_ship_collision;
4
use crate::collisions::asteroid_shot_collision;
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 {
70,921✔
143
        match msg {
70,921✔
144
            Msg::Tick => {
145
                self.handle_loop_update();
48,959✔
146
                while self.asteroids.len() < MAX_ASTEROIDS && self.difficulty > 0 {
49,886✔
147
                    self.spawn_asteroid();
927✔
148
                }
927✔
149
                self.spawn_ufo();
48,959✔
150
                true
48,959✔
151
            }
152
            Msg::Keydown(key) => {
11,099✔
153
                self.handle_keydown(key);
11,099✔
154
                false
11,099✔
155
            }
156
            Msg::Keyup(key) => {
10,863✔
157
                self.handle_keyup(key);
10,863✔
158
                false
10,863✔
159
            }
160
        }
161
    }
70,921✔
162

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

171
    fn handle_loop_update(&mut self) {
97,918✔
172
        let ctx = self.get_context();
97,918✔
173
        if let Some(thrust) = self.keymap.get(&W_CODE_L) {
97,918✔
174
            match thrust {
95,734✔
175
                KeyAction::Down => self.ship.thrust(),
47,946✔
176
                KeyAction::Up => (),
47,788✔
177
            }
178
        }
2,184✔
179
        let mut game_elements: Vec<&mut dyn GameElement> = Vec::new();
97,918✔
180
        game_elements.push(&mut self.ship);
97,918✔
181
        game_elements.push(&mut self.ufo);
97,918✔
182
        game_elements.extend(self.debris.iter_mut().map(|d| d as &mut dyn GameElement));
97,918✔
183
        game_elements.extend(
97,918✔
184
            self.line_debris
97,918✔
185
                .iter_mut()
97,918✔
186
                .map(|d| d as &mut dyn GameElement),
97,918✔
187
        );
188
        game_elements.extend(self.asteroids.iter_mut().map(|a| a as &mut dyn GameElement));
898,154✔
189
        game_elements.extend(self.shots.iter_mut().map(|s| s as &mut dyn GameElement));
125,319✔
190

191
        for ge in game_elements.iter_mut() {
1,251,829✔
192
            ge.update(&ctx);
1,251,829✔
193
        }
1,251,829✔
194
        self.handle_shot_collision();
97,918✔
195
        self.handle_ship_collision();
97,918✔
196
        self.shots.retain(|s| s.alive());
125,319✔
197
        self.debris.extend(
97,918✔
198
            self.asteroids
97,918✔
199
                .iter()
97,918✔
200
                .filter(|a| !a.alive())
898,176✔
201
                .map(|a| a.make_debris()),
97,918✔
202
        );
203
        self.asteroids.retain(|a| a.alive());
898,176✔
204
    }
97,918✔
205

206
    fn spawn_asteroid(&mut self) {
1,027✔
207
        self.asteroids.push(Asteroid::spawn(
1,027✔
208
            self.w as f32,
1,027✔
209
            self.h as f32,
1,027✔
210
            self.maybe_seed,
1,027✔
211
        ));
212
        self.difficulty -= 1;
1,027✔
213
    }
1,027✔
214

215
    fn spawn_ufo(&mut self) {
48,959✔
216
        if let Some(ufo) = self.ufo.maybe_spawn(self.maybe_seed) {
48,959✔
217
            self.ufo = ufo;
17✔
218
        }
48,942✔
219
    }
48,959✔
220

221
    fn handle_shot_collision(&mut self) {
97,921✔
222
        for shot in self.shots.iter_mut().filter(|s| s.alive()) {
125,322✔
223
            let mut maybe_hit_index: Option<usize> = None;
124,487✔
224
            for (i, asteroid) in self
1,141,167✔
225
                .asteroids
124,487✔
226
                .iter()
124,487✔
227
                .filter(|&a| a.sz != AsteroidSize::Destroyed)
1,141,179✔
228
                .enumerate()
124,487✔
229
            {
230
                if asteroid_shot_collision(asteroid, shot) {
1,141,167✔
231
                    self.score += asteroid.score();
12✔
232
                    maybe_hit_index = Some(i);
12✔
233
                    shot.destroy();
12✔
234
                    break;
12✔
235
                }
1,141,155✔
236
            }
237
            if let Some(hit_index) = maybe_hit_index {
124,487✔
238
                let maybe_new_asteroids = self.asteroids[hit_index].split();
12✔
239
                if let Some(new_asteroids) = maybe_new_asteroids {
12✔
240
                    self.asteroids.extend(new_asteroids);
12✔
241
                }
12✔
242
                // Remove destroyed or split
243
                self.asteroids[hit_index].destroy();
12✔
244
            }
124,475✔
245
        }
246
    }
97,921✔
247

248
    fn handle_ship_collision(&mut self) {
98,021✔
249
        if !self.ship.alive() {
98,021✔
250
            return;
8,157✔
251
        }
89,864✔
252
        for asteroid in self
816,375✔
253
            .asteroids
89,864✔
254
            .iter()
89,864✔
255
            .filter(|&a| a.sz != AsteroidSize::Destroyed)
816,396✔
256
        {
257
            if asteroid_ship_collision(asteroid, &self.ship) {
816,375✔
258
                self.ship.destroy();
14✔
259
                self.line_debris.extend(self.ship.spawn_debris(asteroid.v));
14✔
260
                break;
14✔
261
            }
816,361✔
262
        }
263
    }
98,021✔
264

265
    fn add_shot(&mut self) {
1,197✔
266
        let maybe_shot = self.ship.shoot();
1,197✔
267
        if let Some(shot) = maybe_shot {
1,197✔
268
            self.shots.push(shot)
960✔
269
        }
237✔
270
    }
1,197✔
271

272
    fn handle_keydown(&mut self, key_code: u32) {
11,099✔
273
        match key_code {
11,099✔
274
            A_CODE_L | A_CODE_U => self.ship.rotate_left(),
2,512✔
275
            D_CODE_L | D_CODE_U => self.ship.rotate_right(),
2,509✔
276
            H_CODE_L | H_CODE_U => self.ship.hyperspace(),
2,438✔
277
            SPACE_CODE => self.add_shot(),
1,197✔
278
            65..=90 => {
2,443✔
279
                // Force lowercase entry
1,201✔
280
                self.keymap.insert(key_code | 0x20, KeyAction::Down);
1,201✔
281
            }
1,201✔
282
            _ => {
1,242✔
283
                self.keymap.insert(key_code, KeyAction::Down);
1,242✔
284
            }
1,242✔
285
        };
286
    }
11,099✔
287

288
    fn handle_keyup(&mut self, key_code: u32) {
10,863✔
289
        match key_code {
10,863✔
290
            A_CODE_L | A_CODE_U | D_CODE_L | D_CODE_U => self.ship.stop_rotate(),
4,872✔
291
            65..=90 => {
4,796✔
292
                // Force lowercase entry
2,393✔
293
                self.keymap.insert(key_code + 32, KeyAction::Up);
2,393✔
294
            }
2,393✔
295
            _ => {
3,598✔
296
                self.keymap.insert(key_code, KeyAction::Up);
3,598✔
297
            }
3,598✔
298
        }
299
    }
10,863✔
300

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

325
impl Component for Engine {
326
    type Message = Msg;
327
    type Properties = ();
328

329
    fn create(ctx: &Context<Self>) -> Self {
×
UNCOV
330
        Self::create_impl(Some(ctx), BASE_DIFFICULTY, None, None)
×
UNCOV
331
    }
×
332

UNCOV
333
    fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
×
UNCOV
334
        self.update_impl(msg)
×
UNCOV
335
    }
×
336

UNCOV
337
    fn view(&self, _ctx: &Context<Self>) -> Html {
×
338
        let view_box = format!("0 0 {} {}", self.w, self.h);
×
339
        html! {
×
340
        <svg class="svg-container" viewBox={view_box}>
×
UNCOV
341
            {self.render()}
×
342
        </svg>
343
        }
344
    }
×
345
}
346

347
#[cfg(test)]
348
mod tests {
349
    use super::*;
350
    use crate::math::Point;
351
    use crate::math::{point, polar_point};
352
    use core::f32;
353
    use googletest::prelude::*;
354
    use indoc::indoc;
355
    use is_svg::is_svg_string;
356
    use p_test::p_test;
357
    use quickcheck::{Arbitrary, Gen, QuickCheck};
358
    use quickcheck_macros::quickcheck;
359
    use std::collections::HashMap;
360
    use strum::IntoEnumIterator;
361

362
    fn create_test_engine(difficulty: u32, engine_seed: u64, ship_seed: u64) -> Engine {
207✔
363
        Engine::create_impl(None, difficulty, Some(engine_seed), Some(ship_seed))
207✔
364
    }
207✔
365

366
    #[gtest]
367
    fn it_renders() {
368
        let engine = create_test_engine(BASE_DIFFICULTY, 42, 42);
369
        let svg_string = format!("<svg>{:?}</svg>", engine.render());
370
        assert_that!(is_svg_string(&svg_string), is_true(), "{:?}", &svg_string)
371
    }
372

373
    fn create_edge_points(edge_point_count: u32) -> Vec<Point> {
3✔
374
        (0..=edge_point_count)
3✔
375
            .map(|i| Point::from_polar(1.0, f32::consts::PI * i as f32 / edge_point_count as f32))
27✔
376
            .collect()
3✔
377
    }
3✔
378

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

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

434
    #[derive(Clone, Debug)]
435
    pub struct GameKeyInput(pub Option<(u32, KeyAction)>);
436

437
    impl Arbitrary for GameKeyInput {
438
        fn arbitrary(g: &mut Gen) -> Self {
48,959✔
439
            let keys = [
48,959✔
440
                Some(W_CODE_L),
48,959✔
441
                Some(W_CODE_U),
48,959✔
442
                Some(A_CODE_L),
48,959✔
443
                Some(A_CODE_U),
48,959✔
444
                Some(D_CODE_L),
48,959✔
445
                Some(D_CODE_U),
48,959✔
446
                Some(H_CODE_L),
48,959✔
447
                Some(H_CODE_U),
48,959✔
448
                Some(SPACE_CODE),
48,959✔
449
                None,
48,959✔
450
                None,
48,959✔
451
                None,
48,959✔
452
                None,
48,959✔
453
                None,
48,959✔
454
                None,
48,959✔
455
                None,
48,959✔
456
                None,
48,959✔
457
                None,
48,959✔
458
                None,
48,959✔
459
                None,
48,959✔
460
            ];
48,959✔
461
            if let Some(key) = g.choose(&keys).unwrap() {
48,959✔
462
                let key_action_opts = &[KeyAction::Up, KeyAction::Down];
21,962✔
463
                GameKeyInput(Some((*key, g.choose(key_action_opts).unwrap().clone())))
21,962✔
464
            } else {
465
                GameKeyInput(None)
26,997✔
466
            }
467
        }
48,959✔
468
    }
469

470
    fn quickcheck_params() -> (usize, u64) {
1✔
471
        let mut arbitrary_size: usize = 1000;
1✔
472
        let mut tests: u64 = 100;
1✔
473

474
        if let Ok(v) = std::env::var("QUICKCHECK_SIZE") {
1✔
UNCOV
475
            if let Ok(parsed) = v.parse::<usize>() {
×
UNCOV
476
                arbitrary_size = parsed;
×
UNCOV
477
            }
×
478
        }
1✔
479
        if let Ok(v) = std::env::var("QUICKCHECK_TESTS") {
1✔
UNCOV
480
            if let Ok(parsed) = v.parse::<u64>() {
×
UNCOV
481
                tests = parsed;
×
UNCOV
482
            }
×
483
        }
1✔
484

485
        (arbitrary_size, tests)
1✔
486
    }
1✔
487

488
    #[gtest]
489
    fn it_keeps_score_as_engine_runs() {
490
        fn run_engine(
100✔
491
            actions: Vec<GameKeyInput>,
100✔
492
            difficulty: u32,
100✔
493
            engine_seed: u64,
100✔
494
            ship_seed: u64,
100✔
495
        ) -> bool {
100✔
496
            let difficulty = difficulty % 500;
100✔
497
            let mut engine = create_test_engine(difficulty, engine_seed, ship_seed);
100✔
498
            let run_count = QUICKCHECK_RUN_COUNT.fetch_add(1, Ordering::SeqCst) + 1;
100✔
499
            println!(
100✔
500
                "{:>6}: Running Engine PBT for {} iterations, difficulty={}",
501
                run_count,
502
                actions.len(),
100✔
503
                difficulty
504
            );
505
            for game_key_input in actions {
48,959✔
506
                if let Some((key, is_down_action)) = game_key_input.0 {
48,959✔
507
                    match is_down_action {
21,962✔
508
                        KeyAction::Down => engine.update_impl(Msg::Keydown(key)),
11,099✔
509
                        KeyAction::Up => engine.update_impl(Msg::Keyup(key)),
10,863✔
510
                    };
511
                }
26,997✔
512
                engine.update_impl(Msg::Tick);
48,959✔
513

514
                struct AsteroidMap(HashMap<AsteroidSize, i32>);
515
                impl AsteroidMap {
516
                    fn create() -> AsteroidMap {
146,877✔
517
                        let mut res: AsteroidMap = AsteroidMap(HashMap::new());
146,877✔
518
                        for sz in AsteroidSize::iter() {
587,508✔
519
                            res.0.insert(sz, 0);
587,508✔
520
                        }
587,508✔
521
                        res
146,877✔
522
                    }
146,877✔
523
                }
524
                fn count_asteroids(asteroids: &[Asteroid]) -> AsteroidMap {
97,918✔
525
                    let mut counts = AsteroidMap::create();
97,918✔
526
                    for a in asteroids.iter() {
899,082✔
527
                        *counts.0.entry(a.sz).or_insert(0) += 1;
899,082✔
528
                    }
899,082✔
529
                    counts
97,918✔
530
                }
97,918✔
531

532
                let score_before_loop = engine.score;
48,959✔
533
                let shots_before_loop = engine.shots.len();
48,959✔
534
                let asteroids_before_loop = count_asteroids(&engine.asteroids);
48,959✔
535

536
                engine.handle_loop_update();
48,959✔
537

538
                let score_after_loop = engine.score;
48,959✔
539
                let shots_after_loop = engine.shots.len();
48,959✔
540
                let asteroids_after_loop = count_asteroids(&engine.asteroids);
48,959✔
541

542
                if score_after_loop > score_before_loop {
48,959✔
543
                    assert_that!(
10✔
544
                        shots_after_loop,
10✔
545
                        lt(shots_before_loop),
10✔
546
                        "if the score went up, a shot must have contacted a target"
547
                    );
548
                }
48,949✔
549

550
                match engine.ship.alive() {
48,959✔
551
                    true => assert_that!(
44,871✔
552
                        engine.line_debris.len(),
44,871✔
553
                        eq(0),
44,871✔
554
                        "if the ship is alive, no debris from it"
555
                    ),
556
                    false => {
557
                        assert_that!(
4,088✔
558
                            engine.line_debris.len(),
4,088✔
559
                            eq(engine.ship.polygon().len()),
4,088✔
560
                            "if the ship is destroyed, expect the polygon to create LineDebris"
561
                        )
562
                    }
563
                }
564

565
                let destroyed_asteroids = {
48,959✔
566
                    let mut map = AsteroidMap::create();
48,959✔
567

568
                    // Destroyed large are direct
569
                    let destroyed_large = asteroids_before_loop.0[&AsteroidSize::Large]
48,959✔
570
                        - asteroids_after_loop.0[&AsteroidSize::Large];
48,959✔
571
                    map.0.insert(AsteroidSize::Large, destroyed_large);
48,959✔
572

573
                    // Destroyed medium must account for 2 new ones from destroyed large
574
                    let destroyed_medium = asteroids_before_loop.0[&AsteroidSize::Medium]
48,959✔
575
                        - asteroids_after_loop.0[&AsteroidSize::Medium]
48,959✔
576
                        + destroyed_large * 2;
48,959✔
577
                    map.0.insert(AsteroidSize::Medium, destroyed_medium);
48,959✔
578

579
                    // Destroyed small must account for 2 new ones from destroyed medium
580
                    let destroyed_small = asteroids_before_loop.0[&AsteroidSize::Small]
48,959✔
581
                        - asteroids_after_loop.0[&AsteroidSize::Small]
48,959✔
582
                        + destroyed_medium * 2;
48,959✔
583
                    map.0.insert(AsteroidSize::Small, destroyed_small);
48,959✔
584

585
                    map
48,959✔
586
                };
587

588
                let score_from_destroyed = AsteroidSize::iter()
48,959✔
589
                    .map(|sz| destroyed_asteroids.0[&sz] * Asteroid::score_from_size(&sz))
195,836✔
590
                    .reduce(|acc, val| acc + val)
146,877✔
591
                    .expect("Score was not provided by reducer");
48,959✔
592

593
                assert_that!(
48,959✔
594
                    score_after_loop,
48,959✔
595
                    eq(score_from_destroyed + score_before_loop),
48,959✔
596
                    indoc! {"
597
                    Expected score to increase from {} to {},
598
                    asteroids_before={:?}, asteroids_after={:?},
599
                    destroyed={:?}"},
600
                    score_before_loop,
601
                    score_after_loop,
602
                    asteroids_before_loop.0,
603
                    asteroids_after_loop.0,
604
                    destroyed_asteroids.0
605
                );
606
            }
607
            true
100✔
608
        }
100✔
609

610
        let (arbitrary_size, tests) = quickcheck_params();
611
        println!(
612
            "Running with arbitraries of size {}, {} tests",
613
            arbitrary_size, tests
614
        );
615
        QuickCheck::new()
616
            .rng(Gen::new(arbitrary_size))
617
            .tests(tests)
618
            .quickcheck(run_engine as fn(Vec<GameKeyInput>, u32, u64, u64) -> bool)
619
    }
620

621
    #[quickcheck]
622
    fn it_never_spawns_an_asteroid_on_the_ship(engine_seed: u64) {
100✔
623
        let mut engine = create_test_engine(BASE_DIFFICULTY, engine_seed, 0);
100✔
624
        engine.spawn_asteroid();
100✔
625
        engine.handle_ship_collision();
100✔
626
        assert_that!(engine.ship.alive(), is_true());
100✔
627
    }
100✔
628
}
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