From ae4e84bb903bb9843a8cbfb8c1f319bb3f8cbc22 Mon Sep 17 00:00:00 2001 From: CrispyPin Date: Tue, 8 Oct 2024 21:51:31 +0200 Subject: [PATCH] reimplement marble behavior, making it more consistent in different directions. Triggers no longer activate from blocked marbles, and bounces take the same amount of time to travel through a chain of marbles in all directions --- README.md | 6 +- src/marble_engine.rs | 255 +++++++++++++++++++++++-------------- src/marble_engine/board.rs | 4 - src/solution.rs | 2 +- 4 files changed, 167 insertions(+), 100 deletions(-) diff --git a/README.md b/README.md index 6eb45db..0c6ee94 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,12 @@ logic mostly like https://git.crispypin.cc/CrispyPin/marble ## todo -make marble movement not order-dependent (`>ooo <` does not behave symmetrically) +(more levels) +story/lore blueprints scroll level list - +should the output tile consume marbles like the bag instead of needing power? then input and output could be merged to one tile type +make marble movement more consistent (`>o o<` depends on internal marble order) decide on marble data size (u32 or byte?) blueprint rotation diff --git a/src/marble_engine.rs b/src/marble_engine.rs index 2252505..2d5755c 100644 --- a/src/marble_engine.rs +++ b/src/marble_engine.rs @@ -108,110 +108,179 @@ impl Machine { } } } - 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); - let Tile::Marble { value, dir } = tile else { - continue; - }; - let next_pos = dir.step(marble_pos); - if !self.board.pos_in_bounds(next_pos) { + #[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, + } + + let mut marble_events: Vec = self + .marbles + .iter() + .map(|&pos| { + 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 { + return Event::Stay; + }; + + if let Tile::Powerable(PTile::Bag, _) = front_tile { + return Event::Remove; + } + + 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, + }; + + 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 + } + }) + .collect(); + + // resolve bounces + for i in 0..marble_events.len() { + let event = marble_events[i]; + if let Event::Bounce(other_index, dir) = event { + match marble_events[other_index] { + // cancel bounces on marble that are about to disappear + Event::Remove => { + marble_events[i] = Event::MoveTo(self.marbles[other_index], dir.opposite()) + } + // let already bouncing marbles continue + Event::Bounce(_, _) => (), + // interrupt any other movement/staying to bounce + _ => marble_events[other_index] = Event::Bounce(i, dir.opposite()), + } + } + } + + // resolve deletions of tiles + for (i, event) in marble_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 marble_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..(marble_events.len() - 1) { + let event = marble_events[i]; + if let Event::MoveTo(new_pos, _dir) = event { + for other_event in &mut marble_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; + } + } + } + } + } + + // resolve movement + for (i, &event) in marble_events.iter().enumerate() { + if let Event::Remove = event { continue; } - let mut new_tile = None; - let Some(target) = self.board.get_mut(next_pos) else { - continue; + let marble = self.board.get_mut(self.marbles[i]).unwrap(); + let Tile::Marble { value, dir } = marble else { + panic!("invalid marble"); }; - 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 as MarbleValue); - *target = Tile::Marble { - value: new_val, - dir, + 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, }; - 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) => { - // triggers activate even if the marble can't move past it - // making it stay on permanently (or until the marble is knocked away) - *state = true; - triggers.push(next_pos); - let far_pos = dir.step(next_pos); - if let Some(target) = self.board.get_blank_mut(far_pos) { - *target = tile; - 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(target) = self.board.get_blank_mut(far_pos) { - self.marbles[i] = far_pos; - *target = Tile::Marble { - value, - dir: arrow_dir, - }; - new_tile = Some(Tile::Blank); - } else if far_pos == marble_pos { - // always 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); - if let Some(far_target) = self.board.get_blank_mut(far_pos) { - *far_target = Tile::Marble { - value, - dir: new_dir, - }; - self.marbles[i] = far_pos; - new_tile = Some(Tile::Blank); - } } + Event::Bounce(_other, new_dir) => *dir = new_dir, _ => (), } + } - if let Some(t) = new_tile { - *self.board.get_mut(marble_pos).unwrap() = t; + // resolve deletions of marbles + for (i, event) in marble_events.iter().enumerate().rev() { + if let Event::Remove = event { + self.marbles.remove(i); } } - let mut offset = 0; - for i in to_remove { - self.marbles.remove(i - offset); - offset += 1; - } - for pos in triggers { + + // process triggers + for pos in triggers_activated { 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 f353179..0d96a95 100644 --- a/src/marble_engine/board.rs +++ b/src/marble_engine/board.rs @@ -118,10 +118,6 @@ impl Board { sum } - pub fn pos_in_bounds(&self, p: Pos) -> bool { - self.in_bounds(p) - } - fn in_bounds(&self, p: Pos) -> bool { p.x >= 0 && p.y >= 0 && p.x < self.width as isize && p.y < self.height as isize } diff --git a/src/solution.rs b/src/solution.rs index addbba9..12809d6 100644 --- a/src/solution.rs +++ b/src/solution.rs @@ -10,7 +10,7 @@ use crate::{level::Level, userdata_dir}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Solution { solution_id: String, - level_id: String, // redundant? + level_id: String, pub name: String, pub board: String, #[serde(default)]