• 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

96.79
/src/asteroid.rs
1
use std::rc::Rc;
2
use strum_macros::EnumIter;
3

4
use crate::{
5
    common,
6
    debris::Debris,
7
    engine::{GameContext, GameElement},
8
    math::Point,
9
    math::point,
10
};
11
use itertools::Itertools;
12
use rand::RngExt;
13
use yew::{Html, html};
14

15
const MIN_ASTEROID_RADIUS: f32 = 5.0;
16
const MAX_ASTEROID_RADIUS: f32 = 15.0;
17
const MIN_ASTEROID_VELOCITY: f32 = 0.03;
18
const MAX_ASTEROID_VELOCITY: f32 = 0.11;
19
const SPLIT_ANGLE_RADS: f32 = 0.3;
20

21
#[derive(Debug, Copy, Clone, PartialEq, Hash, Eq, EnumIter)]
22
pub enum Size {
23
    Small,
24
    Medium,
25
    Large,
26
    Destroyed,
27
}
28

29
#[derive(Debug, Clone, PartialEq)]
30
pub struct Asteroid {
31
    pub p: Point,
32
    pub v: Point,
33
    edge_points: Rc<Vec<Point>>,
34
    pub sz: Size,
35
    pub hue: u32,
36
}
37

38
impl Asteroid {
39
    #[cfg(test)]
40
    pub fn create(p: Point, v: Point, edge_points: Vec<Point>, sz: Size) -> Asteroid {
6✔
41
        Asteroid {
6✔
42
            p,
6✔
43
            v,
6✔
44
            edge_points: Rc::from(edge_points),
6✔
45
            sz,
6✔
46
            hue: 0,
6✔
47
        }
6✔
48
    }
6✔
49

50
    pub fn spawn(w: f32, h: f32, maybe_seed: Option<u64>) -> Asteroid {
1,427✔
51
        let mut rng = common::rng::get_rng(maybe_seed);
1,427✔
52
        let max_angle_rads = std::f32::consts::PI / 3.0; // 6 side ish
1,427✔
53
        let min_angle_rads = std::f32::consts::PI / 5.5; // 11 side ish
1,427✔
54
        let mut edge_points = Vec::new();
1,427✔
55
        let mut t = rng.random_range(min_angle_rads..max_angle_rads);
1,427✔
56
        let spawn_edge = rng.random_range(0..4);
1,427✔
57
        let p = match spawn_edge {
1,427✔
58
            0 => point!(rng.random_range(0.0..w), 0),
397✔
59
            1 => point!(rng.random_range(0.0..w), h),
272✔
60
            2 => point!(0, rng.random_range(0.0..h)),
355✔
61
            3 => point!(w, rng.random_range(0.0..h)),
403✔
NEW
62
            _ => unreachable!("rng only creates between [0, 4)"),
×
63
        };
64
        while t < std::f32::consts::PI * 2.0 {
11,854✔
65
            let r = rng.random_range(MIN_ASTEROID_RADIUS..=MAX_ASTEROID_RADIUS);
10,427✔
66
            edge_points.push(Point::from_polar(r, t));
10,427✔
67
            t += rng.random_range(min_angle_rads..max_angle_rads);
10,427✔
68
        }
10,427✔
69
        let proto = rng.random_range(0..3);
1,427✔
70
        let sz = match proto {
1,427✔
71
            0 => Size::Large,
424✔
72
            1 => Size::Medium,
532✔
73
            2 => Size::Small,
471✔
UNCOV
74
            _ => Size::Destroyed,
×
75
        };
76
        let hue = rng.random_range(0..360);
1,427✔
77
        Asteroid {
1,427✔
78
            p,
1,427✔
79
            v: Point::from_polar(
1,427✔
80
                rng.random_range(MIN_ASTEROID_VELOCITY..=MAX_ASTEROID_VELOCITY),
1,427✔
81
                rng.random_range(0.0..=2.0 * std::f32::consts::PI),
1,427✔
82
            ),
1,427✔
83
            edge_points: Rc::new(edge_points),
1,427✔
84
            sz,
1,427✔
85
            hue,
1,427✔
86
        }
1,427✔
87
    }
1,427✔
88

89
    pub fn scale(&self) -> f32 {
14,327,181✔
90
        match self.sz {
14,327,181✔
91
            Size::Large => 2.0,
4,389,203✔
92
            Size::Medium => 1.0,
4,440,050✔
93
            Size::Small => 0.55,
5,497,928✔
UNCOV
94
            Size::Destroyed => 0.0,
×
95
        }
96
    }
14,327,181✔
97

98
    pub fn score_from_size(sz: &Size) -> i32 {
195,848✔
99
        match sz {
195,848✔
100
            Size::Large => 10,
48,963✔
101
            Size::Medium => 20,
48,962✔
102
            Size::Small => 50,
48,964✔
103
            Size::Destroyed => 0,
48,959✔
104
        }
105
    }
195,848✔
106

107
    pub fn score(&self) -> i32 {
12✔
108
        Self::score_from_size(&self.sz)
12✔
109
    }
12✔
110

111
    pub fn split(&self) -> Option<[Self; 2]> {
12✔
112
        fn helper(a: &Asteroid, rotation: f32, new_size: Size) -> Asteroid {
24✔
113
            Asteroid {
24✔
114
                p: a.p,
24✔
115
                v: a.v.rotate(rotation),
24✔
116
                edge_points: a.edge_points.clone(),
24✔
117
                sz: new_size,
24✔
118
                hue: a.hue,
24✔
119
            }
24✔
120
        }
24✔
121
        match self.sz {
12✔
122
            Size::Large => Some([
4✔
123
                helper(self, SPLIT_ANGLE_RADS, Size::Medium),
4✔
124
                helper(self, -SPLIT_ANGLE_RADS, Size::Medium),
4✔
125
            ]),
4✔
126
            Size::Medium => Some([
3✔
127
                helper(self, SPLIT_ANGLE_RADS, Size::Small),
3✔
128
                helper(self, -SPLIT_ANGLE_RADS, Size::Small),
3✔
129
            ]),
3✔
130
            Size::Small => Some([
5✔
131
                helper(self, SPLIT_ANGLE_RADS, Size::Destroyed),
5✔
132
                helper(self, -SPLIT_ANGLE_RADS, Size::Destroyed),
5✔
133
            ]),
5✔
UNCOV
134
            Size::Destroyed => None,
×
135
        }
136
    }
12✔
137

138
    pub fn polygon(&self) -> Vec<Point> {
1,957,642✔
139
        self.edge_points
1,957,642✔
140
            .iter()
1,957,642✔
141
            .map(|&p| p * self.scale() + self.p)
14,327,181✔
142
            .collect()
1,957,642✔
143
    }
1,957,642✔
144

145
    pub fn make_debris(&self) -> Debris {
21✔
146
        Debris {
21✔
147
            p: self.p,
21✔
148
            v: self.v,
21✔
149
            hue: self.hue,
21✔
150
        }
21✔
151
    }
21✔
152
}
153

