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

bcpearce / asteroids-rs / 23281277009

19 Mar 2026 05:29AM UTC coverage: 93.666% (+1.6%) from 92.027%
23281277009

push

github

web-flow
Added UFO, improved collisions (#8)

* Added UFO, improved collisions

* Added edge case for asteroid collisions

* Ship thruster render

* Added asteroid and adjusted collision detection

* Generally functional UFO

* Fix some tests

* Fix score and UFO collisions

* More testing

359 of 371 new or added lines in 7 files covered. (96.77%)

1 existing line in 1 file now uncovered.

1109 of 1184 relevant lines covered (93.67%)

590956.52 hits per line

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

90.18
/src/math.rs
1
use core::fmt;
2
use std::ops::{Add, AddAssign, Mul, MulAssign, Sub, SubAssign};
3

4
#[derive(Debug, Copy, Clone, PartialEq)]
5
pub struct Point {
6
    pub x: f32,
7
    pub y: f32,
8
}
9

10
macro_rules! point {
11
    ($x:expr, $y:expr) => {
12
        Point {
13
            x: $x as f32,
14
            y: $y as f32,
15
        }
16
    };
17
}
18
pub(crate) use point;
19

20
macro_rules! polar_point {
21
    ($r:expr, $theta:expr) => {
22
        Point::from_polar($r as f32, $theta as f32)
23
    };
24
}
25
pub(crate) use polar_point;
26

27
#[derive(Debug, Copy, Clone, PartialEq)]
28
pub struct Ellipse {
29
    pub center: Point,
30
    pub ax: f32,
31
    pub by: f32,
32
}
33

34
macro_rules! ellipse {
35
    ($x:expr, $y:expr, $width:expr, $height:expr) => {
36
        Ellipse {
37
            center: point!($x, $y),
38
            ax: $width,
39
            by: $height,
40
        }
41
    };
42
}
43
pub(crate) use ellipse;
44

45
impl Point {
46
    pub fn from_polar(r: f32, theta: f32) -> Point {
859,805✔
47
        let (sin, cos) = theta.sin_cos();
859,805✔
48
        Point {
859,805✔
49
            x: r * cos,
859,805✔
50
            y: r * sin,
859,805✔
51
        }
859,805✔
52
    }
859,805✔
53

54
    pub fn cross(p1: Point, p2: Point) -> f32 {
13,821,376✔
55
        p1.x * p2.y - p2.x * p1.y
13,821,376✔
56
    }
13,821,376✔
57

58
    pub fn dot(p1: Point, p2: Point) -> f32 {
27,642,776✔
59
        p1.x * p2.x + p1.y * p2.y
27,642,776✔
60
    }
27,642,776✔
61

62
    pub fn midpoint(p1: Point, p2: Point) -> Point {
7,887✔
63
        (p1 + p2) * 0.5
7,887✔
64
    }
7,887✔
65

66
    pub fn wrap(&mut self, w: f32, h: f32) {
536,218✔
67
        self.x = if self.x < 0.0 {
536,218✔
68
            w - (self.x % w).abs()
603✔
69
        } else if self.x > w {
535,615✔
70
            self.x % w
797✔
71
        } else {
72
            self.x
534,818✔
73
        };
74
        self.y = if self.y < 0.0 {
536,218✔
75
            h - (self.y % h).abs()
1,951✔
76
        } else if self.y > h {
534,267✔
77
            self.y % h
3,420✔
78
        } else {
79
            self.y
530,847✔
80
        };
81
    }
536,218✔
82

83
    pub fn mag(&self) -> f32 {
237✔
84
        (self.x.powi(2) + self.y.powi(2)).sqrt()
237✔
85
    }
237✔
86

87
    pub fn orthogonal(&self) -> Point {
×
88
        point!(-self.y, self.x)
×
89
    }
×
90

91
    pub fn rotate(&self, theta_rad: f32) -> Point {
15,920✔
92
        if theta_rad.is_normal() {
15,920✔
93
            let (sin, cos) = theta_rad.sin_cos();
15,918✔
94
            Point {
15,918✔
95
                x: self.x * cos - self.y * sin,
15,918✔
96
                y: self.x * sin + self.y * cos,
15,918✔
97
            }
15,918✔
98
        } else {
99
            *self
2✔
100
        }
101
    }
15,920✔
102

103
    pub fn rotate_about(&self, theta_rad: f32, about: Point) -> Point {
15,774✔
104
        let tmp = *self - about;
15,774✔
105
        let tmp = tmp.rotate(theta_rad);
15,774✔
106
        tmp + about
15,774✔
107
    }
15,774✔
108

109
    /// Checks if it exists inside a polygon using ray-casting
110
    pub fn in_polygon(&self, polygon: &[Point]) -> Result<bool, &'static str> {
2,861,028✔
111
        if polygon.len() < 3 {
2,861,028✔
112
            return Err("Polygon must have at least 3 points");
×
113
        }
2,861,028✔
114
        fn check_ray_intersection(origin: Point, p1: Point, p2: Point) -> bool {
13,821,400✔
115
            let ortho = point!(0, 1);
13,821,400✔
116
            let p1_to_origin = origin - p1;
13,821,400✔
117
            let p1_to_p2 = p2 - p1;
13,821,400✔
118
            let den: f32 = Point::dot(p1_to_p2, ortho);
13,821,400✔
119
            if den == 0.0 {
13,821,400✔
120
                return origin.x == p1.x || origin.x == p2.x;
24✔
121
            }
13,821,376✔
122

123
            let t1 = Point::cross(p1_to_p2, p1_to_origin) / den;
13,821,376✔
124
            let t2 = Point::dot(p1_to_origin, ortho) / den;
13,821,376✔
125

126
            (0.0..=1.0).contains(&t2) && t1 >= 0.0
13,821,376✔
127
        }
13,821,400✔
128
        let mut intersections = 0;
2,861,028✔
129
        for (i, j) in (0..polygon.len()).zip(1..(polygon.len() + 1)) {
13,821,400✔
130
            let &p1 = &polygon[i];
13,821,400✔
131
            let &p2 = &polygon[j % polygon.len()];
13,821,400✔
132
            if check_ray_intersection(*self, p1, p2) {
13,821,400✔
133
                intersections += 1;
122,430✔
134
            }
13,698,970✔
135
        }
136
        Ok(intersections % 2 == 1)
2,861,028✔
137
    }
2,861,028✔
138

139
    pub fn in_ellipse(&self, ellipse: &Ellipse) -> bool {
271,258✔
140
        if ellipse.ax == 0.0 {
271,258✔
141
            self.x == ellipse.center.x && (self.y - ellipse.center.y).abs() <= ellipse.by
260,500✔
142
        } else if ellipse.by == 0.0 {
10,758✔
NEW
143
            self.y == ellipse.center.y && (self.x - ellipse.center.x).abs() <= ellipse.ax
×
144
        } else {
145
            (self.x - ellipse.center.x).powi(2) / ellipse.ax.powi(2)
10,758✔
146
                + (self.y - ellipse.center.y).powi(2) / ellipse.by.powi(2)
10,758✔
147
                <= 1.0
10,758✔
148
        }
149
    }
271,258✔
150
}
151

