add comment storage to boards

This commit is contained in:
Crispy 2025-03-28 23:06:27 +01:00
parent 0b9f41cbf6
commit 5c48b531f6
13 changed files with 219 additions and 113 deletions

2
.gitignore vendored
View file

@ -1,4 +1,4 @@
/target /target
/user /user*
*.zip *.zip
version.txt version.txt

View file

@ -1,10 +1,6 @@
use std::time::Instant; use std::time::Instant;
use marble_machinations::{ use marble_machinations::{level::Level, marble_engine::Machine, solution::Solution};
level::Level,
marble_engine::{board::Board, Machine},
solution::Solution,
};
fn main() { fn main() {
aoc_2024_1a(); aoc_2024_1a();
@ -23,7 +19,7 @@ fn benchmark(level: &str, solution: &str) {
let solution: Solution = serde_json::from_str(solution).unwrap(); let solution: Solution = serde_json::from_str(solution).unwrap();
let cycle_count = solution.score.unwrap().cycles; let cycle_count = solution.score.unwrap().cycles;
let mut machine = Machine::new_empty(); let mut machine = Machine::new_empty();
machine.set_board(Board::parse(&solution.board)); machine.set_grid(solution.board.grid);
let start_time = Instant::now(); let start_time = Instant::now();
for (n, stage) in level.stages().iter().enumerate() { for (n, stage) in level.stages().iter().enumerate() {
machine.set_input(stage.input().as_bytes().to_owned()); machine.set_input(stage.input().as_bytes().to_owned());

View file

@ -6,24 +6,21 @@ use std::{
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{marble_engine::board::Board, util::userdata_dir}; use crate::{board::Board, util::userdata_dir};
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Blueprint { pub struct Blueprint {
id: usize, id: usize,
pub name: String, pub name: String,
pub board: String, pub board: Board,
#[serde(skip, default)]
tile_board: Option<Board>,
} }
impl Blueprint { impl Blueprint {
pub fn new(content: &Board, id: usize) -> Self { pub fn new(board: Board, id: usize) -> Self {
Self { Self {
id, id,
name: format!("Blueprint {id}"), name: format!("Blueprint {id}"),
board: content.serialize(), board,
tile_board: Some(content.clone()),
} }
} }
@ -31,17 +28,6 @@ impl Blueprint {
self.id self.id
} }
pub fn convert_board(&mut self) -> &Board {
if self.tile_board.is_none() {
self.tile_board = Some(Board::parse(&self.board));
}
self.tile_board.as_ref().unwrap()
}
pub fn get_board(&self) -> Option<&Board> {
self.tile_board.as_ref()
}
fn path(&self) -> PathBuf { fn path(&self) -> PathBuf {
let dir = userdata_dir().join("blueprints"); let dir = userdata_dir().join("blueprints");
fs::create_dir_all(&dir).unwrap(); fs::create_dir_all(&dir).unwrap();

104
src/board.rs Normal file
View file

@ -0,0 +1,104 @@
use serde::{Deserialize, Serialize};
use crate::marble_engine::{
grid::{Grid, ResizeDeltas},
pos::Pos,
tile::Tile,
};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Comment {
text: String,
pos: Pos,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(from = "CompatBoard")]
pub struct Board {
pub grid: Grid,
pub comments: Vec<Comment>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
enum CompatBoard {
V1(String),
V2 { grid: Grid, comments: Vec<Comment> },
}
impl From<CompatBoard> for Board {
fn from(value: CompatBoard) -> Self {
match value {
CompatBoard::V1(string) => Self {
grid: Grid::parse(&string),
comments: Vec::new(),
},
CompatBoard::V2 { grid, comments } => Self { grid, comments },
}
}
}
impl Default for Board {
fn default() -> Self {
Self {
grid: Grid::new_single(Tile::BLANK),
comments: Vec::new(),
}
}
}
impl Board {
pub fn new(grid: Grid) -> Self {
Self {
grid,
comments: Vec::new(),
}
}
pub fn single_tile(tile: Tile) -> Self {
Self {
grid: Grid::new_single(tile),
comments: Vec::new(),
}
}
pub fn get_rect(&self, pos: Pos, width: usize, height: usize) -> Self {
// TODO filter for comments in the area
let comments = self.comments.clone();
Self {
grid: self.grid.get_rect(pos, width, height),
comments,
}
}
pub fn paste_board(&mut self, pos: Pos, board: &Board) {
// TODO remove overlapping comments
self.grid.paste_grid(pos, &board.grid);
self.comments.extend_from_slice(&board.comments);
}
pub fn grow(&mut self, deltas: &ResizeDeltas) {
self.grid.grow(deltas);
// TODO adjust comments
}
pub fn shrink(&mut self, deltas: &ResizeDeltas) {
self.grid.shrink(deltas);
// TODO adjust comments
}
pub fn from_user_str(source: &str) -> Self {
serde_json::from_str(source).unwrap_or_else(|_| Self {
grid: Grid::parse(source),
comments: Vec::new(),
})
}
pub fn width(&self) -> usize {
self.grid.width()
}
pub fn height(&self) -> usize {
self.grid.height()
}
}

View file

@ -9,8 +9,9 @@ use raylib::prelude::*;
use crate::{ use crate::{
blueprint::Blueprint, blueprint::Blueprint,
board::Board,
level::Level, level::Level,
marble_engine::{board::*, pos::*, tile::*, Machine}, marble_engine::{grid::*, pos::*, tile::*, Machine},
solution::*, solution::*,
theme::*, theme::*,
ui::*, ui::*,
@ -141,7 +142,7 @@ impl Editor {
info_text.set_text(level.description()); info_text.set_text(level.description());
Self { Self {
source_board: Board::parse(&solution.board), source_board: solution.board.clone(),
machine, machine,
sim_state: SimState::Editing, sim_state: SimState::Editing,
view_offset: Vector2::zero(), view_offset: Vector2::zero(),
@ -280,7 +281,7 @@ impl Editor {
fn reset_machine(&mut self) { fn reset_machine(&mut self) {
self.machine.reset(); self.machine.reset();
self.machine.set_board(self.source_board.clone()); self.machine.set_grid(self.source_board.grid.clone());
if let Some(i) = self.stage { if let Some(i) = self.stage {
let bytes = self.level.stages()[i].input().as_bytes(); let bytes = self.level.stages()[i].input().as_bytes();
self.machine.set_input(bytes.to_owned()); self.machine.set_input(bytes.to_owned());
@ -320,7 +321,7 @@ impl Editor {
self.sim_state = SimState::Stepping; self.sim_state = SimState::Stepping;
self.score = Some(Score { self.score = Some(Score {
cycles: self.total_steps + self.machine.step_count(), cycles: self.total_steps + self.machine.step_count(),
tiles: self.source_board.count_tiles(), tiles: self.source_board.grid.count_tiles(),
}); });
} }
} else if !stage.output().as_bytes().starts_with(self.machine.output()) { } else if !stage.output().as_bytes().starts_with(self.machine.output()) {
@ -397,7 +398,7 @@ impl Editor {
fn save_blueprint(&mut self, selection: (Pos, Pos)) { fn save_blueprint(&mut self, selection: (Pos, Pos)) {
let board = self.get_selected_as_board(selection); let board = self.get_selected_as_board(selection);
let id = get_free_id(&self.blueprints, Blueprint::id); let id = get_free_id(&self.blueprints, Blueprint::id);
let mut blueprint = Blueprint::new(&board, id); let mut blueprint = Blueprint::new(board, id);
if !self.new_blueprint_name.is_empty() { if !self.new_blueprint_name.is_empty() {
blueprint.name.clone_from(&self.new_blueprint_name); blueprint.name.clone_from(&self.new_blueprint_name);
} }
@ -442,7 +443,7 @@ impl Editor {
} }
fn set_tile(&mut self, pos: Pos, tile: Tile) { fn set_tile(&mut self, pos: Pos, tile: Tile) {
self.set_area(pos, Board::new_single(tile)); self.set_area(pos, Board::single_tile(tile));
} }
pub fn update(&mut self, rl: &RaylibHandle) { pub fn update(&mut self, rl: &RaylibHandle) {
@ -548,7 +549,7 @@ impl Editor {
if rl.is_key_down(KeyboardKey::KEY_LEFT_CONTROL) { if rl.is_key_down(KeyboardKey::KEY_LEFT_CONTROL) {
if rl.is_key_pressed(KeyboardKey::KEY_V) { if rl.is_key_pressed(KeyboardKey::KEY_V) {
if let Ok(text) = rl.get_clipboard_text() { if let Ok(text) = rl.get_clipboard_text() {
let b = Board::parse(&text); let b = Board::from_user_str(&text);
self.pasting_board = Some(b); self.pasting_board = Some(b);
} }
} else if rl.is_key_pressed(KeyboardKey::KEY_Z) { } else if rl.is_key_pressed(KeyboardKey::KEY_Z) {
@ -563,17 +564,16 @@ impl Editor {
fn draw_board(&self, d: &mut RaylibDrawHandle, textures: &Textures) { fn draw_board(&self, d: &mut RaylibDrawHandle, textures: &Textures) {
if self.sim_state == SimState::Editing { if self.sim_state == SimState::Editing {
self.source_board self.source_board
.grid
.draw(d, textures, self.view_offset, self.zoom); .draw(d, textures, self.view_offset, self.zoom);
} else { } else {
if self.machine.debug_subticks.is_empty() { if self.machine.debug_subticks.is_empty() {
self.machine self.machine
.board() .grid()
.draw(d, textures, self.view_offset, self.zoom); .draw(d, textures, self.view_offset, self.zoom);
} else { } else {
let subframe = &self.machine.debug_subticks[self.machine.subtick_index]; let subframe = &self.machine.debug_subticks[self.machine.subtick_index];
subframe subframe.grid.draw(d, textures, self.view_offset, self.zoom);
.board
.draw(d, textures, self.view_offset, self.zoom);
if let Some(pos) = subframe.pos { if let Some(pos) = subframe.pos {
let p = self.pos_to_screen(pos.to_vec()); let p = self.pos_to_screen(pos.to_vec());
d.draw_texture_ex(textures.get("selection"), p, 0., self.zoom, Color::ORANGE); d.draw_texture_ex(textures.get("selection"), p, 0., self.zoom, Color::ORANGE);
@ -1037,8 +1037,10 @@ impl Editor {
if simple_button((d, &self.mouse), 232, y, 40, 40) { if simple_button((d, &self.mouse), 232, y, 40, 40) {
let min = selection.0.min(selection.1); let min = selection.0.min(selection.1);
let max = selection.0.max(selection.1); let max = selection.0.max(selection.1);
let board = let board = Board::new(Grid::new_empty(
Board::new_empty((max.x - min.x) as usize + 1, (max.y - min.y) as usize + 1); (max.x - min.x) as usize + 1,
(max.y - min.y) as usize + 1,
));
self.set_area(min, board); self.set_area(min, board);
} }
draw_scaled_texture(d, textures.get("eraser"), 236, y + 4, 2.); draw_scaled_texture(d, textures.get("eraser"), 236, y + 4, 2.);
@ -1262,7 +1264,8 @@ impl Editor {
offset.x -= offset.x.rem(tile_size); offset.x -= offset.x.rem(tile_size);
offset.y -= offset.y.rem(tile_size); offset.y -= offset.y.rem(tile_size);
offset += view_offset; offset += view_offset;
board.draw(d, textures, offset, self.zoom); board.grid.draw(d, textures, offset, self.zoom);
// TODO draw comments
if self.mouse.left_click() { if self.mouse.left_click() {
let tile_pos = (self.mouse.pos() - self.view_offset) / tile_size; let tile_pos = (self.mouse.pos() - self.view_offset) / tile_size;
let tile_pos = Vector2::new(tile_pos.x.floor(), tile_pos.y.floor()); let tile_pos = Vector2::new(tile_pos.x.floor(), tile_pos.y.floor());
@ -1355,7 +1358,7 @@ impl Editor {
Tool::Mirror => self.set_tile(pos, Tile::Mirror(self.tool_mirror)), Tool::Mirror => self.set_tile(pos, Tile::Mirror(self.tool_mirror)),
Tool::Digits(_pos) => { Tool::Digits(_pos) => {
self.active_tool = Tool::Digits(Some(pos)); self.active_tool = Tool::Digits(Some(pos));
let tile = self.source_board.get_or_blank(pos); let tile = self.source_board.grid.get_or_blank(pos);
if !matches!(tile, Tile::Open(OpenTile::Digit(_), _)) { if !matches!(tile, Tile::Open(OpenTile::Digit(_), _)) {
self.set_tile(pos, Tile::Open(OpenTile::Digit(0), Claim::Free)); self.set_tile(pos, Tile::Open(OpenTile::Digit(0), Claim::Free));
} }
@ -1363,8 +1366,7 @@ impl Editor {
Tool::Blueprint => { Tool::Blueprint => {
if self.mouse.pos().x > SIDEBAR_WIDTH as f32 { if self.mouse.pos().x > SIDEBAR_WIDTH as f32 {
if let Some(bp) = self.blueprints.get(self.selected_blueprint) { if let Some(bp) = self.blueprints.get(self.selected_blueprint) {
let board = bp.get_board().unwrap().clone(); self.set_area(pos, bp.board.clone());
self.set_area(pos, board);
} }
} }
} }
@ -1390,7 +1392,7 @@ impl Editor {
} }
} }
if let Tool::Blueprint = self.active_tool { if let Tool::Blueprint = self.active_tool {
if let Some(bp) = self.blueprints.get_mut(self.selected_blueprint) { if let Some(bp) = self.blueprints.get(self.selected_blueprint) {
let view_offset = Vector2::new( let view_offset = Vector2::new(
self.view_offset.x.rem(tile_size), self.view_offset.x.rem(tile_size),
self.view_offset.y.rem(tile_size), self.view_offset.y.rem(tile_size),
@ -1399,7 +1401,8 @@ impl Editor {
offset.x -= offset.x.rem(tile_size); offset.x -= offset.x.rem(tile_size);
offset.y -= offset.y.rem(tile_size); offset.y -= offset.y.rem(tile_size);
offset += view_offset; offset += view_offset;
bp.convert_board().draw(d, textures, offset, self.zoom); bp.board.grid.draw(d, textures, offset, self.zoom);
// TODO draw comments
} }
if self.mouse.pos().x < SIDEBAR_WIDTH as f32 { if self.mouse.pos().x < SIDEBAR_WIDTH as f32 {
if self.mouse.scroll() == Some(Scroll::Down) if self.mouse.scroll() == Some(Scroll::Down)

View file

@ -1,5 +1,7 @@
use serde::Deserialize; use serde::Deserialize;
use crate::board::Board;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct Chapter { pub struct Chapter {
pub title: String, pub title: String,
@ -12,7 +14,7 @@ pub struct Level {
name: String, name: String,
description: String, description: String,
#[serde(default)] #[serde(default)]
init_board: Option<String>, init_board: Option<Board>,
/// no stages means sandbox /// no stages means sandbox
#[serde(default)] #[serde(default)]
stages: Vec<Stage>, stages: Vec<Stage>,
@ -61,7 +63,7 @@ impl Level {
self.stages.is_empty() self.stages.is_empty()
} }
pub fn init_board(&self) -> Option<String> { pub fn init_board(&self) -> Option<Board> {
self.init_board.clone() self.init_board.clone()
} }

View file

@ -1,4 +1,5 @@
pub mod blueprint; pub mod blueprint;
pub mod board;
pub mod editor; pub mod editor;
pub mod level; pub mod level;
pub mod marble_engine; pub mod marble_engine;

View file

@ -56,7 +56,6 @@ impl Game {
let levels = get_levels(); let levels = get_levels();
let solutions = get_solutions(); let solutions = get_solutions();
let selected_solution = 0;
Self { Self {
levels, levels,
@ -65,7 +64,7 @@ impl Game {
open_editor: None, open_editor: None,
textures, textures,
selected_level: 0, selected_level: 0,
selected_solution, selected_solution: 0,
delete_solution: None, delete_solution: None,
editing_solution_name: false, editing_solution_name: false,
level_desc_text: ShapedText::new(20), level_desc_text: ShapedText::new(20),
@ -83,7 +82,7 @@ impl Game {
ExitState::ExitAndSave => { ExitState::ExitAndSave => {
let solution = &mut self.solutions.get_mut(editor.level_id()).unwrap() let solution = &mut self.solutions.get_mut(editor.level_id()).unwrap()
[self.selected_solution]; [self.selected_solution];
solution.board = editor.source_board().serialize(); solution.board = editor.source_board().clone();
solution.score = editor.score(); solution.score = editor.score();
solution.save(); solution.save();
self.open_editor = None; self.open_editor = None;
@ -91,7 +90,7 @@ impl Game {
ExitState::Save => { ExitState::Save => {
let solution = &mut self.solutions.get_mut(editor.level_id()).unwrap() let solution = &mut self.solutions.get_mut(editor.level_id()).unwrap()
[self.selected_solution]; [self.selected_solution];
solution.board = editor.source_board().serialize(); solution.board = editor.source_board().clone();
solution.score = editor.score(); solution.score = editor.score();
solution.save(); solution.save();
} }

View file

@ -1,16 +1,16 @@
use raylib::prelude::*; use raylib::prelude::*;
pub mod board; pub mod grid;
pub mod pos; pub mod pos;
pub mod tile; pub mod tile;
use crate::{theme::TILE_TEXTURE_SIZE, ui::draw_usize_small, util::Textures}; use crate::{theme::TILE_TEXTURE_SIZE, ui::draw_usize_small, util::Textures};
use board::Board; use grid::Grid;
use pos::*; use pos::*;
use tile::*; use tile::*;
#[derive(Debug)] #[derive(Debug)]
pub struct Machine { pub struct Machine {
board: Board, grid: Grid,
marbles: Vec<Pos>, marbles: Vec<Pos>,
powered: Vec<Pos>, powered: Vec<Pos>,
input: Vec<u8>, input: Vec<u8>,
@ -23,14 +23,14 @@ pub struct Machine {
#[derive(Debug)] #[derive(Debug)]
pub struct DebugSubTick { pub struct DebugSubTick {
pub board: Board, pub grid: Grid,
pub pos: Option<Pos>, pub pos: Option<Pos>,
} }
impl Machine { impl Machine {
pub fn new_empty() -> Self { pub fn new_empty() -> Self {
Self { Self {
board: Board::new_empty(5, 5), grid: Grid::new_empty(5, 5),
marbles: Vec::new(), marbles: Vec::new(),
powered: Vec::new(), powered: Vec::new(),
input: Vec::new(), input: Vec::new(),
@ -51,14 +51,14 @@ impl Machine {
self.subtick_index = 0; self.subtick_index = 0;
} }
pub fn set_board(&mut self, board: Board) { pub fn set_grid(&mut self, board: Grid) {
self.marbles = board.get_marbles(); self.marbles = board.get_marbles();
self.powered.clear(); self.powered.clear();
self.board = board; self.grid = board;
} }
pub fn board(&self) -> &Board { pub fn grid(&self) -> &Grid {
&self.board &self.grid
} }
pub fn output(&self) -> &[u8] { pub fn output(&self) -> &[u8] {
@ -93,7 +93,7 @@ impl Machine {
for marble in &self.marbles { for marble in &self.marbles {
let x = marble.x; let x = marble.x;
let y = marble.y; let y = marble.y;
if let Some(tile) = self.board.get(*marble) { if let Some(tile) = self.grid.get(*marble) {
let px = x as i32 * tile_size + offset.x as i32; let px = x as i32 * tile_size + offset.x as i32;
let py = y as i32 * tile_size + offset.y as i32; let py = y as i32 * tile_size + offset.y as i32;
if let Tile::Marble { value, dir } = tile { if let Tile::Marble { value, dir } = tile {
@ -114,7 +114,7 @@ impl Machine {
self.subtick_index = 0; self.subtick_index = 0;
self.debug_subticks.clear(); self.debug_subticks.clear();
self.debug_subticks.push(DebugSubTick { self.debug_subticks.push(DebugSubTick {
board: self.board.clone(), grid: self.grid.clone(),
pos: None, pos: None,
}); });
} }
@ -124,7 +124,7 @@ impl Machine {
let mut new_marbles = Vec::new(); let mut new_marbles = Vec::new();
// activate all powered machines // activate all powered machines
for &pos in &self.powered { for &pos in &self.powered {
match self.board.get_mut(pos) { match self.grid.get_mut(pos) {
Some(Tile::Powerable(PTile::Comparator(_), board_power_state)) => { Some(Tile::Powerable(PTile::Comparator(_), board_power_state)) => {
// already handled at the power propagation stage (end of sim step) // already handled at the power propagation stage (end of sim step)
*board_power_state = Power::OFF; *board_power_state = Power::OFF;
@ -138,7 +138,7 @@ impl Machine {
continue; continue;
} }
let front_pos = dir.step(pos); let front_pos = dir.step(pos);
let Some(front_tile) = self.board.get_mut(front_pos) else { let Some(front_tile) = self.grid.get_mut(front_pos) else {
continue; continue;
}; };
// `machine` is being powered, in direction `dir` // `machine` is being powered, in direction `dir`
@ -147,8 +147,8 @@ impl Machine {
if front_tile.is_blank() { if front_tile.is_blank() {
let pos_a = dir.left().step(pos); let pos_a = dir.left().step(pos);
let pos_b = dir.right().step(pos); let pos_b = dir.right().step(pos);
let val_a = self.board.get_or_blank(pos_a).read_value(); let val_a = self.grid.get_or_blank(pos_a).read_value();
let val_b = self.board.get_or_blank(pos_b).read_value(); let val_b = self.grid.get_or_blank(pos_b).read_value();
let value = match op { let value = match op {
MathOp::Add => val_a.wrapping_add(val_b), MathOp::Add => val_a.wrapping_add(val_b),
@ -215,11 +215,11 @@ impl Machine {
let mut influenced_direction = vec![DirInfluence::None; self.marbles.len()]; let mut influenced_direction = vec![DirInfluence::None; self.marbles.len()];
for (i, &pos) in self.marbles.iter().enumerate() { for (i, &pos) in self.marbles.iter().enumerate() {
let Some(Tile::Marble { value: _, dir }) = self.board.get(pos) else { let Some(Tile::Marble { value: _, dir }) = self.grid.get(pos) else {
unreachable!() 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.grid.get(front_pos) else {
continue; continue;
}; };
match front_tile { match front_tile {
@ -252,7 +252,7 @@ impl Machine {
} }
// #### apply all direct bounces #### // #### apply all direct bounces ####
for (i, &pos) in self.marbles.iter().enumerate() { for (i, &pos) in self.marbles.iter().enumerate() {
let Some(Tile::Marble { value: _, dir }) = self.board.get_mut(pos) else { let Some(Tile::Marble { value: _, dir }) = self.grid.get_mut(pos) else {
unreachable!() unreachable!()
}; };
if will_reverse_direction[i] { if will_reverse_direction[i] {
@ -266,7 +266,7 @@ impl Machine {
let mut claim_positions = Vec::new(); let mut claim_positions = Vec::new();
// prepare creating the new marbles // prepare creating the new marbles
for &(pos, _val, _dir) in &new_marbles { for &(pos, _val, _dir) in &new_marbles {
let Some(Tile::Open(OpenTile::Blank, claim)) = self.board.get_mut(pos) else { let Some(Tile::Open(OpenTile::Blank, claim)) = self.grid.get_mut(pos) else {
unreachable!() unreachable!()
}; };
if claim.claim_indirect() { if claim.claim_indirect() {
@ -276,22 +276,22 @@ impl Machine {
// create new marbles // create new marbles
// new marbles are past old_marbles index, so will not move this step // new marbles are past old_marbles index, so will not move this step
for (pos, value, dir) in new_marbles { for (pos, value, dir) in new_marbles {
let Some(Tile::Open(OpenTile::Blank, Claim::ClaimedIndirect)) = self.board.get_mut(pos) let Some(Tile::Open(OpenTile::Blank, Claim::ClaimedIndirect)) = self.grid.get_mut(pos)
else { else {
continue; continue;
}; };
self.board.set(pos, Tile::Marble { value, dir }); self.grid.set(pos, Tile::Marble { value, dir });
self.marbles.push(pos); self.marbles.push(pos);
} }
// #### movement #### // #### movement ####
// mark claims to figure out what spaces can be moved to // mark claims to figure out what spaces can be moved to
for &pos in &self.marbles[..old_marbles] { for &pos in &self.marbles[..old_marbles] {
let Some(Tile::Marble { value: _, dir }) = self.board.get(pos) else { let Some(Tile::Marble { value: _, dir }) = self.grid.get(pos) else {
unreachable!() unreachable!()
}; };
let front_pos = dir.step(pos); let front_pos = dir.step(pos);
let Some(front_tile) = self.board.get_mut(front_pos) else { let Some(front_tile) = self.grid.get_mut(front_pos) else {
continue; continue;
}; };
if let Tile::Open(_type, claim) = front_tile { if let Tile::Open(_type, claim) = front_tile {
@ -305,7 +305,7 @@ impl Machine {
Tile::Button(_) => dir.step(front_pos), Tile::Button(_) => dir.step(front_pos),
_ => continue, _ => continue,
}; };
let Some(target_tile) = self.board.get_mut(target_pos) else { let Some(target_tile) = self.grid.get_mut(target_pos) else {
continue; continue;
}; };
if let Tile::Open(_type, claim) = target_tile { if let Tile::Open(_type, claim) = target_tile {
@ -319,15 +319,15 @@ impl Machine {
let mut removed_marbles = Vec::new(); let mut removed_marbles = Vec::new();
// move marbles // move marbles
for (i, pos) in self.marbles[..old_marbles].iter_mut().enumerate() { for (i, pos) in self.marbles[..old_marbles].iter_mut().enumerate() {
let Some(Tile::Marble { value, dir }) = self.board.get(*pos) else { let Some(Tile::Marble { value, dir }) = self.grid.get(*pos) else {
unreachable!() unreachable!()
}; };
let front_pos = dir.step(*pos); let front_pos = dir.step(*pos);
let Some(front_tile) = self.board.get_mut(front_pos) else { let Some(front_tile) = self.grid.get_mut(front_pos) else {
continue; continue;
}; };
let mut move_to = |tile, target_pos, dir, board: &mut Board| { let mut move_to = |tile, target_pos, dir, board: &mut Grid| {
let value = match tile { let value = match tile {
OpenTile::Blank => value, OpenTile::Blank => value,
OpenTile::Digit(n) => value.wrapping_mul(10).wrapping_add(n as MarbleValue), OpenTile::Digit(n) => value.wrapping_mul(10).wrapping_add(n as MarbleValue),
@ -339,11 +339,11 @@ impl Machine {
if let Tile::Open(space_type, claim_state) = front_tile { if let Tile::Open(space_type, claim_state) = front_tile {
if *claim_state == Claim::Claimed { if *claim_state == Claim::Claimed {
move_to(*space_type, front_pos, dir, &mut self.board); move_to(*space_type, front_pos, dir, &mut self.grid);
} else if *claim_state != Claim::Free { } else if *claim_state != Claim::Free {
// (Free means a marble was just here but moved earlier this tick) // (Free means a marble was just here but moved earlier this tick)
// bounce on failed direct movement // bounce on failed direct movement
self.board.set( self.grid.set(
*pos, *pos,
Tile::Marble { Tile::Marble {
value, value,
@ -379,11 +379,11 @@ impl Machine {
} }
_ => continue, _ => continue,
} }
let Some(target_tile) = self.board.get_mut(target_pos) else { let Some(target_tile) = self.grid.get_mut(target_pos) else {
continue; continue;
}; };
if let Tile::Open(space_type, Claim::ClaimedIndirect) = target_tile { if let Tile::Open(space_type, Claim::ClaimedIndirect) = target_tile {
move_to(*space_type, target_pos, new_dir, &mut self.board); move_to(*space_type, target_pos, new_dir, &mut self.grid);
if is_button { if is_button {
self.powered.push(front_pos); self.powered.push(front_pos);
} }
@ -392,14 +392,14 @@ impl Machine {
} }
for pos in claim_positions { for pos in claim_positions {
if let Some(Tile::Open(_, claim_state)) = self.board.get_mut(pos) { if let Some(Tile::Open(_, claim_state)) = self.grid.get_mut(pos) {
*claim_state = Claim::Free; *claim_state = Claim::Free;
} }
} }
// remove marbles // remove marbles
for &i in removed_marbles.iter().rev() { for &i in removed_marbles.iter().rev() {
self.board.set(self.marbles[i], Tile::BLANK); self.grid.set(self.marbles[i], Tile::BLANK);
self.marbles.swap_remove(i); self.marbles.swap_remove(i);
} }
@ -407,7 +407,7 @@ impl Machine {
let mut i = 0; let mut i = 0;
while i < self.powered.len() { while i < self.powered.len() {
let pos = self.powered[i]; let pos = self.powered[i];
let Some(tile) = self.board.get_mut(pos) else { let Some(tile) = self.grid.get_mut(pos) else {
unreachable!() unreachable!()
}; };
match tile { match tile {
@ -415,7 +415,7 @@ impl Machine {
*state = true; *state = true;
for dir in Direction::ALL { for dir in Direction::ALL {
let target_pos = dir.step(pos); let target_pos = dir.step(pos);
match self.board.get_mut(target_pos) { match self.grid.get_mut(target_pos) {
Some(Tile::Powerable(_, state)) => { Some(Tile::Powerable(_, state)) => {
if !state.get_dir(dir) { if !state.get_dir(dir) {
state.add_dir(dir); state.add_dir(dir);
@ -434,7 +434,7 @@ impl Machine {
} }
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
self.debug_subticks.push(DebugSubTick { self.debug_subticks.push(DebugSubTick {
board: self.board.clone(), grid: self.grid.clone(),
pos: Some(pos), pos: Some(pos),
}); });
} }
@ -442,7 +442,7 @@ impl Machine {
*state = true; *state = true;
for dir in wiretype.directions() { for dir in wiretype.directions() {
let target_pos = dir.step(pos); let target_pos = dir.step(pos);
match self.board.get_mut(target_pos) { match self.grid.get_mut(target_pos) {
Some(Tile::Powerable(_, state)) => { Some(Tile::Powerable(_, state)) => {
if !state.get_dir(*dir) { if !state.get_dir(*dir) {
state.add_dir(*dir); state.add_dir(*dir);
@ -461,7 +461,7 @@ impl Machine {
} }
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
self.debug_subticks.push(DebugSubTick { self.debug_subticks.push(DebugSubTick {
board: self.board.clone(), grid: self.grid.clone(),
pos: Some(pos), pos: Some(pos),
}); });
} }
@ -473,14 +473,14 @@ impl Machine {
continue; continue;
} }
let front_pos = dir.step(pos); let front_pos = dir.step(pos);
let Some(front_tile) = self.board.get_mut(front_pos) else { let Some(front_tile) = self.grid.get_mut(front_pos) else {
continue; continue;
}; };
if matches!(front_tile, Tile::Wire(_, _) | Tile::Powerable(_, _)) { if matches!(front_tile, Tile::Wire(_, _) | Tile::Powerable(_, _)) {
let pos_a = dir.left().step(pos); let pos_a = dir.left().step(pos);
let pos_b = dir.right().step(pos); let pos_b = dir.right().step(pos);
let val_a = self.board.get_or_blank(pos_a).read_value(); let val_a = self.grid.get_or_blank(pos_a).read_value();
let val_b = self.board.get_or_blank(pos_b).read_value(); let val_b = self.grid.get_or_blank(pos_b).read_value();
let result = match comp { let result = match comp {
Comparison::LessThan => val_a < val_b, Comparison::LessThan => val_a < val_b,
@ -489,7 +489,7 @@ impl Machine {
Comparison::NotEqual => val_a != val_b, Comparison::NotEqual => val_a != val_b,
}; };
if result { if result {
match self.board.get_mut(front_pos) { match self.grid.get_mut(front_pos) {
Some(Tile::Powerable(_, state)) => { Some(Tile::Powerable(_, state)) => {
if !state.get_dir(dir) { if !state.get_dir(dir) {
state.add_dir(dir); state.add_dir(dir);
@ -509,14 +509,14 @@ impl Machine {
} }
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
self.debug_subticks.push(DebugSubTick { self.debug_subticks.push(DebugSubTick {
board: self.board.clone(), grid: self.grid.clone(),
pos: Some(pos), pos: Some(pos),
}); });
} }
Tile::Powerable(_, _state) => { Tile::Powerable(_, _state) => {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
self.debug_subticks.push(DebugSubTick { self.debug_subticks.push(DebugSubTick {
board: self.board.clone(), grid: self.grid.clone(),
pos: Some(pos), pos: Some(pos),
}); });
} }

View file

@ -1,4 +1,5 @@
use raylib::prelude::*; use raylib::prelude::*;
use serde::{Deserialize, Serialize};
use super::{tile::*, Pos, PosInt}; use super::{tile::*, Pos, PosInt};
use crate::{ use crate::{
@ -6,8 +7,9 @@ use crate::{
util::{draw_scaled_texture, Textures}, util::{draw_scaled_texture, Textures},
}; };
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Board { #[serde(into = "String", from = "String")]
pub struct Grid {
tiles: Vec<Tile>, tiles: Vec<Tile>,
width: usize, width: usize,
height: usize, height: usize,
@ -21,7 +23,7 @@ pub struct ResizeDeltas {
pub y_neg: usize, pub y_neg: usize,
} }
impl Board { impl Grid {
pub fn parse(source: &str) -> Self { pub fn parse(source: &str) -> Self {
let mut rows = Vec::new(); let mut rows = Vec::new();
@ -48,7 +50,7 @@ impl Board {
} }
} }
pub fn serialize(&self) -> String { pub fn to_str(&self) -> String {
let mut out = String::new(); let mut out = String::new();
for y in 0..self.height { for y in 0..self.height {
for x in 0..self.width { for x in 0..self.width {
@ -125,7 +127,7 @@ impl Board {
} }
} }
pub fn paste_board(&mut self, pos: Pos, source: &Board) { pub fn paste_grid(&mut self, pos: Pos, source: &Grid) {
for x in 0..source.width() { for x in 0..source.width() {
for y in 0..source.height() { for y in 0..source.height() {
let offset = (x, y).into(); let offset = (x, y).into();
@ -136,8 +138,8 @@ impl Board {
} }
} }
pub fn get_rect(&self, pos: Pos, width: usize, height: usize) -> Board { pub fn get_rect(&self, pos: Pos, width: usize, height: usize) -> Grid {
let mut out = Board::new_empty(width, height); let mut out = Grid::new_empty(width, height);
for x in 0..width { for x in 0..width {
for y in 0..height { for y in 0..height {
let offset = (x, y).into(); let offset = (x, y).into();
@ -152,27 +154,27 @@ impl Board {
pub fn grow(&mut self, deltas: &ResizeDeltas) { pub fn grow(&mut self, deltas: &ResizeDeltas) {
let new_width = self.width + deltas.x_neg + deltas.x_pos; let new_width = self.width + deltas.x_neg + deltas.x_pos;
let new_height = self.height + deltas.y_neg + deltas.y_pos; let new_height = self.height + deltas.y_neg + deltas.y_pos;
let mut new_board = Board::new_empty(new_width, new_height); let mut new_grid = Grid::new_empty(new_width, new_height);
for x in 0..self.width { for x in 0..self.width {
for y in 0..self.height { for y in 0..self.height {
let tile = self.get_unchecked((x, y).into()); let tile = self.get_unchecked((x, y).into());
new_board.set((x + deltas.x_neg, y + deltas.y_neg).into(), tile); new_grid.set((x + deltas.x_neg, y + deltas.y_neg).into(), tile);
} }
} }
*self = new_board; *self = new_grid;
} }
pub fn shrink(&mut self, deltas: &ResizeDeltas) { pub fn shrink(&mut self, deltas: &ResizeDeltas) {
let new_width = self.width - deltas.x_neg - deltas.x_pos; let new_width = self.width - deltas.x_neg - deltas.x_pos;
let new_height = self.height - deltas.y_neg - deltas.y_pos; let new_height = self.height - deltas.y_neg - deltas.y_pos;
let mut new_board = Board::new_empty(new_width, new_height); let mut new_grid = Grid::new_empty(new_width, new_height);
for x in 0..new_width { for x in 0..new_width {
for y in 0..new_height { for y in 0..new_height {
let tile = self.get_unchecked((x + deltas.x_neg, y + deltas.y_neg).into()); let tile = self.get_unchecked((x + deltas.x_neg, y + deltas.y_neg).into());
new_board.set((x, y).into(), tile); new_grid.set((x, y).into(), tile);
} }
} }
*self = new_board; *self = new_grid;
} }
pub fn width(&self) -> usize { pub fn width(&self) -> usize {
@ -231,3 +233,15 @@ impl Board {
} }
} }
} }
impl From<String> for Grid {
fn from(value: String) -> Self {
Self::parse(&value)
}
}
impl From<Grid> for String {
fn from(val: Grid) -> String {
val.to_str()
}
}

View file

@ -1,10 +1,11 @@
use std::ops::Add; use std::ops::Add;
use raylib::prelude::*; use raylib::prelude::*;
use serde::{Deserialize, Serialize};
pub type PosInt = i16; pub type PosInt = i16;
#[derive(Debug, Default, Clone, Copy, PartialEq)] #[derive(Debug, Default, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Pos { pub struct Pos {
pub x: PosInt, pub x: PosInt,
pub y: PosInt, pub y: PosInt,

View file

@ -6,14 +6,14 @@ use std::{
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{level::Level, util::userdata_dir}; use crate::{board::Board, level::Level, util::userdata_dir};
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Solution { pub struct Solution {
solution_id: usize, solution_id: usize,
level_id: String, level_id: String,
pub name: String, pub name: String,
pub board: String, pub board: Board,
#[serde(default)] #[serde(default)]
pub score: Option<Score>, pub score: Option<Score>,
} }
@ -30,7 +30,7 @@ impl Solution {
solution_id: id, solution_id: id,
level_id: level.id().to_owned(), level_id: level.id().to_owned(),
name: format!("Unnamed {id}"), name: format!("Unnamed {id}"),
board: level.init_board().unwrap_or(String::from(" ")), board: level.init_board().unwrap_or_default(),
score: None, score: None,
} }
} }

View file

@ -1,9 +1,9 @@
use marble_machinations::marble_engine::{board::Board, Machine}; use marble_machinations::marble_engine::{grid::Grid, Machine};
#[test] #[test]
fn creating_marbles_cause_indirect_claim() { fn creating_marbles_cause_indirect_claim() {
let mut eng = Machine::new_empty(); let mut eng = Machine::new_empty();
eng.set_board(Board::parse( eng.set_grid(Grid::parse(
" "
I I
o 2 o 2