marble-machinations/src/marble_engine.rs

592 lines
16 KiB
Rust

use raylib::prelude::*;
pub mod grid;
pub mod pos;
pub mod tile;
use crate::{theme::TILE_TEXTURE_SIZE, ui::draw_usize_small, util::Textures};
use grid::Grid;
use pos::*;
use tile::*;
#[derive(Debug)]
pub struct Machine {
grid: Grid,
marbles: Vec<Pos>,
input: Vec<u8>,
input_index: usize,
output: Vec<u8>,
steps: usize,
pub subtick_index: usize,
pub debug_subticks: Vec<DebugSubTick>,
// used across steps
powered: Vec<Pos>,
// used within steps
new_marbles: Vec<(Pos, MarbleValue, Direction, bool)>,
influenced_direction: Vec<(bool, DirInfluence)>,
claim_positions: Vec<Pos>,
removed_marbles: Vec<usize>,
}
#[derive(Debug)]
pub struct DebugSubTick {
pub grid: Grid,
pub pos: Option<Pos>,
}
#[derive(Clone, Copy, Debug)]
enum DirInfluence {
None,
One(Direction),
Multiple,
}
impl Machine {
pub fn new_empty() -> Self {
Self {
grid: Grid::new_empty(5, 5),
marbles: Vec::new(),
input: Vec::new(),
input_index: 0,
output: Vec::new(),
steps: 0,
subtick_index: 0,
debug_subticks: Vec::new(),
powered: Vec::new(),
new_marbles: Vec::new(),
influenced_direction: Vec::new(),
claim_positions: Vec::new(),
removed_marbles: Vec::new(),
}
}
pub fn reset(&mut self) {
self.steps = 0;
self.input_index = 0;
self.output.clear();
self.powered.clear();
self.debug_subticks.clear();
self.subtick_index = 0;
}
pub fn set_grid(&mut self, board: Grid) {
self.marbles = board.get_marbles();
self.powered.clear();
self.grid = board;
}
pub fn grid(&self) -> &Grid {
&self.grid
}
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.grid.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;
#[cfg(debug_assertions)]
{
self.subtick_index = 0;
self.debug_subticks.clear();
self.debug_subticks.push(DebugSubTick {
grid: self.grid.clone(),
pos: None,
});
}
self.step_power_complete();
if self.marbles.is_empty() {
return;
}
self.step_find_bounces();
self.step_apply_direct_bounces();
self.step_prepare_creating_marbles();
let old_marbles = self.marbles.len();
self.step_create_marbles();
// #### movement ####
// mark claims to figure out what spaces can be moved to
self.step_mark_claims(old_marbles);
self.step_move_marbles(old_marbles);
for pos in self.claim_positions.drain(..) {
if let Some(Tile::Open(_, claim_state)) = self.grid.get_mut(pos) {
*claim_state = Claim::Free;
}
}
// remove marbles
for &i in self.removed_marbles.iter().rev() {
self.grid.set(self.marbles[i], Tile::BLANK);
self.marbles.swap_remove(i);
}
self.step_propagate_power();
#[cfg(debug_assertions)]
{
self.debug_subticks.push(DebugSubTick {
grid: self.grid.clone(),
pos: None,
});
self.subtick_index = self.debug_subticks.len() - 1;
}
}
#[cfg_attr(feature = "inline_less", inline(never), no_mangle)]
fn step_power_complete(&mut self) {
// activate all powered machines
for &pos in &self.powered {
match self.grid.get_mut(pos) {
Some(Tile::Powerable(PTile::Comparator(_), board_power_state)) => {
// already handled at the power propagation stage (end of sim step)
*board_power_state = Power::OFF;
}
Some(Tile::Powerable(machine, board_power_state)) => {
let state = *board_power_state;
*board_power_state = Power::OFF;
let machine = *machine;
for dir in Direction::ALL {
if !state.get_dir(dir) {
continue;
}
let front_pos = dir.step(pos);
let Some(front_tile) = self.grid.get_mut(front_pos) else {
continue;
};
// `machine` is being powered, in direction `dir`
match machine {
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.grid.get_or_blank(pos_a).read_value();
let val_b = self.grid.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(),
};
self.new_marbles.push((front_pos, value, dir, false));
}
}
PTile::IO => {
if front_tile == &Tile::BLANK && self.input_index < self.input.len()
{
let value = self.input[self.input_index] as MarbleValue;
self.new_marbles.push((front_pos, value, dir, true));
}
}
PTile::Silo => {
if front_tile == &Tile::BLANK {
self.new_marbles.push((front_pos, 0, dir, false));
}
}
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(),
_ => (),
};
}
PTile::Comparator(_) => unreachable!(),
}
}
}
Some(Tile::Button(state) | Tile::Wire(_, state)) => {
*state = false;
}
_ => unreachable!("non-powerable tile at {pos:?} in self.powered"),
};
}
self.powered.clear();
}
#[cfg_attr(feature = "inline_less", inline(never), no_mangle)]
fn step_find_bounces(&mut self) {
// #### find all direct bounces ####
// todo store in tile to remove search through self.marbles
self.influenced_direction = vec![(false, DirInfluence::None); self.marbles.len()];
for (i, &pos) in self.marbles.iter().enumerate() {
let Some(Tile::Marble { value: _, dir }) = self.grid.get(pos) else {
unreachable!()
};
let front_pos = dir.step(pos);
let Some(front_tile) = self.grid.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
self.influenced_direction[i].0 = true;
// the other marble will bounce too, either
let other_index =
self.marbles.iter().position(|m| *m == front_pos).unwrap();
let influence = &mut self.influenced_direction[other_index].1;
*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
self.influenced_direction[i].0 = true;
}
}
_ => (),
}
}
}
#[cfg_attr(feature = "inline_less", inline(never), no_mangle)]
fn step_apply_direct_bounces(&mut self) {
// #### apply all direct bounces ####
for (i, &pos) in self.marbles.iter().enumerate() {
let Some(Tile::Marble { value: _, dir }) = self.grid.get_mut(pos) else {
unreachable!()
};
if self.influenced_direction[i].0 {
*dir = dir.opposite();
} else if let DirInfluence::One(new_dir) = self.influenced_direction[i].1 {
*dir = new_dir;
}
}
}
#[cfg_attr(feature = "inline_less", inline(never), no_mangle)]
fn step_prepare_creating_marbles(&mut self) {
// #### new marbles ####
self.claim_positions.clear(); // already drained
// prepare creating the new marbles
for &(pos, _val, _dir, _is_input) in &self.new_marbles {
let Some(Tile::Open(OpenTile::Blank, claim)) = self.grid.get_mut(pos) else {
unreachable!()
};
if claim.claim_indirect() {
self.claim_positions.push(pos);
}
}
}
#[cfg_attr(feature = "inline_less", inline(never), no_mangle)]
fn step_create_marbles(&mut self) {
// create new marbles
let mut advance_input = false;
// new marbles are past old_marbles index, so will not move this step
for (pos, value, dir, is_input) in self.new_marbles.drain(..) {
let Some(Tile::Open(OpenTile::Blank, Claim::ClaimedIndirect)) = self.grid.get_mut(pos)
else {
continue;
};
advance_input |= is_input;
self.grid.set(pos, Tile::Marble { value, dir });
self.marbles.push(pos);
}
if advance_input {
self.input_index += 1;
}
}
#[cfg_attr(feature = "inline_less", inline(never), no_mangle)]
fn step_mark_claims(&mut self, old_marbles: usize) {
for &pos in &self.marbles[..old_marbles] {
let Some(Tile::Marble { value: _, dir }) = self.grid.get(pos) else {
unreachable!()
};
let front_pos = dir.step(pos);
let Some(front_tile) = self.grid.get_mut(front_pos) else {
continue;
};
if let Tile::Open(_type, claim) = front_tile {
if claim.claim() {
self.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.grid.get_mut(target_pos) else {
continue;
};
if let Tile::Open(_type, claim) = target_tile {
if claim.claim_indirect() {
self.claim_positions.push(front_pos);
}
}
}
}
}
#[cfg_attr(feature = "inline_less", inline(never), no_mangle)]
fn step_move_marbles(&mut self, old_marbles: usize) {
self.removed_marbles.clear();
// move marbles
for (i, pos) in self.marbles[..old_marbles].iter_mut().enumerate() {
let Some(Tile::Marble { value, dir }) = self.grid.get(*pos) else {
unreachable!()
};
let front_pos = dir.step(*pos);
let Some(front_tile) = self.grid.get_mut(front_pos) else {
continue;
};
let mut move_to = |tile, target_pos, dir, board: &mut Grid| {
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.grid);
} else if *claim_state != Claim::Free {
// (Free means a marble was just here but moved earlier this tick)
// bounce on failed direct movement
self.grid.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, _) => {
self.removed_marbles.push(i);
continue;
}
Tile::Powerable(PTile::IO, _) => {
self.removed_marbles.push(i);
self.output.push(value as u8);
continue;
}
_ => continue,
}
let Some(target_tile) = self.grid.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.grid);
if is_button {
self.powered.push(front_pos);
}
}
}
}
}
#[cfg_attr(feature = "inline_less", inline(never), no_mangle)]
fn step_propagate_power(&mut self) {
// propagate power
let mut i = 0;
while i < self.powered.len() {
let pos = self.powered[i];
let Some(tile) = self.grid.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.grid.get_mut(target_pos) {
Some(Tile::Powerable(_, state)) => {
if !state.get_dir(dir) {
state.add_dir(dir);
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);
}
}
_ => (),
}
}
#[cfg(debug_assertions)]
self.debug_subticks.push(DebugSubTick {
grid: self.grid.clone(),
pos: Some(pos),
});
}
Tile::Wire(wiretype, state) => {
*state = true;
for dir in wiretype.directions() {
let target_pos = dir.step(pos);
match self.grid.get_mut(target_pos) {
Some(Tile::Powerable(_, state)) => {
if !state.get_dir(*dir) {
state.add_dir(*dir);
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);
}
}
_ => (),
}
}
#[cfg(debug_assertions)]
self.debug_subticks.push(DebugSubTick {
grid: self.grid.clone(),
pos: Some(pos),
});
}
Tile::Powerable(PTile::Comparator(comp), state) => {
let comp = *comp;
let state = *state;
for dir in Direction::ALL {
if !state.get_dir(dir) {
continue;
}
let front_pos = dir.step(pos);
let Some(front_tile) = self.grid.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.grid.get_or_blank(pos_a).read_value();
let val_b = self.grid.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 {
match self.grid.get_mut(front_pos) {
Some(Tile::Powerable(_, state)) => {
if !state.get_dir(dir) {
state.add_dir(dir);
self.powered.push(front_pos);
}
}
Some(Tile::Wire(_, state)) => {
if !*state {
*state = true;
self.powered.push(front_pos);
}
}
_ => (),
}
}
}
}
#[cfg(debug_assertions)]
self.debug_subticks.push(DebugSubTick {
grid: self.grid.clone(),
pos: Some(pos),
});
}
Tile::Powerable(_, _state) => {
#[cfg(debug_assertions)]
self.debug_subticks.push(DebugSubTick {
grid: self.grid.clone(),
pos: Some(pos),
});
}
_ => {
dbg!(tile);
unreachable!()
}
}
i += 1;
}
}
}