diff --git a/README.md b/README.md index 280a5fe..2c68f81 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ logic mostly like https://git.crispypin.cc/CrispyPin/marble ## todo +- show level info in editor +- comments - accessibility - background colour setting - hotkeys for everything @@ -11,9 +13,7 @@ logic mostly like https://git.crispypin.cc/CrispyPin/marble - make direct power (comparator -> machine) work, (needs storing power direction in machine tiles) - cut selections, copy to system clipboard - timestamps in solutions and blueprints -- multiple input/output sets - tooltips -- show level info in editor - lock tile types for early levels to make it less overwhelming - display tool variant more clearly (it's not obvious there are more states) - option to use 8-bit marbles? @@ -60,9 +60,11 @@ logic mostly like https://git.crispypin.cc/CrispyPin/marble "sortorder": 5, "name": "Zeroes", "description": "learn how to output data", - "init_board": null, - "inputs": [], - "outputs": [0, 0, 0, 0, 0, 0, 0, 0], + "init_board": "", + "stages": [{ + "input": [], + "output": [0, 0, 0, 0, 0, 0, 0, 0], + }] } ``` ### solution diff --git a/levels/01_intro/01_output.json b/levels/01_intro/01_output.json index e2ef487..646a4e9 100644 --- a/levels/01_intro/01_output.json +++ b/levels/01_intro/01_output.json @@ -4,6 +4,12 @@ "name": "Zero", "description": "learn how to output data", "init_board": "\n o \n\n I\n\n", - "inputs": [], - "outputs": [0] + "stages": [ + { + "input": [], + "output": [ + 0 + ] + } + ] } \ No newline at end of file diff --git a/levels/01_intro/02_digits.json b/levels/01_intro/02_digits.json index 5f29471..d84bdbd 100644 --- a/levels/01_intro/02_digits.json +++ b/levels/01_intro/02_digits.json @@ -3,7 +3,8 @@ "sort_order": 12, "name": "Digits", "description": "place digits and use number keys to assign them values", - "init_board": null, - "inputs": [], - "outputs": [4, 8, 16] + "stages": [{ + "input": [], + "output": [4, 8, 16] + }] } \ No newline at end of file diff --git a/levels/01_intro/03_loop.json b/levels/01_intro/03_loop.json index e097900..3fb5c57 100644 --- a/levels/01_intro/03_loop.json +++ b/levels/01_intro/03_loop.json @@ -4,6 +4,8 @@ "name": "Loop", "description": "repeated output", "init_board": "\n \n o\n\n\n\n ^ \n\n", - "inputs": [], - "outputs": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + "stages": [{ + "input": [], + "output": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + }] } \ No newline at end of file diff --git a/levels/01_intro/04_copy_input.json b/levels/01_intro/04_copy_input.json index cf4d266..028bdb2 100644 --- a/levels/01_intro/04_copy_input.json +++ b/levels/01_intro/04_copy_input.json @@ -3,7 +3,8 @@ "sort_order": 14, "name": "Copy Cat", "description": "read input and output the same thing", - "init_board": null, - "inputs": "Hello, world!", - "outputs": "Hello, world!" + "stages": [{ + "input": "Hello, world!", + "output": "Hello, world!" + }] } \ No newline at end of file diff --git a/levels/01_intro/05_copy_odd.json b/levels/01_intro/05_copy_odd.json index 7db2780..26d25d9 100644 --- a/levels/01_intro/05_copy_odd.json +++ b/levels/01_intro/05_copy_odd.json @@ -3,7 +3,8 @@ "sort_order": 15, "name": "Odd Cat", "description": "copy only the odd numbers from the input", - "init_board": null, - "inputs": [112, 92, 51, 79, 112, 96, 84, 59, 195, 208, 137, 196, 68, 204, 82, 148, 251, 56, 105, 38, 63, 204, 240, 220, 180, 54, 211, 17, 82, 17, 181, 43], - "outputs": [51, 79, 59, 195, 137, 251, 105, 63, 211, 17, 17, 181, 43] + "stages": [{ + "input": [112, 92, 51, 79, 112, 96, 84, 59, 195, 208, 137, 196, 68, 204, 82, 148, 251, 56, 105, 38, 63, 204, 240, 220, 180, 54, 211, 17, 82, 17, 181, 43], + "output": [51, 79, 59, 195, 137, 251, 105, 63, 211, 17, 17, 181, 43] + }] } \ No newline at end of file diff --git a/levels/02_lists/count_fives.json b/levels/02_lists/count_fives.json index 965a0ca..8a9549f 100644 --- a/levels/02_lists/count_fives.json +++ b/levels/02_lists/count_fives.json @@ -3,7 +3,8 @@ "sort_order": 23, "name": "Fives", "description": "count how many fives are in the input", - "init_board": null, - "inputs": [182, 236, 71, 5, 5, 242, 29, 99, 19, 230, 217, 5, 67, 5, 223, 224, 70, 243, 3, 74, 242, 5, 171, 31, 96, 5, 169, 70, 5, 163, 72, 5, 172, 148, 5, 208, 28, 220, 17, 184, 172, 238, 5, 105, 119, 5, 106, 100, 73, 53, 42, 221, 155, 5, 74, 100, 161, 36, 16, 239, 193, 164, 64, 162, 222, 155, 107, 14, 45, 52, 159, 31, 199, 124, 129, 0], - "outputs": [12] + "stages": [{ + "input": [182, 236, 71, 5, 5, 242, 29, 99, 19, 230, 217, 5, 67, 5, 223, 224, 70, 243, 3, 74, 242, 5, 171, 31, 96, 5, 169, 70, 5, 163, 72, 5, 172, 148, 5, 208, 28, 220, 17, 184, 172, 238, 5, 105, 119, 5, 106, 100, 73, 53, 42, 221, 155, 5, 74, 100, 161, 36, 16, 239, 193, 164, 64, 162, 222, 155, 107, 14, 45, 52, 159, 31, 199, 124, 129, 0], + "output": [12] +}] } \ No newline at end of file diff --git a/levels/02_lists/list_length.json b/levels/02_lists/list_length.json index 3acd749..3a6d8b8 100644 --- a/levels/02_lists/list_length.json +++ b/levels/02_lists/list_length.json @@ -3,7 +3,8 @@ "sort_order": 22, "name": "Length", "description": "count how many numbers are in the input, until the first zero", - "init_board": null, - "inputs": [182, 236, 71, 5, 5, 242, 29, 99, 19, 230, 217, 5, 67, 5, 223, 224, 70, 243, 3, 74, 242, 5, 171, 31, 96, 5, 169, 70, 5, 163, 72, 5, 172, 148, 5, 208, 28, 220, 17, 184, 172, 0], - "outputs": [41] + "stages": [{ + "input": [182, 236, 71, 5, 5, 242, 29, 99, 19, 230, 217, 5, 67, 5, 223, 224, 70, 243, 3, 74, 242, 5, 171, 31, 96, 5, 169, 70, 5, 163, 72, 5, 172, 148, 5, 208, 28, 220, 17, 184, 172, 0], + "output": [41] + }] } \ No newline at end of file diff --git a/levels/02_lists/null_separation.json b/levels/02_lists/null_separation.json index ea5280e..402912c 100644 --- a/levels/02_lists/null_separation.json +++ b/levels/02_lists/null_separation.json @@ -3,7 +3,8 @@ "sort_order": 21, "name": "Null Separation", "description": "output everything after the first zero in the input data", - "init_board": null, - "inputs": "9834726\u0000Hello, worlg!", - "outputs": "Hello, worlg!" + "stages": [{ + "input": "9834726\u0000Hello, worlg!", + "output": "Hello, worlg!" + }] } \ No newline at end of file diff --git a/levels/02_lists/reverse_input.json b/levels/02_lists/reverse_input.json index d479e47..91ad03d 100644 --- a/levels/02_lists/reverse_input.json +++ b/levels/02_lists/reverse_input.json @@ -3,7 +3,8 @@ "sort_order": 24, "name": "Reverse", "description": "read input until zero and output the same thing in reverse", - "init_board": null, - "inputs": "tnropmi yrev\u0000", - "outputs": "very impornt" + "stages": [{ + "input": "tnropmi yrev\u0000", + "output": "very impornt" + }] } \ No newline at end of file diff --git a/levels/03_text/ascii_to_lower.json b/levels/03_text/ascii_to_lower.json index f1d095b..5c19e65 100644 --- a/levels/03_text/ascii_to_lower.json +++ b/levels/03_text/ascii_to_lower.json @@ -3,7 +3,8 @@ "sort_order": 35, "name": "Lowercase", "description": "Convert text to lowercase", - "init_board": null, - "inputs": "I CraVeD tHE strEnGTH AND CerTAinTy oF STeeL", - "outputs": "i craved the strength and certainty of steel" + "stages": [{ + "input": "I CraVeD tHE strEnGTH AND CerTAinTy oF STeeL", + "output": "i craved the strength and certainty of steel" + }] } \ No newline at end of file diff --git a/levels/03_text/output_decimal.json b/levels/03_text/output_decimal.json index 2f6f794..fcc388e 100644 --- a/levels/03_text/output_decimal.json +++ b/levels/03_text/output_decimal.json @@ -3,7 +3,8 @@ "sort_order": 31, "name": "Numbers", "description": "Convert input numbers to text, separated by spaces (32)\n'0' = 48, '1' = 49, '2' = 50, and so on", - "init_board": null, - "inputs": [85, 114, 32, 103, 97, 121, 58, 51], - "outputs": "85 114 32 103 97 121 58 51" + "stages": [{ + "input": [85, 114, 32, 103, 97, 121, 58, 51], + "output": "85 114 32 103 97 121 58 51" + }] } \ No newline at end of file diff --git a/levels/03_text/parse_decimal.json b/levels/03_text/parse_decimal.json index 1ccb3d1..ece2bef 100644 --- a/levels/03_text/parse_decimal.json +++ b/levels/03_text/parse_decimal.json @@ -3,7 +3,8 @@ "sort_order": 33, "name": "Numbers 2", "description": "Convert input numbers from text, separated by spaces (32)\n'0' = 48, '1' = 49, '2' = 50, and so on", - "init_board": null, - "inputs": "85 114 32 103 97 121 58 51", - "outputs": [85, 114, 32, 103, 97, 121, 58, 51] + "stages": [{ + "input": "85 114 32 103 97 121 58 51", + "output": [85, 114, 32, 103, 97, 121, 58, 51] + }] } \ No newline at end of file diff --git a/levels/sandbox.json b/levels/sandbox.json index d255bf0..cebfb5e 100644 --- a/levels/sandbox.json +++ b/levels/sandbox.json @@ -2,9 +2,5 @@ "id": "sandbox", "sort_order": 100000, "name": "Sandbox", - "description": "make whatever you want here", - "is_sandbox": true, - "init_board": null, - "inputs": "", - "outputs": [] + "description": "make whatever you want here" } \ No newline at end of file diff --git a/src/editor.rs b/src/editor.rs index 467a445..b6769c9 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -54,24 +54,29 @@ pub struct Editor { tool_mirror: MirrorType, tool_wire: WireType, input_text_selected: bool, + stage: Option, new_blueprint_name: String, blueprint_name_selected: bool, sim_speed: u8, time_since_step: f32, exit_state: ExitState, exit_menu: bool, + total_steps: usize, popup: EndPopup, score: Option, + blueprints: Vec, selected_blueprint: usize, blueprint_scroll: usize, - step_time: u128, - max_step_time: u128, - start_time: Instant, pasting_board: Option, + /// draw grid, directions and values of marbles draw_overlay: bool, undo_history: Vec, undo_index: usize, + // debug/profiling + step_time: u128, + max_step_time: u128, + start_time: Instant, } #[derive(Debug, Clone)] @@ -124,16 +129,28 @@ pub enum ExitState { impl Editor { pub fn new(solution: Solution, level: Level) -> Self { + let mut output_as_text = true; + let mut input_as_text = true; + let mut stage = None; + let mut machine = Machine::new_empty(); + if let Some(i) = level.stages().first() { + stage = Some(0); + output_as_text = i.output().is_text(); + input_as_text = i.input().is_text(); + machine.set_input(i.input().as_bytes().to_owned()); + } + Self { source_board: Board::parse(&solution.board), - machine: Machine::new_empty(level.inputs().to_owned(), 1), + machine, sim_state: SimState::Editing, view_offset: Vector2::zero(), zoom: 1., active_tool: Tool::None, - output_as_text: level.output_is_text(), - input_as_text: level.input_is_text(), + output_as_text, + input_as_text, input_text_selected: false, + stage, new_blueprint_name: String::new(), blueprint_name_selected: false, sim_speed: 3, @@ -148,6 +165,7 @@ impl Editor { exit_menu: false, popup: EndPopup::None, score: solution.score, + total_steps: 0, blueprints: get_blueprints(), selected_blueprint: usize::MAX, blueprint_scroll: 0, @@ -244,17 +262,29 @@ impl Editor { pos * TILE_TEXTURE_SIZE * self.zoom + self.view_offset } - fn start_sim(&mut self) { + fn init_sim(&mut self) { self.max_step_time = 0; + self.total_steps = 0; self.start_time = Instant::now(); + if !self.level.is_sandbox() { + self.stage = Some(0); + } + self.reset_machine(); + } + + fn reset_machine(&mut self) { self.machine.reset(); self.machine.set_board(self.source_board.clone()); + if let Some(i) = self.stage { + let bytes = self.level.stages()[i].input().as_bytes(); + self.machine.set_input(bytes.to_owned()); + } } fn step_pressed(&mut self) { match self.sim_state { SimState::Editing => { - self.start_sim(); + self.init_sim(); // self.step(); } SimState::Running => (), @@ -266,22 +296,31 @@ impl Editor { fn step(&mut self) { self.machine.step(); - if self.popup != EndPopup::None { - self.popup = EndPopup::Dismissed; - } - if !self.level.outputs().is_empty() && self.popup == EndPopup::None { - if self.level.outputs() == self.machine.output() { - self.popup = EndPopup::Success; - println!("completed in {:?}", self.start_time.elapsed()); - self.exit_state = ExitState::Save; - self.sim_state = SimState::Stepping; - self.score = Some(Score { - cycles: self.machine.step_count(), - tiles: self.source_board.count_tiles(), - }); - } else if !self.level.outputs().starts_with(self.machine.output()) { - self.popup = EndPopup::Failure; - self.sim_state = SimState::Stepping; + if let Some(i) = self.stage { + if self.popup != EndPopup::None { + self.popup = EndPopup::Dismissed; + } + let stage = &self.level.stages()[i]; + if self.popup == EndPopup::None { + if stage.output().as_bytes() == self.machine.output() { + if i < self.level.stages().len() { + self.stage = Some(i + 1); + self.total_steps += self.machine.step_count(); + self.reset_machine(); + } else { + self.popup = EndPopup::Success; + println!("completed in {:?}", self.start_time.elapsed()); + self.exit_state = ExitState::Save; + self.sim_state = SimState::Stepping; + self.score = Some(Score { + cycles: self.machine.step_count(), + tiles: self.source_board.count_tiles(), + }); + } + } else if !stage.output().as_bytes().starts_with(self.machine.output()) { + self.popup = EndPopup::Failure; + self.sim_state = SimState::Stepping; + } } } } @@ -439,7 +478,7 @@ impl Editor { if rl.is_key_pressed(KeyboardKey::KEY_ENTER) { match self.sim_state { SimState::Editing => { - self.start_sim(); + self.init_sim(); self.sim_state = SimState::Running; } SimState::Running => { @@ -654,32 +693,33 @@ impl Editor { d.draw_text("save", 90, 10, 20, Color::WHITE); } - if simple_button(d, 150, 4, 32, 32) { - self.undo() - } - let undo_icon = if self.undo_index > 0 { - "undo" - } else { - "undo_disabled" - }; - draw_scaled_texture(d, textures.get(undo_icon), 150, 4, 2.); + if self.sim_state == SimState::Editing { + if simple_button(d, 150, 4, 32, 32) { + self.undo() + } + let undo_icon = if self.undo_index > 0 { + "undo" + } else { + "undo_disabled" + }; + draw_scaled_texture(d, textures.get(undo_icon), 150, 4, 2.); - if simple_button(d, 186, 4, 32, 32) { - self.redo() + if simple_button(d, 186, 4, 32, 32) { + self.redo() + } + let redo_icon = if self.undo_index < self.undo_history.len() { + "redo" + } else { + "redo_disabled" + }; + draw_scaled_texture(d, textures.get(redo_icon), 186, 4, 2.); } - let redo_icon = if self.undo_index < self.undo_history.len() { - "redo" - } else { - "redo_disabled" - }; - draw_scaled_texture(d, textures.get(redo_icon), 186, 4, 2.); - simple_toggle_button(d, &mut self.draw_overlay, 223, 4, 32, 32, 4); match self.sim_state { SimState::Editing => { if simple_button(d, 260, 4, 32, 32) { - self.start_sim(); + self.init_sim(); self.sim_state = SimState::Running; } draw_scaled_texture(d, textures.get("play"), 260, 4, 2.); @@ -716,9 +756,20 @@ impl Editor { slider(d, &mut self.sim_speed, 0, MAX_SPEED_POWER, 368, 24, 48, 12); draw_usize(d, textures, self.machine.step_count(), 420, 4, 9, 2); + if self.stage > Some(0) { + draw_usize( + d, + textures, + self.total_steps + self.machine.step_count(), + 420, + 44, + 9, + 2, + ); + } - draw_usize(d, textures, self.step_time as usize, 540, 42, 9, 1); - draw_usize(d, textures, self.max_step_time as usize, 540, 60, 9, 1); + draw_usize(d, textures, self.step_time as usize, 260, 42, 9, 1); + draw_usize(d, textures, self.max_step_time as usize, 260, 60, 9, 1); d.draw_text("input:", 603, 8, 10, Color::WHITE); if simple_button(d, 600, 20, 35, 15) { @@ -936,32 +987,35 @@ impl Editor { for (box_index, index) in (output_start..output_end).enumerate() { let x = output_x + output_cell_width * box_index as i32; - let expected_byte = self.level.outputs().get(index); + let (mut top_color, mut bottom_color) = (BG_LIGHT, BG_MEDIUM); + let real_byte = self.machine.output().get(index); - let (top_color, bottom_color) = + if let Some(stage_index) = self.stage { + let stage = &self.level.stages()[stage_index]; + let expected_byte = stage.output().as_bytes().get(index); + if let (Some(&real_byte), Some(&expected_byte)) = (real_byte, expected_byte) { - if expected_byte == real_byte { + (top_color, bottom_color) = if expected_byte == real_byte { (Color::GREEN, Color::DARKGREEN) } else { (Color::RED, Color::DARKRED) - } - } else { - (BG_LIGHT, BG_MEDIUM) - }; - - d.draw_rectangle(x, y, output_cell_width - 5, 30, top_color); - d.draw_rectangle(x, y + 35, output_cell_width - 5, 30, bottom_color); - if let Some(&expected_byte) = expected_byte { - let top_text = if self.output_as_text - && (expected_byte.is_ascii_graphic() || expected_byte.is_ascii_whitespace()) - { - format!("{:?}", expected_byte as char) - } else { - format!("{expected_byte}") - }; - d.draw_text(&top_text, x + 2, y + 5, 20, Color::WHITE); + }; + } + d.draw_rectangle(x, y, output_cell_width - 5, 30, top_color); + if let Some(&expected_byte) = expected_byte { + let top_text = if self.output_as_text + && (expected_byte.is_ascii_graphic() || expected_byte.is_ascii_whitespace()) + { + format!("{:?}", expected_byte as char) + } else { + format!("{expected_byte}") + }; + d.draw_text(&top_text, x + 2, y + 5, 20, Color::WHITE); + } } + d.draw_rectangle(x, y + 35, output_cell_width - 5, 30, bottom_color); + if let Some(&real_byte) = real_byte { let bottom_text = if self.output_as_text && (real_byte.is_ascii_graphic() || real_byte.is_ascii_whitespace()) diff --git a/src/level.rs b/src/level.rs index b71551c..0a23ed8 100644 --- a/src/level.rs +++ b/src/level.rs @@ -7,10 +7,16 @@ pub struct Level { name: String, description: String, #[serde(default)] - is_sandbox: bool, init_board: Option, - inputs: IOData, - outputs: IOData, + /// no stages means sandbox + #[serde(default)] + stages: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct Stage { + input: IOData, + output: IOData, } #[derive(Debug, Clone, Deserialize)] @@ -27,6 +33,10 @@ impl IOData { IOData::Text(t) => t.as_bytes(), } } + + pub fn is_text(&self) -> bool { + matches!(self, IOData::Text(_)) + } } impl Level { @@ -47,26 +57,40 @@ impl Level { } pub fn is_sandbox(&self) -> bool { - self.is_sandbox + self.stages.is_empty() } pub fn init_board(&self) -> Option { self.init_board.clone() } - pub fn inputs(&self) -> &[u8] { - self.inputs.as_bytes() + pub fn stages(&self) -> &[Stage] { + &self.stages } - pub fn outputs(&self) -> &[u8] { - self.outputs.as_bytes() + // pub fn inputs(&self) -> &[u8] { + // self.inputs.as_bytes() + // } + + // pub fn outputs(&self) -> &[u8] { + // self.outputs.as_bytes() + // } + + // pub fn input_is_text(&self) -> bool { + // matches!(self.inputs, IOData::Text(_)) + // } + + // pub fn output_is_text(&self) -> bool { + // matches!(self.outputs, IOData::Text(_)) + // } +} + +impl Stage { + pub fn input(&self) -> &IOData { + &self.input } - pub fn input_is_text(&self) -> bool { - matches!(self.inputs, IOData::Text(_)) - } - - pub fn output_is_text(&self) -> bool { - matches!(self.outputs, IOData::Text(_)) + pub fn output(&self) -> &IOData { + &self.output } } diff --git a/src/marble_engine.rs b/src/marble_engine.rs index 422301e..32adb3e 100644 --- a/src/marble_engine.rs +++ b/src/marble_engine.rs @@ -21,12 +21,12 @@ pub struct Machine { } impl Machine { - pub fn new_empty(input: Vec, width: usize) -> Self { + pub fn new_empty() -> Self { Self { - board: Board::new_empty(width, width), + board: Board::new_empty(5, 5), marbles: Vec::new(), powered: Vec::new(), - input, + input: Vec::new(), input_index: 0, output: Vec::new(), steps: 0,