From 2387c4b87c7d1d7a25857973c2e8fedf059c3e26 Mon Sep 17 00:00:00 2001 From: CrispyPin Date: Mon, 30 Sep 2024 00:59:49 +0200 Subject: [PATCH] init --- .gitignore | 1 + Cargo.lock | 16 + Cargo.toml | 9 + README.md | 31 ++ programs/cat.mbl | 5 + programs/flipper.mbl | 9 + programs/hello.mbl | 14 + programs/reverse_list.mbl | 14 + rustfmt.toml | 1 + src/grid.rs | 65 ++++ src/main.rs | 617 ++++++++++++++++++++++++++++++++++++++ 11 files changed, 782 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 programs/cat.mbl create mode 100644 programs/flipper.mbl create mode 100644 programs/hello.mbl create mode 100644 programs/reverse_list.mbl create mode 100644 rustfmt.toml create mode 100644 src/grid.rs 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..3515922 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,16 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "marble" +version = "0.1.0" +dependencies = [ + "owo-colors", +] + +[[package]] +name = "owo-colors" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb37767f6569cd834a413442455e0f066d0d522de8630436e2a1761d9726ba56" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..03c633e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "marble" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +owo-colors = "4.1.0" diff --git a/README.md b/README.md new file mode 100644 index 0000000..11a4a64 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# marble "programming language" +Source code is ascii art that describes a marble track. Marbles carry one byte of data each and move around interacting with parts of the board. Marbles can be created and destroyed arbitrarily by the various operators. + +## physics +Marbles (`o`) start out moving down (unless emitted by something). +Colliding marbles will start moving in opposite directions. They don't do this if both have the same direction, meaning you can stack marbles up against an obstacle without them bouncing backwards + +## power +Power is only created by marbles moving over triggers (`*`), and then spreads via wires (`|-+`). The connections are not symmetric, so diodes can effectively be constructed to separate parts of a circuit. + +In the example below, the left input would activate both printers, but the right input would only trigger the right printer. +``` +* * ++-| +P P +``` + +## symbols +- `^v<>` Arrows: changes a marbles direction and moves it to the front, as long as there is a free space in the target location (or the marble is already at the target location) +- `\/` Mirrors: reflects marbles like an arrow, but depending on the incoming direction +- `#` Blocks: block marbles from moving further, without changing their direction +- `|-+` Wires: powers adjacent tiles when powered, in the directions indicated +- `*` Trigger: powers all 4 adjacent tiles when a marble moves over it +- `F` Flipper: flips the direction of a wire, arrow and mirror tiles when powered +- `B` Bag: consumes marbles that move into it, and keeps count of how many marbles it has. When powered, it emits an empty (0) marble moving away from the bag. +- `ASMDR` Math: Add/Subtract/Multiply/Divide/Remainder of the marble on the left and the right relative to the powered direction. Outputs a marble forward as long as at least one input exists +- `LG=!` Logic gates: compare the left and right input (both treated as 0 if absent) and power forward if Left is Less/Greater/Equal/Not Equal with Right +- `P` Print: Prints the value of the front marble when powered +- `I` Input: Outputs a marble with the next byte from the program input stream, if there are any +- `0123456789` Digits: decimal digits are added to a marbles value after multiplying the marble with 10, this means that a marble passing over `123` would contain the number 123 afterwards. Digits are consumed by marbles passing over them. + diff --git a/programs/cat.mbl b/programs/cat.mbl new file mode 100644 index 0000000..9048027 --- /dev/null +++ b/programs/cat.mbl @@ -0,0 +1,5 @@ +v -| +o |P +*I * B + +^ \ No newline at end of file diff --git a/programs/flipper.mbl b/programs/flipper.mbl new file mode 100644 index 0000000..c2edf85 --- /dev/null +++ b/programs/flipper.mbl @@ -0,0 +1,9 @@ +v < + +o *-| + | +> /F- + | + *-| + +^ < diff --git a/programs/hello.mbl b/programs/hello.mbl new file mode 100644 index 0000000..4e4e363 --- /dev/null +++ b/programs/hello.mbl @@ -0,0 +1,14 @@ + o o o o o + + + 1 1 1 1 + 7 0 0 0 1 + 2 1 8 8 1 + + v < < < < + + *-| + P- + + + B diff --git a/programs/reverse_list.mbl b/programs/reverse_list.mbl new file mode 100644 index 0000000..de89c99 --- /dev/null +++ b/programs/reverse_list.mbl @@ -0,0 +1,14 @@ +o +> v + + * / + +-----+ + | | + F | +> > > > \ v | +o o o o | +5 5 5 5 *-| | +1 2 3 4 P- | +^ ^ ^ ^ B | + vF----+ + # \ No newline at end of file diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..218e203 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +hard_tabs = true diff --git a/src/grid.rs b/src/grid.rs new file mode 100644 index 0000000..c0f19fd --- /dev/null +++ b/src/grid.rs @@ -0,0 +1,65 @@ +#[derive(Debug, Default, Clone, Copy, PartialEq)] +pub struct Pos { + pub x: isize, + pub y: isize, +} + +impl From<(usize, usize)> for Pos { + fn from(value: (usize, usize)) -> Self { + Self { + x: value.0 as isize, + y: value.1 as isize, + } + } +} + +#[derive(Debug)] +pub struct Grid { + rows: Vec>, + width: usize, + height: usize, +} + +impl Grid +where + T: Default + Copy, +{ + pub fn new(rows: Vec>) -> Self { + Self { + width: rows[0].len(), + height: rows.len(), + rows, + } + } + + pub fn in_bounds(&self, p: Pos) -> bool { + p.x >= 0 && p.y >= 0 && p.x < self.width as isize && p.y < self.height as isize + } + + pub fn get(&self, p: Pos) -> T { + if self.in_bounds(p) { + self.rows[p.y as usize][p.x as usize] + } else { + T::default() + } + } + + pub fn get_mut(&mut self, p: Pos) -> &mut T { + if self.in_bounds(p) { + &mut self.rows[p.y as usize][p.x as usize] + } else { + panic!( + "position {p:?} out of bounds, size is {}x{}", + self.width, self.height + ); + } + } + + pub fn width(&self) -> usize { + self.width + } + + pub fn height(&self) -> usize { + self.height + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..4cc8776 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,617 @@ +use std::{env, fs, io::stdin, process::exit}; + +use owo_colors::OwoColorize; + +mod grid; +use grid::*; + +#[derive(Debug)] +struct Machine { + grid: Grid, + marbles: Vec, + + input: Vec, + input_index: usize, + output: Vec, + state: State, + steps: usize, +} + +#[derive(Debug, Default, Clone, Copy)] +enum Tile { + #[default] + Blank, + Block, + Bag { + count: u8, + }, + Marble { + value: u8, + dir: Direction, + }, + Trigger, + Digit(u8), + Wire(WireType, bool), + Gate(GateType), + Print, + Input, + Flip, + Math(MathOp), + Mirror(MirrorType), + Arrow(Direction), +} + +#[derive(Debug, Clone, Copy)] +enum MirrorType { + Forward, + Back, +} + +#[derive(Debug, Clone, Copy)] +enum MathOp { + Add, + Sub, + Mul, + Div, + Rem, +} + +#[derive(Debug, Clone, Copy)] +enum GateType { + LessThan, + GreaterThan, + Equal, + NotEqual, +} + +#[derive(Debug, Clone, Copy)] +enum WireType { + Vertical, + Horizontal, + Cross, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +enum Direction { + Up, + Down, + Left, + Right, +} + +#[derive(Debug, Default, PartialEq)] +enum State { + #[default] + Running, + EndOfProgram, +} + +fn main() { + let args: Vec<_> = env::args().collect(); + if args.len() <= 1 { + println!("usage: marble source_file [input_file]"); + exit(0); + } + let filename = &args[1]; + let source = fs::read_to_string(filename).unwrap_or_else(|err| { + println!("Error reading file: {err}"); + exit(1); + }); + let input_data = args + .get(2) + .map(|path| { + fs::read(path).unwrap_or_else(|err| { + println!("Error reading file: {err}"); + exit(1); + }) + }) + .unwrap_or_default(); + + let mut interpreter = Machine::new(&source, input_data); + + loop { + interpreter.show(); + let mut input_text = String::new(); + stdin().read_line(&mut input_text).unwrap(); + let action: Vec<_> = input_text.trim().split_ascii_whitespace().collect(); + if input_text.starts_with("input ") { + interpreter.input.extend( + &input_text + .strip_suffix('\n') + .unwrap_or(&input_text) + .as_bytes()[6..], + ); + continue; + } + match action.as_slice() { + [] | ["step"] => interpreter.step_once(), + ["dbg" | "debug"] => { + dbg!(&interpreter); + } + ["step", num] => _ = num.parse().map(|n| interpreter.step(n)), + ["run"] => interpreter.run(), + ["q" | "exit" | "quit"] => break, + + _ => println!("{}", "unrecognised command".red()), + } + } +} + +impl Machine { + fn new(source: &str, input: Vec) -> Self { + let (rows, marbles) = parse(source); + let grid = Grid::new(rows); + Self { + // width: grid[0].len(), + // height: grid.len(), + grid, + marbles, + input, + input_index: 0, + output: Vec::new(), + state: State::default(), + steps: 0, + } + } + + fn run(&mut self) { + while self.state == State::Running { + self.step_once(); + self.show(); + } + } + + fn step(&mut self, n: usize) { + for _ in 0..n { + if self.state != State::Running { + break; + } + self.step_once(); + } + } + + fn show(&self) { + // dbg!(&self); + // print!("\x1B[2J"); // clear screen + for y in 0..self.grid.height() { + for x in 0..self.grid.width() { + let tile = self.grid.get((x, y).into()); + let c = match tile { + // Tile::Marble { value: _, dir } => match dir { + // Direction::Up => '~', + // Direction::Down => 'u', + // Direction::Left => '{', + // Direction::Right => '}', + // }, + Tile::Marble { value: _, dir: _ } => 'o', + Tile::Blank => ' ', + Tile::Block => '#', + Tile::Bag { count: _ } => 'B', + Tile::Trigger => '*', + Tile::Wire(w, _state) => match w { + WireType::Vertical => '|', + WireType::Horizontal => '-', + WireType::Cross => '+', + }, + Tile::Gate(g) => match g { + GateType::LessThan => 'L', + GateType::GreaterThan => 'G', + GateType::Equal => '=', + GateType::NotEqual => '!', + }, + Tile::Print => 'P', + Tile::Input => 'I', + Tile::Flip => 'F', + Tile::Math(m) => match m { + MathOp::Add => 'A', + MathOp::Sub => 'S', + MathOp::Mul => 'M', + MathOp::Div => 'D', + MathOp::Rem => 'R', + }, + Tile::Mirror(m) => match m { + MirrorType::Forward => '/', + MirrorType::Back => '\\', + }, + Tile::Arrow(d) => match d { + Direction::Up => '^', + Direction::Down => 'v', + Direction::Left => '<', + Direction::Right => '>', + }, + Tile::Digit(d) => d as char, + }; + print!("{c}"); + } + println!(); + } + println!("output: {}", String::from_utf8_lossy(&self.output)); + println!("input: {}", String::from_utf8_lossy(&self.input)); + println!("steps: {}", self.steps); + } + + fn step_once(&mut self) { + self.steps += 1; + let mut to_remove = Vec::new(); + let mut triggers = Vec::new(); + for i in 0..self.marbles.len() { + let marble_pos = self.marbles[i]; + let tile = self.grid.get(marble_pos); + + if let Tile::Marble { value, dir } = tile { + let next_pos = dir.step(marble_pos); + if !self.grid.in_bounds(next_pos) { + continue; + } + let mut new_tile = None; + let target = self.grid.get_mut(next_pos); + match target { + Tile::Blank => { + *target = tile; + self.marbles[i] = next_pos; + new_tile = Some(Tile::Blank); + } + Tile::Digit(d) => { + let new_val = value.wrapping_mul(10).wrapping_add(*d - b'0'); + *target = Tile::Marble { + value: new_val, + dir, + }; + self.marbles[i] = next_pos; + new_tile = Some(Tile::Blank); + } + Tile::Bag { count } => { + *count += 1; + to_remove.push(i); + new_tile = Some(Tile::Blank); + } + Tile::Marble { + value: _other_value, + dir: other_dir, + } => { + // bounce off other marbles + if *other_dir != dir { + new_tile = Some(Tile::Marble { + value, + dir: dir.opposite(), + }); + *other_dir = dir; + } + } + Tile::Trigger => { + triggers.push(next_pos); + let far_pos = dir.step(next_pos); + let far_target = self.grid.get_mut(far_pos); + if let Tile::Blank = far_target { + *far_target = Tile::Marble { value, dir }; + self.marbles[i] = far_pos; + new_tile = Some(Tile::Blank); + } + } + Tile::Arrow(arrow_dir) => { + let far_pos = arrow_dir.step(next_pos); + let arrow_dir = *arrow_dir; + let far_target = self.grid.get_mut(far_pos); + if let Tile::Blank = far_target { + self.marbles[i] = far_pos; + *far_target = Tile::Marble { + value, + dir: arrow_dir, + }; + new_tile = Some(Tile::Blank); + } else if far_pos == marble_pos { + // bounce on reverse arrow + new_tile = Some(Tile::Marble { + value, + dir: dir.opposite(), + }); + } + } + Tile::Mirror(mirror) => { + let new_dir = mirror.new_dir(dir); + let far_pos = new_dir.step(next_pos); + let far_target = self.grid.get_mut(far_pos); + if let Tile::Blank = far_target { + *far_target = Tile::Marble { + value, + dir: new_dir, + }; + self.marbles[i] = far_pos; + new_tile = Some(Tile::Blank); + } + } + Tile::Block => (), + _ => { + new_tile = Some(Tile::Marble { + value, + dir: dir.opposite(), + }); + } + } + + if let Some(t) = new_tile { + *self.grid.get_mut(marble_pos) = t; + } + } + } + let mut offset = 0; + for i in to_remove { + self.marbles.remove(i - offset); + offset += 1; + } + for pos in triggers { + for dir in Direction::ALL { + self.propagate_power(dir, dir.step(pos)); + } + } + for y in 0..self.grid.height() { + for x in 0..self.grid.width() { + if let Tile::Wire(_, state) = self.grid.get_mut((x, y).into()) { + if *state { + *state = false; + } + } + } + } + } + + fn propagate_power(&mut self, dir: Direction, pos: Pos) { + // let came_from = dir.opposite(); + if !self.grid.in_bounds(pos) { + return; + } + let tile = self.grid.get_mut(pos); + let front_pos = dir.step(pos); + match tile { + Tile::Wire(wiretype, state) => { + if *state { + return; + } + *state = true; + let dirs = wiretype.directions(); + for d in dirs { + self.propagate_power(*d, d.step(pos)); + } + } + Tile::Print => { + let sample = self.grid.get(front_pos); + if let Tile::Marble { value, dir: _ } = sample { + self.output.push(value); + } + } + Tile::Bag { count } => { + if *count > 0 { + *count -= 1; + if self.grid.get(front_pos).is_blank() { + *self.grid.get_mut(front_pos) = Tile::Marble { value: 0, dir }; + self.marbles.push(front_pos); + } + } + } + Tile::Input => { + if self.input_index < self.input.len() && self.grid.get(front_pos).is_blank() { + let value = self.input[self.input_index]; + *self.grid.get_mut(front_pos) = Tile::Marble { value, dir }; + self.marbles.push(front_pos); + self.input_index += 1; + } + } + Tile::Math(op) => { + let op = *op; + let pos_a = dir.left().step(pos); + let pos_b = dir.right().step(pos); + let val_a = self.grid.get(pos_a).read_value(); + let val_b = self.grid.get(pos_b).read_value(); + if (!self.grid.get(pos_a).is_blank() || !self.grid.get(pos_b).is_blank()) + && self.grid.get(front_pos).is_blank() + { + let result = match op { + MathOp::Add => val_a.wrapping_add(val_b), + MathOp::Sub => val_a.wrapping_sub(val_b), + MathOp::Mul => val_a.wrapping_mul(val_b), + MathOp::Div => val_a / val_b, + MathOp::Rem => val_a % val_b, + }; + println!("{op:?} a:{val_a} b:{val_b}"); + *self.grid.get_mut(front_pos) = Tile::Marble { value: result, dir }; + self.marbles.push(front_pos); + } + } + Tile::Flip => { + let m = self.grid.get_mut(front_pos); + match m { + Tile::Wire(wire_type, _) => { + *wire_type = match *wire_type { + WireType::Vertical => WireType::Horizontal, + WireType::Horizontal => WireType::Vertical, + WireType::Cross => WireType::Cross, + }; + } + Tile::Mirror(mirror) => { + *mirror = match *mirror { + MirrorType::Forward => MirrorType::Back, + MirrorType::Back => MirrorType::Forward, + }; + } + Tile::Arrow(dir) => { + *dir = dir.opposite(); + } + _ => (), + }; + } + Tile::Gate(gate) => { + let gate = *gate; + let pos_a = dir.left().step(pos); + let pos_b = dir.right().step(pos); + let val_a = self.grid.get(pos_a).read_value(); + let val_b = self.grid.get(pos_b).read_value(); + + let result = match gate { + GateType::LessThan => val_a < val_b, + GateType::GreaterThan => val_a > val_b, + GateType::Equal => val_a == val_b, + GateType::NotEqual => val_a != val_b, + }; + if result { + self.propagate_power(dir, dir.step(pos)); + } + } + Tile::Marble { value: _, dir: _ } + | Tile::Trigger + | Tile::Digit(_) + | Tile::Mirror(_) + | Tile::Arrow(_) + | Tile::Block + | Tile::Blank => (), + } + } +} + +impl Tile { + fn read_value(&self) -> u8 { + if let Tile::Marble { value, dir: _ } = self { + *value + } else { + 0 + } + } + + fn is_blank(&self) -> bool { + matches!(self, Tile::Blank) + } +} + +impl Direction { + const ALL: [Direction; 4] = [ + Direction::Up, + Direction::Down, + Direction::Left, + Direction::Right, + ]; + + fn opposite(&self) -> Direction { + match self { + Direction::Up => Direction::Down, + Direction::Down => Direction::Up, + Direction::Left => Direction::Right, + Direction::Right => Direction::Left, + } + } + + fn right(&self) -> Direction { + match self { + Direction::Up => Direction::Right, + Direction::Down => Direction::Left, + Direction::Left => Direction::Up, + Direction::Right => Direction::Down, + } + } + + fn left(&self) -> Direction { + self.right().opposite() + } + + fn step(&self, mut pos: Pos) -> Pos { + // dbg!(&x, &y); + match self { + Direction::Up => pos.y -= 1, + Direction::Down => pos.y += 1, + Direction::Left => pos.x -= 1, + Direction::Right => pos.x += 1, + } + pos + } + + // fn step(&self, x: &mut usize, y: &mut usize) { + // // dbg!(&x, &y); + // match self { + // Direction::Up => *y -= 1, + // Direction::Down => *y += 1, + // Direction::Left => *x -= 1, + // Direction::Right => *x += 1, + // } + // } +} + +impl WireType { + fn directions(self) -> &'static [Direction] { + match self { + WireType::Vertical => &[Direction::Up, Direction::Down], + WireType::Horizontal => &[Direction::Left, Direction::Right], + WireType::Cross => &Direction::ALL, + } + } +} + +impl MirrorType { + fn new_dir(self, dir: Direction) -> Direction { + match self { + MirrorType::Forward => match dir { + Direction::Up => Direction::Right, + Direction::Down => Direction::Left, + Direction::Left => Direction::Down, + Direction::Right => Direction::Up, + }, + MirrorType::Back => match dir { + Direction::Up => Direction::Left, + Direction::Down => Direction::Right, + Direction::Left => Direction::Up, + Direction::Right => Direction::Down, + }, + } + } +} + +fn parse(source: &str) -> (Vec>, Vec) { + let mut grid = Vec::new(); + let mut marbles = Vec::new(); + + let mut width = 0; + for (y, line) in source.lines().enumerate() { + width = width.max(line.len()); + let mut tiles = Vec::new(); + for (x, char) in line.chars().enumerate() { + tiles.push(match char { + 'o' => { + marbles.push((x, y).into()); + Tile::Marble { + value: 0, + dir: Direction::Down, + } + } + '*' => Tile::Trigger, + '-' => Tile::Wire(WireType::Horizontal, false), + '|' => Tile::Wire(WireType::Vertical, false), + '+' => Tile::Wire(WireType::Cross, false), + '/' => Tile::Mirror(MirrorType::Forward), + '\\' => Tile::Mirror(MirrorType::Back), + '^' => Tile::Arrow(Direction::Up), + 'v' => Tile::Arrow(Direction::Down), + '<' => Tile::Arrow(Direction::Left), + '>' => Tile::Arrow(Direction::Right), + '=' => Tile::Gate(GateType::Equal), + '!' => Tile::Gate(GateType::NotEqual), + 'L' => Tile::Gate(GateType::LessThan), + 'G' => Tile::Gate(GateType::GreaterThan), + 'P' => Tile::Print, + 'I' => Tile::Input, + 'F' => Tile::Flip, + 'A' => Tile::Math(MathOp::Add), + 'S' => Tile::Math(MathOp::Sub), + 'M' => Tile::Math(MathOp::Mul), + 'D' => Tile::Math(MathOp::Div), + 'R' => Tile::Math(MathOp::Rem), + 'B' => Tile::Bag { count: 0 }, + d @ '0'..='9' => Tile::Digit(d as u8), + '#' => Tile::Block, + _ => Tile::Blank, + }); + } + grid.push(tiles); + } + for line in &mut grid { + line.resize(width, Tile::Blank); + } + + (grid, marbles) +}