diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..a72590e --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,45 @@ +# Design + +## Tetris + +Matrix Multiplication for rotating pieces. + +Each piece (shape) contains a Mat4 containing a representation of it's shape. +For example: + +``` +0 1 0 0 +0 1 0 0 +0 1 0 0 +0 1 0 0 +``` + +This is the classic `line` piece. + +And here it is on it's `up` side + +``` +0 0 0 0 +1 1 1 1 +0 0 0 0 +0 0 0 0 +``` + +And here is the `t` piece + +``` +0 1 0 +1 1 1 +0 0 0 +``` + +A matrix multiplication is applied to this Mat6 to achieve a piece rotation. + +When that matrix is updated, the 4 blocks parented to the shape are moved to reflect this new shape. + +This matrix also allows us to do checks to see if any of the blocks in the shape would intersect with another piece on the board. +We can also check if a piece would go out of bounds during a move or rotation. + +We can use this to "plan -> validate -> commit" changes based on user input. + +Question: How the fuck do matrix multiplications work?? diff --git a/src/bin/tetris/main.rs b/src/bin/tetris/main.rs index 43f4364..e13e145 100644 --- a/src/bin/tetris/main.rs +++ b/src/bin/tetris/main.rs @@ -1,6 +1,8 @@ // Bevy basically forces "complex types" with Querys #![allow(clippy::type_complexity)] +use bevy::render::render_resource::encase::matrix::AsMutMatrixParts; + use games::*; // *TODO: Detect when piece is going to go out of bounds and restirct parent from moving there @@ -24,7 +26,7 @@ fn main() { falling .run_if(in_state(Falling::On)) .run_if(clock_cycle(1.0)), - set_piece.run_if( + set_shape.run_if( any_component_added:: .or(any_component_changed::) .or(any_component_added::) @@ -49,33 +51,6 @@ const SCALE: f32 = 30.0; const X_MAX: u32 = 10; const Y_MAX: u32 = 20; -/// A shape, e.g., the long piece -#[derive(Component, Debug, Default)] -enum Shape { - #[default] - O, - T, - L, - J, - S, - Z, - I, -} - -impl Display for Shape { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Shape::O => write!(f, "O"), - Shape::T => write!(f, "T"), - Shape::L => write!(f, "L"), - Shape::J => write!(f, "J"), - Shape::S => write!(f, "S"), - Shape::Z => write!(f, "Z"), - Shape::I => write!(f, "I"), - } - } -} - // The blocks making up this shape #[derive(Component)] #[relationship_target(relationship = ShapeBlock)] @@ -111,7 +86,7 @@ struct Line(u8); #[derive(Component, Debug)] struct Block; -#[derive(Component, Event, Debug, Clone, Copy)] +#[derive(Component, Event, Debug, Clone, Copy, PartialEq)] #[require(GridPosition)] struct RelativePosition { x: i8, @@ -363,146 +338,258 @@ fn init_debug_ui(mut commands: Commands) { }); } -#[rustfmt::skip] -fn block_positions(s: &Shape, o: &Orientation) -> [RelativePosition;4] { - match s { - Shape::O => [ - (0,1).into(),(1,1).into(), - (0,0).into(),(1,0).into() - ], - Shape::T => match o { - Orientation::Up => [ - (0,1).into(), - (-1,0).into(),(0,0).into(),(1,0).into(), - ], - Orientation::Down => [ - (-1,0).into(),(0,0).into(),(1,0).into(), - (0,-1).into(), - ], - Orientation::Right => [ - (0,1).into(), - (0,0).into(), (1,0).into(), - (0,-1).into(), - ], - Orientation::Left => [ - (0,1).into(), - (-1,0).into(),(0,0).into(), - (0,-1).into(), - ] - }, - Shape::L => match o { - Orientation::Up => [ - (0,1).into(), - (0,0).into(), - (0,-1).into(),(1,-1).into(), - ], - Orientation::Down => [ - (-1,1).into(),(0,1).into(), - (0,0).into(), - (0,-1).into(), - ], - Orientation::Right => [ - (-1,0).into(),(0,0).into(),(1,0).into(), - (-1,-1).into(), - ], - Orientation::Left => [ - (1,1).into(), - (-1,0).into(),(0,0).into(),(1,0).into(), - ], - }, - Shape::J => match o { - Orientation::Up => [ - (0,1).into(), - (0,0).into(), - (-1,-1).into(),(0,-1).into(), - ], - Orientation::Down => [ - (0,1).into(),(1,1).into(), - (0,0).into(), - (0,-1).into(), - ], - Orientation::Left => [ - (-1,0).into(),(0,0).into(),(1,0).into(), - (1,-1).into(), - ], - Orientation::Right => [ - (-1,1).into(), - (-1,0).into(),(0,0).into(),(1,0).into() - ], - }, - Shape::S => match o { - Orientation::Up => [ - (0,0).into(),(1,0).into(), - (-1,-1).into(),(0,-1).into(), - ], - Orientation::Down => [ - (0,1).into(),(1,1).into(), - (-1,0).into(),(0,0).into(), - ], - Orientation::Right => [ - (-1,1).into(), - (-1,0).into(),(0,0).into(), - (0,-1).into(), - ], - Orientation::Left => [ - (0,1).into(), - (0,0).into(),(1,0).into(), - (1,-1).into(), - ], - }, - Shape::Z => match o { - Orientation::Up => [ - (-1,0).into(),(0,0).into(), - (0,-1).into(),(1,-1).into(), - ], - Orientation::Down => [ - (-1,1).into(),(0,1).into(), - (0,0).into(),(1,0).into(), - ], - Orientation::Left => [ - (1,1).into(), - (0,0).into(),(1,0).into(), - (0,-1).into(), - ], - Orientation::Right => [ - (0,1).into(), - (-1,0).into(),(0,0).into(), - (-1,-1).into(), - ], - }, - // TODO: This does not match tetris! - Shape::I => match o { - Orientation::Up => [ - (0,2).into(), - (0,1).into(), - (0,0).into(), - (0,-1).into(), - ], - Orientation::Down => [ - (-1,2).into(), - (-1,1).into(), - (-1,0).into(), - (-1,-1).into(), - ], - Orientation::Left => [ - (-2,0).into(),(-1,0).into(),(0,0).into(),(1,0).into(), - ], - Orientation::Right => [ - (-2,1).into(),(-1,1).into(),(0,1).into(),(1,1).into(), - ] +#[derive(Component, Debug)] +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(); + } + + // TODO: return impl Iterator + fn relative_coordinates(&self) -> Vec { + todo!() + } + + fn computed_coordinates(&self, center: &GridPosition) -> Vec { + todo!() + } + + 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 set_piece( +#[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_relative_coordinates() { + let shape = Shape::new_t(); + + let expected = vec![ + (-1, 0).into(), + (0, 0).into(), + (1, 0).into(), + (0, 1).into(), + ]; + + assert_eq!(shape.relative_coordinates(), expected); + } + + #[test] + fn test_computed_coordinates() { + let shape = Shape::new_t(); + + let center = GridPosition { x: 5, y: 5 }; + + let expected = vec![ + (4, 5).into(), + (5, 5).into(), + (6, 5).into(), + (5, 6).into(), + ]; + + assert_eq!(shape.computed_coordinates(¢er), expected); + } +} + +fn set_shape( query: Query< (Entity, &Shape, &Orientation), - Or<( - Added, - Changed, - Added, - Changed, - )>, + Or<(Added, Changed)>, >, mut blocks: Query<&mut RelativePosition, With>, mut commands: Commands, @@ -513,7 +600,9 @@ fn set_piece( let mesh = visuals.mesh.clone(); let mat = visuals.material.clone(); - let positions = block_positions(s, o); + let positions: [RelativePosition;4] = todo!(); + + // todo: map positions to coordinates if blocks.is_empty() { commands @@ -607,13 +696,13 @@ fn kb_input( Falling::On => Falling::Off, Falling::Off => Falling::On, }), - KeyCode::Digit1 => *s = Shape::T, - KeyCode::Digit2 => *s = Shape::O, - KeyCode::Digit3 => *s = Shape::L, - KeyCode::Digit4 => *s = Shape::J, - KeyCode::Digit5 => *s = Shape::S, - KeyCode::Digit6 => *s = Shape::Z, - KeyCode::Digit7 => *s = Shape::I, + 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(), _ => (), } }); @@ -657,7 +746,7 @@ fn clock_cycle(n: f32) -> impl FnMut(Res