Compare commits

...

2 commits

Author SHA1 Message Date
635f0cf641 rewrite physics 2024-12-09 02:18:47 +01:00
0275e0c9b5 store tile vacancy in grid 2024-12-08 23:16:45 +01:00
5 changed files with 253 additions and 195 deletions

View file

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

View file

@ -14,7 +14,7 @@ use crate::{
marble_engine::{
board::Board,
pos::{Pos, PosInt},
tile::{Direction, GateType, MathOp, MirrorType, PTile, Tile, WireType},
tile::{Direction, GateType, MarbleTarget, MathOp, MirrorType, OpenTile, PTile, Tile, WireType},
Machine,
},
simple_button, simple_option_button, slider,
@ -722,7 +722,7 @@ impl Editor {
let max = selection.0.max(selection.1);
for x in min.x..=max.x {
for y in min.y..=max.y {
self.source_board.set(Pos { x, y }, Tile::Blank);
self.source_board.set(Pos { x, y }, Tile::default());
}
}
}
@ -935,7 +935,7 @@ impl Editor {
let pos = *pos;
for n in 0..10 {
if d.is_key_pressed(unsafe { transmute::<u32, KeyboardKey>(b'0' as u32 + n) }) {
self.set_tile(pos, Tile::Digit(n as u8));
self.set_tile(pos, Tile::Open(OpenTile::Digit(n as u8), MarbleTarget::Free));
}
}
}
@ -994,9 +994,8 @@ impl Editor {
Tool::Digits(_pos) => {
self.active_tool = Tool::Digits(Some(pos));
if let Some(tile) = self.source_board.get_mut(pos) {
if let Tile::Digit(_) = tile {
} else {
*tile = Tile::Digit(0);
if ! matches!(tile, Tile::Open(OpenTile::Digit(_), _)) {
*tile = Tile::Open(OpenTile::Digit(0), MarbleTarget::Free);
}
}
}
@ -1019,7 +1018,7 @@ impl Editor {
if d.is_mouse_button_down(MouseButton::MOUSE_BUTTON_LEFT)
&& self.active_tool == Tool::Erase
{
self.set_tile(tile_pos.into(), Tile::Blank)
self.set_tile(tile_pos.into(), Tile::default())
}
if let Tool::SelectArea(selection) = &mut self.active_tool {
if d.is_mouse_button_down(MouseButton::MOUSE_BUTTON_LEFT) {

View file

@ -1,5 +1,3 @@
use std::hint::unreachable_unchecked;
use raylib::prelude::*;
pub mod board;
@ -16,7 +14,6 @@ pub struct Machine {
board: Board,
marbles: Vec<Pos>,
powered: Vec<Pos>,
events: Vec<Event>,
flipper_events: Vec<Pos>,
input: Vec<u8>,
@ -25,19 +22,6 @@ pub struct Machine {
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 {
pub fn new_empty(input: Vec<u8>, width: usize) -> Self {
@ -45,7 +29,6 @@ impl Machine {
board: Board::new_empty(width, width),
marbles: Vec::new(),
powered: Vec::new(),
events: Vec::new(),
flipper_events: Vec::new(),
input,
input_index: 0,
@ -58,10 +41,12 @@ impl Machine {
self.steps = 0;
self.input_index = 0;
self.output.clear();
self.powered.clear();
}
pub fn set_board(&mut self, board: Board) {
self.marbles = board.get_marbles();
self.powered.clear();
self.board = board;
}
@ -121,7 +106,7 @@ impl Machine {
// reset wires
for &p in &self.powered {
let Some(Tile::Powerable(_, state)) = self.board.get_mut(p) else {
unsafe { unreachable_unchecked() }
unreachable!()
};
*state = false;
}
@ -131,169 +116,229 @@ impl Machine {
return;
}
self.events.clear();
for &pos in &self.marbles {
let marble = self.board.get(pos).unwrap();
let Tile::Marble { value, dir } = marble else {
panic!("broken marble");
#[derive(Clone, Copy, Debug)]
enum DirInfluence {
None,
One(Direction),
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 Some(front_tile) = self.board.get(front_pos) else {
self.events.push(Event::Stay);
continue;
};
if let Tile::Powerable(PTile::Bag, _) = front_tile {
self.events.push(Event::Remove);
continue;
}
if let Tile::Powerable(PTile::IO, _) = front_tile {
self.output.push(value as u8);
self.events.push(Event::Remove);
continue;
}
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,
};
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::Blank);
}
}
// 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;
match front_tile {
Tile::Marble {
value: _,
dir: other_dir,
} => {
if other_dir != dir {
// this marble is facing another marble, and will therefore definitely bounce
will_reverse_direction[i] = true;
// the other marble will bounce too, either
let other_index =
self.marbles.iter().position(|m| *m == front_pos).unwrap();
let influence = &mut influenced_direction[other_index];
*influence = match *influence {
DirInfluence::None => DirInfluence::One(dir),
DirInfluence::One(_) => DirInfluence::Multiple,
DirInfluence::Multiple => DirInfluence::Multiple,
}
}
}
}
}
// resolve movement
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::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);
Tile::Arrow(arrow_dir) => {
if arrow_dir == dir.opposite() {
// bounce on a reverse facing arrow
will_reverse_direction[i] = true;
}
*new_tile = Tile::Marble {
value,
dir: new_dir,
};
}
Event::Bounce(_other, new_dir) => *dir = new_dir,
_ => (),
}
}
// resolve deletions of marbles
for (i, event) in self.events.iter().enumerate().rev() {
if let Event::Remove = event {
self.marbles.remove(i);
// apply all direct bounces
for (i, &pos) in self.marbles.iter().enumerate() {
let Some(Tile::Marble { value: _, dir }) = self.board.get_mut(pos) else {
unreachable!()
};
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
self.flipper_events.clear();
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 {
self.propagate_power(dir, dir.step(pos));
}

View file

@ -26,7 +26,7 @@ impl Board {
rows.push(tiles);
}
for line in &mut rows {
line.resize(width, Tile::Blank);
line.resize(width, Tile::default());
}
Board::new(rows)
@ -44,7 +44,7 @@ impl Board {
}
pub fn new_empty(width: usize, height: usize) -> Self {
let rows = vec![vec![Tile::Blank; width]; height];
let rows = vec![vec![Tile::default(); width]; height];
Self {
rows,
width,
@ -65,7 +65,7 @@ impl Board {
for row in &self.rows {
for tile in row {
match tile {
Tile::Blank | Tile::Block => (),
Tile::Open(OpenTile::Blank, _) | Tile::Block => (),
_ => sum += 1,
}
}
@ -104,7 +104,7 @@ impl Board {
pub fn get_blank_mut(&mut self, p: Pos) -> Option<&mut Tile> {
if self.in_bounds(p) {
let tile = &mut self.rows[p.y as usize][p.x as usize];
if tile == &Tile::Blank {
if let Tile::Open(OpenTile::Blank, _) = tile{
return Some(tile);
}
}
@ -147,7 +147,7 @@ impl Board {
if p.x < 0 {
let len = p.x.unsigned_abs() as usize;
for row in &mut self.rows {
let mut new_row = vec![Tile::Blank; len];
let mut new_row = vec![Tile::default(); len];
new_row.append(row);
*row = new_row;
}
@ -156,21 +156,21 @@ impl Board {
} else if p.x as usize >= self.width {
let new_width = p.x as usize + 1;
for row in &mut self.rows {
row.resize(new_width, Tile::Blank);
row.resize(new_width, Tile::default());
}
self.width = new_width;
}
if p.y < 0 {
let len = p.y.unsigned_abs() as usize;
let mut new_rows = vec![vec![Tile::Blank; self.width]; len];
let mut new_rows = vec![vec![Tile::default(); self.width]; len];
new_rows.append(&mut self.rows);
self.rows = new_rows;
offset_y = len;
self.height += len;
} else if p.y as usize >= self.height {
let new_height = p.y as usize + 1;
self.rows.resize(new_height, vec![Tile::Blank; self.width]);
self.rows.resize(new_height, vec![Tile::default(); self.width]);
self.height = new_height;
}
(offset_x as PosInt, offset_y as PosInt)

View file

@ -2,21 +2,31 @@ use crate::marble_engine::Pos;
pub type MarbleValue = u32;
#[derive(Debug, Default, Clone, Copy, PartialEq)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Tile {
#[default]
Blank,
Open(OpenTile, MarbleTarget),
Block,
Marble {
value: MarbleValue,
dir: Direction,
},
Digit(u8),
Marble { value: MarbleValue, dir: Direction },
Mirror(MirrorType),
Arrow(Direction),
Powerable(PTile, bool),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum MarbleTarget {
Free,
ClaimedIndirect,
BlockedIndirect,
Claimed,
Blocked,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum OpenTile {
Blank,
Digit(u8),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PTile {
Trigger,
@ -66,6 +76,12 @@ pub enum Direction {
Right,
}
impl Default for Tile {
fn default() -> Self {
Tile::Open(OpenTile::Blank, MarbleTarget::Free)
}
}
impl Tile {
pub const fn from_char(c: char) -> Tile {
match c {
@ -95,19 +111,18 @@ impl Tile {
'D' => Tile::Powerable(PTile::Math(MathOp::Div), false),
'R' => Tile::Powerable(PTile::Math(MathOp::Rem), false),
'B' => Tile::Powerable(PTile::Bag, false),
d @ '0'..='9' => Tile::Digit(d as u8 - b'0'),
d @ '0'..='9' => Tile::Open(OpenTile::Digit(d as u8 - b'0'), MarbleTarget::Free),
'#' => Tile::Block,
' ' => Tile::Blank,
_ => Tile::Blank,
_ => Tile::Open(OpenTile::Blank, MarbleTarget::Free),
}
}
pub fn to_char(self) -> char {
match self {
Tile::Blank => ' ',
Tile::Open(OpenTile::Blank, _) => ' ',
Tile::Block => '#',
Tile::Marble { value: _, dir: _ } => 'o',
Tile::Digit(n) => (b'0' + n) as char,
Tile::Open(OpenTile::Digit(n), _) => (b'0' + n) as char,
Tile::Mirror(dir) => match dir {
MirrorType::Forward => '/',
MirrorType::Back => '\\',
@ -146,23 +161,23 @@ impl Tile {
}
pub fn is_blank(&self) -> bool {
matches!(self, Tile::Blank)
matches!(self, Tile::Open(OpenTile::Blank, _))
}
pub fn read_value(&self) -> MarbleValue {
match self {
Tile::Marble { value, dir: _ } => *value,
Tile::Digit(d) => *d as MarbleValue,
Tile::Open(OpenTile::Digit(d), _) => *d as MarbleValue,
_ => 0,
}
}
pub fn texture(&self) -> String {
match self {
Tile::Blank => "",
Tile::Open(OpenTile::Blank, _) => "",
Tile::Block => "block",
Tile::Marble { value: _, dir: _ } => "marble",
Tile::Digit(n) => return format!("tile_digit_{n}"),
Tile::Open(OpenTile::Digit(n), _) => return format!("tile_digit_{n}"),
Tile::Mirror(mirror) => mirror.texture_name(),
Tile::Arrow(dir) => dir.arrow_tile_texture_name(),
Tile::Powerable(tile, state) => {