From 9919ac8ab52f8adce58eaec4d45faf5723f069fc Mon Sep 17 00:00:00 2001 From: mehbark Date: Sun, 6 Jul 2025 14:12:30 -0400 Subject: [PATCH] board parsing and display --- .gitignore | 1 + Cargo.lock | 189 ++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 8 +++ src/main.rs | 172 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 370 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..47959d1 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,189 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "deprecate-until" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a3767f826efbbe5a5ae093920b58b43b01734202be697e1354914e862e8e704" +dependencies = [ + "proc-macro2", + "quote", + "semver", + "syn", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "hashbrown" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" + +[[package]] +name = "indexmap" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "integer-sqrt" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276ec31bcb4a9ee45f58bec6f9ec700ae4cf4f4f8f2fa7e06cb406bd5ffdd770" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "pathfinding" +version = "4.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ac35caa284c08f3721fb33c2741b5f763decaf42d080c8a6a722154347017e" +dependencies = [ + "deprecate-until", + "indexmap", + "integer-sqrt", + "num-traits", + "rustc-hash", + "thiserror", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + +[[package]] +name = "sokoban" +version = "0.1.0" +dependencies = [ + "bitvec", + "pathfinding", +] + +[[package]] +name = "syn" +version = "2.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..998a120 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "sokoban" +version = "0.1.0" +edition = "2024" + +[dependencies] +bitvec = "1.0.1" +pathfinding = "4.14.0" diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..6352395 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,172 @@ +use std::{ + error::Error, + fmt::{self, Display}, + io::{self, prelude::*}, + str::FromStr, +}; + +use bitvec::vec::BitVec; + +type Pos = (i16, i16); + +#[derive(Debug)] +struct BoardConsts { + width: i16, + walls: BitVec, + goals: BitVec, +} + +#[derive(Debug)] +struct Board { + pub player: Pos, + boxes: BitVec, +} + +#[derive(Debug)] +enum BoardParseError { + NoPlayer, + DuplicatePlayer(Pos, Pos), +} + +impl Display for BoardParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + BoardParseError::NoPlayer => write!(f, "no player found"), + BoardParseError::DuplicatePlayer((x0, y0), (x1, y1)) => { + write!(f, "player found at ({x0}, {y0}) and ({x1}, {y1})") + } + } + } +} + +impl Error for BoardParseError {} + +#[allow( + clippy::cast_possible_truncation, + clippy::cast_possible_wrap, + clippy::cast_sign_loss +)] +impl Board { + pub fn height(&self, consts: &BoardConsts) -> i16 { + self.boxes.len() as i16 / consts.width + } + + pub fn in_bounds(&self, consts: &BoardConsts, (x, y): Pos) -> bool { + (0..consts.width).contains(&x) && (0..self.height(consts)).contains(&y) + } + + fn index_of(&self, consts: &BoardConsts, (x, y): Pos) -> usize { + (x + y * consts.width) as usize + } + + pub fn wall_at(&self, consts: &BoardConsts, pos: Pos) -> bool { + if !self.in_bounds(consts, pos) { + return true; + } + + *consts.walls.get(self.index_of(consts, pos)).unwrap() + } + + pub fn goal_at(&self, consts: &BoardConsts, pos: Pos) -> bool { + if !self.in_bounds(consts, pos) { + return false; + } + + *consts.goals.get(self.index_of(consts, pos)).unwrap() + } + + pub fn box_at(&self, consts: &BoardConsts, pos: Pos) -> bool { + if !self.in_bounds(consts, pos) { + return false; + } + + *self.boxes.get(self.index_of(consts, pos)).unwrap() + } + + pub fn parse(src: &str) -> Result<(BoardConsts, Board), BoardParseError> { + let width = src.lines().map(str::len).max().unwrap_or(0) as i16; + let mut player = Err(BoardParseError::NoPlayer); + let mut walls = BitVec::new(); + let mut goals = BitVec::new(); + let mut boxes = BitVec::new(); + + for (y, line) in src.lines().enumerate() { + let padding = (0..(width - line.len() as i16)).map(|_| b' '); + for (x, b) in line.bytes().chain(padding).enumerate() { + let pos = (x as i16, y as i16); + let (has_player, has_wall, has_goal, has_box) = match b { + b'.' => (false, false, true, false), + b'@' => (true, false, false, false), + b'+' => (true, false, true, false), + b'$' => (false, false, false, true), + b'*' => (false, false, true, true), + b'#' => (false, true, false, false), + // ignore invalid characters + _ => (false, false, false, false), + }; + if has_player { + if let Ok(player) = player { + return Err(BoardParseError::DuplicatePlayer(player, pos)); + } + player = Ok(pos); + } + walls.push(has_wall); + goals.push(has_goal); + boxes.push(has_box); + } + } + + let consts = BoardConsts { + width, + walls, + goals, + }; + let board = Board { + player: player?, + boxes, + }; + Ok((consts, board)) + } + + fn write(&self, consts: &BoardConsts, f: &mut impl Write) -> Result<(), Box> { + for y in 0..self.height(consts) { + for x in 0..consts.width { + if self.player == (x, y) { + if self.goal_at(consts, (x, y)) { + write!(f, "+")?; + } else { + write!(f, "@")?; + } + continue; + } + write!( + f, + "{}", + match ( + self.wall_at(consts, (x, y)), + self.goal_at(consts, (x, y)), + self.box_at(consts, (x, y)) + ) { + (true, false, false) => '#', + (false, true, false) => '.', + (false, false, true) => '$', + (false, true, true) => '*', + (false, false, false) => ' ', + (wall, goal, box_here) => panic!( + "bad board state at ({x}, {y}) with wall={wall} goal={goal} box={box_here}" + ), + } + )?; + } + writeln!(f)?; + } + Ok(()) + } +} + +fn main() -> Result<(), Box> { + let src = io::read_to_string(io::stdin())?; + let (consts, board) = Board::parse(&src)?; + board.write(&consts, &mut io::stdout())?; + Ok(()) +}