Skip to content

Instantly share code, notes, and snippets.

@IanSSenne
Created July 17, 2024 23:46
Show Gist options
  • Save IanSSenne/f013d9e437fc28724a070ed612273f4b to your computer and use it in GitHub Desktop.
Save IanSSenne/f013d9e437fc28724a070ed612273f4b to your computer and use it in GitHub Desktop.
A small program to compute and animate (in the terminal) piston sequences from a config file.
use lazy_static::lazy_static;
use pathfinding::prelude::bfs;
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
fs,
thread::sleep,
time::{Duration, Instant},
};
#[derive(Serialize, Deserialize, Debug)]
struct V2 {
x: i32,
y: i32,
}
#[derive(Serialize, Deserialize, Debug)]
struct Config {
pub width: i32,
pub height: i32,
pub initial_state: Vec<char>,
pub start: V2,
pub goal: V2,
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
struct Pos {
pub pos: (i32, i32),
pub width: i32,
pub height: i32,
pub state: Vec<char>,
pub activation_idx: Option<usize>,
}
struct PistonDef {
direction: (i32, i32),
}
lazy_static! {
static ref DIRECTION_DEFINITIONS: HashMap<char, PistonDef> = {
let mut m = HashMap::new();
m.insert('u', PistonDef { direction: (0, -1) });
m.insert('d', PistonDef { direction: (0, 1) });
m.insert('l', PistonDef { direction: (-1, 0) });
m.insert('r', PistonDef { direction: (1, 0) });
m
};
}
impl Pos {
fn is_solid(&self, x: i32, y: i32) -> Option<bool> {
if x < 0 || y < 0 || x >= self.width as i32 || y >= self.height as i32 {
return None;
}
return Some(
self.pos.1 == y && self.pos.0 == x || self.state[(y * self.width + x) as usize] != '_',
);
}
fn successors(&self) -> Vec<Pos> {
let mut successors = vec![];
for i in 0..self.state.len() {
let x = i as i32 % self.width as i32;
let y = i as i32 / self.width as i32;
if self.state[i] == '_' || self.state[i] == '@' {
continue;
}
let piston = DIRECTION_DEFINITIONS
.get(&self.state[i].to_ascii_lowercase())
.unwrap();
let dx = piston.direction.0;
let dy = piston.direction.1;
let mut push_distance = 1;
let mut blocks: Vec<Option<char>> = Vec::new();
while let Some(solid) = self.is_solid(x + dx * push_distance, y + dy * push_distance) {
if !solid {
break;
}
if self.pos.0 == x + dx * push_distance && self.pos.1 == y + dy * push_distance {
blocks.push(None);
} else {
blocks.push(Some(
self.state[((y + dy * push_distance) * self.width
+ (x + dx * push_distance))
as usize],
));
}
push_distance += 1;
}
let end_x = x + dx * push_distance;
let end_y = y + dy * push_distance;
if end_x < 0 || end_y < 0 || end_x >= self.width as i32 || end_y >= self.height as i32 {
continue; // can't push it off the board
}
if push_distance > 1 {
// there is a solid block in front of it that can be pushed
let mut new_state = self.state.clone();
new_state[((y + dy) * self.width + (x + dx)) as usize] = '_';
let mut idx = 2;
let mut new_x = self.pos.0;
let mut new_y = self.pos.1;
for block in blocks.iter() {
if let Some(block) = block {
new_state[((y + dy * idx) * self.width + (x + dx * idx)) as usize] =
block.clone();
} else {
new_x = x + dx * idx;
new_y = y + dy * idx;
}
idx += 1;
}
successors.push(Pos {
pos: (new_x, new_y),
state: new_state,
width: self.width,
height: self.height,
activation_idx: Some(i),
});
} else if self.state[i].is_lowercase()
&& !self.is_solid(x + dx, y + dy).unwrap_or(true)
&& self.is_solid(x + dx * 2, y + dy * 2).unwrap_or(false)
{
// there is a lowercase block in front of it that can be pushed
let mut new_state = self.state.clone();
let idx1 = ((y + dy) * self.width + x + dx) as usize;
let idx2 = ((y + dy * 2) * self.width + (x + dx * 2)) as usize;
new_state[idx1] = new_state[idx2];
new_state[idx2] = '_';
successors.push(Pos {
pos: if self.pos.0 == x + dx * 2 && self.pos.1 == y + dy * 2 {
(x + dx, y + dy)
} else {
self.pos
},
state: new_state,
width: self.width,
height: self.height,
activation_idx: Some(i),
});
}
}
return successors;
}
pub fn print(&self) {
let mut index = 0;
let activation_idx = self.activation_idx.unwrap_or(usize::MAX);
for y in 0..self.height {
for x in 0..self.width {
if self.pos.0 == x && self.pos.1 == y {
print!("#");
} else if index == activation_idx {
print!(
"{}",
match &self.state[index] {
'u' => "\x1B[1m\x1B[32m^\x1B[0m",
'l' => "\x1B[1m\x1B[32m<\x1B[0m",
'd' => "\x1B[1m\x1B[32mv\x1B[0m",
'r' => "\x1B[1m\x1B[32m>\x1B[0m",
'U' => "\x1B[1m^\x1B[0m",
'L' => "\x1B[1m<\x1B[0m",
'D' => "\x1B[1mv\x1B[0m",
'R' => "\x1B[1m>\x1B[0m",
_ => panic!("Invalid state"),
}
);
} else {
if self.state[index] == '_' {
print!(" ");
} else {
print!(
"{}",
match &self.state[(y * self.width + x) as usize] {
'u' => "\x1B[32m^\x1B[0m",
'd' => "\x1B[32mv\x1B[0m",
'l' => "\x1B[32m<\x1B[0m",
'r' => "\x1B[32m>\x1B[0m",
'@' => "@",
'U' => "^",
'D' => "v",
'L' => "<",
'R' => ">",
_ => panic!("Invalid state"),
}
);
}
}
index += 1;
}
println!();
}
}
}
fn main() {
let input_file = fs::read_to_string("config.json").expect("Unable to read file");
let config: Config = serde_json::from_str(&input_file).unwrap();
println!("{:?}", config);
let goal: Pos = Pos {
pos: (config.goal.x, config.goal.y),
state: vec![],
width: config.width,
height: config.height,
activation_idx: None,
};
let start = Instant::now();
let result = bfs(
&Pos {
pos: (config.start.x, config.start.y),
state: config.initial_state.clone(),
width: config.width,
height: config.height,
activation_idx: None,
},
|p| p.successors(),
|p| p.pos == goal.pos,
);
let end = start.elapsed();
println!("Time elapsed: {:?}", end);
sleep(Duration::from_secs(1));
match result {
Some(path) => {
for pos in path.iter() {
println!("------------");
pos.print();
sleep(Duration::from_millis(400));
}
sleep(Duration::from_millis(1000));
}
None => println!("No path found!"),
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment