Created
July 17, 2024 23:46
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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