Compare commits

...

4 Commits

Author SHA1 Message Date
Elijah Voigt 21ad8763e4 Remove the orientation enum 1 day ago
Elijah Voigt a6c96a6588 Fixed line clearing bug 1 day ago
Elijah Voigt 728e36171b Adding score and next piece preview 1 day ago
Elijah Voigt c3335e9263 Move tests to separate file, escape -> pause 2 days ago

@ -1,10 +1,18 @@
#![feature(try_blocks)]
// Bevy basically forces "complex types" with Querys // Bevy basically forces "complex types" with Querys
#![allow(clippy::type_complexity)] #![allow(clippy::type_complexity)]
use itertools::Itertools;
use games::*; use games::*;
use itertools::Itertools;
#[cfg(test)]
mod test;
// TODO: When line is "full" (has 10 children) clear line and add to score // TODO: Space key: skip to end
// TODO: When piece is near wall and rotates, move it over if it fits
// TODO: Make falling based on a timer resource ticking
// This allows us to tune the falling rate over time
// TODO: Preview next batch of pieces that will drop
fn main() { fn main() {
App::new() App::new()
@ -14,35 +22,59 @@ fn main() {
game_type: GameType::Two, game_type: GameType::Two,
..default() ..default()
}) })
.init_state::<Falling>() .init_state::<GameState>()
.add_systems(Startup, (init_world, init_debug_ui)) .init_resource::<ShapesBuffer>()
.init_resource::<Score>()
.add_systems(Startup, (init_world, init_debug_ui, init_ui))
// Input and basic systems
.add_systems( .add_systems(
Update, Update,
( (
kb_input.run_if(on_event::<KeyboardInput>), kb_input.run_if(on_event::<KeyboardInput>),
falling toggle_state_visibility::<GameState>.run_if(state_changed::<GameState>),
.run_if(in_state(Falling::On)) ),
.run_if(clock_cycle(1.0)), )
.add_systems(
Update,
(
update_next_shapes
.run_if(resource_changed::<ShapesBuffer>.or(resource_added::<ShapesBuffer>)),
add_piece
.run_if(not(any_with_component::<Shape>))
.after(update_next_shapes),
update_shape_blocks update_shape_blocks
.run_if(any_component_added::<Shape>.or(any_component_changed::<Shape>)), .run_if(any_component_added::<Shape>.or(any_component_changed::<Shape>)),
sync_singleton_to_ui::<Shape>.run_if(any_component_changed::<Shape>), falling
sync_singleton_to_ui::<Orientation>.run_if(any_component_changed::<Orientation>), .run_if(in_state(GameState::Falling))
update_position, .run_if(clock_cycle(1.0)),
add_piece.run_if(not(any_with_component::<Shape>)), update_position.run_if(any_component_changed::<GridPosition>),
deactivate_shape.run_if(any_component_removed::<Shape>),
check_line_removal,
// Clearing lines systems
clear_line.run_if(any_component_changed::<LineBlocks>), clear_line.run_if(any_component_changed::<LineBlocks>),
adjust_block_lines.run_if(any_component_changed::<Line>), adjust_block_lines
.run_if(any_component_changed::<Line>)
.after(clear_line),
),
)
// UI systems
.add_systems(
Update,
(
sync_resource_to_ui::<ShapesBuffer>.run_if(resource_changed::<ShapesBuffer>),
sync_resource_to_ui::<Score>.run_if(resource_changed::<Score>),
sync_singleton_to_ui::<Shape>.run_if(any_component_changed::<Shape>),
), ),
) )
.add_systems(Update, draw_grid) .add_systems(Update, draw_grid)
.add_observer(deactive_shape)
.run(); .run();
} }
const SCALE: f32 = 30.0; const SCALE: f32 = 30.0;
// Declare the size of the play area // Declare the size of the play area
const X_MAX: u32 = 10; const X_MAX: usize = 10;
const Y_MAX: u32 = 20; const Y_MAX: usize = 20;
// The blocks making up this shape // The blocks making up this shape
#[derive(Component)] #[derive(Component)]
@ -82,8 +114,8 @@ struct Block;
#[derive(Component, Debug, Clone, Copy, PartialEq)] #[derive(Component, Debug, Clone, Copy, PartialEq)]
#[require(Transform, Visibility)] #[require(Transform, Visibility)]
struct GridPosition { struct GridPosition {
x: u32, x: usize,
y: u32, y: usize,
} }
impl GridPosition { impl GridPosition {
@ -99,8 +131,8 @@ impl GridPosition {
Err(GameError::OutOfBoundsDown) Err(GameError::OutOfBoundsDown)
} else { } else {
Ok(GridPosition { Ok(GridPosition {
x: x as u32, x: x as usize,
y: y as u32, y: y as usize,
}) })
} }
} }
@ -144,8 +176,8 @@ impl From<&GridPosition> for Vec3 {
} }
} }
impl From<(u32, u32)> for GridPosition { impl From<(usize, usize)> for GridPosition {
fn from((x, y): (u32, u32)) -> GridPosition { fn from((x, y): (usize, usize)) -> GridPosition {
GridPosition { x, y } GridPosition { x, y }
} }
} }
@ -167,57 +199,43 @@ impl std::ops::AddAssign<&GridPosition> for GridPosition {
} }
} }
#[derive(Component, Default, Event, Clone, Debug)] #[derive(States, Clone, Eq, PartialEq, Debug, Hash, Default, Component)]
enum Orientation { enum GameState {
#[default] #[default]
Up, Falling,
Left, Pause,
Down,
Right,
} }
impl Orientation { #[derive(Resource, Debug)]
fn next(&self) -> Self { struct Visuals {
match self { material: Handle<ColorMaterial>,
Self::Up => Self::Left, mesh: Handle<Mesh>,
Self::Left => Self::Down,
Self::Down => Self::Right,
Self::Right => Self::Up,
}
}
fn prev(&self) -> Self {
match self {
Self::Up => Self::Right,
Self::Right => Self::Down,
Self::Down => Self::Left,
Self::Left => Self::Up,
}
}
} }
impl Display for Orientation { #[derive(Resource, Debug, Default)]
struct Score(usize);
impl Display for Score {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { write!(f, "{}", self.0)
Orientation::Up => write!(f, "up"),
Orientation::Down => write!(f, "down"),
Orientation::Left => write!(f, "<-"),
Orientation::Right => write!(f, "->"),
}
} }
} }
#[derive(States, Clone, Eq, PartialEq, Debug, Hash, Default, Component)] /// ShapesBuffer resource stores non-active shapes
enum Falling { #[derive(Resource, Debug, Default)]
#[default] struct ShapesBuffer {
On, /// Next stores a vector of 2N shapes that will come up in play
Off, next: VecDeque<Shape>,
} }
#[derive(Resource, Debug)] impl Display for ShapesBuffer {
struct Visuals { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
material: Handle<ColorMaterial>, if let Some(shape) = self.next.front() {
mesh: Handle<Mesh>, write!(f, "{shape}")
} else {
write!(f, "ERR")
}
}
} }
fn init_world( fn init_world(
@ -233,11 +251,60 @@ fn init_world(
mesh: meshes.add(Rectangle::new(SCALE, SCALE)), mesh: meshes.add(Rectangle::new(SCALE, SCALE)),
}); });
(0..20).for_each(|i| { (0..Y_MAX).for_each(|i| {
info!("Spawning line {i}");
commands.spawn((Line(i), LineBlocks::default())); commands.spawn((Line(i), LineBlocks::default()));
}); });
} }
fn init_ui(mut commands: Commands) {
commands
.spawn((
Node {
align_self: AlignSelf::Center,
justify_self: JustifySelf::End,
flex_direction: FlexDirection::Column,
..default()
},
BackgroundColor(BLACK.into()),
))
.with_children(|parent| {
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::<ShapesBuffer>::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::<Score>::default()));
});
});
commands
.spawn((
Node {
align_self: AlignSelf::Center,
justify_self: JustifySelf::Center,
..default()
},
GameState::Pause,
))
.with_children(|parent| {
parent.spawn(Text::new("Paused"));
});
}
fn init_debug_ui(mut commands: Commands) { fn init_debug_ui(mut commands: Commands) {
commands commands
.spawn(( .spawn((
@ -253,10 +320,6 @@ fn init_debug_ui(mut commands: Commands) {
Node::default(), Node::default(),
children![ children![
(Text::new("SHAPE"), SyncSingleton::<Shape>::default()), (Text::new("SHAPE"), SyncSingleton::<Shape>::default()),
(
Text::new("ORIENTATION"),
SyncSingleton::<Orientation>::default()
),
], ],
)); ));
}); });
@ -374,10 +437,6 @@ impl Shape {
} }
} }
fn rotate(&mut self) {
*self = self.rotated();
}
fn coordinates( fn coordinates(
&self, &self,
center: &GridPosition, center: &GridPosition,
@ -449,13 +508,13 @@ fn update_position(
// TODO: Inline this to when movement occurs // TODO: Inline this to when movement occurs
fn update_shape_blocks( fn update_shape_blocks(
query: Query<(Entity, &Shape, &Orientation, &GridPosition), Or<(Added<Shape>, Changed<Shape>)>>, query: Query<(Entity, &Shape, &GridPosition), Or<(Added<Shape>, Changed<Shape>)>>,
mut blocks: Query<&mut GridPosition, (With<ShapeBlock>, Without<Shape>)>, mut blocks: Query<&mut GridPosition, (With<ShapeBlock>, Without<Shape>)>,
mut commands: Commands, mut commands: Commands,
visuals: Res<Visuals>, visuals: Res<Visuals>,
) { ) {
query.iter().for_each(|(e, s, o, center)| { query.iter().for_each(|(e, s, center)| {
info!("Setting piece: {e:?} {o:?} {center:?}\n{}", s.as_ascii()); debug!("Setting piece: {e:?} {center:?}\n{}", s.as_ascii());
if blocks.is_empty() { if blocks.is_empty() {
let mesh = Mesh2d(visuals.mesh.clone()); let mesh = Mesh2d(visuals.mesh.clone());
@ -480,9 +539,9 @@ fn update_shape_blocks(
fn kb_input( fn kb_input(
mut events: EventReader<KeyboardInput>, mut events: EventReader<KeyboardInput>,
mut query: Query<(Entity, &Orientation, &mut Shape)>, mut query: Query<(Entity, &mut Shape)>,
curr: Res<State<Falling>>, curr: Res<State<GameState>>,
mut next: ResMut<NextState<Falling>>, mut next: ResMut<NextState<GameState>>,
mut commands: Commands, mut commands: Commands,
) { ) {
events.read().for_each( events.read().for_each(
@ -490,7 +549,7 @@ fn kb_input(
key_code, state, .. key_code, state, ..
}| { }| {
if let ButtonState::Pressed = state { if let ButtonState::Pressed = state {
query.iter_mut().for_each(|(e, o, mut s)| { query.iter_mut().for_each(|(e, mut s)| {
match key_code { match key_code {
// Up arrow should rotate if in falling mode // Up arrow should rotate if in falling mode
// Only move up if in falling::off mode // Only move up if in falling::off mode
@ -506,9 +565,9 @@ fn kb_input(
KeyCode::ArrowRight => { KeyCode::ArrowRight => {
commands.entity(e).trigger(Movement::Right); commands.entity(e).trigger(Movement::Right);
} }
KeyCode::Space => next.set(match curr.get() { KeyCode::Escape => next.set(match curr.get() {
Falling::On => Falling::Off, GameState::Falling => GameState::Pause,
Falling::Off => Falling::On, GameState::Pause => GameState::Falling,
}), }),
KeyCode::Digit1 => *s = Shape::new_t(), KeyCode::Digit1 => *s = Shape::new_t(),
KeyCode::Digit2 => *s = Shape::new_o(), KeyCode::Digit2 => *s = Shape::new_o(),
@ -529,7 +588,7 @@ fn draw_grid(mut gizmos: Gizmos) {
gizmos gizmos
.grid_2d( .grid_2d(
Isometry2d::IDENTITY, Isometry2d::IDENTITY,
UVec2::new(X_MAX, Y_MAX), UVec2::new(X_MAX as u32, Y_MAX as u32),
Vec2::new(SCALE, SCALE), Vec2::new(SCALE, SCALE),
GREEN, GREEN,
) )
@ -557,13 +616,11 @@ fn clock_cycle(n: f32) -> impl FnMut(Res<Time>, Local<f32>) -> bool {
} }
} }
fn add_piece(mut commands: Commands) { fn add_piece(mut commands: Commands, mut shapes: ResMut<ShapesBuffer>) {
// TODO: Choose a different piece
commands commands
.spawn(( .spawn((
Orientation::default(),
GridPosition::default(), GridPosition::default(),
Shape::default(), shapes.next.pop_front().unwrap(),
)) ))
.observe(movement); .observe(movement);
} }
@ -571,38 +628,60 @@ fn add_piece(mut commands: Commands) {
/// When a line reaches 10 blocks, clear it /// When a line reaches 10 blocks, clear it
fn clear_line( fn clear_line(
changed_lines: Query<Entity, Changed<LineBlocks>>, changed_lines: Query<Entity, Changed<LineBlocks>>,
mut lines: Query<(Entity, &LineBlocks, &mut Line)>, mut lines: Query<&mut Line>,
line_blocks: Query<&LineBlocks>,
mut score: ResMut<Score>,
mut commands: Commands, mut commands: Commands,
) { ) {
let cleared_lines: Vec<usize> = changed_lines let mut cleared_lines: Vec<usize> = changed_lines
.iter() .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))| { .filter_map(|(e, lb, Line(i))| {
if lb.0.len() == 10 { if lb.0.len() == 10 {
commands.entity(e).despawn_related::<LineBlocks>(); commands.entity(e).despawn_related::<LineBlocks>().insert(LineBlocks::default());
score.0 += 1;
info!("New score: {:?}", score.0);
Some(*i) Some(*i)
} else { } else {
None None
} }
}) })
.sorted()
.collect(); .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() { #[cfg(debug_assertions)]
info!("Processing line {cleared_line_number} ({idx})"); {
let cleared_line_number = cleared_line_number - idx; debug_assert_eq!(lines.iter().count(), 20, "There should be 20 lines");
lines.iter_mut().for_each(|(_, _, mut l)| { // Check that all line numbers are present
let dest = if l.0 > cleared_line_number { lines.iter().map(|Line(i)| i).sorted().enumerate().for_each(|(i, line_num)| {
l.0 - 1 debug_assert_eq!(i, *line_num, "Line numbers should match their sorted index");
} else if l.0 == cleared_line_number { });
(Y_MAX - (idx + 1) as u32) as usize }
} else {
l.0 let original_cleared_lines_len = cleared_lines.len();
};
info!("Moving line {:?} to {:?}", l, dest); // Iterate over all lines in reverse sorted order (largest to smallest)
l.0 = dest; 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();
}
});
} }
} }
@ -614,7 +693,7 @@ fn adjust_block_lines(
query.iter().for_each(|(e, Line(i))| { query.iter().for_each(|(e, Line(i))| {
parent.iter_descendants(e).for_each(|block| { parent.iter_descendants(e).for_each(|block| {
if let Ok(mut gp) = blocks.get_mut(block) { if let Ok(mut gp) = blocks.get_mut(block) {
gp.y = *i as u32; gp.y = *i;
} }
}); });
}); });
@ -646,7 +725,7 @@ fn movement(
Movement::Right => (center.with_offset(1, 0), *this_shape), Movement::Right => (center.with_offset(1, 0), *this_shape),
Movement::Rotate => (Ok(*center), this_shape.rotated()), Movement::Rotate => (Ok(*center), this_shape.rotated()),
}; };
info!( debug!(
"Proposed change: {:?}\n{}", "Proposed change: {:?}\n{}",
new_center, new_center,
new_shape.as_ascii() new_shape.as_ascii()
@ -698,112 +777,50 @@ fn movement(
} }
} }
fn check_line_removal(
mut events: RemovedComponents<Line>,
) {
events.read().for_each(|e| {
info!("Line entity {:?} removed", e);
});
}
// TODO: Just despawn? // TODO: Just despawn?
fn deactive_shape( fn deactivate_shape(
trigger: Trigger<OnRemove, Shape>, mut events: RemovedComponents<Shape>,
grid_positions: Query<&GridPosition>, grid_positions: Query<&GridPosition>,
parent: Query<&ShapeBlocks>, parent: Query<&ShapeBlocks>,
lines: Query<(Entity, &Line), With<LineBlocks>>, lines: Query<(Entity, &Line), With<LineBlocks>>,
mut commands: Commands, mut commands: Commands,
) { ) {
parent.iter_descendants(trigger.target()).for_each(|block| { events.read().for_each(|target| {
let GridPosition { y, .. } = grid_positions.get(block).unwrap(); parent.iter_descendants(target).for_each(|block| {
let parent_line = lines let GridPosition { y, .. } = grid_positions.get(block).unwrap();
.iter() let parent_line = lines
.find_map(|(e, Line(i))| (*y == *i as u32).then_some(e)) .iter()
.unwrap(); .find_map(|(e, Line(i))| (*y == *i).then_some(e))
commands .unwrap(); // TODO: This crashed once kinda late in a game... why?
.entity(parent_line) commands
.add_one_related::<LineBlock>(block); .entity(parent_line)
.add_one_related::<LineBlock>(block);
});
commands.entity(target).despawn();
}); });
commands.entity(trigger.target()).despawn();
} }
#[cfg(test)] fn update_next_shapes(mut buffer: ResMut<ShapesBuffer>) {
mod test { // If the buffer contains less than n+1 shapes (where n is the number of possible shapes)
use super::*; // Ideally we have between 1n and 2n shapes in the `next` buffer
while buffer.next.len() < 8 {
#[test] // TODO: Shuffle these!
fn test_shape_t() { buffer.next.extend([
let mut shape = Shape::new_t(); Shape::new_o(),
Shape::new_t(),
let expected_up = "010\n\ Shape::new_l(),
111\n\ Shape::new_j(),
000\n"; Shape::new_s(),
Shape::new_z(),
let expected_right = "010\n\ Shape::new_i(),
011\n\ ]);
010\n";
let expected_down = "000\n\
111\n\
010\n";
let expected_left = "010\n\
110\n\
010\n";
assert_eq!(shape.as_ascii(), expected_up);
shape.rotate();
assert_eq!(shape.as_ascii(), expected_right);
shape.rotate();
assert_eq!(shape.as_ascii(), expected_down);
shape.rotate();
assert_eq!(shape.as_ascii(), expected_left);
shape.rotate();
assert_eq!(shape.as_ascii(), expected_up);
}
#[test]
fn test_shape_i() {
let mut shape = Shape::new_i();
let expected_up = "0010\n\
0010\n\
0010\n\
0010\n";
let expected_right = "0000\n\
0000\n\
1111\n\
0000\n";
let expected_down = "0100\n\
0100\n\
0100\n\
0100\n";
let expected_left = "0000\n\
1111\n\
0000\n\
0000\n";
assert_eq!(shape.as_ascii(), expected_up);
shape.rotate();
assert_eq!(shape.as_ascii(), expected_right);
shape.rotate();
assert_eq!(shape.as_ascii(), expected_down);
shape.rotate();
assert_eq!(shape.as_ascii(), expected_left);
shape.rotate();
assert_eq!(shape.as_ascii(), expected_up);
}
#[test]
fn test_coordinates() {
let shape = Shape::new_t();
let center = GridPosition { x: 5, y: 5 };
let expected: Vec<Result<GridPosition, GameError>> = vec![
Ok((5, 6).into()),
Ok((4, 5).into()),
Ok((5, 5).into()),
Ok((6, 5).into()),
];
let actual: Vec<Result<GridPosition, GameError>> = shape.coordinates(&center).collect();
assert_eq!(actual, expected);
} }
} }

@ -0,0 +1,85 @@
use super::*;
#[test]
fn test_shape_t() {
let mut shape = Shape::new_t();
let expected_up = "010\n\
111\n\
000\n";
let expected_right = "010\n\
011\n\
010\n";
let expected_down = "000\n\
111\n\
010\n";
let expected_left = "010\n\
110\n\
010\n";
assert_eq!(shape.as_ascii(), expected_up);
shape = shape.rotated();
assert_eq!(shape.as_ascii(), expected_right);
shape = shape.rotated();
assert_eq!(shape.as_ascii(), expected_down);
shape = shape.rotated();
assert_eq!(shape.as_ascii(), expected_left);
shape = shape.rotated();
assert_eq!(shape.as_ascii(), expected_up);
}
#[test]
fn test_shape_i() {
let mut shape = Shape::new_i();
let expected_up = "0010\n\
0010\n\
0010\n\
0010\n";
let expected_right = "0000\n\
0000\n\
1111\n\
0000\n";
let expected_down = "0100\n\
0100\n\
0100\n\
0100\n";
let expected_left = "0000\n\
1111\n\
0000\n\
0000\n";
assert_eq!(shape.as_ascii(), expected_up);
shape = shape.rotated();
assert_eq!(shape.as_ascii(), expected_right);
shape = shape.rotated();
assert_eq!(shape.as_ascii(), expected_down);
shape = shape.rotated();
assert_eq!(shape.as_ascii(), expected_left);
shape = shape.rotated();
assert_eq!(shape.as_ascii(), expected_up);
}
#[test]
fn test_coordinates() {
let shape = Shape::new_t();
let center = GridPosition { x: 5, y: 5 };
let expected: Vec<Result<GridPosition, GameError>> = vec![
Ok((5, 6).into()),
Ok((4, 5).into()),
Ok((5, 5).into()),
Ok((6, 5).into()),
];
let actual: Vec<Result<GridPosition, GameError>> = shape.coordinates(&center).collect();
assert_eq!(actual, expected);
}
Loading…
Cancel
Save