board parsing and display

This commit is contained in:
mehbark 2025-07-06 14:12:30 -04:00
commit 9919ac8ab5
4 changed files with 370 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

189
Cargo.lock generated Normal file
View file

@ -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",
]

8
Cargo.toml Normal file
View file

@ -0,0 +1,8 @@
[package]
name = "sokoban"
version = "0.1.0"
edition = "2024"
[dependencies]
bitvec = "1.0.1"
pathfinding = "4.14.0"

172
src/main.rs Normal file
View file

@ -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<dyn Error>> {
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<dyn Error>> {
let src = io::read_to_string(io::stdin())?;
let (consts, board) = Board::parse(&src)?;
board.write(&consts, &mut io::stdout())?;
Ok(())
}