152
impl Add for Point {
153
    type Output = Self;
154

155
    fn add(self, other: Self) -> Self::Output {
6,741,415✔
156
        Self {
6,741,415✔
157
            x: self.x + other.x,
6,741,415✔
158
            y: self.y + other.y,
6,741,415✔
159
        }
6,741,415✔
160
    }
6,741,415✔
161
}
162

163
impl AddAssign for Point {
164
    fn add_assign(&mut self, other: Self) {
721,427✔
165
        self.x += other.x;
721,427✔
166
        self.y += other.y;
721,427✔
167
    }
721,427✔
168
}
169

170
impl Sub for Point {
171
    type Output = Self;
172
    fn sub(self, other: Self) -> Self::Output {
27,674,349✔
173
        Self {
27,674,349✔
174
            x: self.x - other.x,
27,674,349✔
175
            y: self.y - other.y,
27,674,349✔
176
        }
27,674,349✔
177
    }
27,674,349✔
178
}
179

180
impl SubAssign for Point {
181
    fn sub_assign(&mut self, other: Self) {
1✔
182
        self.x -= other.x;
1✔
183
        self.y -= other.y;
1✔
184
    }
1✔
185
}
186

187
impl Mul<f32> for Point {
188
    type Output = Self;
189
    fn mul(self, mag: f32) -> Self::Output {
6,605,361✔
190
        Self {
6,605,361✔
191
            x: self.x * mag,
6,605,361✔
192
            y: self.y * mag,
6,605,361✔
193
        }
6,605,361✔
194
    }
6,605,361✔
195
}
196

