This commit is contained in:
Crispy 2024-09-30 00:59:49 +02:00
commit 2387c4b87c
11 changed files with 782 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

16
Cargo.lock generated Normal file
View file

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

9
Cargo.toml Normal file
View file

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

31
README.md Normal file
View file

@ -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.

5
programs/cat.mbl Normal file
View file

@ -0,0 +1,5 @@
v -|
o |P
*I * B
^

9
programs/flipper.mbl Normal file
View file

@ -0,0 +1,9 @@
v <
o *-|
|
> /F-
|
*-|
^ <

14
programs/hello.mbl Normal file
View file

@ -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

14
programs/reverse_list.mbl Normal file
View file

@ -0,0 +1,14 @@
o
> v
* /
+-----+
| |
F |
> > > > \ v |
o o o o |
5 5 5 5 *-| |
1 2 3 4 P- |
^ ^ ^ ^ B |
vF----+
#

1
rustfmt.toml Normal file
View file

@ -0,0 +1 @@
hard_tabs = true

65
src/grid.rs Normal file
View file

@ -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<T> {
rows: Vec<Vec<T>>,
width: usize,
height: usize,
}
impl<T> Grid<T>
where
T: Default + Copy,
{
pub fn new(rows: Vec<Vec<T>>) -> 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
}
}

617
src/main.rs Normal file
View file

@ -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<Tile>,
marbles: Vec<Pos>,
input: Vec<u8>,
input_index: usize,
output: Vec<u8>,
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<u8>) -> 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<Tile>>, Vec<Pos>) {
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)
}