You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

861 lines
26 KiB
Rust

// Bevy basically forces "complex types" with Querys
#![allow(clippy::type_complexity)]
use games::*;
use itertools::Itertools;
#[cfg(test)]
mod test;
// 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()
.add_plugins(BaseGamePlugin {
name: "falling-block-adventure".into(),
target_resolution: (640.0, 480.0).into(),
game_type: GameType::Two,
..default()
})
.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>),
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>)),
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>),
// Clearing lines systems
clear_line.run_if(any_component_changed::<LineBlocks>),
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>),
sync_singleton_to_ui::<Orientation>.run_if(any_component_changed::<Orientation>),
)
)
.add_systems(Update, draw_grid)
.run();
}
const SCALE: f32 = 30.0;
// Declare the size of the play area
const X_MAX: usize = 10;
const Y_MAX: usize = 20;
// The blocks making up this shape
#[derive(Component)]
#[relationship_target(relationship = ShapeBlock)]
struct ShapeBlocks(Vec<Entity>);
/// A part of a piece, i.e., a single square of a piece
#[derive(Component, Debug)]
#[relationship(relationship_target = ShapeBlocks)]
struct ShapeBlock {
#[relationship]
shape: Entity,
}
// The blocks making up this shape
#[derive(Component, Default, Debug)]
#[relationship_target(relationship = LineBlock)]
struct LineBlocks(Vec<Entity>);
/// A part of a piece, i.e., a single square of a piece
#[derive(Component, Debug)]
#[require(Transform, Visibility)]
#[relationship(relationship_target = LineBlocks)]
struct LineBlock {
#[relationship]
line: Entity,
}
// A line holds up to 10 blocks before being cleared
#[derive(Component, Debug, Clone, Copy)]
struct Line(usize);
// Just marks a block either of a shape or line
#[derive(Component, Debug)]
struct Block;
#[derive(Component, Debug, Clone, Copy, PartialEq)]
#[require(Transform, Visibility)]
struct GridPosition {
x: usize,
y: usize,
}
impl GridPosition {
fn with_offset(self, other_x: isize, other_y: isize) -> Result<GridPosition, GameError> {
let x = self.x as isize + other_x;
let y = self.y as isize + other_y;
if x >= X_MAX as isize {
Err(GameError::OutOfBoundsLeft)
} else if x < 0 {
Err(GameError::OutOfBoundsRight)
} else if y < 0 {
Err(GameError::OutOfBoundsDown)
} else {
Ok(GridPosition {
x: x as usize,
y: y as usize,
})
}
}
}
impl Display for GridPosition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "({},{})", self.x, self.y)
}
}
#[derive(Error, Debug, PartialEq)]
enum GameError {
#[error("Coordinates are out of bounds: Left")]
OutOfBoundsLeft,
#[error("Coordinates are out of bounds: Right")]
OutOfBoundsRight,
#[error("Coordinates are out of bounds: Down")]
OutOfBoundsDown,
#[error("Coordiante collision")]
Collision,
}
impl Default for GridPosition {
fn default() -> Self {
GridPosition { x: 5, y: Y_MAX }
}
}
impl From<&GridPosition> for Vec3 {
fn from(GridPosition { x, y }: &GridPosition) -> Vec3 {
// Grid Positions start in the bottom left of the area
// So (0, 0) is the bottom left, (0, 9) is the bottom right, etc
let x_0 = -SCALE * 5.0 + (0.5 * SCALE);
let x = x_0 + ((*x as f32) * SCALE);
let y_0 = -SCALE * 10.0 + (0.5 * SCALE);
let y = y_0 + ((*y as f32) * SCALE);
Vec3::new(x, y, 0.0)
}
}
impl From<(usize, usize)> for GridPosition {
fn from((x, y): (usize, usize)) -> GridPosition {
GridPosition { x, y }
}
}
impl std::ops::Add for GridPosition {
type Output = Self;
fn add(self, GridPosition { x: x2, y: y2 }: Self) -> Self {
GridPosition {
x: self.x + x2,
y: self.y + y2,
}
}
}
impl std::ops::AddAssign<&GridPosition> for GridPosition {
fn add_assign(&mut self, rhs: &GridPosition) {
*self = *self + *rhs;
}
}
#[derive(Component, Default, Event, Clone, Debug)]
enum Orientation {
#[default]
Up,
Left,
Down,
Right,
}
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,
}
}
}
impl Display for Orientation {
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, "->"),
}
}
}
#[derive(States, Clone, Eq, PartialEq, Debug, Hash, Default, Component)]
enum GameState {
#[default]
Falling,
Pause,
}
#[derive(Resource, Debug)]
struct Visuals {
material: Handle<ColorMaterial>,
mesh: Handle<Mesh>,
}
#[derive(Resource, Debug, Default)]
struct Score(usize);
impl Display for Score {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
/// 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>,
}
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(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
) {
commands.insert_resource(Visuals {
material: materials.add(ColorMaterial {
color: WHITE.into(),
..default()
}),
mesh: meshes.add(Rectangle::new(SCALE, SCALE)),
});
(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((
Node {
top: Val::Px(0.0),
left: Val::Px(0.0),
..default()
},
DebuggingState::On,
))
.with_children(|parent| {
parent.spawn((
Node::default(),
children![
(Text::new("SHAPE"), SyncSingleton::<Shape>::default()),
(
Text::new("ORIENTATION"),
SyncSingleton::<Orientation>::default()
),
],
));
});
}
#[derive(Component, Debug, Clone, Copy)]
enum Shape {
M4(Mat4),
M3(Mat3),
}
impl Default for Shape {
fn default() -> Self {
Self::new_t()
}
}
impl Display for Shape {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_ascii())
}
}
impl Shape {
fn from_mat4(input: Mat4) -> Self {
Self::M4(input)
}
fn from_mat3(input: Mat3) -> Self {
Self::M3(input)
}
fn new_o() -> Self {
Self::from_mat4(Mat4::from_cols_array_2d(&[
[0., 0., 0., 0.],
[0., 1., 1., 0.],
[0., 1., 1., 0.],
[0., 0., 0., 0.],
]))
}
fn new_t() -> Self {
Self::from_mat3(Mat3::from_cols_array_2d(&[
[0., 1., 0.],
[1., 1., 1.],
[0., 0., 0.],
]))
}
fn new_l() -> Self {
Self::from_mat4(Mat4::from_cols_array_2d(&[
[0., 0., 0., 0.],
[0., 1., 0., 0.],
[0., 1., 0., 0.],
[0., 1., 1., 0.],
]))
}
fn new_j() -> Self {
Self::from_mat4(Mat4::from_cols_array_2d(&[
[0., 0., 0., 0.],
[0., 0., 1., 0.],
[0., 0., 1., 0.],
[0., 1., 1., 0.],
]))
}
fn new_s() -> Self {
Self::from_mat4(Mat4::from_cols_array_2d(&[
[0., 0., 0., 0.],
[0., 1., 1., 0.],
[1., 1., 0., 0.],
[0., 0., 0., 0.],
]))
}
fn new_z() -> Self {
Self::from_mat4(Mat4::from_cols_array_2d(&[
[0., 0., 0., 0.],
[1., 1., 0., 0.],
[0., 1., 1., 0.],
[0., 0., 0., 0.],
]))
}
fn new_i() -> Self {
Self::from_mat4(Mat4::from_cols_array_2d(&[
[0., 0., 1., 0.],
[0., 0., 1., 0.],
[0., 0., 1., 0.],
[0., 0., 1., 0.],
]))
}
// Rotates 90 degrees to the right
// https://stackoverflow.com/a/8664879
fn rotated(&self) -> Self {
match self {
Self::M4(inner) => {
let mut new_self = inner.transpose();
for i in 0..4 {
let col = new_self.col_mut(i);
*col = Vec4::new(col[3], col[2], col[1], col[0]);
}
Self::M4(new_self)
}
Self::M3(inner) => {
let mut new_self = inner.transpose();
for i in 0..3 {
let col = new_self.col_mut(i);
*col = Vec3::new(col[2], col[1], col[0]);
}
Self::M3(new_self)
}
}
}
fn rotate(&mut self) {
*self = self.rotated();
}
fn coordinates(
&self,
center: &GridPosition,
) -> impl Iterator<Item = Result<GridPosition, GameError>> {
let mut v: Vec<Result<GridPosition, GameError>> = Vec::new();
match self {
Self::M4(inner) => {
for (i, y) in (-1..3).rev().enumerate() {
let c = inner.col(i);
for (j, x) in (-1..3).enumerate() {
if c[j] == 1.0 {
v.push(center.with_offset(x, y));
}
}
}
}
Self::M3(inner) => {
for (i, y) in (-1..2).rev().enumerate() {
let c = inner.col(i);
for (j, x) in (-1..2).enumerate() {
if c[j] == 1.0 {
v.push(center.with_offset(x, y));
}
}
}
}
};
v.into_iter()
}
fn as_ascii(&self) -> String {
let mut output = String::default();
match self {
Self::M4(this) => {
for i in 0..4 {
let col = this.col(i).to_array();
output += format!("{}{}{}{}\n", col[0], col[1], col[2], col[3]).as_str();
}
}
Self::M3(this) => {
for i in 0..3 {
let col = this.col(i).to_array();
output += format!("{}{}{}\n", col[0], col[1], col[2]).as_str();
}
}
};
output
}
}
fn update_position(
mut changed: Query<
(Entity, &GridPosition, &mut Transform),
Or<(Added<GridPosition>, Changed<GridPosition>)>,
>,
) {
changed.iter_mut().for_each(|(e, gp, mut t)| {
let v3: Vec3 = gp.into();
debug!(
"Updating {e} with grid position {:?} to coordinates {:?}",
gp, v3
);
t.translation = gp.into();
});
}
// TODO: Inline this to when movement occurs
fn update_shape_blocks(
query: Query<(Entity, &Shape, &Orientation, &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());
if blocks.is_empty() {
let mesh = Mesh2d(visuals.mesh.clone());
let mat = MeshMaterial2d(visuals.material.clone());
commands
.entity(e)
.with_related_entities::<ShapeBlock>(|parent| {
s.coordinates(center).for_each(|gp| {
parent
.spawn((mesh.clone(), mat.clone(), gp.unwrap(), Block))
.observe(movement);
});
});
} else {
let mut p = s.coordinates(center);
blocks.iter_mut().for_each(|mut gp| {
*gp = p.next().unwrap().unwrap();
});
}
});
}
fn kb_input(
mut events: EventReader<KeyboardInput>,
mut query: Query<(Entity, &mut Shape)>,
curr: Res<State<GameState>>,
mut next: ResMut<NextState<GameState>>,
mut commands: Commands,
) {
events.read().for_each(
|KeyboardInput {
key_code, state, ..
}| {
if let ButtonState::Pressed = state {
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
KeyCode::ArrowUp => {
commands.entity(e).trigger(Movement::Rotate);
}
KeyCode::ArrowDown => {
commands.entity(e).trigger(Movement::Down);
}
KeyCode::ArrowLeft => {
commands.entity(e).trigger(Movement::Left);
}
KeyCode::ArrowRight => {
commands.entity(e).trigger(Movement::Right);
}
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(),
KeyCode::Digit3 => *s = Shape::new_l(),
KeyCode::Digit4 => *s = Shape::new_j(),
KeyCode::Digit5 => *s = Shape::new_s(),
KeyCode::Digit6 => *s = Shape::new_z(),
KeyCode::Digit7 => *s = Shape::new_i(),
_ => (),
}
});
}
},
);
}
fn draw_grid(mut gizmos: Gizmos) {
gizmos
.grid_2d(
Isometry2d::IDENTITY,
UVec2::new(X_MAX as u32, Y_MAX as u32),
Vec2::new(SCALE, SCALE),
GREEN,
)
.outer_edges();
}
fn falling(mut shape: Query<Entity, With<Shape>>, mut commands: Commands) {
shape.iter_mut().for_each(|e| {
info!("Making {:?} fall", e);
commands.entity(e).trigger(Movement::Down);
});
}
// Run condition that returns `true` every `n` seconds
// TODO: Update a resource with the current tick
fn clock_cycle(n: f32) -> impl FnMut(Res<Time>, Local<f32>) -> bool {
move |t: Res<Time>, mut buf: Local<f32>| -> bool {
*buf += t.delta_secs();
if *buf > n {
*buf = 0.0;
true
} else {
false
}
}
}
fn add_piece(mut commands: Commands, mut shapes: ResMut<ShapesBuffer>) {
commands
.spawn((
Orientation::default(),
GridPosition::default(),
shapes.next.pop_front().unwrap(),
))
.observe(movement);
}
/// 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 score: ResMut<Score>,
mut commands: Commands,
) {
let cleared_lines: Vec<usize> = changed_lines
.iter()
.filter_map(|e| lines.get(e).ok())
.filter_map(|(e, lb, Line(i))| {
if lb.0.len() == 10 {
commands.entity(e).despawn_related::<LineBlocks>();
score.0 += 1;
info!("New score: {:?}", score.0);
Some(*i)
} else {
None
}
})
.collect();
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;
});
}
}
fn adjust_block_lines(
query: Query<(Entity, &Line), Changed<Line>>,
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) {
gp.y = *i;
}
});
});
}
#[derive(Event, Copy, Clone, PartialEq)]
enum Movement {
Down,
Left,
Right,
Rotate,
}
// TODO: When out of bounds left/right, try to move piece away from wall
fn movement(
trigger: Trigger<Movement>,
mut grid_positions: Query<&mut GridPosition, Or<(With<ShapeBlock>, With<ShapeBlocks>)>>,
mut shape: Query<&mut Shape>,
inactive: Query<&GridPosition, (Without<ShapeBlock>, Without<ShapeBlocks>)>,
mut commands: Commands,
) {
if let (Ok(this_shape), Ok(center)) = (
shape.get_mut(trigger.target()),
grid_positions.get(trigger.target()),
) {
let (new_center, new_shape) = match trigger.event() {
Movement::Down => (center.with_offset(0, -1), *this_shape),
Movement::Left => (center.with_offset(-1, 0), *this_shape),
Movement::Right => (center.with_offset(1, 0), *this_shape),
Movement::Rotate => (Ok(*center), this_shape.rotated()),
};
info!(
"Proposed change: {:?}\n{}",
new_center,
new_shape.as_ascii()
);
match new_center {
Err(GameError::OutOfBoundsLeft) | Err(GameError::OutOfBoundsRight) => (), // Do nothing
Err(GameError::OutOfBoundsDown) => {
commands.entity(trigger.target()).remove::<Shape>();
}
Err(GameError::Collision) => panic!("This shouldn't happen!"),
Ok(new_center) => {
let new_blocks = new_shape.coordinates(&new_center);
for block_gp in new_blocks {
match block_gp {
Err(GameError::OutOfBoundsLeft) | Err(GameError::OutOfBoundsRight) => {
return;
} // Do nothing
Err(GameError::OutOfBoundsDown) => {
commands.entity(trigger.target()).remove::<Shape>();
return;
}
Err(GameError::Collision) => panic!("This shouldn't happen!"),
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 *trigger.event() == Movement::Down {
// De-activate this piece
commands.entity(trigger.target()).remove::<Shape>();
}
// Regardless, cancel the move
return;
}
}
}
}
}
// Update center
let mut gp = grid_positions.get_mut(trigger.target()).unwrap();
*gp = new_center;
// Update shape/rotation
let mut s = shape.get_mut(trigger.target()).unwrap();
*s = new_shape;
}
}
} else {
warn!("Triggered movement on non-shape entity");
}
}
// TODO: Just despawn?
fn deactivate_shape(
mut events: RemovedComponents<Shape>,
grid_positions: Query<&GridPosition>,
parent: Query<&ShapeBlocks>,
lines: Query<(Entity, &Line), With<LineBlocks>>,
mut commands: Commands,
) {
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();
});
}
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(),
]);
}
}