From 570b4916bc91c63c6a7b27d8fee3ea110104b96a Mon Sep 17 00:00:00 2001 From: Elijah Voigt Date: Thu, 4 Dec 2025 21:34:07 -0800 Subject: [PATCH] saving my place --- src/bin/tetris/main.rs | 193 +++++++++++++++++++++-------------------- 1 file changed, 97 insertions(+), 96 deletions(-) diff --git a/src/bin/tetris/main.rs b/src/bin/tetris/main.rs index b1b8f80..ff5ff37 100644 --- a/src/bin/tetris/main.rs +++ b/src/bin/tetris/main.rs @@ -28,6 +28,7 @@ fn main() { .init_resource::() .init_resource::() .init_resource::() + .add_message::() .add_systems( Startup, ( @@ -68,6 +69,7 @@ fn main() { falling .run_if(in_state(GameState::Play)) .run_if(on_timer(Duration::from_secs(1))), + movement.run_if(on_message::), update_position.run_if(any_component_changed::), update_shape_blocks .run_if(any_component_changed::.or(any_component_changed::)) @@ -80,14 +82,13 @@ fn main() { adjust_block_lines .run_if(any_component_changed::) .after(clear_line), - assert_grid_position_uniqueness.run_if(any_component_changed::), sync_health .run_if(any_component_changed::.or(any_component_added::)), damage_on_place_shape.run_if(any_component_removed::), damage_on_clear_line.run_if(any_component_removed::), damage_over_time.run_if(on_timer(Duration::from_secs(5))), check_level_goal.run_if(resource_changed::), - check_level_fail.run_if(any_component_removed::), + assert_grid_position_uniqueness.run_if(any_component_changed::), toggle_art.run_if(any_component_added::), ), ) @@ -182,7 +183,7 @@ impl Display for GridPosition { } } -#[derive(Error, Debug, PartialEq)] +#[derive(Error, Debug, PartialEq, Clone, Copy)] enum OutOfBoundsError { #[error("Coordinates are out of bounds: Left")] Left, @@ -990,9 +991,7 @@ fn on_add_shape_layout( .entity(e) .with_related_entities::(|parent| { sl.coordinates_at(center).for_each(|gp| { - parent - .spawn((mesh.clone(), MeshMaterial2d(mat.clone()), art.clone(), gp.unwrap(), Block, TETRIS)) - .observe(movement); + parent.spawn((mesh.clone(), MeshMaterial2d(mat.clone()), art.clone(), gp.unwrap(), Block, TETRIS)); }); }); } @@ -1002,6 +1001,7 @@ fn kb_input( mut query: Query>, // Single? curr: Res>, mut next: ResMut>, + mut messages: MessageWriter, mut commands: Commands, ) { events.read().for_each( @@ -1009,43 +1009,43 @@ fn kb_input( key_code, state, .. }| { if let ButtonState::Pressed = state { - query.iter_mut().for_each(|e| { + query.iter_mut().for_each(|entity| { match curr.get() { GameState::Play => match key_code { // Up arrow should rotate if in falling mode // Only move up if in falling::off mode KeyCode::ArrowUp => { - commands.entity(e).trigger(|entity| Movement { + messages.write(Movement { entity, direction: MovementDirection::Rotate, }); } KeyCode::ArrowDown => { - commands.entity(e).trigger(|entity| Movement { + messages.write(Movement { entity, direction: MovementDirection::Down, }); } KeyCode::ArrowLeft => { - commands.entity(e).trigger(|entity| Movement { + messages.write(Movement { entity, direction: MovementDirection::Left, }); } KeyCode::ArrowRight => { - commands.entity(e).trigger(|entity| Movement { + messages.write(Movement { entity, direction: MovementDirection::Right, }); } KeyCode::Enter => { - commands.entity(e).trigger(|entity| Movement { + messages.write(Movement { entity, direction: MovementDirection::Skip, }); } KeyCode::Space => { - commands.entity(e).trigger(|entity| Swap { entity }); + commands.entity(entity).trigger(|entity| Swap { entity }); } KeyCode::Escape => next.set(GameState::Pause), _ => (), @@ -1072,10 +1072,10 @@ fn kb_input( ); } -fn falling(mut shape: Query>, mut commands: Commands) { - shape.iter_mut().for_each(|e| { - debug!("Making {:?} fall", e); - commands.entity(e).trigger(|entity| Movement { +fn falling(mut shape: Query>, mut messages: MessageWriter) { + shape.iter_mut().for_each(|entity| { + debug!("Making {:?} fall", entity); + messages.write(Movement { entity, direction: MovementDirection::Down, }); @@ -1095,7 +1095,6 @@ fn add_piece(mut commands: Commands, mut shapes: ResMut) { shape, TETRIS, )) - .observe(movement) .observe(swap); } @@ -1205,7 +1204,7 @@ enum MovementDirection { // TODO: When out of bounds left/right, try to move piece away from wall fn movement( - event: On, + mut messages: MessageReader, mut grid_positions: Query< &mut GridPosition, Or<(With, With, Without)>, @@ -1213,80 +1212,97 @@ fn movement( mut shape_layouts: Query<&mut ShapeLayout>, inactive: Query<&GridPosition, (Without, Without, With)>, mut commands: Commands, + mut next_state: ResMut>, ) { - if let (Ok(this_shape_layout), Ok(center)) = ( - shape_layouts.get_mut(event.entity), - grid_positions.get(event.entity), - ) { - let new_positions = match event.event().direction { - MovementDirection::Down => vec![center.with_offset(0, -1)], - MovementDirection::Left => vec![center.with_offset(-1, 0)], - MovementDirection::Right => vec![center.with_offset(1, 0)], - MovementDirection::Rotate => vec![Ok(*center)], - MovementDirection::Skip => (1..=center.y) - .map(|i| center.with_offset(0, -(i as isize))) - .collect(), - }; - let new_shape_layout = match event.event().direction { - MovementDirection::Down - | MovementDirection::Left - | MovementDirection::Right - | MovementDirection::Skip => this_shape_layout.clone(), - MovementDirection::Rotate => this_shape_layout.rotated(), - }; - debug!( - "Proposed change: {:?}\n{}", - new_positions, - new_shape_layout.as_ascii() - ); - for position in new_positions { - match position { - Err(OutOfBoundsError::Left) | Err(OutOfBoundsError::Right) => (), // Do nothing - Err(OutOfBoundsError::Down) => { - commands.entity(event.entity).remove::(); - } - Ok(new_center) => { - let new_blocks = new_shape_layout.coordinates_at(&new_center); - for block_gp in new_blocks { - match block_gp { - Err(OutOfBoundsError::Left) | Err(OutOfBoundsError::Right) => { - return; - } // Do nothing - Err(OutOfBoundsError::Down) => { - commands.entity(event.entity).remove::(); - return; - } - Ok(gp) => { - for other_gp in inactive.iter() { - // If there would be a collision between blocks - if gp == *other_gp { - // And we are moving down - if event.event().direction == MovementDirection::Down { - // De-activate this piece - commands.entity(event.entity).remove::(); - } - // Regardless, cancel the move - return; + messages.read().for_each(|message| { + if let (Ok(this_shape_layout), Ok(center)) = ( + shape_layouts.get_mut(message.entity), + grid_positions.get(message.entity), + ) { + let new_positions = match message.direction { + MovementDirection::Down => vec![center.with_offset(0, -1)], + MovementDirection::Left => vec![center.with_offset(-1, 0)], + MovementDirection::Right => vec![center.with_offset(1, 0)], + MovementDirection::Rotate => vec![Ok(*center)], + MovementDirection::Skip => (1..=center.y) + .map(|i| center.with_offset(0, -(i as isize))) + .collect(), + }; + let new_shape_layout = match message.direction { + MovementDirection::Down + | MovementDirection::Left + | MovementDirection::Right + | MovementDirection::Skip => this_shape_layout.clone(), + MovementDirection::Rotate => this_shape_layout.rotated(), + }; + debug!( + "Proposed change: {:?}\n{}", + new_positions, + new_shape_layout.as_ascii() + ); + // For each of the proposed positions + for position in new_positions { + if let Ok(new_center) = position { + let new_blocks: Vec> = new_shape_layout + .coordinates_at(&new_center) + .collect(); + + // If the move would cause one block to be out of bounds, + // Not a valid move so return. + if new_blocks.contains(&Err(OutOfBoundsError::Left)) + || new_blocks.contains(&Err(OutOfBoundsError::Right)) { + return + } + + // If would be out of bounds at the bottom of play + // Deactivate shape as we have hit the floor + if new_blocks.contains(&Err(OutOfBoundsError::Down)) { + commands.entity(message.entity).remove::(); + return; + } + + // If all positions are within bounds, check for collisions + if new_blocks.iter().all(|b| b.is_ok()) { + for block_gp in new_blocks.iter().map(|b| b.unwrap()) { + for other_gp in inactive.iter() { + // If there would be a collision between blocks + if block_gp == *other_gp { + // Check if this pieces is out of bounds at the top + let out_of_bounds_top = block_gp.y >= Y_MAX; + // If so, transition to the game over state + if out_of_bounds_top { + next_state.set(GameState::GameOver); + } + // We are moving down + if [ + MovementDirection::Down, + MovementDirection::Skip, + ].contains(&message.direction) { + // De-activate this piece + commands.entity(message.entity).remove::(); } + // Regardless, cancel the move + return; } } } } + debug!("Checks passed for {position:?}, committing change"); // Update center - let mut gp = grid_positions.get_mut(event.entity).unwrap(); + let mut gp = grid_positions.get_mut(message.entity).unwrap(); *gp = new_center; // Update shape/rotation - let mut sl = shape_layouts.get_mut(event.entity).unwrap(); + let mut sl = shape_layouts.get_mut(message.entity).unwrap(); *sl = new_shape_layout.clone(); } } + } else { + warn!("Oned movement on non-shape entity"); } - } else { - warn!("Oned movement on non-shape entity"); - } + }); } fn swap( @@ -1359,16 +1375,14 @@ fn update_next_shapes(mut buffer: ResMut) { fn assert_grid_position_uniqueness( grid_positions: Query<(Entity, &GridPosition, Option<&LineBlock>, Option<&ShapeBlock>), With>, - mut next: ResMut>, ) { grid_positions.iter_combinations().for_each(|[(e1, a, lb1, sb1), (e2, b, lb2, sb2)]| { if a == b { - error!("Two entities are in the same grid position: {a:?} <-> {b:?}!"); - error!("\tE1: {:?} (Parent Block: {:?}| Parent Line: {:?})!", e1, lb1, sb1); - error!("\tE2: {:?} (Parent Block: {:?}| Parent Line: {:?})!", e2, lb2, sb2); - next.set(GameState::Pause); + error!("Entities {e1:?} and {e2:?} @ GP {a:?}/{b:?}!"); + error!("\t{:?}: (Block: {:?}| Line: {:?})!", e1, sb1, lb1); + error!("\t{:?}: (Block: {:?}| Line: {:?})!", e2, sb2, lb2); } - // assert_ne!(a, b, "Two entities are in the same grid position!"); + assert_ne!(a, b, "Two entities are in the same grid position!"); }); } @@ -1470,19 +1484,6 @@ fn check_level_goal( } } -fn check_level_fail( - query: Query<&GridPosition, (With, Without, Without)>, - mut next: ResMut>, -) { - // Check all blocks that are unassigned - query.iter().for_each(|gp| { - // If any block is above the top line, the game is over - if gp.y >= Y_MAX { - next.set(GameState::GameOver); - } - }); -} - fn toggle_art( state: Res>, // TODO: Correctness: Query for added or changed entities