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:
parent
82d0ff0f37
commit
ae4e84bb90
4 changed files with 167 additions and 100 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)]
|
||||
|
|
Loading…
Reference in a new issue