diff --git a/Cargo.lock b/Cargo.lock index fdeadbb..b6b34e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -192,6 +192,12 @@ dependencies = [ "either", ] +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + [[package]] name = "lazy_static" version = "1.5.0" @@ -247,6 +253,8 @@ name = "marble2" version = "0.1.0" dependencies = [ "raylib", + "serde", + "serde_json", ] [[package]] @@ -433,12 +441,50 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "serde" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "serde_json" +version = "1.0.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + [[package]] name = "shlex" version = "1.3.0" diff --git a/Cargo.toml b/Cargo.toml index 1c4bd62..2ac2d13 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,3 +5,5 @@ edition = "2021" [dependencies] raylib = "5.0.2" +serde = { version = "1.0.210", features = ["derive"] } +serde_json = "1.0.128" diff --git a/README.md b/README.md index 24d8313..2e361f1 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,20 @@ logic mostly like https://git.crispypin.cc/CrispyPin/marble -file hierarchy +## todo +level selection + +solution saving & loading +input/output display +grow grid automatically while editing +sim/speed control gui +make marble movement not order-dependent (`>ooo <` does not behave symmetrically) +blueprints + +decide on marble data size (u32 or byte?) +blueprint rotation + +## file hierarchy ``` - assets/ - storage/ @@ -28,27 +41,33 @@ file hierarchy `00_zeroes.json` ```json { - "id": "00_zeroes", - "name": "Zeroes", - "description": "learn how to output data", - "init_board": null, - "inputs": [], - "outputs": [0, 0, 0, 0, 0, 0, 0, 0] + "id": "00_zeroes", + "name": "Zeroes", + "description": "learn how to output data", + "init_board": null, + "inputs": [], + "outputs": [0, 0, 0, 0, 0, 0, 0, 0] } ``` `00_zeroes/solution_0.json` ```json { - "level_id": "00_zeroes", //redundant, useful if sharing solution files? - "name": "unnamed 1", - "board": "oo\nP*\n|-" + "level_id": "00_zeroes", //redundant, useful if sharing solution files? + "solution_id": "solution_0", + "name": "unnamed 1", + "board": "oo\nP*\n|-", + "score": { + "cycles": 8, + "tiles": 6, + "area": 6, + } } ``` `blueprints/blueprint_0.json` ```json { - "name": "fast printer", - "board": "oo\nP*\n|-" + "name": "fast printer", + "board": "oo\nP*\n|-" } ``` \ No newline at end of file diff --git a/levels/00_zeroes.json b/levels/00_zeroes.json new file mode 100644 index 0000000..42467fc --- /dev/null +++ b/levels/00_zeroes.json @@ -0,0 +1,8 @@ +{ + "id": "00_zeroes", + "name": "Zeroes", + "description": "learn how to output data", + "init_board": null, + "inputs": [], + "outputs": [0, 0, 0, 0, 0, 0, 0, 0] +} \ No newline at end of file diff --git a/levels/01_cat.json b/levels/01_cat.json new file mode 100644 index 0000000..ba72b4f --- /dev/null +++ b/levels/01_cat.json @@ -0,0 +1,8 @@ +{ + "id": "01_cat", + "name": "Copy Cat", + "description": "learn how to meow", + "init_board": null, + "inputs": [72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 103, 33], + "outputs": [72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 103, 33] +} \ No newline at end of file diff --git a/src/editor.rs b/src/editor.rs index b30b98d..1fd4ec2 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -3,17 +3,19 @@ use std::ops::Rem; use raylib::prelude::*; use crate::{ + level::Level, marble_engine::{ board::{Board, Pos}, tile::{Direction, GateType, MathOp, MirrorType, PTile, Tile, WireType}, Machine, }, - text_input, texture_button, Textures, + text_input, texture_option_button, Textures, }; #[derive(Debug)] pub struct Editor { source_board: Board, + level: Level, machine: Machine, sim_state: SimState, view_offset: Vector2, @@ -70,6 +72,7 @@ impl Editor { tool_menu_arrow: Direction::Right, tool_menu_mirror: MirrorType::Forward, tool_menu_wire: WireType::Vertical, + level: Level::new_sandbox(), } } @@ -188,6 +191,7 @@ impl Editor { } pub fn draw(&mut self, d: &mut RaylibDrawHandle, textures: &Textures) { + d.clear_background(Color::new(64, 64, 64, 255)); self.draw_board(d, textures); let height = d.get_screen_height(); @@ -242,7 +246,7 @@ impl Editor { let border = 4.; let gap = 2.; let bound_offset = 32. + gap * 2. + border * 2.; - texture_button( + texture_option_button( d, Vector2 { x: 320. + col as f32 * bound_offset - if col < 0 { 15. } else { 0. }, diff --git a/src/level.rs b/src/level.rs new file mode 100644 index 0000000..f6d19ac --- /dev/null +++ b/src/level.rs @@ -0,0 +1,50 @@ +use serde::Deserialize; + +use crate::marble_engine::board::Board; + +#[derive(Debug, Deserialize)] +pub struct Level { + id: String, + name: String, + description: String, + init_board: Option, + inputs: Vec, + outputs: Vec, +} + +impl Level { + pub fn new_sandbox() -> Self { + Self { + id: "sandbox".into(), + name: "Sandbox".into(), + description: String::new(), + init_board: None, + inputs: Vec::new(), + outputs: Vec::new(), + } + } + + pub fn id(&self) -> &str { + &self.id + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn description(&self) -> &str { + &self.description + } + + pub fn init_board(&self) -> Option { + self.init_board.as_deref().map(Board::parse) + } + + pub fn inputs(&self) -> &[u8] { + &self.inputs + } + + pub fn outputs(&self) -> &[u8] { + &self.outputs + } +} diff --git a/src/main.rs b/src/main.rs index 78ef78f..a09c4d3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,31 @@ -use std::fs::read_to_string; +use std::{ + collections::HashMap, + fs::{read_dir, read_to_string}, +}; -use editor::Editor; -use marble_engine::board::Board; use raylib::prelude::*; mod editor; +mod level; mod marble_engine; +mod solution; mod util; + +use editor::Editor; +use level::Level; +use marble_engine::board::Board; +use solution::Solution; use util::*; +struct Game { + levels: Vec, + solutions: HashMap>, + open_editor: Option, + textures: Textures, + selected_level: usize, + selected_solution: usize, +} + fn main() { let (mut rl, thread) = raylib::init() .resizable() @@ -20,18 +37,156 @@ fn main() { rl.set_exit_key(None); rl.set_trace_log(TraceLogLevel::LOG_WARNING); - let mut textures = Textures::default(); - textures.load_dir("assets", &mut rl, &thread); - textures.load_dir("assets/tiles", &mut rl, &thread); + let mut game = Game::new(&mut rl, &thread); + game.run(&mut rl, &thread); + // let board = Board::parse(&read_to_string("boards/adder.mbl").unwrap()); + // game.load_board(board); +} - let mut game = Editor::new_sandbox(); - let board = Board::parse(&read_to_string("boards/adder.mbl").unwrap()); - game.load_board(board); +impl Game { + fn new(rl: &mut RaylibHandle, thread: &RaylibThread) -> Self { + let mut textures = Textures::default(); + textures.load_dir("assets", rl, &thread); + textures.load_dir("assets/tiles", rl, &thread); - while !rl.window_should_close() { - game.input(&rl); - let mut d = rl.begin_drawing(&thread); + Self { + levels: get_levels(), + solutions: HashMap::new(), + open_editor: None, + textures, + selected_level: 0, + selected_solution: 0, + } + } + + fn run(&mut self, rl: &mut RaylibHandle, thread: &RaylibThread) { + while !rl.window_should_close() { + let mut d = rl.begin_drawing(&thread); + if let Some(editor) = &mut self.open_editor { + editor.input(&d); + editor.draw(&mut d, &self.textures); + } else { + self.draw(&mut d); + } + } + } + + fn draw(&mut self, d: &mut RaylibDrawHandle) { d.clear_background(Color::new(64, 64, 64, 255)); - game.draw(&mut d, &textures); + let level_list_width = 320; + let screen_height = d.get_screen_height(); + d.draw_rectangle(0, 0, level_list_width, screen_height, Color::GRAY); + // let (a, b, c) = d.gui_scroll_panel( + // Rectangle { + // x: 10., + // y: 10., + // width: level_list_width as f32 - 20., + // height: screen_height as f32 - 40., + // }, + // Some(rstr!("text")), + // Rectangle{}, + // scroll, + // view, + // ); + + let clicked = d.is_mouse_button_pressed(MouseButton::MOUSE_BUTTON_LEFT); + let mouse_pos = d.get_mouse_position(); + + for (i, level) in self.levels.iter().enumerate() { + let level_entry_height = 48; + let y = 10 + i as i32 * level_entry_height; + let bounds = Rectangle { + x: 5., + y: y as f32 - 5., + width: level_list_width as f32 - 10., + height: level_entry_height as f32 - 5., + }; + if clicked && bounds.check_collision_point_rec(mouse_pos) && self.selected_level != i { + self.selected_solution = 0; + self.selected_level = i; + } + if self.selected_level == i { + d.draw_rectangle_rec(bounds, Color::DARKCYAN); + } + d.draw_text(level.name(), 10, y, 20, Color::WHITE); + let solution_count = self + .solutions + .get(level.id()) + .map(Vec::len) + .unwrap_or_default(); + let subtext = format!("solutions: {solution_count}"); + let subtext_color = if solution_count > 0 { + Color::GOLD + } else { + Color::LIGHTGRAY + }; + d.draw_text(&subtext, 10, y + 20, 10, subtext_color); + } + + if let Some(level) = self.levels.get(self.selected_level) { + d.draw_text(level.name(), level_list_width + 10, 10, 30, Color::CYAN); + d.draw_text(level.id(), level_list_width + 10, 40, 10, Color::GRAY); + + let mut y = 60; + if let Some(solutions) = self.solutions.get_mut(level.id()) { + let solution_entry_height = 40; + for (solution_index, solution) in solutions.iter().enumerate() { + simple_option_button( + d, + level_list_width + 10, + y, + 200, + solution_entry_height, + solution_index, + &mut self.selected_solution, + ); + let name_color = if solution.score.is_some() { + Color::LIME + } else { + Color::ORANGE + }; + d.draw_text(&solution.name, level_list_width + 15, y + 5, 20, name_color); + d.draw_text( + &solution.score_text(), + level_list_width + 15, + y + 25, + 10, + Color::WHITE, + ); + y += solution_entry_height + 10; + } + + // d.gui_button(bounds, text) + if simple_button(d, level_list_width + 10, y, 200, 30) { + let n = solutions.len(); + solutions.push(Solution::new(level.id().to_owned(), n)); + } + d.draw_text( + "new solution", + level_list_width + 15, + y + 5, + 20, + Color::WHITE, + ); + } else { + self.solutions.insert(level.id().to_owned(), Vec::new()); + } + } } } + +fn get_levels() -> Vec { + let mut levels = Vec::::new(); + for d in read_dir("levels").unwrap().flatten() { + let l = read_to_string(d.path()) + .ok() + .as_deref() + .map(|s| serde_json::from_str(s).ok()) + .flatten(); + if let Some(level) = l { + levels.push(level); + } + } + levels.sort_by(|a, b| a.id().cmp(b.id())); + levels +} diff --git a/src/marble_engine/board.rs b/src/marble_engine/board.rs index 82d9247..f042e95 100644 --- a/src/marble_engine/board.rs +++ b/src/marble_engine/board.rs @@ -10,7 +10,7 @@ pub struct Pos { } impl Pos { - pub const fn to_vec(&self) -> Vector2 { + pub const fn to_vec(self) -> Vector2 { Vector2 { x: self.x as f32, y: self.y as f32, diff --git a/src/solution.rs b/src/solution.rs new file mode 100644 index 0000000..199e148 --- /dev/null +++ b/src/solution.rs @@ -0,0 +1,40 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Solution { + solution_id: String, + level_id: String, // redundant? + pub name: String, + pub board: String, + pub score: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Score { + pub cycles: u32, + pub tiles: u32, + pub area: u32, +} + +impl Solution { + pub fn new(level_id: String, number: usize) -> Self { + Self { + solution_id: format!("solution_{number}"), + level_id, + name: format!("Unnamed {number}"), + board: " \n".repeat(20), // todo remove when auto resizing is implemented + // score: Some(Score { cycles: 5, tiles: 88, area: 987 }), + score: None, + } + } + pub fn score_text(&self) -> String { + if let Some(score) = &self.score { + format!( + "C: {} T: {} A: {}", + score.cycles, score.tiles, score.area + ) + } else { + "unsolved".into() + } + } +} diff --git a/src/util.rs b/src/util.rs index ea54f63..2a250a6 100644 --- a/src/util.rs +++ b/src/util.rs @@ -28,6 +28,58 @@ impl Textures { } } +pub fn simple_button(d: &mut RaylibDrawHandle, x: i32, y: i32, width: i32, height: i32) -> bool { + let mouse_pos = d.get_mouse_position(); + let bounds = Rectangle { + x: x as f32, + y: y as f32, + width: width as f32, + height: height as f32, + }; + let mut pressed = false; + let color = if bounds.check_collision_point_rec(mouse_pos) { + if d.is_mouse_button_pressed(MouseButton::MOUSE_BUTTON_LEFT) { + pressed = true; + } + Color::DARKCYAN + } else { + Color::GRAY + }; + d.draw_rectangle(x, y, width, height, color); + pressed +} + +pub fn simple_option_button( + d: &mut RaylibDrawHandle, + x: i32, + y: i32, + width: i32, + height: i32, + option: T, + current: &mut T, +) where + T: PartialEq, +{ + let color = if &option == current { + Color::DARKCYAN + } else { + Color::GRAY + }; + let bounds = Rectangle { + x: x as f32, + y: y as f32, + width: width as f32, + height: height as f32, + }; + let mouse_pos = d.get_mouse_position(); + if d.is_mouse_button_pressed(MouseButton::MOUSE_BUTTON_LEFT) + && bounds.check_collision_point_rec(mouse_pos) + { + *current = option; + } + d.draw_rectangle_rec(bounds, color); +} + pub fn text_input( d: &mut RaylibDrawHandle, bounds: Rectangle, @@ -78,7 +130,7 @@ pub fn text_input( changed } -pub fn texture_button( +pub fn texture_option_button( d: &mut RaylibDrawHandle, pos: Vector2, texture: &Texture2D,