197
impl Mul<Point> for f32 {
198
    type Output = Point;
199
    fn mul(self, point: Point) -> Self::Output {
×
200
        Point {
×
201
            x: point.x * self,
×
202
            y: point.y * self,
×
203
        }
×
204
    }
×
205
}
206

207
impl Mul<Point> for Point {
208
    type Output = f32;
209
    fn mul(self, point: Point) -> Self::Output {
×
210
        Self::dot(self, point)
×
211
    }
×
212
}
213

214
impl MulAssign<f32> for Point {
215
    fn mul_assign(&mut self, mag: f32) {
1✔
216
        self.x *= mag;
1✔
217
        self.y *= mag;
1✔
218
    }
1✔
219
}
220

221
impl fmt::Display for Point {
222
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
991✔
223
        write!(f, "{},{}", self.x, self.y)
991✔
224
    }
991✔
225
}
226

227
#[cfg(test)]
228
mod point_tests {
229
    use core::f32;
230

231
    use super::*;
232
    use googletest::prelude::*;
233
    use p_test::p_test;
234
    use quickcheck::{Arbitrary, Gen, TestResult};
235
    use quickcheck_macros::quickcheck;
236

237
    #[gtest]
238
    fn it_adds() {
239
        let p1 = Point { x: 1.0, y: 5.0 };
240
        let p2 = Point { x: 2.5, y: 4.2 };
241
        let p3 = p1 + p2;
242
        expect_that!(p3.x, near(3.5, 1e-6));
243
        expect_that!(p3.y, near(9.2, 1e-6));
244
    }
245

246
    #[gtest]
247
    fn it_add_assigns() {
248
        let mut p1 = Point { x: 1.0, y: 5.0 };
249
        let p2 = Point { x: 2.5, y: 4.2 };
250
        p1 += p2;
251
        expect_that!(p1.x, near(3.5, 1e-6));
252
        expect_that!(p1.y, near(9.2, 1e-6));
253
    }
254

255
    #[gtest]
256
    fn it_subs() {
257
        let p1 = Point { x: 1.0, y: 5.0 };
258
        let p2 = Point { x: 2.5, y: 4.2 };
259
        let p3 = p1 - p2;
260
        expect_that!(p3.x, near(-1.5, 1e-6));
261
        expect_that!(p3.y, near(0.8, 1e-6));
262
    }
263

264
    #[gtest]
265
    fn it_sub_assigns() {
266
        let mut p1 = Point { x: 1.0, y: 5.0 };
267
        let p2 = Point { x: 2.5, y: 4.2 };
268
        p1 -= p2;
269
        expect_that!(p1.x, near(-1.5, 1e-6));
270
        expect_that!(p1.y, near(0.8, 1e-6));
271
    }
272

273
    #[gtest]
274
    fn it_muls() {
275
        let p1 = Point { x: 1.0, y: 5.0 } * 3.0;
276
        expect_that!(p1.x, near(3.0, 1e-6));
277
        expect_that!(p1.y, near(15.0, 1e-6));
278
    }
279

280
    #[gtest]
281
    fn it_mul_assigns() {
282
        let mut p1 = Point { x: 1.0, y: 5.0 };
283
        p1 *= 3.0;
284
        expect_that!(p1.x, near(3.0, 1e-6));
285
        expect_that!(p1.y, near(15.0, 1e-6));
286
    }
287

288
    #[gtest]
289
    fn it_converts_from_polar() {
290
        let p1 = Point::from_polar(2.0, std::f32::consts::PI * 0.5);
291
        expect_that!(p1.x, near(0.0, 1e-6));
292
        expect_that!(p1.y, near(2.0, 1e-6));
293
        let p1 = Point::from_polar(2.0, std::f32::consts::PI * -0.25);
294
        expect_that!(p1.x, near(1.41421356237, 1e-6));
295
        expect_that!(p1.y, near(-1.41421356237, 1e-6));
296
    }
297

298
    #[gtest]
299
    fn it_does_equality() {
300
        let p1 = point!(1, 1);
301
        let p2 = point!(1, 1);
302
        assert_that!(p1, eq(p2));
303
    }
304

305
    #[p_test(
306
        "at origin", (point!(0.0, 0.0), true),
307
        "outside at(n2.0,0.0)", (point!(-2.0, 0.0), false), 
308
        "outside at(2.0,0.0)", (point!(2.0, 0.0), false), 
309
        "inside at(0.5,0.5)", (point!(0.5, 0.5), true),
310
        "on edge(1.0,0.0)", (point!(1.0, 0.0), true),
311
        "on edge(1.0,n1.0)", (point!(1.0, -1.0), true),
312
        "outside at(0.0,2.0", (point!(0.0, 2.0), false),
313
        "outside at(NaN,0.0)", (point!(f32::NAN, 0.0), false),
314
    )]
