From a6c96a65884bbb43e1eb2bdf2ea4ce98a144311a Mon Sep 17 00:00:00 2001 From: Elijah Voigt Date: Sat, 25 Oct 2025 22:51:14 -0700 Subject: [PATCH] Fixed line clearing bug --- src/bin/tetris/main.rs | 140 +++++++++++++++++++++++------------------ 1 file changed, 78 insertions(+), 62 deletions(-) diff --git a/src/bin/tetris/main.rs b/src/bin/tetris/main.rs index 4713670..2ba2ed8 100644 --- a/src/bin/tetris/main.rs +++ b/src/bin/tetris/main.rs @@ -1,3 +1,4 @@ +#![feature(try_blocks)] // Bevy basically forces "complex types" with Querys #![allow(clippy::type_complexity)] @@ -27,39 +28,44 @@ fn main() { .add_systems(Startup, (init_world, init_debug_ui, init_ui)) // Input and basic systems .add_systems( - Update, ( + Update, + ( kb_input.run_if(on_event::), toggle_state_visibility::.run_if(state_changed::), - ) + ), ) .add_systems( Update, ( - update_next_shapes.run_if(resource_changed::.or(resource_added::)), - add_piece.run_if(not(any_with_component::)).after(update_next_shapes), + update_next_shapes + .run_if(resource_changed::.or(resource_added::)), + add_piece + .run_if(not(any_with_component::)) + .after(update_next_shapes), update_shape_blocks .run_if(any_component_added::.or(any_component_changed::)), - falling .run_if(in_state(GameState::Falling)) .run_if(clock_cycle(1.0)), update_position.run_if(any_component_changed::), - deactivate_shape.run_if(any_component_removed::), - + check_line_removal, // Clearing lines systems clear_line.run_if(any_component_changed::), - adjust_block_lines.run_if(any_component_changed::).after(clear_line), + adjust_block_lines + .run_if(any_component_changed::) + .after(clear_line), ), ) // UI systems - .add_systems(Update, + .add_systems( + Update, ( sync_resource_to_ui::.run_if(resource_changed::), sync_resource_to_ui::.run_if(resource_changed::), sync_singleton_to_ui::.run_if(any_component_changed::), sync_singleton_to_ui::.run_if(any_component_changed::), - ) + ), ) .add_systems(Update, draw_grid) .run(); @@ -304,32 +310,26 @@ fn init_ui(mut commands: Commands) { BackgroundColor(BLACK.into()), )) .with_children(|parent| { - parent.spawn(( - Node { + parent + .spawn((Node { flex_direction: FlexDirection::Column, align_items: AlignItems::Center, ..default() - }, - )).with_children(|parent|{ - parent.spawn(Text::new("Next:")); - parent.spawn(( - Text::new("???"), - SyncResource::::default(), - )); - }); - parent.spawn(( - Node { + },)) + .with_children(|parent| { + parent.spawn(Text::new("Next:")); + parent.spawn((Text::new("???"), SyncResource::::default())); + }); + parent + .spawn((Node { flex_direction: FlexDirection::Column, align_items: AlignItems::Center, ..default() - }, - )).with_children(|parent|{ - parent.spawn(Text::new("Score:")); - parent.spawn(( - Text::new("???"), - SyncResource::::default(), - )); - }); + },)) + .with_children(|parent| { + parent.spawn(Text::new("Score:")); + parent.spawn((Text::new("???"), SyncResource::::default())); + }); }); commands @@ -563,7 +563,7 @@ fn update_shape_blocks( visuals: Res, ) { query.iter().for_each(|(e, s, o, center)| { - info!("Setting piece: {e:?} {o:?} {center:?}\n{}", s.as_ascii()); + debug!("Setting piece: {e:?} {o:?} {center:?}\n{}", s.as_ascii()); if blocks.is_empty() { let mesh = Mesh2d(visuals.mesh.clone()); @@ -678,16 +678,17 @@ fn add_piece(mut commands: Commands, mut shapes: ResMut) { /// When a line reaches 10 blocks, clear it fn clear_line( changed_lines: Query>, - mut lines: Query<(Entity, &LineBlocks, &mut Line)>, + mut lines: Query<&mut Line>, + line_blocks: Query<&LineBlocks>, mut score: ResMut, mut commands: Commands, ) { - let cleared_lines: Vec = changed_lines + let mut cleared_lines: Vec = changed_lines .iter() - .filter_map(|e| lines.get(e).ok()) + .filter_map(|e| try { (Some(e)?, line_blocks.get(e).ok()?, lines.get(e).ok()?) } ) .filter_map(|(e, lb, Line(i))| { if lb.0.len() == 10 { - commands.entity(e).despawn_related::(); + commands.entity(e).despawn_related::().insert(LineBlocks::default()); score.0 += 1; info!("New score: {:?}", score.0); Some(*i) @@ -695,24 +696,42 @@ fn clear_line( None } }) + .sorted() .collect(); - info!("Cleared lines: {:?}", cleared_lines); + if !cleared_lines.is_empty() { + info!("Cleared lines: {:?}", cleared_lines); - for (idx, cleared_line_number) in cleared_lines.into_iter().sorted().enumerate() { - info!("Processing line {cleared_line_number} ({idx})"); - let cleared_line_number = cleared_line_number - idx; - lines.iter_mut().sorted_by(|(_, _, i), (_, _, j)| i.0.cmp(&j.0)).for_each(|(_, _, mut l)| { - let dest = if l.0 > cleared_line_number { - l.0 - 1 - } else if l.0 == cleared_line_number { - Y_MAX - 1 - } else { - l.0 - }; - info!("Moving line {:?} to {:?}", l, dest); - l.0 = dest; - }); + #[cfg(debug_assertions)] + { + debug_assert_eq!(lines.iter().count(), 20, "There should be 20 lines"); + // Check that all line numbers are present + lines.iter().map(|Line(i)| i).sorted().enumerate().for_each(|(i, line_num)| { + debug_assert_eq!(i, *line_num, "Line numbers should match their sorted index"); + }); + } + + let original_cleared_lines_len = cleared_lines.len(); + + // Iterate over all lines in reverse sorted order (largest to smallest) + lines + .iter_mut() + .sorted_by(|i, j| i.0.cmp(&j.0)) + .rev() + .for_each(|mut l| { + // If the current index is in the set of cleared lines, move it to the top + // Otherwise, move it down by the number of cleared lines + if cleared_lines.contains(&l.0) { + // Move to the N-offset line number (top, top-1, etc) + let offset = original_cleared_lines_len - cleared_lines.len(); + info!("Moving line {:?}->{:?}", l.0, Y_MAX - 1 - offset); + l.0 = Y_MAX - 1 - offset; + cleared_lines.pop(); + } else { + info!("Moving line {:?}->{:?}", l.0, l.0 - cleared_lines.len()); + l.0 -= cleared_lines.len(); + } + }); } } @@ -721,17 +740,6 @@ fn adjust_block_lines( parent: Query<&LineBlocks>, mut blocks: Query<&mut GridPosition>, ) { - #[cfg(debug_assertions)] - { - // Check that all line numbers are present - let expected_line_numbers = 0..Y_MAX; - let actual_line_numbers = query.iter().map(|(_, Line(i))| i).sorted(); - query.iter().map(|(_, Line(i))| i).sorted().for_each(|i| info!("Line #: {i}")); - std::iter::zip(expected_line_numbers, actual_line_numbers).for_each(|(a, b)| { - debug_assert_eq!(a as usize, *b); - }); - } - query.iter().for_each(|(e, Line(i))| { parent.iter_descendants(e).for_each(|block| { if let Ok(mut gp) = blocks.get_mut(block) { @@ -767,7 +775,7 @@ fn movement( Movement::Right => (center.with_offset(1, 0), *this_shape), Movement::Rotate => (Ok(*center), this_shape.rotated()), }; - info!( + debug!( "Proposed change: {:?}\n{}", new_center, new_shape.as_ascii() @@ -819,6 +827,14 @@ fn movement( } } +fn check_line_removal( + mut events: RemovedComponents, +) { + events.read().for_each(|e| { + info!("Line entity {:?} removed", e); + }); +} + // TODO: Just despawn? fn deactivate_shape( mut events: RemovedComponents,