diff --git a/Cargo.lock b/Cargo.lock index aa9e497..44f1862 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,102 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "anyhow" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" + +[[package]] +name = "itoa" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" + [[package]] name = "mino" version = "0.1.0" +dependencies = [ + "mino-code-gen", +] + +[[package]] +name = "mino-code-gen" +version = "0.1.0" +dependencies = [ + "anyhow", + "serde", + "serde_json", +] + +[[package]] +name = "proc-macro2" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" + +[[package]] +name = "serde" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e326c9ec8042f1b5da33252c8a37e9ffbd2c9bef0155215b6e6c80c790e05f91" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42a3df25b0713732468deadad63ab9da1f1fd75a48a15024b50363f128db627e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "syn" +version = "1.0.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" diff --git a/Cargo.toml b/Cargo.toml index da3e926..9a6de7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,5 @@ [workspace] members = [ - "mino" + "mino", + "mino-code-gen", ] diff --git a/mino-code-gen/Cargo.toml b/mino-code-gen/Cargo.toml new file mode 100644 index 0000000..dae0bf8 --- /dev/null +++ b/mino-code-gen/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "mino-code-gen" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" diff --git a/mino-code-gen/src/lib.rs b/mino-code-gen/src/lib.rs new file mode 100644 index 0000000..48e0f8f --- /dev/null +++ b/mino-code-gen/src/lib.rs @@ -0,0 +1,155 @@ +use anyhow::{Context as _, Result}; +use std::collections::{BTreeMap, HashMap}; +use std::path::Path; + +pub fn compile_ruleset(in_path: impl AsRef, out_path: impl AsRef) -> Result<()> { + let mut in_file = std::fs::File::open(in_path.as_ref()) + .with_context(|| format!("could not open input file {}", in_path.as_ref().display()))?; + let mut out_file = std::fs::File::create(out_path.as_ref()) + .with_context(|| format!("could not open output file {}", out_path.as_ref().display()))?; + let rules_data: RulesData = + serde_json::from_reader(&mut in_file).context("error parsing input file")?; + compile(&rules_data) + .emit(&mut out_file) + .context("error writing to output file") +} + +/// Input data (JSON file) +#[derive(serde::Deserialize)] +struct RulesData { + shapes: BTreeMap>, +} + +/// Output data (Rust code) +struct RulesCode { + // establish consistent mapping from shape name to integer + shape_indices: HashMap, + extents: Extents>, + cells_bits: Vec, + cells_indices: Vec, +} + +struct Extents { + x0: E, + x1: E, + y0: E, + y1: E, +} + +fn compile(rules: &RulesData) -> RulesCode { + let shape_indices: HashMap = rules + .shapes + .keys() + .enumerate() + .map(|(i, sh)| (sh.clone(), i)) + .collect(); + + let len_per_shape_rot = shape_indices.len() * 4; + + let mut extents = Extents { + x0: vec![i16::MAX; len_per_shape_rot], + x1: vec![i16::MIN; len_per_shape_rot], + y0: vec![i16::MAX; len_per_shape_rot], + y1: vec![i16::MIN; len_per_shape_rot], + }; + + let mut cells_bits = vec![]; + let mut cells_indices = vec![usize::MAX; len_per_shape_rot]; + + for (shape, &i0) in shape_indices.iter() { + let coords = rules.shapes.get(shape).unwrap(); + for r in 0u8..4 { + let i = i0 * 4 + r as usize; + let (exts, data) = process_rotated_coords(coords, r); + extents.x0[i] = exts.x0; + extents.x1[i] = exts.x1; + extents.y0[i] = exts.y0; + extents.y1[i] = exts.y1; + cells_indices[i] = find_or_insert(&mut cells_bits, &data); + } + } + + RulesCode { + shape_indices, + extents, + cells_bits, + cells_indices, + } +} + +fn process_rotated_coords(coords: &[(i16, i16)], rot: u8) -> (Extents, Vec) { + let mut exts = Extents { + x0: i16::MAX, + x1: i16::MIN, + y0: i16::MAX, + y1: i16::MIN, + }; + for &(x0, y0) in coords.iter() { + let (x, y) = rotate(x0, y0, rot); + exts.x0 = std::cmp::min(exts.x0, x); + exts.x1 = std::cmp::max(exts.x1, x + 1); + exts.y0 = std::cmp::min(exts.y0, y); + exts.y1 = std::cmp::max(exts.y1, y + 1); + } + + let mut data = vec![0u16; (exts.y1 - exts.y0) as usize]; + for &(x0, y0) in coords.iter() { + let (x, y) = rotate(x0, y0, rot); + data[(y - exts.y0) as usize] |= 1 << (x - exts.x0); + } + (exts, data) +} + +fn rotate(x: i16, y: i16, r: u8) -> (i16, i16) { + let mut xy = (x, y); + for _ in 0..r { + let (x, y) = xy; + xy = (y, -x); + } + xy +} + +/// Finds the first occurence of `needle` in `haystack`, or appends a suffix of it (up to +/// the entire length), returning the index within `haystack` that now contains `needle`. +fn find_or_insert(haystack: &mut Vec, needle: &[T]) -> usize { + for i in 0.. { + let len = std::cmp::min(haystack.len() - i, needle.len()); + if haystack[i..i + len] == needle[..len] { + haystack.extend_from_slice(&needle[len..]); + return i; + } + } + unreachable!() +} + +impl RulesCode { + fn emit(&self, out: &mut impl std::io::Write) -> std::io::Result<()> { + writeln!(out, "pub mod shape_indices {{")?; + for (name, &index) in self.shape_indices.iter() { + writeln!(out, "pub const {name}: usize = {index:?};")?; + } + writeln!(out, "}}")?; + + writeln!(out, "pub mod extents {{")?; + for (name, arr) in [ + ("X0", &self.extents.x0), + ("X1", &self.extents.x1), + ("Y0", &self.extents.y0), + ("Y1", &self.extents.y1), + ] { + let len = arr.len(); + writeln!(out, "pub const {name}: [i16; {len}] = {arr:?};")?; + } + writeln!(out, "}}")?; + + let arr = &self.cells_bits; + let len = self.cells_bits.len(); + writeln!(out, "pub const CELLS_BITS: [u16; {len}] = {arr:#x?};")?; + + let arr = &self.cells_indices; + let len = self.cells_indices.len(); + writeln!(out, "pub const CELLS_INDEX: [usize; {len}] = {arr:?};")?; + + Ok(()) + } +} diff --git a/mino/Cargo.toml b/mino/Cargo.toml index cf074c5..a651aac 100644 --- a/mino/Cargo.toml +++ b/mino/Cargo.toml @@ -5,8 +5,12 @@ version = "0.1.0" edition = "2021" [features] -default = ["ascii"] +default = ["ascii", "srs"] ascii = [] +srs = [] + +[build-dependencies] +mino-code-gen = { path = "../mino-code-gen" } [dependencies] # tracing = { version = "0.1", default-features = false} diff --git a/mino/build.rs b/mino/build.rs new file mode 100644 index 0000000..2eedbc6 --- /dev/null +++ b/mino/build.rs @@ -0,0 +1,11 @@ +use std::path::Path; + +fn main() { + let out_dir = std::env::var_os("OUT_DIR").unwrap(); + let out_dir = Path::new(&out_dir); + + let srs_in = "../assets/srs.json"; + let srs_out = out_dir.join("srs.rs"); + println!("cargo:rerun-if-changed={}", srs_in); + mino_code_gen::compile_ruleset(srs_in, srs_out).unwrap() +} diff --git a/mino/src/lib.rs b/mino/src/lib.rs index 68ae8bf..7026d02 100644 --- a/mino/src/lib.rs +++ b/mino/src/lib.rs @@ -7,6 +7,9 @@ pub mod piece; pub use location::Loc; pub use matrix::Mat; +#[cfg(feature = "srs")] +pub mod srs; + /// Allows constructing a `Mat` constant with string literals: /// /// ``` diff --git a/mino/src/srs.rs b/mino/src/srs.rs new file mode 100644 index 0000000..9bcbfc2 --- /dev/null +++ b/mino/src/srs.rs @@ -0,0 +1,94 @@ +//! Implementation of SRS shapes and movement quirks. + +use crate::location::Rot; +use crate::piece::{Cells, Shape as GenericShape}; + +mod code_gen { + include!(concat!(env!("OUT_DIR"), "/srs.rs")); +} + +#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] +#[repr(u8)] +pub enum Shape { + I = code_gen::shape_indices::I as u8, + J = code_gen::shape_indices::J as u8, + L = code_gen::shape_indices::L as u8, + O = code_gen::shape_indices::O as u8, + S = code_gen::shape_indices::S as u8, + T = code_gen::shape_indices::T as u8, + Z = code_gen::shape_indices::Z as u8, +} + +impl GenericShape<'static> for Shape { + fn cells(&self, r: Rot) -> Cells<'static> { + let i = (*self as usize) * 4 + r as usize; + let xs = code_gen::extents::X0[i]..code_gen::extents::X1[i]; + let ys = code_gen::extents::Y0[i]..code_gen::extents::Y1[i]; + let idx = code_gen::CELLS_INDEX[i]; + let data = &code_gen::CELLS_BITS[idx..idx + ys.len()]; + Cells::new(xs, ys, data) + } +} + +pub type Piece = crate::piece::Piece; + +#[cfg(test)] +mod test { + use super::*; + use crate::location::Loc; + + use alloc::vec::Vec; + use core::ops::Range; + + fn get_cells(s: Shape, r: Rot) -> (Range, Range, Vec<(i16, i16)>) { + let piece = Piece { + shape: s, + loc: Loc { x: 0, y: 0, r }, + }; + let mut coords = piece.cells().coords().collect::>(); + coords.sort(); + let (xs, ys) = piece.cells().extents(); + (xs, ys, coords) + } + + fn compute_cells(init: &[(i16, i16)], r: Rot) -> (Range, Range, Vec<(i16, i16)>) { + let mut coords = init.iter().map(|xy| rotate(*xy, r)).collect::>(); + coords.sort(); + let min_x = coords.iter().map(|&(x, _)| x).min().unwrap(); + let max_x = coords.iter().map(|&(x, _)| x).max().unwrap(); + let min_y = coords.iter().map(|&(_, y)| y).min().unwrap(); + let max_y = coords.iter().map(|&(_, y)| y).max().unwrap(); + let xs = min_x..max_x + 1; + let ys = min_y..max_y + 1; + (xs, ys, coords) + } + + fn rotate(mut xy: (i16, i16), r: Rot) -> (i16, i16) { + for _ in 0..(r as u8) { + let (x, y) = xy; + xy = (y, -x); + } + xy + } + + fn test_cells_all_rotations(s: Shape, expected_coords: &[(i16, i16)]) { + for r in (0..4).map(Rot::from) { + let (act_xs, act_ys, act_coords) = get_cells(s, r); + let (exp_xs, exp_ys, exp_coords) = compute_cells(expected_coords, r); + assert_eq!(act_xs, exp_xs, "{s:?},{r:?}"); + assert_eq!(act_ys, exp_ys, "{s:?},{r:?}"); + assert_eq!(act_coords, exp_coords, "{s:?},{r:?}"); + } + } + + #[test] + fn test_srs_cells() { + test_cells_all_rotations(Shape::I, &[(-1, 0), (0, 0), (1, 0), (2, 0)]); + test_cells_all_rotations(Shape::J, &[(-1, 1), (-1, 0), (0, 0), (1, 0)]); + test_cells_all_rotations(Shape::L, &[(1, 1), (-1, 0), (0, 0), (1, 0)]); + test_cells_all_rotations(Shape::O, &[(0, 0), (1, 0), (0, 1), (1, 1)]); + test_cells_all_rotations(Shape::S, &[(-1, 0), (0, 0), (0, 1), (1, 1)]); + test_cells_all_rotations(Shape::T, &[(0, 1), (-1, 0), (0, 0), (1, 0)]); + test_cells_all_rotations(Shape::Z, &[(1, 0), (0, 0), (0, 1), (-1, 1)]); + } +}