315
    fn it_determines_point_in_polygon(point: Point, expect_inside: bool) {
316
        let polygon = vec![point!(-1, -1), point!(-1, 1), point!(1, 1), point!(1, -1)];
317
        assert_that!(point.in_polygon(&polygon).unwrap(), eq(expect_inside));
318
    }
319

320
    #[p_test(
321
        "at origin", (point!(0.0, 0.0),ellipse!(0, 0, 1.0, 0.5), true),
322
        "inside at(0.0,0.4)", (point!(0.0,0.4),ellipse!(0, 0, 1.0, 0.5), true),
323
        "inside at(0.0,n0.4)", (point!(0.0,-0.4),ellipse!(0, 0, 1.0, 0.5), true),
324
        "inside at(0.9,0.0)", (point!(0.9,0.0),ellipse!(0, 0, 1.0, 0.5), true),
325
        "inside at(n0.9,0.0)", (point!(-0.9,0.0),ellipse!(0, 0, 1.0, 0.5), true),
326
        "outside at(1.1,0.0)", (point!(1.1,0.0),ellipse!(0, 0, 1.0, 0.5), false),
327
        "on edge at(0.0,0.5)", (point!(0.0,0.5),ellipse!(0, 0, 1.0, 0.5), true),
328
        "on edge at(n1.0,0.0)", (point!(-1.0,0.0),ellipse!(0, 0, 1.0, 0.5), true),
329
    )]
330
    fn it_determines_point_in_ellipse(point: Point, ellipse: Ellipse, expect_inside: bool) {
331
        assert_that!(point.in_ellipse(&ellipse), eq(expect_inside));
332
    }
333

334
    #[quickcheck]
335
    fn it_keeps_the_same_magnitude_on_rotation(p: Point, theta_rad: f32) -> TestResult {
459✔
336
        if p.x.is_finite()
459✔
337
            && p.y.is_finite()
440✔
338
            && theta_rad.is_finite()
417✔
339
            && p.x.abs() < 1e18
395✔
340
            && p.y.abs() < 1e18
196✔
341
        {
342
            let actual = p.mag();
100✔
343
            let expected = p.rotate(theta_rad).mag();
100✔
344
            if actual - expected < 1e-6 {
100✔
345
                return TestResult::passed();
88✔
346
            }
12✔
347
            let relative_error = ((actual - expected) / expected).abs();
12✔
348
            assert_that!(relative_error, lt(1e-6));
12✔
349
            TestResult::passed()
12✔
350
        } else {
351
            TestResult::discard()
359✔
352
        }
353
    }
459✔
354

355
    #[quickcheck]
356
    fn it_always_contains_points_within_minor_axis(
10,000✔
357
        width: f32,
10,000✔
358
        height: f32,
10,000✔
359
        p: Point,
10,000✔
360
        theta_rad: f32,
10,000✔
361
    ) -> TestResult {
10,000✔
362
        const MAX_AXIS: f32 = 1e5;
363
        if p.x.is_finite()
10,000✔
364
            && p.y.is_finite()
9,526✔
365
            && theta_rad.is_finite()
9,075✔
366
            && (-MAX_AXIS..MAX_AXIS).contains(&p.x)
8,634✔
367
            && (-MAX_AXIS..MAX_AXIS).contains(&p.y)
1,279✔
368
            && (0.0..MAX_AXIS).contains(&width)
190✔
369
            && (0.0..MAX_AXIS).contains(&height)
12✔
370
        {
NEW
371
            let inside = polar_point!(width.min(height), theta_rad) + p;
×
NEW
372
            TestResult::from_bool(inside.in_ellipse(&ellipse!(p.x, p.y, width, height)))
×
373
        } else {
374
            TestResult::discard()
10,000✔
375
        }
376
    }
10,000✔
377

378
    impl Arbitrary for Point {
379
        fn arbitrary(g: &mut Gen) -> Self {
10,459✔
380
            Point {
10,459✔
381
                x: f32::arbitrary(g),
10,459✔
382
                y: f32::arbitrary(g),
10,459✔
383
            }
10,459✔
384
        }
10,459✔
385
    }
386
}
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