154
impl GameElement for Asteroid {
155
    fn update(&mut self, ctx: &GameContext) {
1,137,173✔
156
        self.p += self.v * ctx.t;
1,137,173✔
157
        self.p.wrap(ctx.w, ctx.h);
1,137,173✔
158
    }
1,137,173✔
159

160
    fn alive(&self) -> bool {
1,796,352✔
161
        !matches!(self.sz, Size::Destroyed)
1,796,352✔
162
    }
1,796,352✔
163

164
    fn render(&self) -> Html {
100✔
165
        let hsl = format!("hsl({}, 100%, 50%", self.hue);
100✔
166
        match self.sz {
100✔
167
            Size::Destroyed => {
UNCOV
168
                html! {<circle cx={self.p.x.to_string()} cy={self.p.y.to_string()} r="0.1" stroke={hsl}/>}
×
169
            }
170
            _ => {
171
                let points = self.polygon().into_iter().join(" ");
100✔
172
                html! {<polygon points={points} stroke={hsl}/>}
100✔
173
            }
174
        }
175
    }
100✔
176

177
    fn destroy(&mut self) {
12✔
178
        self.sz = Size::Destroyed;
12✔
179
    }
12✔
180
}
181

182
#[cfg(test)]
183
mod tests {
184
    use super::*;
185
    use crate::common::tests::PositiveFloat;
186
    use googletest::prelude::*;
187
    use is_svg::is_svg_string;
188
    use quickcheck::TestResult;
189
    use quickcheck_macros::quickcheck;
190

191
    #[quickcheck]
192
    fn it_spawns_an_asteroid_in_bounds(
100✔
193
        w: PositiveFloat,
100✔
194
        h: PositiveFloat,
100✔
195
        seed: u64,
100✔
196
    ) -> TestResult {
100✔
197
        let a = Asteroid::spawn(w.0, h.0, Some(seed));
100✔
198
        TestResult::from_bool(a.p.x <= w.0 && a.p.y <= h.0)
100✔
199
    }
100✔
200

201
    #[quickcheck]
202
    fn it_stays_in_bounds(
100✔
203
        w: PositiveFloat,
100✔
204
        h: PositiveFloat,
100✔
205
        t: PositiveFloat,
100✔
206
        iter_count: u32,
100✔
207
        seed: u64,
100✔
208
    ) -> Result<()> {
100✔
209
        let w = w.0;
100✔
210
        let h = h.0;
100✔
211
        let t = t.0 % 10_000.0;
100✔
212
        let mut a = Asteroid::spawn(w, h, Some(seed));
100✔
213
        let ctx = GameContext { w, h, t };
100✔
214
        let iter_count = iter_count % 5000; // limit to 5000 iterations
100✔
215
        for i in 0..iter_count {
239,019✔
216
            a.update(&ctx);
239,019✔
217
            let fail_msg = || format!("Failed on iteration {}", i);
239,019✔
218
            verify_that!(a.p.x, ge(0.0)).with_failure_message(fail_msg)?;
239,019✔
219
            verify_that!(a.p.y, ge(0.0)).with_failure_message(fail_msg)?;
239,019✔
220
            verify_that!(a.p.x, le(w)).with_failure_message(fail_msg)?;
239,019✔
221
            verify_that!(a.p.y, le(h)).with_failure_message(fail_msg)?;
239,019✔
222
        }
223
        Ok(())
100✔
224
    }
100✔
225

226
    #[quickcheck]
227
    fn it_is_a_polygon(w: PositiveFloat, h: PositiveFloat, seed: u64) -> TestResult {
100✔
228
        let a = Asteroid::spawn(w.0, h.0, Some(seed));
100✔
229
        TestResult::from_bool(a.edge_points.len() >= 3)
100✔
230
    }
100✔
231

232
    #[quickcheck]
233
    fn it_renders_valid_svg(w: PositiveFloat, h: PositiveFloat, seed: u64) -> TestResult {
100✔
234
        let a = Asteroid::spawn(w.0, h.0, Some(seed));
100✔
235
        let svg_wrap = format!("<svg>{:?}</svg>", a.render());
100✔
236
        TestResult::from_bool(is_svg_string(svg_wrap))
100✔
237
    }
100✔
238
}
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