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 logic mostly like https://git.crispypin.cc/CrispyPin/marble
## todo ## todo
make marble movement not order-dependent (`>ooo <` does not behave symmetrically) (more levels)
story/lore
blueprints blueprints
scroll level list 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?) decide on marble data size (u32 or byte?)
blueprint rotation 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 { #[derive(Debug, Clone, Copy)]
continue; enum Event {
}; Stay,
let next_pos = dir.step(marble_pos); /// (new_pos, new_dir)
if !self.board.pos_in_bounds(next_pos) { 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; continue;
} }
let mut new_tile = None; let marble = self.board.get_mut(self.marbles[i]).unwrap();
let Some(target) = self.board.get_mut(next_pos) else { let Tile::Marble { value, dir } = marble else {
continue; panic!("invalid marble");
}; };
match target { match event {
Tile::Blank => { Event::MoveTo(new_pos, new_dir) => {
*target = tile; let mut value = *value;
self.marbles[i] = next_pos; self.board.set(self.marbles[i], Tile::Blank);
new_tile = Some(Tile::Blank); self.marbles[i] = new_pos;
} let new_tile = self.board.get_mut(new_pos).unwrap();
Tile::Digit(d) => { if let Tile::Digit(n) = new_tile {
let new_val = value.wrapping_mul(10).wrapping_add(*d as MarbleValue); value = value.wrapping_mul(10).wrapping_add(*n as MarbleValue);
*target = Tile::Marble { }
value: new_val, *new_tile = Tile::Marble {
dir, 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 { // resolve deletions of marbles
*self.board.get_mut(marble_pos).unwrap() = t; 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 { // process triggers
self.marbles.remove(i - offset); for pos in triggers_activated {
offset += 1;
}
for pos in triggers {
for dir in Direction::ALL { for dir in Direction::ALL {
self.propagate_power(dir, dir.step(pos)); self.propagate_power(dir, dir.step(pos));
} }

View file

@ -118,10 +118,6 @@ impl Board {
sum sum
} }
pub fn pos_in_bounds(&self, p: Pos) -> bool {
self.in_bounds(p)
}
fn in_bounds(&self, p: Pos) -> bool { 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 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)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Solution { pub struct Solution {
solution_id: String, solution_id: String,
level_id: String, // redundant? level_id: String,
pub name: String, pub name: String,
pub board: String, pub board: String,
#[serde(default)] #[serde(default)]