495 lines
13 KiB
Rust
495 lines
13 KiB
Rust
use raylib::prelude::*;
|
|
|
|
pub mod board;
|
|
pub mod pos;
|
|
pub mod tile;
|
|
use board::Board;
|
|
use pos::*;
|
|
use tile::*;
|
|
|
|
use crate::{ui::draw_usize_small, Textures, TILE_TEXTURE_SIZE};
|
|
|
|
#[derive(Debug)]
|
|
pub struct Machine {
|
|
board: Board,
|
|
marbles: Vec<Pos>,
|
|
powered: Vec<Pos>,
|
|
input: Vec<u8>,
|
|
input_index: usize,
|
|
output: Vec<u8>,
|
|
steps: usize,
|
|
}
|
|
|
|
impl Machine {
|
|
pub fn new_empty() -> Self {
|
|
Self {
|
|
board: Board::new_empty(5, 5),
|
|
marbles: Vec::new(),
|
|
powered: 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();
|
|
self.powered.clear();
|
|
}
|
|
|
|
pub fn set_board(&mut self, board: Board) {
|
|
self.marbles = board.get_marbles();
|
|
self.powered.clear();
|
|
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_index(&self) -> usize {
|
|
self.input_index
|
|
}
|
|
|
|
pub fn step_count(&self) -> usize {
|
|
self.steps
|
|
}
|
|
|
|
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,
|
|
textures: &Textures,
|
|
offset: Vector2,
|
|
scale: f32,
|
|
) {
|
|
let tile_size = (TILE_TEXTURE_SIZE * scale) as i32;
|
|
for marble in &self.marbles {
|
|
let x = marble.x;
|
|
let y = marble.y;
|
|
if let Some(tile) = self.board.get(*marble) {
|
|
let px = x as i32 * tile_size + offset.x as i32;
|
|
let py = y as i32 * tile_size + offset.y as i32;
|
|
if let Tile::Marble { value, dir } = tile {
|
|
let texture = textures.get(dir.arrow_texture_name());
|
|
let pos = Vector2::new(px as f32, py as f32);
|
|
let faded_white = Color::new(255, 255, 255, 100);
|
|
d.draw_texture_ex(texture, pos, 0., scale, faded_white);
|
|
draw_usize_small(d, textures, value as usize, px, py, scale);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn step(&mut self) {
|
|
self.steps += 1;
|
|
|
|
let old_marbles = self.marbles.len();
|
|
|
|
let mut new_marbles = Vec::new();
|
|
// activate all powered machines
|
|
for &pos in &self.powered {
|
|
match self.board.get_mut(pos) {
|
|
Some(Tile::Powerable(machine, state)) => {
|
|
*state = false;
|
|
let machine = *machine;
|
|
for dir in Direction::ALL {
|
|
let front_pos = dir.step(pos);
|
|
let source_pos = dir.opposite().step(pos);
|
|
match self.board.get(source_pos) {
|
|
Some(Tile::Wire(wiretype, true)) => {
|
|
if !wiretype.has_output(dir) {
|
|
continue;
|
|
}
|
|
}
|
|
Some(Tile::Button(true)) => (),
|
|
_ => continue,
|
|
}
|
|
let Some(front_tile) = self.board.get_mut(front_pos) else {
|
|
continue;
|
|
};
|
|
// `machine`` is being powered, in direction `dir``
|
|
match machine {
|
|
PTile::Comparator(_) => (), // handled at the power propagation stage (end of step)
|
|
PTile::Math(op) => {
|
|
if front_tile.is_blank() {
|
|
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 value = 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(),
|
|
};
|
|
new_marbles.push((front_pos, value, dir));
|
|
}
|
|
}
|
|
PTile::IO => {
|
|
if front_tile == &Tile::BLANK && self.input_index < self.input.len()
|
|
{
|
|
let value = self.input[self.input_index] as MarbleValue;
|
|
self.input_index += 1;
|
|
new_marbles.push((front_pos, value, dir));
|
|
}
|
|
}
|
|
PTile::Silo => {
|
|
if front_tile == &Tile::BLANK {
|
|
new_marbles.push((front_pos, 0, dir));
|
|
}
|
|
}
|
|
PTile::Flipper => {
|
|
match front_tile {
|
|
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.flip(),
|
|
Tile::Arrow(dir) => *dir = dir.opposite(),
|
|
_ => (),
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Some(Tile::Button(_state)) => (),
|
|
Some(Tile::Wire(_, _state)) => (),
|
|
_ => unreachable!(),
|
|
};
|
|
}
|
|
|
|
// old wires have to be reset after machine processing,
|
|
// so they can figure out which directions they are powered from
|
|
for &p in &self.powered {
|
|
match self.board.get_mut(p) {
|
|
Some(Tile::Button(state)) => *state = false,
|
|
Some(Tile::Wire(_, state)) => *state = false,
|
|
_ => (),
|
|
}
|
|
}
|
|
self.powered.clear();
|
|
|
|
if self.marbles.is_empty() {
|
|
return;
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
enum DirInfluence {
|
|
None,
|
|
One(Direction),
|
|
Multiple,
|
|
}
|
|
|
|
// #### find all direct bounces ####
|
|
let mut will_reverse_direction = vec![false; self.marbles.len()];
|
|
// todo store in tile to remove search through self.marbles
|
|
let mut influenced_direction = vec![DirInfluence::None; self.marbles.len()];
|
|
|
|
for (i, &pos) in self.marbles.iter().enumerate() {
|
|
let Some(Tile::Marble { value: _, dir }) = self.board.get(pos) else {
|
|
unreachable!()
|
|
};
|
|
let front_pos = dir.step(pos);
|
|
let Some(front_tile) = self.board.get(front_pos) else {
|
|
continue;
|
|
};
|
|
match front_tile {
|
|
Tile::Marble {
|
|
value: _,
|
|
dir: other_dir,
|
|
} => {
|
|
if other_dir != dir {
|
|
// this marble is facing another marble, and will therefore definitely bounce
|
|
will_reverse_direction[i] = true;
|
|
// the other marble will bounce too, either
|
|
let other_index =
|
|
self.marbles.iter().position(|m| *m == front_pos).unwrap();
|
|
let influence = &mut influenced_direction[other_index];
|
|
*influence = match *influence {
|
|
DirInfluence::None => DirInfluence::One(dir),
|
|
DirInfluence::One(_) => DirInfluence::Multiple,
|
|
DirInfluence::Multiple => DirInfluence::Multiple,
|
|
}
|
|
}
|
|
}
|
|
Tile::Arrow(arrow_dir) => {
|
|
if arrow_dir == dir.opposite() {
|
|
// bounce on a reverse facing arrow
|
|
will_reverse_direction[i] = true;
|
|
}
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
// #### apply all direct bounces ####
|
|
for (i, &pos) in self.marbles.iter().enumerate() {
|
|
let Some(Tile::Marble { value: _, dir }) = self.board.get_mut(pos) else {
|
|
unreachable!()
|
|
};
|
|
if will_reverse_direction[i] {
|
|
*dir = dir.opposite();
|
|
} else if let DirInfluence::One(new_dir) = influenced_direction[i] {
|
|
*dir = new_dir;
|
|
}
|
|
}
|
|
|
|
// #### new marbles ####
|
|
let mut claim_positions = Vec::new();
|
|
// prepare creating the new marbles
|
|
for &(pos, _val, _dir) in &new_marbles {
|
|
let Some(Tile::Open(OpenTile::Blank, claim)) = self.board.get_mut(pos) else {
|
|
unreachable!()
|
|
};
|
|
if claim.claim_indirect() {
|
|
claim_positions.push(pos);
|
|
}
|
|
}
|
|
// create new marbles
|
|
// new marbles are past old_marbles index, so will not move this step
|
|
for (pos, value, dir) in new_marbles {
|
|
let Some(Tile::Open(OpenTile::Blank, Claim::ClaimedIndirect)) = self.board.get_mut(pos) else {
|
|
continue;
|
|
};
|
|
self.board.set(pos, Tile::Marble { value, dir });
|
|
self.marbles.push(pos);
|
|
}
|
|
|
|
// #### movement ####
|
|
// mark claims to figure out what spaces can be moved to
|
|
for &pos in &self.marbles[..old_marbles] {
|
|
let Some(Tile::Marble { value: _, dir }) = self.board.get(pos) else {
|
|
unreachable!()
|
|
};
|
|
let front_pos = dir.step(pos);
|
|
let Some(front_tile) = self.board.get_mut(front_pos) else {
|
|
continue;
|
|
};
|
|
if let Tile::Open(_type, claim) = front_tile {
|
|
if claim.claim() {
|
|
claim_positions.push(front_pos);
|
|
}
|
|
} else {
|
|
let target_pos = match front_tile {
|
|
Tile::Arrow(d) => d.step(front_pos),
|
|
Tile::Mirror(m) => m.new_dir(dir).step(front_pos),
|
|
Tile::Button(_) => dir.step(front_pos),
|
|
_ => continue,
|
|
};
|
|
let Some(target_tile) = self.board.get_mut(target_pos) else {
|
|
continue;
|
|
};
|
|
if let Tile::Open(_type, claim) = target_tile {
|
|
if claim.claim_indirect() {
|
|
claim_positions.push(front_pos);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut removed_marbles = Vec::new();
|
|
// move marbles
|
|
for (i, pos) in self.marbles[..old_marbles].iter_mut().enumerate() {
|
|
let Some(Tile::Marble { value, dir }) = self.board.get(*pos) else {
|
|
unreachable!()
|
|
};
|
|
let front_pos = dir.step(*pos);
|
|
let Some(front_tile) = self.board.get_mut(front_pos) else {
|
|
continue;
|
|
};
|
|
|
|
let mut move_to = |tile, target_pos, dir, board: &mut Board| {
|
|
let value = match tile {
|
|
OpenTile::Blank => value,
|
|
OpenTile::Digit(n) => value.wrapping_mul(10).wrapping_add(n as MarbleValue),
|
|
};
|
|
board.set(*pos, Tile::BLANK);
|
|
board.set(target_pos, Tile::Marble { value, dir });
|
|
*pos = target_pos;
|
|
};
|
|
|
|
if let Tile::Open(space_type, claim_state) = front_tile {
|
|
if *claim_state == Claim::Claimed {
|
|
move_to(*space_type, front_pos, dir, &mut self.board);
|
|
} else if *claim_state != Claim::Free {
|
|
// (Free means a marble was just here but moved earlier this tick)
|
|
// bounce on failed direct movement
|
|
self.board.set(
|
|
*pos,
|
|
Tile::Marble {
|
|
value,
|
|
dir: dir.opposite(),
|
|
},
|
|
);
|
|
}
|
|
} else {
|
|
let target_pos;
|
|
let mut is_button = false;
|
|
let mut new_dir = dir;
|
|
match front_tile {
|
|
Tile::Arrow(d) => {
|
|
target_pos = d.step(front_pos);
|
|
new_dir = *d;
|
|
}
|
|
Tile::Mirror(m) => {
|
|
new_dir = m.new_dir(dir);
|
|
target_pos = new_dir.step(front_pos);
|
|
}
|
|
Tile::Button(_) => {
|
|
is_button = true;
|
|
target_pos = dir.step(front_pos);
|
|
}
|
|
Tile::Powerable(PTile::Silo, _) => {
|
|
removed_marbles.push(i);
|
|
continue;
|
|
}
|
|
Tile::Powerable(PTile::IO, _) => {
|
|
removed_marbles.push(i);
|
|
self.output.push(value as u8);
|
|
continue;
|
|
}
|
|
_ => continue,
|
|
}
|
|
let Some(target_tile) = self.board.get_mut(target_pos) else {
|
|
continue;
|
|
};
|
|
if let Tile::Open(space_type, Claim::ClaimedIndirect) = target_tile {
|
|
move_to(*space_type, target_pos, new_dir, &mut self.board);
|
|
if is_button {
|
|
self.powered.push(front_pos);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for pos in claim_positions {
|
|
if let Some(Tile::Open(_, claim_state)) = self.board.get_mut(pos) {
|
|
*claim_state = Claim::Free;
|
|
}
|
|
}
|
|
|
|
// remove marbles
|
|
for &i in removed_marbles.iter().rev() {
|
|
self.board.set(self.marbles[i], Tile::BLANK);
|
|
self.marbles.swap_remove(i);
|
|
}
|
|
|
|
// propagate power
|
|
let mut i = 0;
|
|
while i < self.powered.len() {
|
|
let pos = self.powered[i];
|
|
let Some(tile) = self.board.get_mut(pos) else {
|
|
unreachable!()
|
|
};
|
|
match tile {
|
|
Tile::Button(state) => {
|
|
*state = true;
|
|
for dir in Direction::ALL {
|
|
let target_pos = dir.step(pos);
|
|
match self.board.get_mut(target_pos) {
|
|
Some(Tile::Powerable(_, state)) => {
|
|
if !*state {
|
|
*state = true;
|
|
self.powered.push(target_pos);
|
|
}
|
|
}
|
|
Some(Tile::Wire(_, state)) => {
|
|
// only push it if it hasnt already been pushed
|
|
if !*state {
|
|
*state = true;
|
|
self.powered.push(target_pos);
|
|
}
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
}
|
|
Tile::Wire(wiretype, state) => {
|
|
*state = true;
|
|
for dir in wiretype.directions() {
|
|
let target_pos = dir.step(pos);
|
|
match self.board.get_mut(target_pos) {
|
|
Some(Tile::Powerable(_, state)) => {
|
|
if !*state {
|
|
*state = true;
|
|
self.powered.push(target_pos);
|
|
}
|
|
}
|
|
Some(Tile::Wire(_, state)) => {
|
|
// only push it if it hasnt already been pushed
|
|
if !*state {
|
|
*state = true;
|
|
self.powered.push(target_pos);
|
|
}
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
}
|
|
Tile::Powerable(PTile::Comparator(comp), state) => {
|
|
*state = true;
|
|
let comp = *comp;
|
|
for dir in Direction::ALL {
|
|
let front_pos = dir.step(pos);
|
|
let source_pos = dir.opposite().step(pos);
|
|
match self.board.get(source_pos) {
|
|
Some(Tile::Wire(wiretype, true)) => {
|
|
if !wiretype.has_output(dir) {
|
|
continue;
|
|
}
|
|
}
|
|
Some(Tile::Button(true)) => (),
|
|
_ => continue,
|
|
}
|
|
let Some(front_tile) = self.board.get_mut(front_pos) else {
|
|
continue;
|
|
};
|
|
if matches!(front_tile, Tile::Wire(_, _) | Tile::Powerable(_, _)) {
|
|
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 comp {
|
|
Comparison::LessThan => val_a < val_b,
|
|
Comparison::GreaterThan => val_a > val_b,
|
|
Comparison::Equal => val_a == val_b,
|
|
Comparison::NotEqual => val_a != val_b,
|
|
};
|
|
if result {
|
|
self.powered.push(front_pos);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// state may be false if it was powered by a machine in earlier step
|
|
Tile::Powerable(_, state) => *state = true,
|
|
_ => {
|
|
dbg!(tile);
|
|
unreachable!()
|
|
}
|
|
}
|
|
i += 1;
|
|
}
|
|
}
|
|
}
|