diff --git a/README.md b/README.md index baa20b9..ebd3e6c 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ logic mostly like https://git.crispypin.cc/CrispyPin/marble ## todo - undo/redo - more levels +- make marble movement symmetric and order-independent - make power propagation not recursive - story/lore - cut selections, copy to system clipboard diff --git a/src/editor.rs b/src/editor.rs index dbbadd8..2120942 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -14,7 +14,7 @@ use crate::{ marble_engine::{ board::Board, pos::{Pos, PosInt}, - tile::{Direction, GateType, MarbleTarget, MathOp, MirrorType, OpenTile, PTile, Tile, WireType}, + tile::{Direction, GateType, MathOp, MirrorType, PTile, Tile, WireType}, Machine, }, simple_button, simple_option_button, slider, @@ -722,7 +722,7 @@ impl Editor { let max = selection.0.max(selection.1); for x in min.x..=max.x { for y in min.y..=max.y { - self.source_board.set(Pos { x, y }, Tile::default()); + self.source_board.set(Pos { x, y }, Tile::Blank); } } } @@ -935,7 +935,7 @@ impl Editor { let pos = *pos; for n in 0..10 { if d.is_key_pressed(unsafe { transmute::(b'0' as u32 + n) }) { - self.set_tile(pos, Tile::Open(OpenTile::Digit(n as u8), MarbleTarget::Free)); + self.set_tile(pos, Tile::Digit(n as u8)); } } } @@ -994,8 +994,9 @@ impl Editor { Tool::Digits(_pos) => { self.active_tool = Tool::Digits(Some(pos)); if let Some(tile) = self.source_board.get_mut(pos) { - if ! matches!(tile, Tile::Open(OpenTile::Digit(_), _)) { - *tile = Tile::Open(OpenTile::Digit(0), MarbleTarget::Free); + if let Tile::Digit(_) = tile { + } else { + *tile = Tile::Digit(0); } } } @@ -1018,7 +1019,7 @@ impl Editor { if d.is_mouse_button_down(MouseButton::MOUSE_BUTTON_LEFT) && self.active_tool == Tool::Erase { - self.set_tile(tile_pos.into(), Tile::default()) + self.set_tile(tile_pos.into(), Tile::Blank) } if let Tool::SelectArea(selection) = &mut self.active_tool { if d.is_mouse_button_down(MouseButton::MOUSE_BUTTON_LEFT) { diff --git a/src/marble_engine.rs b/src/marble_engine.rs index c1e36be..bb897fe 100644 --- a/src/marble_engine.rs +++ b/src/marble_engine.rs @@ -1,3 +1,5 @@ +use std::hint::unreachable_unchecked; + use raylib::prelude::*; pub mod board; @@ -14,6 +16,7 @@ pub struct Machine { board: Board, marbles: Vec, powered: Vec, + events: Vec, flipper_events: Vec, input: Vec, @@ -22,6 +25,19 @@ pub struct Machine { steps: usize, } +#[derive(Debug, Clone, Copy)] +enum Event { + Stay, + /// (new_pos, new_dir) + MoveTo(Pos, Direction), + /// (new_pos, new_dir, trigger_pos) + Trigger(Pos, Direction, Pos), + /// (other, new_dir) + /// other marble should be set to reverse of new_dir + /// and should be cancelled if it had a movement event planned + Bounce(usize, Direction), + Remove, +} impl Machine { pub fn new_empty(input: Vec, width: usize) -> Self { @@ -29,6 +45,7 @@ impl Machine { board: Board::new_empty(width, width), marbles: Vec::new(), powered: Vec::new(), + events: Vec::new(), flipper_events: Vec::new(), input, input_index: 0, @@ -41,12 +58,10 @@ impl Machine { 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; } @@ -106,7 +121,7 @@ impl Machine { // reset wires for &p in &self.powered { let Some(Tile::Powerable(_, state)) = self.board.get_mut(p) else { - unreachable!() + unsafe { unreachable_unchecked() } }; *state = false; } @@ -116,229 +131,169 @@ impl Machine { 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!() + self.events.clear(); + for &pos in &self.marbles { + let marble = self.board.get(pos).unwrap(); + let Tile::Marble { value, dir } = marble else { + panic!("broken marble"); }; let front_pos = dir.step(pos); let Some(front_tile) = self.board.get(front_pos) else { + self.events.push(Event::Stay); 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, + + if let Tile::Powerable(PTile::Bag, _) = front_tile { + self.events.push(Event::Remove); + continue; + } + if let Tile::Powerable(PTile::IO, _) = front_tile { + self.output.push(value as u8); + self.events.push(Event::Remove); + continue; + } + + let can_move_to = |tile| matches!(tile, Some(Tile::Blank | Tile::Digit(_))); + + let can_move_over = |tile| match tile { + Tile::Mirror(mirror) => { + let new_dir = mirror.new_dir(dir); + let target_pos = new_dir.step(front_pos); + let target = self.board.get(target_pos); + if can_move_to(target) { + Some((target_pos, new_dir)) + } else { + None + } + } + Tile::Arrow(new_dir) => { + let target_pos = new_dir.step(front_pos); + let target = self.board.get(target_pos); + if target_pos == pos || can_move_to(target) { + Some((target_pos, new_dir)) + } else { + None + } + } + _ => None, + }; + let e = if can_move_to(Some(front_tile)) { + Event::MoveTo(front_pos, dir) + } else if let Tile::Powerable(PTile::Trigger, _) = front_tile { + let target_pos = dir.step(front_pos); + let target = self.board.get(target_pos); + if can_move_to(target) { + Event::Trigger(target_pos, dir, front_pos) + } else { + Event::Stay + } + } else if let Some((new_pos, new_dir)) = can_move_over(front_tile) { + Event::MoveTo(new_pos, new_dir) + } else if let Tile::Marble { + value: _, + dir: other_dir, + } = front_tile + { + if other_dir != dir { + Event::Bounce( + self.marbles.iter().position(|m| m == &front_pos).unwrap(), + dir.opposite(), + ) + } else { + Event::Stay + } + } else { + Event::Stay + }; + self.events.push(e); + } + + // resolve bounces + for i in 0..self.events.len() { + let event = self.events[i]; + if let Event::Bounce(other_index, dir) = event { + match self.events[other_index] { + // cancel bounces on marble that are about to disappear + Event::Remove => { + self.events[i] = Event::MoveTo(self.marbles[other_index], dir.opposite()) + } + // let already bouncing marbles continue + Event::Bounce(_, _) => (), + // interrupt any other movement/staying to bounce + _ => self.events[other_index] = Event::Bounce(i, dir.opposite()), + } + } + } + + // resolve deletions of tiles + for (i, event) in self.events.iter().enumerate() { + if let Event::Remove = event { + self.board.set(self.marbles[i], Tile::Blank); + } + } + + // resolve triggers + let mut triggers_activated = Vec::new(); + for event in &mut self.events { + if let Event::Trigger(new_pos, dir, trigger_pos) = event { + triggers_activated.push(*trigger_pos); + *event = Event::MoveTo(*new_pos, *dir); + } + } + + // resolve collisions (multiple marbles entering the same space) + for i in 0..(self.events.len() - 1) { + let event = self.events[i]; + if let Event::MoveTo(new_pos, _dir) = event { + for other_event in &mut self.events[(i + 1)..] { + if let Event::MoveTo(other_pos, _other_dir) = other_event { + // todo: maybe sort by direction so the sucessful direction is consistent + if other_pos == &new_pos { + *other_event = Event::Stay; } } } - Tile::Arrow(arrow_dir) => { - if arrow_dir == dir.opposite() { - // bounce on a reverse facing arrow - will_reverse_direction[i] = true; + } + } + + // resolve movement + for (i, &event) in self.events.iter().enumerate() { + if let Event::Remove = event { + continue; + } + let marble = self.board.get_mut(self.marbles[i]).unwrap(); + let Tile::Marble { value, dir } = marble else { + panic!("invalid marble"); + }; + match event { + Event::MoveTo(new_pos, new_dir) => { + let mut value = *value; + self.board.set(self.marbles[i], Tile::Blank); + self.marbles[i] = new_pos; + let new_tile = self.board.get_mut(new_pos).unwrap(); + if let Tile::Digit(n) = new_tile { + value = value.wrapping_mul(10).wrapping_add(*n as MarbleValue); } + *new_tile = Tile::Marble { + value, + dir: new_dir, + }; } + Event::Bounce(_other, new_dir) => *dir = new_dir, _ => (), } } - // 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; - } - } - } - let mut claim_positions = Vec::new(); - // mark claims to figure out what spaces can be moved to - for &pos in &self.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 { - *claim = match claim { - MarbleTarget::Free => { - claim_positions.push(front_pos); - MarbleTarget::Claimed - } - MarbleTarget::ClaimedIndirect => MarbleTarget::Claimed, - MarbleTarget::BlockedIndirect => MarbleTarget::Claimed, - MarbleTarget::Claimed => MarbleTarget::Blocked, - MarbleTarget::Blocked => MarbleTarget::Blocked, - }; - } else { - let target_pos; - match front_tile { - Tile::Arrow(d) => { - target_pos = d.step(front_pos); - } - Tile::Mirror(m) => { - target_pos = m.new_dir(dir).step(front_pos); - } - Tile::Powerable(PTile::Trigger, _) => { - target_pos = 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 { - *claim = match claim { - MarbleTarget::Free => { - claim_positions.push(front_pos); - MarbleTarget::ClaimedIndirect - } - MarbleTarget::ClaimedIndirect => MarbleTarget::BlockedIndirect, - MarbleTarget::BlockedIndirect => MarbleTarget::BlockedIndirect, - MarbleTarget::Claimed => MarbleTarget::Claimed, - MarbleTarget::Blocked => MarbleTarget::Blocked, - }; - } + // resolve deletions of marbles + for (i, event) in self.events.iter().enumerate().rev() { + if let Event::Remove = event { + self.marbles.remove(i); } } - let mut triggers_activated = Vec::new(); - let mut removed_marbles = Vec::new(); - // move marbles - for (i, pos) in self.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; - }; - if let Tile::Open(space_type, claim_state) = front_tile { - if *claim_state == MarbleTarget::Claimed { - let value = match space_type { - OpenTile::Blank => value, - OpenTile::Digit(n) => { - value.wrapping_mul(10).wrapping_add(*n as MarbleValue) - } - }; - self.board.set(*pos, Tile::default()); - self.board.set(front_pos, Tile::Marble { value, dir }); - *pos = front_pos; - } else if *claim_state != MarbleTarget::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_trigger = 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::Powerable(PTile::Trigger, _) => { - is_trigger = true; - target_pos = dir.step(front_pos); - } - Tile::Powerable(PTile::Bag, _) => { - removed_marbles.push(i); - self.board.set(*pos, Tile::default()); - continue; - } - Tile::Powerable(PTile::IO, _) => { - removed_marbles.push(i); - self.board.set(*pos, Tile::default()); - 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, MarbleTarget::ClaimedIndirect) = target_tile { - let value = match space_type { - OpenTile::Blank => value, - OpenTile::Digit(n) => { - value.wrapping_mul(10).wrapping_add(*n as MarbleValue) - } - }; - self.board.set(*pos, Tile::default()); - self.board.set( - target_pos, - Tile::Marble { - value, - dir: new_dir, - }, - ); - *pos = target_pos; - if is_trigger { - triggers_activated.push(front_pos); - } - } - } - } - - for pos in claim_positions { - if let Some(Tile::Open(_, claim_state)) = self.board.get_mut(pos) { - *claim_state = MarbleTarget::Free; - } - } - - // remove marbles - for &i in removed_marbles.iter().rev() { - self.marbles.swap_remove(i); - } - // process triggers self.flipper_events.clear(); for pos in triggers_activated { - let Some(Tile::Powerable(PTile::Trigger, state)) = self.board.get_mut(pos) else { - unreachable!() - }; - self.powered.push(pos); - *state = true; for dir in Direction::ALL { self.propagate_power(dir, dir.step(pos)); } diff --git a/src/marble_engine/board.rs b/src/marble_engine/board.rs index 6052ff5..907e0db 100644 --- a/src/marble_engine/board.rs +++ b/src/marble_engine/board.rs @@ -26,7 +26,7 @@ impl Board { rows.push(tiles); } for line in &mut rows { - line.resize(width, Tile::default()); + line.resize(width, Tile::Blank); } Board::new(rows) @@ -44,7 +44,7 @@ impl Board { } pub fn new_empty(width: usize, height: usize) -> Self { - let rows = vec![vec![Tile::default(); width]; height]; + let rows = vec![vec![Tile::Blank; width]; height]; Self { rows, width, @@ -65,7 +65,7 @@ impl Board { for row in &self.rows { for tile in row { match tile { - Tile::Open(OpenTile::Blank, _) | Tile::Block => (), + Tile::Blank | Tile::Block => (), _ => sum += 1, } } @@ -104,7 +104,7 @@ impl Board { pub fn get_blank_mut(&mut self, p: Pos) -> Option<&mut Tile> { if self.in_bounds(p) { let tile = &mut self.rows[p.y as usize][p.x as usize]; - if let Tile::Open(OpenTile::Blank, _) = tile{ + if tile == &Tile::Blank { return Some(tile); } } @@ -147,7 +147,7 @@ impl Board { if p.x < 0 { let len = p.x.unsigned_abs() as usize; for row in &mut self.rows { - let mut new_row = vec![Tile::default(); len]; + let mut new_row = vec![Tile::Blank; len]; new_row.append(row); *row = new_row; } @@ -156,21 +156,21 @@ impl Board { } else if p.x as usize >= self.width { let new_width = p.x as usize + 1; for row in &mut self.rows { - row.resize(new_width, Tile::default()); + row.resize(new_width, Tile::Blank); } self.width = new_width; } if p.y < 0 { let len = p.y.unsigned_abs() as usize; - let mut new_rows = vec![vec![Tile::default(); self.width]; len]; + let mut new_rows = vec![vec![Tile::Blank; self.width]; len]; new_rows.append(&mut self.rows); self.rows = new_rows; offset_y = len; self.height += len; } else if p.y as usize >= self.height { let new_height = p.y as usize + 1; - self.rows.resize(new_height, vec![Tile::default(); self.width]); + self.rows.resize(new_height, vec![Tile::Blank; self.width]); self.height = new_height; } (offset_x as PosInt, offset_y as PosInt) diff --git a/src/marble_engine/tile.rs b/src/marble_engine/tile.rs index c18a8a9..89e4072 100644 --- a/src/marble_engine/tile.rs +++ b/src/marble_engine/tile.rs @@ -2,31 +2,21 @@ use crate::marble_engine::Pos; pub type MarbleValue = u32; -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Default, Clone, Copy, PartialEq)] pub enum Tile { - Open(OpenTile, MarbleTarget), + #[default] + Blank, Block, - Marble { value: MarbleValue, dir: Direction }, + Marble { + value: MarbleValue, + dir: Direction, + }, + Digit(u8), Mirror(MirrorType), Arrow(Direction), Powerable(PTile, bool), } -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum MarbleTarget { - Free, - ClaimedIndirect, - BlockedIndirect, - Claimed, - Blocked, -} - -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum OpenTile { - Blank, - Digit(u8), -} - #[derive(Debug, Clone, Copy, PartialEq)] pub enum PTile { Trigger, @@ -76,12 +66,6 @@ pub enum Direction { Right, } -impl Default for Tile { - fn default() -> Self { - Tile::Open(OpenTile::Blank, MarbleTarget::Free) - } -} - impl Tile { pub const fn from_char(c: char) -> Tile { match c { @@ -111,18 +95,19 @@ impl Tile { '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::Open(OpenTile::Digit(d as u8 - b'0'), MarbleTarget::Free), + d @ '0'..='9' => Tile::Digit(d as u8 - b'0'), '#' => Tile::Block, - _ => Tile::Open(OpenTile::Blank, MarbleTarget::Free), + ' ' => Tile::Blank, + _ => Tile::Blank, } } pub fn to_char(self) -> char { match self { - Tile::Open(OpenTile::Blank, _) => ' ', + Tile::Blank => ' ', Tile::Block => '#', Tile::Marble { value: _, dir: _ } => 'o', - Tile::Open(OpenTile::Digit(n), _) => (b'0' + n) as char, + Tile::Digit(n) => (b'0' + n) as char, Tile::Mirror(dir) => match dir { MirrorType::Forward => '/', MirrorType::Back => '\\', @@ -161,23 +146,23 @@ impl Tile { } pub fn is_blank(&self) -> bool { - matches!(self, Tile::Open(OpenTile::Blank, _)) + matches!(self, Tile::Blank) } pub fn read_value(&self) -> MarbleValue { match self { Tile::Marble { value, dir: _ } => *value, - Tile::Open(OpenTile::Digit(d), _) => *d as MarbleValue, + Tile::Digit(d) => *d as MarbleValue, _ => 0, } } pub fn texture(&self) -> String { match self { - Tile::Open(OpenTile::Blank, _) => "", + Tile::Blank => "", Tile::Block => "block", Tile::Marble { value: _, dir: _ } => "marble", - Tile::Open(OpenTile::Digit(n), _) => return format!("tile_digit_{n}"), + Tile::Digit(n) => return format!("tile_digit_{n}"), Tile::Mirror(mirror) => mirror.texture_name(), Tile::Arrow(dir) => dir.arrow_tile_texture_name(), Tile::Powerable(tile, state) => {