use crate::{Body, SCREEN_HEIGHT, SCREEN_WIDTH};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Bounds {
x: i16,
y: i16,
width: u16,
height: u16,
}
impl Bounds {
pub const fn new(x: i16, y: i16, width: u16, height: u16) -> Self {
Self {
x,
y,
width,
height,
}
}
pub fn of(body: &Body, width: u16, height: u16) -> Self {
let (x, y) = body.draw_pos();
Self::new(x, y, width, height)
}
pub const fn screen() -> Self {
Self::new(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)
}
pub const fn x(&self) -> i16 {
self.x
}
pub const fn y(&self) -> i16 {
self.y
}
pub const fn width(&self) -> u16 {
self.width
}
pub const fn height(&self) -> u16 {
self.height
}
pub const fn is_empty(&self) -> bool {
self.width == 0 || self.height == 0
}
pub const fn right(&self) -> i16 {
self.x.saturating_add_unsigned(self.width)
}
pub const fn bottom(&self) -> i16 {
self.y.saturating_add_unsigned(self.height)
}
pub const fn overlaps(&self, other: Bounds) -> bool {
!self.is_empty()
&& !other.is_empty()
&& self.x < other.right()
&& self.right() > other.x
&& self.y < other.bottom()
&& self.bottom() > other.y
}
pub const fn on_screen(&self) -> bool {
self.overlaps(Self::screen())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sprite(x: i16, y: i16) -> Bounds {
Bounds::new(x, y, 8, 8)
}
#[test]
fn a_rectangle_keeps_the_corner_and_the_size_it_was_given() {
let bounds = Bounds::new(-3, 4, 16, 7);
assert_eq!((bounds.x(), bounds.y()), (-3, 4));
assert_eq!((bounds.width(), bounds.height()), (16, 7));
assert!(!bounds.is_empty());
const LEVEL: Bounds = Bounds::new(0, 0, 256, 128);
assert_eq!(LEVEL.right(), 256);
}
#[test]
fn a_rectangle_with_a_side_of_nothing_is_nothing() {
for empty in [
Bounds::new(0, 0, 0, 8),
Bounds::new(0, 0, 8, 0),
Bounds::new(0, 0, 0, 0),
] {
assert!(empty.is_empty(), "{empty:?}");
assert!(!empty.overlaps(sprite(0, 0)), "{empty:?} hit something");
assert!(!sprite(0, 0).overlaps(empty), "something hit {empty:?}");
assert!(!empty.overlaps(empty), "{empty:?} hit itself");
assert!(!empty.on_screen(), "{empty:?} was on screen");
}
let empty = Bounds::new(5, 6, 0, 0);
assert_eq!((empty.right(), empty.bottom()), (5, 6));
}
#[test]
fn the_far_edges_are_one_past_the_last_pixel() {
let bounds = Bounds::new(3, 100, 16, 4);
assert_eq!(bounds.right(), 19);
assert_eq!(bounds.bottom(), 104);
}
#[test]
fn rectangles_overlap_on_the_pixels_they_share() {
assert!(sprite(0, 0).overlaps(sprite(0, 0)));
assert!(sprite(0, 0).overlaps(sprite(7, 7)));
assert!(!sprite(0, 0).overlaps(sprite(8, 0)));
assert!(!sprite(0, 0).overlaps(sprite(0, 8)));
assert!(!sprite(0, 0).overlaps(sprite(0, 9)));
assert!(!sprite(0, 0).overlaps(sprite(9, 0)));
let wide = Bounds::new(-4, -4, 32, 32);
assert!(wide.overlaps(sprite(0, 0)));
}
#[test]
fn overlapping_is_mutual() {
let wide = Bounds::new(-4, -4, 32, 32);
for (ours, theirs) in [
(sprite(0, 0), sprite(7, 7)),
(sprite(0, 0), sprite(8, 0)),
(sprite(0, 0), wide),
(sprite(40, 40), sprite(0, 0)),
] {
assert_eq!(
ours.overlaps(theirs),
theirs.overlaps(ours),
"{ours:?} and {theirs:?} disagree about their overlap"
);
}
}
#[test]
fn a_rectangle_is_on_screen_until_the_last_pixel_of_it_leaves() {
assert!(sprite(0, 0).on_screen());
assert!(sprite(-7, 60).on_screen());
assert!(!sprite(-8, 60).on_screen());
assert!(sprite(SCREEN_WIDTH as i16 - 1, 60).on_screen());
assert!(!sprite(SCREEN_WIDTH as i16, 60).on_screen());
assert!(sprite(60, -7).on_screen());
assert!(!sprite(60, -8).on_screen());
assert!(sprite(60, SCREEN_HEIGHT as i16 - 1).on_screen());
assert!(!sprite(60, SCREEN_HEIGHT as i16).on_screen());
}
#[test]
fn a_body_covers_a_rectangle_from_where_it_draws() {
let mut body = Body::new(10.0, 20.0);
assert_eq!(Bounds::of(&body, 8, 8), sprite(10, 20));
assert!(Bounds::of(&body, 0, 8).is_empty());
for _ in 0..3 {
body.move_by(0.5, 0.4);
}
assert_eq!(
body.y() as i16,
21,
"the exact position is already a row on"
);
assert_eq!(
body.draw_pos(),
(11, 20),
"and the drawn one is holding back"
);
assert_eq!(Bounds::of(&body, 8, 8), sprite(11, 20));
}
#[test]
fn the_screen_is_the_rectangle_everything_is_drawn_in() {
const PLAY_AREA: Bounds = Bounds::screen();
assert_eq!((PLAY_AREA.x(), PLAY_AREA.y()), (0, 0));
assert_eq!(
(PLAY_AREA.right(), PLAY_AREA.bottom()),
(SCREEN_WIDTH as i16, SCREEN_HEIGHT as i16)
);
assert!(PLAY_AREA.on_screen());
}
const CORNERS: [i16; 9] = [
i16::MIN,
i16::MIN + 1,
-129,
-1,
0,
1,
127,
i16::MAX - 1,
i16::MAX,
];
const SIDES: [u16; 7] = [0, 1, 8, 127, 128, 32768, u16::MAX];
#[test]
fn no_rectangle_anywhere_can_be_asked_a_question_it_cannot_answer() {
for &x in &CORNERS {
for &y in &CORNERS {
for &width in &SIDES {
for &height in &SIDES {
let bounds = Bounds::new(x, y, width, height);
assert_eq!((bounds.x(), bounds.y()), (x, y));
assert_eq!((bounds.width(), bounds.height()), (width, height));
assert_eq!(bounds.is_empty(), width == 0 || height == 0);
assert!(bounds.right() >= x, "{bounds:?} wrapped its right edge");
assert!(bounds.bottom() >= y, "{bounds:?} wrapped its bottom edge");
let has_pixels = bounds.right() > x && bounds.bottom() > y;
assert_eq!(bounds.overlaps(bounds), has_pixels, "{bounds:?}");
if !has_pixels {
assert!(!bounds.on_screen(), "{bounds:?} was on screen");
}
for other in [Bounds::screen(), sprite(0, 0), Bounds::new(y, x, 4, 4)] {
assert_eq!(
bounds.overlaps(other),
other.overlaps(bounds),
"{bounds:?} and {other:?} disagree"
);
}
}
}
}
}
}
#[test]
fn a_rectangle_at_the_end_of_the_world_does_not_wrap_around_it() {
let far = Bounds::new(i16::MAX - 1, i16::MAX - 1, 8, 8);
assert_eq!((far.right(), far.bottom()), (i16::MAX, i16::MAX));
assert!(!far.on_screen());
assert!(!far.overlaps(sprite(0, 0)));
let corner = Bounds::new(i16::MAX, i16::MAX, 8, 8);
assert_eq!((corner.right(), corner.bottom()), (i16::MAX, i16::MAX));
assert!(!corner.overlaps(sprite(0, 0)));
let near = Bounds::new(i16::MIN, i16::MIN, 8, 8);
assert_eq!((near.right(), near.bottom()), (i16::MIN + 8, i16::MIN + 8));
assert!(!near.on_screen());
}
}