diff --git a/README.md b/README.md index ebd3e6c..baa20b9 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,6 @@ 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/marble_engine.rs b/src/marble_engine.rs index 624079f..c1e36be 100644 --- a/src/marble_engine.rs +++ b/src/marble_engine.rs @@ -1,5 +1,3 @@ -use std::hint::unreachable_unchecked; - use raylib::prelude::*; pub mod board; @@ -16,7 +14,6 @@ pub struct Machine { board: Board, marbles: Vec, powered: Vec, - events: Vec, flipper_events: Vec, input: Vec, @@ -25,19 +22,6 @@ 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 { @@ -45,7 +29,6 @@ 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, @@ -58,10 +41,12 @@ 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; } @@ -121,7 +106,7 @@ impl Machine { // reset wires for &p in &self.powered { let Some(Tile::Powerable(_, state)) = self.board.get_mut(p) else { - unsafe { unreachable_unchecked() } + unreachable!() }; *state = false; } @@ -131,169 +116,229 @@ impl Machine { return; } - 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"); + #[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 { - self.events.push(Event::Stay); continue; }; - - 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::Open(_, _))); - - 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::default()); - } - } - - // 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; + 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, } } } - } - } - - // 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::default()); - self.marbles[i] = new_pos; - let new_tile = self.board.get_mut(new_pos).unwrap(); - if let Tile::Open(OpenTile::Digit(n), _) = new_tile { - value = value.wrapping_mul(10).wrapping_add(*n as MarbleValue); + Tile::Arrow(arrow_dir) => { + if arrow_dir == dir.opposite() { + // bounce on a reverse facing arrow + will_reverse_direction[i] = true; } - *new_tile = Tile::Marble { - value, - dir: new_dir, - }; } - Event::Bounce(_other, new_dir) => *dir = new_dir, _ => (), } } - - // resolve deletions of marbles - for (i, event) in self.events.iter().enumerate().rev() { - if let Event::Remove = event { - self.marbles.remove(i); + // 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, + }; + } + } + } + + 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/tile.rs b/src/marble_engine/tile.rs index fe430d7..c18a8a9 100644 --- a/src/marble_engine/tile.rs +++ b/src/marble_engine/tile.rs @@ -16,8 +16,8 @@ pub enum Tile { pub enum MarbleTarget { Free, ClaimedIndirect, - Claimed, BlockedIndirect, + Claimed, Blocked, }