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

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

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;
#[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 next_pos = dir.step(marble_pos);
if !self.board.pos_in_bounds(next_pos) {
continue;
}
let mut new_tile = None;
let Some(target) = self.board.get_mut(next_pos) else {
continue;
let front_pos = dir.step(pos);
let Some(front_tile) = self.board.get(front_pos) else {
return Event::Stay;
};
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,
};
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(),
});
}
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 far_pos = new_dir.step(next_pos);
if let Some(far_target) = self.board.get_blank_mut(far_pos) {
*far_target = Tile::Marble {
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 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,
};
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)]