marble-machinations/src/marble_engine.rs
2024-10-05 15:57:20 +02:00

375 lines
9.8 KiB
Rust

use raylib::prelude::*;
pub mod board;
pub mod tile;
use board::{Board, Pos};
use tile::*;
#[derive(Debug)]
pub struct Machine {
board: Board,
marbles: Vec<Pos>,
input: Vec<u8>,
input_index: usize,
output: Vec<u8>,
steps: usize,
}
impl Machine {
pub fn new_empty(width: usize) -> Self {
Self {
board: Board::new_empty(width, width),
marbles: Vec::new(),
input: Vec::new(),
input_index: 0,
output: Vec::new(),
steps: 0,
}
}
pub fn reset(&mut self) {
self.steps = 0;
self.input_index = 0;
self.output.clear();
}
pub fn set_board(&mut self, board: Board) {
self.marbles.clear();
for y in 0..board.height() {
for x in 0..board.width() {
if let Some(Tile::Marble { value: _, dir: _ }) = board.get((x, y).into()) {
self.marbles.push((x, y).into());
}
}
}
self.board = board;
}
pub fn board(&self) -> &Board {
&self.board
}
pub fn output(&self) -> &[u8] {
&self.output
}
pub fn input(&self) -> &[u8] {
&self.input
}
pub fn input_mut(&mut self) -> &mut Vec<u8> {
&mut self.input
}
pub fn set_input(&mut self, bytes: Vec<u8>) {
self.input_index = self.input_index.min(bytes.len());
self.input = bytes;
}
pub fn draw_marble_values(&self, d: &mut RaylibDrawHandle, offset: Vector2, zoom: i32) {
let tile_size = 16 << zoom;
for x in 0..self.board.width() {
for y in 0..self.board.height() {
if let Some(tile) = self.board.get((x, y).into()) {
let px = x as i32 * tile_size + offset.x as i32 + tile_size / 2;
let py = y as i32 * tile_size + offset.y as i32 + tile_size / 2;
if let Tile::Marble { value, dir: _ } = tile {
d.draw_text(
&format!("{value}"),
px - tile_size / 2 + 2,
py - tile_size / 2 + 2,
20,
Color::MAGENTA,
)
}
}
}
}
}
pub fn step(&mut self) {
self.steps += 1;
// reset wires
for y in 0..self.board.height() {
for x in 0..self.board.width() {
if let Tile::Powerable(_, state) = self.board.get_mut((x, y).into()) {
*state = false;
}
}
}
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.board.get_or_blank(marble_pos);
if let Tile::Marble { value, dir } = tile {
let next_pos = dir.step(marble_pos);
if !self.board.in_bounds(next_pos) {
continue;
}
let mut new_tile = None;
let target = self.board.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') as MarbleValue);
*target = Tile::Marble {
value: new_val,
dir,
};
self.marbles[i] = next_pos;
new_tile = Some(Tile::Blank);
}
Tile::Powerable(PTile::Bag, _) => {
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::Powerable(PTile::Trigger, state) => {
*state = true;
triggers.push(next_pos);
let far_pos = dir.step(next_pos);
if let Some(Tile::Blank) = self.board.get(far_pos) {
self.board.set(far_pos, 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;
if let Some(Tile::Blank) = self.board.get(far_pos) {
self.marbles[i] = far_pos;
self.board.set(
far_pos,
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.board.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);
}
}
_ => (),
}
if let Some(t) = new_tile {
*self.board.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));
}
}
}
fn propagate_power(&mut self, dir: Direction, pos: Pos) {
if !self.board.in_bounds(pos) {
return;
}
let tile = self.board.get_mut(pos);
let front_pos = dir.step(pos);
if let Tile::Powerable(tile, state) = tile {
if let PTile::Trigger = tile {
return;
}
if *state {
return;
}
*state = true;
match tile {
PTile::Wire(wiretype) => {
let dirs = wiretype.directions();
for d in dirs {
self.propagate_power(*d, d.step(pos));
}
}
PTile::Output => {
let sample = self.board.get_or_blank(front_pos);
if let Tile::Marble { value, dir: _ } = sample {
self.output.push(value as u8);
}
}
PTile::Bag => {
if let Some(Tile::Blank) = self.board.get(front_pos) {
*self.board.get_mut(front_pos) = Tile::Marble { value: 0, dir };
self.marbles.push(front_pos);
}
}
PTile::Input => {
if self.input_index < self.input.len()
&& self.board.get_or_blank(front_pos).is_blank()
{
let value = self.input[self.input_index] as MarbleValue;
*self.board.get_mut(front_pos) = Tile::Marble { value, dir };
self.marbles.push(front_pos);
self.input_index += 1;
}
}
PTile::Math(op) => {
let op = *op;
let pos_a = dir.left().step(pos);
let pos_b = dir.right().step(pos);
let val_a = self.board.get_or_blank(pos_a).read_value();
let val_b = self.board.get_or_blank(pos_b).read_value();
if (!self.board.get_or_blank(pos_a).is_blank()
|| !self.board.get_or_blank(pos_b).is_blank())
&& self.board.get_or_blank(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.checked_div(val_b).unwrap_or_default(),
MathOp::Rem => val_a.checked_rem(val_b).unwrap_or_default(),
};
// println!("{op:?} a:{val_a} b:{val_b}");
*self.board.get_mut(front_pos) = Tile::Marble { value: result, dir };
self.marbles.push(front_pos);
}
}
PTile::Flipper => {
let m = self.board.get_mut(front_pos);
match m {
Tile::Powerable(PTile::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();
}
_ => (),
};
}
PTile::Gate(gate) => {
let gate = *gate;
let pos_a = dir.left().step(pos);
let pos_b = dir.right().step(pos);
let val_a = self.board.get_or_blank(pos_a).read_value();
let val_b = self.board.get_or_blank(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));
}
}
PTile::Trigger => (),
}
}
}
}
pub fn parse(source: &str) -> Board {
let mut rows = Vec::new();
let mut width = 0;
for line in source.lines() {
width = width.max(line.len());
let mut tiles = Vec::new();
for char in line.chars() {
tiles.push(tile_to_char(char));
}
rows.push(tiles);
}
for line in &mut rows {
line.resize(width, Tile::Blank);
}
Board::new(rows)
}
pub const fn tile_to_char(c: char) -> Tile {
match c {
'o' => Tile::Marble {
value: 0,
dir: Direction::Down,
},
'*' => Tile::Powerable(PTile::Trigger, false),
'-' => Tile::Powerable(PTile::Wire(WireType::Horizontal), false),
'|' => Tile::Powerable(PTile::Wire(WireType::Vertical), false),
'+' => Tile::Powerable(PTile::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::Powerable(PTile::Gate(GateType::Equal), false),
'!' => Tile::Powerable(PTile::Gate(GateType::NotEqual), false),
'L' => Tile::Powerable(PTile::Gate(GateType::LessThan), false),
'G' => Tile::Powerable(PTile::Gate(GateType::GreaterThan), false),
'P' => Tile::Powerable(PTile::Output, false),
'I' => Tile::Powerable(PTile::Input, false),
'F' => Tile::Powerable(PTile::Flipper, false),
'A' => Tile::Powerable(PTile::Math(MathOp::Add), false),
'S' => Tile::Powerable(PTile::Math(MathOp::Sub), false),
'M' => Tile::Powerable(PTile::Math(MathOp::Mul), false),
'D' => Tile::Powerable(PTile::Math(MathOp::Div), false),
'R' => Tile::Powerable(PTile::Math(MathOp::Rem), false),
'B' => Tile::Powerable(PTile::Bag, false),
d @ '0'..='9' => Tile::Digit(d as u8),
'#' => Tile::Block,
' ' => Tile::Blank,
_ => Tile::Blank,
}
}