From e09ee105ea9296d2d77ed4b42678bbd0d3fe5e9e Mon Sep 17 00:00:00 2001 From: Elijah Voigt Date: Fri, 19 Dec 2025 21:44:37 -0800 Subject: [PATCH] bounds checking works --- tetris/TODO.md | 2 +- tetris/src/blocks.rs | 176 ++++++++++++++++++++------------ tetris/src/debug.rs | 35 +++---- tetris/src/main.rs | 5 +- tetris/src/test/shape_layout.rs | 2 +- 5 files changed, 131 insertions(+), 89 deletions(-) diff --git a/tetris/TODO.md b/tetris/TODO.md index 3261f02..672618d 100644 --- a/tetris/TODO.md +++ b/tetris/TODO.md @@ -2,10 +2,10 @@ ## Bugs +* Piece should stop moving when it hits the edge of the grid ## Features -* Drawing the grid where the piece can go * Placing pieces in the grid * Placing multiple pieces stacking * Clearing lines when they are full diff --git a/tetris/src/blocks.rs b/tetris/src/blocks.rs index f38b81c..e67b355 100644 --- a/tetris/src/blocks.rs +++ b/tetris/src/blocks.rs @@ -32,7 +32,10 @@ impl Plugin for BlocksPlugin { .init_asset_loader::() .add_message::() .add_systems(OnEnter(Loading(true)), load_assets.run_if(run_once)) - .add_systems(OnEnter(GameState::Setup), (setup_camera, setup_blocks)) + .add_systems( + OnEnter(GameState::Setup), + (setup_camera, setup_blocks, setup_grid), + ) .add_systems( Update, ( @@ -102,11 +105,42 @@ struct GridPosition { pub y: usize, } +#[derive(Debug, Error)] +enum GridPositionError { + #[error("X > X_MAX")] + XOver, + #[error("X < 0")] + XUnder, + #[error("Y < 0")] + YUnder, + #[error("Y < 0 && X < 0")] + BothUnder, +} + impl GridPosition { fn with_relative_offset(&self, other: &RelativePosition) -> GridPosition { - GridPosition { - x: self.x.checked_add_signed(other.x).unwrap(), - y: self.y.checked_add_signed(other.y).unwrap(), + self.add(*other).unwrap() + } + + fn add(&self, other: RelativePosition) -> Result { + let x = self.x.checked_add_signed(other.x); + let y = self.y.checked_add_signed(other.y); + + match (x, y) { + // Both underflow + (None, None) => Err(GridPositionError::BothUnder), + // Just y underflows + (None, Some(_)) => Err(GridPositionError::XUnder), + // y underflow or x overflow + (Some(x), None) => match x { + 0..X_MAX => Err(GridPositionError::YUnder), + _ => Err(GridPositionError::XOver), + } + // both good, may x overflow + (Some(x), Some(y)) => match x { + 0..X_MAX => Ok(GridPosition { x, y }), + _ => Err(GridPositionError::XOver), + } } } } @@ -119,31 +153,6 @@ impl Into for &GridPosition { } } -impl std::ops::AddAssign for GridPosition { - fn add_assign(&mut self, rhs: RelativePosition) { - self.x = self.x.strict_add_signed(rhs.x); - self.y = self.y.strict_add_signed(rhs.y); - } -} - -impl std::ops::SubAssign for GridPosition { - fn sub_assign(&mut self, rhs: RelativePosition) { - self.x = self.x.strict_sub_signed(rhs.x); - self.y = self.y.strict_sub_signed(rhs.y); - } -} - -impl std::ops::Add for GridPosition { - type Output = GridPosition; - - fn add(self, rhs: RelativePosition) -> GridPosition { - GridPosition { - x: self.x.strict_add_signed(rhs.x), - y: self.y.strict_add_signed(rhs.y), - } - } -} - /// Block positions relative to the shape's center #[derive(Component, PartialEq, Debug, Clone, Copy)] pub(crate) struct RelativePosition { @@ -258,8 +267,9 @@ impl ShapeLayout { // TODO: Just hard-code this ShapeLayout { matrix: self.rotated_matrix(Orientation::Right), - symmetrical: false - }.rotated_matrix(Orientation::Right) + symmetrical: false, + } + .rotated_matrix(Orientation::Right) } Orientation::Left => { // The main algorithm @@ -353,6 +363,7 @@ fn setup_blocks( server: Res, mut checklist: ResMut, ) { + // TODO: WARN: THIS WILL CAUSE ISSUES WHEN WE LOAD MULTIPLE SHAPES let h: Handle = server .get_handle(all_assets.handles[0].path().unwrap()) .unwrap(); @@ -362,6 +373,30 @@ fn setup_blocks( checklist.spawn_shape = true; } +fn setup_grid( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + mut checklist: ResMut, +) { + let m: Mesh = Rectangle::new(SCALE, SCALE).into(); + let mh: Handle = meshes.add(m); + let m2d = Mesh2d(mh); + let c: Color = WHITE.with_alpha(0.5).into(); + let ch: Handle = materials.add(c); + let mat = MeshMaterial2d(ch); + let t = Transform::from_xyz(0.0, 0.0, -1.0); + + for x in 0..X_MAX { + for y in 0..Y_MAX { + let gp = GridPosition { x, y }; + commands.spawn((gp, m2d.clone(), mat.clone(), t.clone())); + } + } + + checklist.spawn_grid = true; +} + /// Blocks <- Shape Relationship #[derive(Component)] #[relationship(relationship_target = ShapeBlocks)] @@ -428,7 +463,9 @@ fn update_grid_position_transform( >, ) { query.iter_mut().for_each(|(gp, mut t)| { - t.translation = gp.into(); + let new_t: Vec3 = gp.into(); + t.translation.x = new_t.x; + t.translation.y = new_t.y; }); } @@ -440,19 +477,22 @@ fn propogate_grid_position( parent.iter().for_each(|(parent_gp, sbs)| { sbs.iter().for_each(|e| { let (mut gp, rp) = children.get_mut(e).unwrap(); - *gp = parent_gp.clone() + *rp; + *gp = parent_gp.with_relative_offset(rp); }); }); } /// When a block's relative position changes, update it's grid position fn propogate_relative_position( - mut children: Query<(&mut GridPosition, &RelativePosition, &ShapeBlock), Changed>, + mut children: Query< + (&mut GridPosition, &RelativePosition, &ShapeBlock), + Changed, + >, parent: Query<&GridPosition, Without>, ) { children.iter_mut().for_each(|(mut gp, rp, sb)| { let parent_gp = parent.get(sb.0).unwrap(); - *gp = parent_gp.clone() + *rp; + *gp = parent_gp.with_relative_offset(rp); }); } @@ -472,7 +512,7 @@ fn propogate_orientation( } /// Movement message used to propose/plan a move -#[derive(Debug, Message)] +#[derive(Debug, Message, PartialEq)] enum Movement { Rotate, Left, @@ -532,33 +572,41 @@ impl Orientation { fn handle_movement( mut moves: MessageReader, - mut grid_positions: Query<&mut GridPosition, With>, - mut orientations: Query<&mut Orientation, With>, + mut shapes: Query<(&ShapeLayout, &mut GridPosition, &mut Orientation)>, ) { - moves.read().for_each(|m| match m { - Movement::Left => { - grid_positions.iter_mut().for_each(|mut gp| { - debug!("moving shape left"); - *gp -= RelativePosition::new(1, 0); - }); - } - Movement::Right => { - grid_positions.iter_mut().for_each(|mut gp| { - debug!("moving shape right"); - *gp += RelativePosition::new(1, 0); - }); - } - Movement::Down => { - grid_positions.iter_mut().for_each(|mut gp| { - debug!("moving shape down"); - *gp -= RelativePosition::new(0, 1); - }); - } - Movement::Rotate => { - orientations.iter_mut().for_each(|mut o| { - debug!("rotating shape"); - *o = o.rotated(); - }); - } - }) + moves.read().for_each(|m| { + shapes.iter_mut().for_each(|(shape_layout, mut gp, mut o)| { + // First determine if proposed positions are valid + // Determine next orientation + let proposed_orientation = if *m == Movement::Rotate { + o.rotated() + } else { + *o + }; + + // And next position + let proposed_position = match m { + Movement::Left => gp.add((-1, 0).into()), + Movement::Right => gp.add((1, 0).into()), + Movement::Down => gp.add((0, -1).into()), + Movement::Rotate => Ok(gp.clone()), + }; + + if let Ok(pp) = proposed_position { + // Check that new positions are all valid + if shape_layout + .positions(proposed_orientation) + .iter() + .all(|rp| pp.add(*rp).is_ok()) { + // then commit the movement + *gp = pp; + *o = proposed_orientation; + } else { + // invalid position! + } + } else { + // Invalid move! + } + }); + }); } diff --git a/tetris/src/debug.rs b/tetris/src/debug.rs index b307c35..2450218 100644 --- a/tetris/src/debug.rs +++ b/tetris/src/debug.rs @@ -25,7 +25,7 @@ impl Plugin for DebugPlugin { log_transition::.run_if(state_changed::), log_transition::.run_if(state_changed::), log_transition::.run_if(state_changed::), - log_transition::.run_if(state_changed::) + log_transition::.run_if(state_changed::), ), ) .add_systems( @@ -45,11 +45,11 @@ impl Plugin for DebugPlugin { sync_state_to_ui::, ) .run_if(state_changed::), - ) + ), ) .add_systems( Update, - draw_outline_gizmos.run_if(in_state(DebugOutlines(true))) + draw_outline_gizmos.run_if(in_state(DebugOutlines(true))), ); } } @@ -100,21 +100,15 @@ fn setup_ui(mut commands: Commands) { ..default() }, DebugState(true), - children![ - ( - Node { - ..default() - }, - children![ - ( - DebugState(true), - checkbox((), Spawn((Text::new("outlines"), ThemedText))), - observe(toggle_outline_state), - observe(check_box), - ), - ] - ), - ] + children![( + Node { ..default() }, + children![( + DebugState(true), + checkbox((), Spawn((Text::new("outlines"), ThemedText))), + observe(toggle_outline_state), + observe(check_box), + ),] + ),], )); commands.spawn(( @@ -169,10 +163,7 @@ impl fmt::Display for DebugOutlines { } } -fn toggle_outline_state( - event: On>, - mut next: ResMut>, -) { +fn toggle_outline_state(event: On>, mut next: ResMut>) { next.set(DebugOutlines(event.event().value)); } diff --git a/tetris/src/main.rs b/tetris/src/main.rs index b656387..f83d338 100644 --- a/tetris/src/main.rs +++ b/tetris/src/main.rs @@ -97,14 +97,17 @@ struct AllAssets { } /// A "checklist" to know if we can progress from setup to the game +/// TODO: Turn this into a hash-map +/// Systems on sartup insert `key:false` and update to `key;true` #[derive(Default, Resource)] struct SetupChecklist { spawn_shape: bool, + spawn_grid: bool, } impl SetupChecklist { fn done(&self) -> bool { - self.spawn_shape + self.spawn_shape && self.spawn_grid } } diff --git a/tetris/src/test/shape_layout.rs b/tetris/src/test/shape_layout.rs index e95debd..7c80a45 100644 --- a/tetris/src/test/shape_layout.rs +++ b/tetris/src/test/shape_layout.rs @@ -26,7 +26,7 @@ fn test_shape_layout_01c() { let actual = ShapeLayout::new(vec![vec![0, 1, 0], vec![1, 1, 1]]).positions(Orientation::Down); let expected: [RelativePosition; 4] = - [(0, -1).into(), (-1, 0).into(), (0, 0).into(), (1, 0).into(),]; + [(0, -1).into(), (-1, 0).into(), (0, 0).into(), (1, 0).into()]; debug_assert_eq!(expected, actual); }