rewrite physics

This commit is contained in:
Crispy 2024-12-09 02:18:47 +01:00
parent 0275e0c9b5
commit 635f0cf641
3 changed files with 208 additions and 164 deletions

View file

@ -6,7 +6,6 @@ logic mostly like https://git.crispypin.cc/CrispyPin/marble
## todo ## todo
- undo/redo - undo/redo
- more levels - more levels
- make marble movement symmetric and order-independent
- make power propagation not recursive - make power propagation not recursive
- story/lore - story/lore
- cut selections, copy to system clipboard - cut selections, copy to system clipboard

View file

@ -1,5 +1,3 @@
use std::hint::unreachable_unchecked;
use raylib::prelude::*; use raylib::prelude::*;
pub mod board; pub mod board;
@ -16,7 +14,6 @@ pub struct Machine {
board: Board, board: Board,
marbles: Vec<Pos>, marbles: Vec<Pos>,
powered: Vec<Pos>, powered: Vec<Pos>,
events: Vec<Event>,
flipper_events: Vec<Pos>, flipper_events: Vec<Pos>,
input: Vec<u8>, input: Vec<u8>,
@ -25,19 +22,6 @@ pub struct Machine {
steps: usize, steps: usize,
} }
#[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,
}
impl Machine { impl Machine {
pub fn new_empty(input: Vec<u8>, width: usize) -> Self { pub fn new_empty(input: Vec<u8>, width: usize) -> Self {
@ -45,7 +29,6 @@ impl Machine {
board: Board::new_empty(width, width), board: Board::new_empty(width, width),
marbles: Vec::new(), marbles: Vec::new(),
powered: Vec::new(), powered: Vec::new(),
events: Vec::new(),
flipper_events: Vec::new(), flipper_events: Vec::new(),
input, input,
input_index: 0, input_index: 0,
@ -58,10 +41,12 @@ impl Machine {
self.steps = 0; self.steps = 0;
self.input_index = 0; self.input_index = 0;
self.output.clear(); self.output.clear();
self.powered.clear();
} }
pub fn set_board(&mut self, board: Board) { pub fn set_board(&mut self, board: Board) {
self.marbles = board.get_marbles(); self.marbles = board.get_marbles();
self.powered.clear();
self.board = board; self.board = board;
} }
@ -121,7 +106,7 @@ impl Machine {
// reset wires // reset wires
for &p in &self.powered { for &p in &self.powered {
let Some(Tile::Powerable(_, state)) = self.board.get_mut(p) else { let Some(Tile::Powerable(_, state)) = self.board.get_mut(p) else {
unsafe { unreachable_unchecked() } unreachable!()
}; };
*state = false; *state = false;
} }
@ -131,169 +116,229 @@ impl Machine {
return; return;
} }
self.events.clear(); #[derive(Clone, Copy, Debug)]
for &pos in &self.marbles { enum DirInfluence {
let marble = self.board.get(pos).unwrap(); None,
let Tile::Marble { value, dir } = marble else { One(Direction),
panic!("broken marble"); Multiple,
}
// find all direct bounces
let mut will_reverse_direction = vec![false; self.marbles.len()];
// todo store in tile to remove search through self.marbles
let mut influenced_direction = vec![DirInfluence::None; self.marbles.len()];
for (i, &pos) in self.marbles.iter().enumerate() {
let Some(Tile::Marble { value: _, dir }) = self.board.get(pos) else {
unreachable!()
}; };
let front_pos = dir.step(pos); let front_pos = dir.step(pos);
let Some(front_tile) = self.board.get(front_pos) else { let Some(front_tile) = self.board.get(front_pos) else {
self.events.push(Event::Stay);
continue; continue;
}; };
match front_tile {
if let Tile::Powerable(PTile::Bag, _) = front_tile { Tile::Marble {
self.events.push(Event::Remove); value: _,
continue; dir: other_dir,
} } => {
if let Tile::Powerable(PTile::IO, _) = front_tile { if other_dir != dir {
self.output.push(value as u8); // this marble is facing another marble, and will therefore definitely bounce
self.events.push(Event::Remove); will_reverse_direction[i] = true;
continue; // the other marble will bounce too, either
} let other_index =
self.marbles.iter().position(|m| *m == front_pos).unwrap();
let can_move_to = |tile| matches!(tile, Some(Tile::Open(_, _))); let influence = &mut influenced_direction[other_index];
*influence = match *influence {
let can_move_over = |tile| match tile { DirInfluence::None => DirInfluence::One(dir),
Tile::Mirror(mirror) => { DirInfluence::One(_) => DirInfluence::Multiple,
let new_dir = mirror.new_dir(dir); DirInfluence::Multiple => DirInfluence::Multiple,
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,
};
let e = 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
};
self.events.push(e);
}
// resolve bounces
for i in 0..self.events.len() {
let event = self.events[i];
if let Event::Bounce(other_index, dir) = event {
match self.events[other_index] {
// cancel bounces on marble that are about to disappear
Event::Remove => {
self.events[i] = Event::MoveTo(self.marbles[other_index], dir.opposite())
}
// let already bouncing marbles continue
Event::Bounce(_, _) => (),
// interrupt any other movement/staying to bounce
_ => self.events[other_index] = Event::Bounce(i, dir.opposite()),
}
}
}
// resolve deletions of tiles
for (i, event) in self.events.iter().enumerate() {
if let Event::Remove = event {
self.board.set(self.marbles[i], Tile::default());
}
}
// resolve triggers
let mut triggers_activated = Vec::new();
for event in &mut self.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..(self.events.len() - 1) {
let event = self.events[i];
if let Event::MoveTo(new_pos, _dir) = event {
for other_event in &mut self.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;
} }
} }
} }
} Tile::Arrow(arrow_dir) => {
} if arrow_dir == dir.opposite() {
// bounce on a reverse facing arrow
// resolve movement will_reverse_direction[i] = true;
for (i, &event) in self.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::default());
self.marbles[i] = new_pos;
let new_tile = self.board.get_mut(new_pos).unwrap();
if let Tile::Open(OpenTile::Digit(n), _) = new_tile {
value = value.wrapping_mul(10).wrapping_add(*n as MarbleValue);
} }
*new_tile = Tile::Marble {
value,
dir: new_dir,
};
} }
Event::Bounce(_other, new_dir) => *dir = new_dir,
_ => (), _ => (),
} }
} }
// apply all direct bounces
// resolve deletions of marbles for (i, &pos) in self.marbles.iter().enumerate() {
for (i, event) in self.events.iter().enumerate().rev() { let Some(Tile::Marble { value: _, dir }) = self.board.get_mut(pos) else {
if let Event::Remove = event { unreachable!()
self.marbles.remove(i); };
if will_reverse_direction[i] {
*dir = dir.opposite();
} else {
if let DirInfluence::One(new_dir) = influenced_direction[i] {
*dir = new_dir;
}
} }
} }
let mut claim_positions = Vec::new();
// mark claims to figure out what spaces can be moved to
for &pos in &self.marbles {
let Some(Tile::Marble { value: _, dir }) = self.board.get(pos) else {
unreachable!()
};
let front_pos = dir.step(pos);
let Some(front_tile) = self.board.get_mut(front_pos) else {
continue;
};
if let Tile::Open(_type, claim) = front_tile {
*claim = match claim {
MarbleTarget::Free => {
claim_positions.push(front_pos);
MarbleTarget::Claimed
}
MarbleTarget::ClaimedIndirect => MarbleTarget::Claimed,
MarbleTarget::BlockedIndirect => MarbleTarget::Claimed,
MarbleTarget::Claimed => MarbleTarget::Blocked,
MarbleTarget::Blocked => MarbleTarget::Blocked,
};
} else {
let target_pos;
match front_tile {
Tile::Arrow(d) => {
target_pos = d.step(front_pos);
}
Tile::Mirror(m) => {
target_pos = m.new_dir(dir).step(front_pos);
}
Tile::Powerable(PTile::Trigger, _) => {
target_pos = dir.step(front_pos);
}
_ => continue,
}
let Some(target_tile) = self.board.get_mut(target_pos) else {
continue;
};
if let Tile::Open(_type, claim) = target_tile {
*claim = match claim {
MarbleTarget::Free => {
claim_positions.push(front_pos);
MarbleTarget::ClaimedIndirect
}
MarbleTarget::ClaimedIndirect => MarbleTarget::BlockedIndirect,
MarbleTarget::BlockedIndirect => MarbleTarget::BlockedIndirect,
MarbleTarget::Claimed => MarbleTarget::Claimed,
MarbleTarget::Blocked => MarbleTarget::Blocked,
};
}
}
}
let mut triggers_activated = Vec::new();
let mut removed_marbles = Vec::new();
// move marbles
for (i, pos) in self.marbles.iter_mut().enumerate() {
let Some(Tile::Marble { value, dir }) = self.board.get(*pos) else {
unreachable!()
};
let front_pos = dir.step(*pos);
let Some(front_tile) = self.board.get_mut(front_pos) else {
continue;
};
if let Tile::Open(space_type, claim_state) = front_tile {
if *claim_state == MarbleTarget::Claimed {
let value = match space_type {
OpenTile::Blank => value,
OpenTile::Digit(n) => {
value.wrapping_mul(10).wrapping_add(*n as MarbleValue)
}
};
self.board.set(*pos, Tile::default());
self.board.set(front_pos, Tile::Marble { value, dir });
*pos = front_pos;
} else if *claim_state != MarbleTarget::Free {
// (Free means a marble was just here but moved earlier this tick)
// bounce on failed direct movement
self.board.set(
*pos,
Tile::Marble {
value,
dir: dir.opposite(),
},
);
}
} else {
let target_pos;
let mut is_trigger = false;
let mut new_dir = dir;
match front_tile {
Tile::Arrow(d) => {
target_pos = d.step(front_pos);
new_dir = *d;
}
Tile::Mirror(m) => {
new_dir = m.new_dir(dir);
target_pos = new_dir.step(front_pos);
}
Tile::Powerable(PTile::Trigger, _) => {
is_trigger = true;
target_pos = dir.step(front_pos);
}
Tile::Powerable(PTile::Bag, _) => {
removed_marbles.push(i);
self.board.set(*pos, Tile::default());
continue;
}
Tile::Powerable(PTile::IO, _) => {
removed_marbles.push(i);
self.board.set(*pos, Tile::default());
self.output.push(value as u8);
continue;
}
_ => continue,
}
let Some(target_tile) = self.board.get_mut(target_pos) else {
continue;
};
if let Tile::Open(space_type, MarbleTarget::ClaimedIndirect) = target_tile {
let value = match space_type {
OpenTile::Blank => value,
OpenTile::Digit(n) => {
value.wrapping_mul(10).wrapping_add(*n as MarbleValue)
}
};
self.board.set(*pos, Tile::default());
self.board.set(
target_pos,
Tile::Marble {
value,
dir: new_dir,
},
);
*pos = target_pos;
if is_trigger {
triggers_activated.push(front_pos);
}
}
}
}
for pos in claim_positions {
if let Some(Tile::Open(_, claim_state)) = self.board.get_mut(pos) {
*claim_state = MarbleTarget::Free;
}
}
// remove marbles
for &i in removed_marbles.iter().rev() {
self.marbles.swap_remove(i);
}
// process triggers // process triggers
self.flipper_events.clear(); self.flipper_events.clear();
for pos in triggers_activated { for pos in triggers_activated {
let Some(Tile::Powerable(PTile::Trigger, state)) = self.board.get_mut(pos) else {
unreachable!()
};
self.powered.push(pos);
*state = true;
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

@ -16,8 +16,8 @@ pub enum Tile {
pub enum MarbleTarget { pub enum MarbleTarget {
Free, Free,
ClaimedIndirect, ClaimedIndirect,
Claimed,
BlockedIndirect, BlockedIndirect,
Claimed,
Blocked, Blocked,
} }