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

This commit is contained in:
Crispy 2024-10-08 21:51:31 +02:00
parent 82d0ff0f37
commit ae4e84bb90
4 changed files with 167 additions and 100 deletions

View file

@ -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<Event> = 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));
}

View file

@ -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
}

View file

@ -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)]