Compare commits

...

4 Commits

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

@ -1,10 +1,18 @@
#![feature(try_blocks)]
// Bevy basically forces "complex types" with Querys
#![allow(clippy::type_complexity)]
use itertools::Itertools;
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() {
App::new()
@ -14,35 +22,59 @@ fn main() {
game_type: GameType::Two,
..default()
})
.init_state::<Falling>()
.add_systems(Startup, (init_world, init_debug_ui))
.init_state::<GameState>()
.init_resource::<ShapesBuffer>()
.init_resource::<Score>()
.add_systems(Startup, (init_world, init_debug_ui, init_ui))
// Input and basic systems
.add_systems(
Update,
(
kb_input.run_if(on_event::<KeyboardInput>),
falling
.run_if(in_state(Falling::On))
.run_if(clock_cycle(1.0)),
toggle_state_visibility::<GameState>.run_if(state_changed::<GameState>),
),
)
.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
.run_if(any_component_added::<Shape>.or(any_component_changed::<Shape>)),
sync_singleton_to_ui::<Shape>.run_if(any_component_changed::<Shape>),
sync_singleton_to_ui::<Orientation>.run_if(any_component_changed::<Orientation>),
update_position,
add_piece.run_if(not(any_with_component::<Shape>)),
falling
.run_if(in_state(GameState::Falling))
.run_if(clock_cycle(1.0)),
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>),
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_observer(deactive_shape)
.run();
}
const SCALE: f32 = 30.0;
// Declare the size of the play area
const X_MAX: u32 = 10;
const Y_MAX: u32 = 20;
const X_MAX: usize = 10;
const Y_MAX: usize = 20;
// The blocks making up this shape
#[derive(Component)]
@ -82,8 +114,8 @@ struct Block;
#[derive(Component, Debug, Clone, Copy, PartialEq)]
#[require(Transform, Visibility)]
struct GridPosition {
x: u32,
y: u32,
x: usize,
y: usize,
}
impl GridPosition {
@ -99,8 +131,8 @@ impl GridPosition {
Err(GameError::OutOfBoundsDown)
} else {
Ok(GridPosition {
x: x as u32,
y: y as u32,
x: x as usize,
y: y as usize,
})
}
}
@ -144,8 +176,8 @@ impl From<&GridPosition> for Vec3 {
}
}
impl From<(u32, u32)> for GridPosition {
fn from((x, y): (u32, u32)) -> GridPosition {
impl From<(usize, usize)> for GridPosition {
fn from((x, y): (usize, usize)) -> GridPosition {
GridPosition { x, y }
}
}
@ -167,57 +199,43 @@ impl std::ops::AddAssign<&GridPosition> for GridPosition {
}
}
#[derive(Component, Default, Event, Clone, Debug)]
enum Orientation {
#[derive(States, Clone, Eq, PartialEq, Debug, Hash, Default, Component)]
enum GameState {
#[default]
Up,
Left,
Down,
Right,
Falling,
Pause,
}
impl Orientation {
fn next(&self) -> Self {
match self {
Self::Up => Self::Left,
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,
}
}
#[derive(Resource, Debug)]
struct Visuals {
material: Handle<ColorMaterial>,
mesh: Handle<Mesh>,
}
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 {
match self {
Orientation::Up => write!(f, "up"),
Orientation::Down => write!(f, "down"),
Orientation::Left => write!(f, "<-"),
Orientation::Right => write!(f, "->"),
}
write!(f, "{}", self.0)
}
}
#[derive(States, Clone, Eq, PartialEq, Debug, Hash, Default, Component)]
enum Falling {
#[default]
On,
Off,
/// ShapesBuffer resource stores non-active shapes
#[derive(Resource, Debug, Default)]
struct ShapesBuffer {
/// Next stores a vector of 2N shapes that will come up in play
next: VecDeque<Shape>,
}
#[derive(Resource, Debug)]
struct Visuals {
material: Handle<ColorMaterial>,
mesh: Handle<Mesh>,
impl Display for ShapesBuffer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(shape) = self.next.front() {
write!(f, "{shape}")
} else {
write!(f, "ERR")
}
}
}
fn init_world(
@ -233,11 +251,60 @@ fn init_world(
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()));
});
}
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) {
commands
.spawn((
@ -253,10 +320,6 @@ fn init_debug_ui(mut commands: Commands) {
Node::default(),
children![
(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(
&self,
center: &GridPosition,
@ -449,13 +508,13 @@ fn update_position(
// TODO: Inline this to when movement occurs
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 commands: Commands,
visuals: Res<Visuals>,
) {
query.iter().for_each(|(e, s, o, center)| {
info!("Setting piece: {e:?} {o:?} {center:?}\n{}", s.as_ascii());
query.iter().for_each(|(e, s, center)| {
debug!("Setting piece: {e:?} {center:?}\n{}", s.as_ascii());
if blocks.is_empty() {
let mesh = Mesh2d(visuals.mesh.clone());
@ -480,9 +539,9 @@ fn update_shape_blocks(
fn kb_input(
mut events: EventReader<KeyboardInput>,
mut query: Query<(Entity, &Orientation, &mut Shape)>,
curr: Res<State<Falling>>,
mut next: ResMut<NextState<Falling>>,
mut query: Query<(Entity, &mut Shape)>,
curr: Res<State<GameState>>,
mut next: ResMut<NextState<GameState>>,
mut commands: Commands,
) {
events.read().for_each(
@ -490,7 +549,7 @@ fn kb_input(
key_code, 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 {
// Up arrow should rotate if in falling mode
// Only move up if in falling::off mode
@ -506,9 +565,9 @@ fn kb_input(
KeyCode::ArrowRight => {
commands.entity(e).trigger(Movement::Right);
}
KeyCode::Space => next.set(match curr.get() {
Falling::On => Falling::Off,
Falling::Off => Falling::On,
KeyCode::Escape => next.set(match curr.get() {
GameState::Falling => GameState::Pause,
GameState::Pause => GameState::Falling,
}),
KeyCode::Digit1 => *s = Shape::new_t(),
KeyCode::Digit2 => *s = Shape::new_o(),
@ -529,7 +588,7 @@ fn draw_grid(mut gizmos: Gizmos) {
gizmos
.grid_2d(
Isometry2d::IDENTITY,
UVec2::new(X_MAX, Y_MAX),
UVec2::new(X_MAX as u32, Y_MAX as u32),
Vec2::new(SCALE, SCALE),
GREEN,
)
@ -557,13 +616,11 @@ fn clock_cycle(n: f32) -> impl FnMut(Res<Time>, Local<f32>) -> bool {
}
}
fn add_piece(mut commands: Commands) {
// TODO: Choose a different piece
fn add_piece(mut commands: Commands, mut shapes: ResMut<ShapesBuffer>) {
commands
.spawn((
Orientation::default(),
GridPosition::default(),
Shape::default(),
shapes.next.pop_front().unwrap(),
))
.observe(movement);
}
@ -571,38 +628,60 @@ fn add_piece(mut commands: Commands) {
/// When a line reaches 10 blocks, clear it
fn clear_line(
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,
) {
let cleared_lines: Vec<usize> = changed_lines
let mut cleared_lines: Vec<usize> = 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::<LineBlocks>();
commands.entity(e).despawn_related::<LineBlocks>().insert(LineBlocks::default());
score.0 += 1;
info!("New score: {:?}", score.0);
Some(*i)
} else {
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().for_each(|(_, _, mut l)| {
let dest = if l.0 > cleared_line_number {
l.0 - 1
} else if l.0 == cleared_line_number {
(Y_MAX - (idx + 1) as u32) as usize
} 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();
}
});
}
}
@ -614,7 +693,7 @@ fn adjust_block_lines(
query.iter().for_each(|(e, Line(i))| {
parent.iter_descendants(e).for_each(|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::Rotate => (Ok(*center), this_shape.rotated()),
};
info!(
debug!(
"Proposed change: {:?}\n{}",
new_center,
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?
fn deactive_shape(
trigger: Trigger<OnRemove, Shape>,
fn deactivate_shape(
mut events: RemovedComponents<Shape>,
grid_positions: Query<&GridPosition>,
parent: Query<&ShapeBlocks>,
lines: Query<(Entity, &Line), With<LineBlocks>>,
mut commands: Commands,
) {
parent.iter_descendants(trigger.target()).for_each(|block| {
let GridPosition { y, .. } = grid_positions.get(block).unwrap();
let parent_line = lines
.iter()
.find_map(|(e, Line(i))| (*y == *i as u32).then_some(e))
.unwrap();
commands
.entity(parent_line)
.add_one_related::<LineBlock>(block);
events.read().for_each(|target| {
parent.iter_descendants(target).for_each(|block| {
let GridPosition { y, .. } = grid_positions.get(block).unwrap();
let parent_line = lines
.iter()
.find_map(|(e, Line(i))| (*y == *i).then_some(e))
.unwrap(); // TODO: This crashed once kinda late in a game... why?
commands
.entity(parent_line)
.add_one_related::<LineBlock>(block);
});
commands.entity(target).despawn();
});
commands.entity(trigger.target()).despawn();
}
#[cfg(test)]
mod test {
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.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);
fn update_next_shapes(mut buffer: ResMut<ShapesBuffer>) {
// If the buffer contains less than n+1 shapes (where n is the number of possible shapes)
// Ideally we have between 1n and 2n shapes in the `next` buffer
while buffer.next.len() < 8 {
// TODO: Shuffle these!
buffer.next.extend([
Shape::new_o(),
Shape::new_t(),
Shape::new_l(),
Shape::new_j(),
Shape::new_s(),
Shape::new_z(),
Shape::new_i(),
]);
}
}

@ -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