From 310f93f587c54f34cefaf7d03c4690d47439c552 Mon Sep 17 00:00:00 2001 From: lakret Date: Tue, 27 Jun 2023 22:56:20 +0200 Subject: [PATCH 01/22] rename --- {aoc_examples => prototypes}/2016d13.ipynb | 0 {aoc_examples => prototypes}/2021d12.ipynb | 0 {aoc_examples => prototypes}/inputs/2021d12 | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename {aoc_examples => prototypes}/2016d13.ipynb (100%) rename {aoc_examples => prototypes}/2021d12.ipynb (100%) rename {aoc_examples => prototypes}/inputs/2021d12 (100%) diff --git a/aoc_examples/2016d13.ipynb b/prototypes/2016d13.ipynb similarity index 100% rename from aoc_examples/2016d13.ipynb rename to prototypes/2016d13.ipynb diff --git a/aoc_examples/2021d12.ipynb b/prototypes/2021d12.ipynb similarity index 100% rename from aoc_examples/2021d12.ipynb rename to prototypes/2021d12.ipynb diff --git a/aoc_examples/inputs/2021d12 b/prototypes/inputs/2021d12 similarity index 100% rename from aoc_examples/inputs/2021d12 rename to prototypes/inputs/2021d12 From e95860d7fe1313fd0e2c24cdcd1af2d3c61ef74c Mon Sep 17 00:00:00 2001 From: lakret Date: Tue, 27 Jun 2023 23:28:29 +0200 Subject: [PATCH 02/22] tab buttons I --- search_viz/src/main.rs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/search_viz/src/main.rs b/search_viz/src/main.rs index f545e6a..9970755 100644 --- a/search_viz/src/main.rs +++ b/search_viz/src/main.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use std::error::Error; -use egui::{vec2, Color32, Frame, Margin, Rect, RichText, Sense, Slider, Stroke, TextEdit, Vec2}; +use egui::{vec2, Button, Color32, Frame, Margin, Rect, RichText, Sense, Slider, Stroke, TextEdit, Vec2}; use instant::{Duration, Instant}; mod bfs; @@ -13,6 +13,8 @@ const START_COLOR: Color32 = Color32::from_rgb(0, 0, 255); const GOAL_COLOR: Color32 = Color32::from_rgb(0, 255, 0); const PATH_COLOR: Color32 = Color32::from_rgb(255, 255, 0); const EXPLORED_COLOR: Color32 = Color32::from_rgb(0, 50, 100); +const DFS_TAB_COLOR: Color32 = Color32::from_rgb(20, 100, 30); +const TAB_BTN_SIZE: Vec2 = vec2(200.0, 50.0); // when compiling natively #[cfg(not(target_arch = "wasm32"))] @@ -96,6 +98,8 @@ impl UserInput { #[derive(Debug, Clone, PartialEq)] pub struct TemplateApp { user_input: UserInput, + show_dfs_tab: bool, + // BFS validated: Validated, path: Vec, explored: HashMap>, @@ -120,6 +124,7 @@ impl Default for TemplateApp { path_and_explored_by_generation_for_animation(validated.fav_number, validated.start, validated.goal); Self { + show_dfs_tab: false, user_input, validated, path, @@ -170,6 +175,22 @@ impl eframe::App for TemplateApp { .inner_margin(Margin::same(20.0)), ) .show(ctx, |ui| { + ui.horizontal_top(|ui| { + let bfs_tab_btn = Button::new("Breadth-First Search Example") + .min_size(TAB_BTN_SIZE) + .fill(EXPLORED_COLOR); + if ui.add(bfs_tab_btn).clicked() { + self.show_dfs_tab = false; + } + + let dfs_tab_btn = Button::new("Depth-First Search Example") + .min_size(TAB_BTN_SIZE) + .fill(DFS_TAB_COLOR); + if ui.add(dfs_tab_btn).clicked() { + self.show_dfs_tab = true; + } + }); + ui.collapsing( RichText::new("Breadth-First Search Graph Algorithm Demo").heading(), |ui| { From 5b2a7ab14b276b5ad406c78ddd2804f693358391 Mon Sep 17 00:00:00 2001 From: lakret Date: Wed, 28 Jun 2023 21:17:30 +0200 Subject: [PATCH 03/22] restructure --- search_viz/src/main.rs | 168 ++++++++++++++++++++++++----------------- 1 file changed, 98 insertions(+), 70 deletions(-) diff --git a/search_viz/src/main.rs b/search_viz/src/main.rs index 9970755..fdf3ca5 100644 --- a/search_viz/src/main.rs +++ b/search_viz/src/main.rs @@ -95,11 +95,17 @@ impl UserInput { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Tabs { + BFS, + DFS, +} + #[derive(Debug, Clone, PartialEq)] pub struct TemplateApp { - user_input: UserInput, - show_dfs_tab: bool, + tab: Tabs, // BFS + user_input: UserInput, validated: Validated, path: Vec, explored: HashMap>, @@ -124,7 +130,8 @@ impl Default for TemplateApp { path_and_explored_by_generation_for_animation(validated.fav_number, validated.start, validated.goal); Self { - show_dfs_tab: false, + // TODO: return it to be the BFS default + tab: Tabs::DFS, user_input, validated, path, @@ -176,85 +183,91 @@ impl eframe::App for TemplateApp { ) .show(ctx, |ui| { ui.horizontal_top(|ui| { - let bfs_tab_btn = Button::new("Breadth-First Search Example") - .min_size(TAB_BTN_SIZE) - .fill(EXPLORED_COLOR); - if ui.add(bfs_tab_btn).clicked() { - self.show_dfs_tab = false; - } - - let dfs_tab_btn = Button::new("Depth-First Search Example") - .min_size(TAB_BTN_SIZE) - .fill(DFS_TAB_COLOR); - if ui.add(dfs_tab_btn).clicked() { - self.show_dfs_tab = true; - } + ui.selectable_value( + &mut self.tab, + Tabs::BFS, + RichText::new("Breadth-First Search Example").size(26.0), + ); + ui.selectable_value( + &mut self.tab, + Tabs::DFS, + RichText::new("Depth-First Search Example").size(26.0), + ); }); - ui.collapsing( - RichText::new("Breadth-First Search Graph Algorithm Demo").heading(), - |ui| { - ui.label("This demo allows you to try out an example Advent of Code problem for BFS."); - ui.horizontal(|ui| { - ui.label("You can find the source code for this example "); - ui.hyperlink_to("here", "https://github.com/lakret/gir"); - ui.label("."); - }); - }, - ); + if self.tab == Tabs::BFS { + self.bfs_ui(ui); + } else { + self.dfs_ui(ui); + } + }); - ui.hyperlink_to("Advent of Code 2016, Day 13", "https://adventofcode.com/2016/day/13"); - ui.add_space(15.0); + // critical to make sure the animation is running + ctx.request_repaint_after(Duration::from_millis(50)); + } +} +impl TemplateApp { + fn bfs_ui(&mut self, ui: &mut egui::Ui) { + ui.collapsing( + RichText::new("Breadth-First Search Graph Algorithm Demo").heading(), + |ui| { + ui.label("This demo allows you to try out an example Advent of Code problem for BFS."); ui.horizontal(|ui| { - ui.label("Favorite Number: "); - ui.add(TextEdit::singleline(&mut self.user_input.fav_number).margin(vec2(10.0, 6.0))); - - ui.add( - Slider::new(&mut self.user_input.levels, 1..=200) - .text("Levels to Draw") - .integer(), - ); + ui.label("You can find the source code for this example "); + ui.hyperlink_to("here", "https://github.com/lakret/gir"); + ui.label("."); }); - ui.add_space(15.0); - - match self.user_input.validate() { - Ok(new_validated) => { - let (path, explored) = path_and_explored_by_generation_for_animation( - new_validated.fav_number, - new_validated.start, - new_validated.goal, - ); - self.path = path; - self.explored = explored; - self.validated = new_validated; - } - Err(msg) => { - ui.label(RichText::new(msg.to_string()).color(Color32::DARK_RED)); - } - } - - ui.horizontal(|ui| { - let animate_bfs_checkbox = ui.checkbox(&mut self.animate_bfs, "Play Animation"); - if animate_bfs_checkbox.changed() && self.animate_bfs { - self.time = Instant::now(); - } + }, + ); + + ui.hyperlink_to("Tutorial Video", "https://youtu.be/ZDy3tqn-DKA"); + ui.hyperlink_to("Advent of Code 2016, Day 13", "https://adventofcode.com/2016/day/13"); + ui.add_space(15.0); + + ui.horizontal(|ui| { + ui.label("Favorite Number: "); + ui.add(TextEdit::singleline(&mut self.user_input.fav_number).margin(vec2(10.0, 6.0))); + + ui.add( + Slider::new(&mut self.user_input.levels, 1..=200) + .text("Levels to Draw") + .integer(), + ); + }); + ui.add_space(15.0); + + match self.user_input.validate() { + Ok(new_validated) => { + let (path, explored) = path_and_explored_by_generation_for_animation( + new_validated.fav_number, + new_validated.start, + new_validated.goal, + ); + self.path = path; + self.explored = explored; + self.validated = new_validated; + } + Err(msg) => { + ui.label(RichText::new(msg.to_string()).color(Color32::DARK_RED)); + } + } - ui.add(Slider::new(&mut self.time_tick, 0..=100).text("Tick").integer()); + ui.horizontal(|ui| { + let animate_bfs_checkbox = ui.checkbox(&mut self.animate_bfs, "Play Animation"); + if animate_bfs_checkbox.changed() && self.animate_bfs { + self.time = Instant::now(); + } - ui.checkbox(&mut self.show_path, "Show Path"); - }); - ui.add_space(15.0); + ui.add(Slider::new(&mut self.time_tick, 0..=100).text("Tick").integer()); - egui::ScrollArea::both().show(ui, |ui| self.draw_animated_grid(ui)); - }); + ui.checkbox(&mut self.show_path, "Show Path"); + }); + ui.add_space(15.0); - // critical to make sure the animation is running - ctx.request_repaint_after(Duration::from_millis(50)); + egui::ScrollArea::both().show(ui, |ui| self.draw_animated_grid(ui)); } -} -impl TemplateApp { fn draw_animated_grid(&mut self, ui: &mut egui::Ui) { if self.animate_bfs { // each animation tick is 1/20 of a second = 50 milliseconds @@ -308,6 +321,21 @@ impl TemplateApp { } } } + + fn dfs_ui(&mut self, ui: &mut egui::Ui) { + ui.collapsing( + RichText::new("Depth-First Search Graph Algorithm Demo").heading(), + |ui| { + ui.label("This demo allows you to play tic-tac-toe against the computer."); + ui.label("Computer will use depth-first search to select its moves."); + ui.horizontal(|ui| { + ui.label("You can find the source code for this example "); + ui.hyperlink_to("here", "https://github.com/lakret/gir"); + ui.label("."); + }); + }, + ); + } } fn logical_pos_to_screen_rect(pos: Pos, min_x: f32, min_y: f32) -> Rect { From 8065f9c5b5a1f2da9c62f8769c4a8e896a3a8736 Mon Sep 17 00:00:00 2001 From: lakret Date: Fri, 7 Jul 2023 03:11:57 +0200 Subject: [PATCH 04/22] extracted bfs and dfs into separate modules --- search_viz/src/bfs_ui.rs | 135 +++++++++++++++++++++++++++++++++ search_viz/src/dfs_ui.rs | 18 +++++ search_viz/src/main.rs | 157 ++------------------------------------- 3 files changed, 158 insertions(+), 152 deletions(-) create mode 100644 search_viz/src/bfs_ui.rs create mode 100644 search_viz/src/dfs_ui.rs diff --git a/search_viz/src/bfs_ui.rs b/search_viz/src/bfs_ui.rs new file mode 100644 index 0000000..f2175b9 --- /dev/null +++ b/search_viz/src/bfs_ui.rs @@ -0,0 +1,135 @@ +use egui::Rect; +use egui::{vec2, Color32, RichText, Sense, Slider, Stroke, TextEdit, Vec2}; +use instant::Instant; + +use crate::bfs::*; +use crate::TemplateApp; + +const CELL_SIZE: f32 = 16.0; +const WALL_COLOR: Color32 = Color32::from_rgb(125, 0, 255); +const START_COLOR: Color32 = Color32::from_rgb(0, 0, 255); +const GOAL_COLOR: Color32 = Color32::from_rgb(0, 255, 0); +const PATH_COLOR: Color32 = Color32::from_rgb(255, 255, 0); +const EXPLORED_COLOR: Color32 = Color32::from_rgb(0, 50, 100); + +pub fn ui(state: &mut TemplateApp, ui: &mut egui::Ui) { + ui.collapsing( + RichText::new("Breadth-First Search Graph Algorithm Demo").heading(), + |ui| { + ui.label("This demo allows you to try out an example Advent of Code problem for BFS."); + ui.horizontal(|ui| { + ui.label("You can find the source code for this example "); + ui.hyperlink_to("here", "https://github.com/lakret/gir"); + ui.label("."); + }); + }, + ); + + ui.hyperlink_to("Tutorial Video", "https://youtu.be/ZDy3tqn-DKA"); + ui.hyperlink_to("Advent of Code 2016, Day 13", "https://adventofcode.com/2016/day/13"); + ui.add_space(15.0); + + ui.horizontal(|ui| { + ui.label("Favorite Number: "); + ui.add(TextEdit::singleline(&mut state.user_input.fav_number).margin(vec2(10.0, 6.0))); + + ui.add( + Slider::new(&mut state.user_input.levels, 1..=200) + .text("Levels to Draw") + .integer(), + ); + }); + ui.add_space(15.0); + + match state.user_input.validate() { + Ok(new_validated) => { + let (path, explored) = path_and_explored_by_generation_for_animation( + new_validated.fav_number, + new_validated.start, + new_validated.goal, + ); + state.path = path; + state.explored = explored; + state.validated = new_validated; + } + Err(msg) => { + ui.label(RichText::new(msg.to_string()).color(Color32::DARK_RED)); + } + } + + ui.horizontal(|ui| { + let animate_bfs_checkbox = ui.checkbox(&mut state.animate_bfs, "Play Animation"); + if animate_bfs_checkbox.changed() && state.animate_bfs { + state.time = Instant::now(); + } + + ui.add(Slider::new(&mut state.time_tick, 0..=100).text("Tick").integer()); + + ui.checkbox(&mut state.show_path, "Show Path"); + }); + ui.add_space(15.0); + + egui::ScrollArea::both().show(ui, |ui| draw_animated_grid(state, ui)); +} + +fn draw_animated_grid(state: &mut TemplateApp, ui: &mut egui::Ui) { + if state.animate_bfs { + // each animation tick is 1/20 of a second = 50 milliseconds + state.time_tick = (state.time.elapsed().as_secs_f32() * 20.0).ceil() as u32 % 100; + } + + let (response, painter) = ui.allocate_painter(Vec2::splat(state.validated.levels as f32 * CELL_SIZE), Sense::hover()); + + let rect = response.rect; + painter.rect_stroke(rect, 0.0, Stroke::new(1.0, WALL_COLOR)); + + let min_x = *rect.x_range().start(); + let min_y = *rect.y_range().start(); + for y in 0..state.validated.levels { + for x in 0..state.validated.levels { + let pos = Pos { x, y }; + let cell = logical_pos_to_screen_rect(pos, min_x, min_y); + + if !pos.is_open(state.validated.fav_number) { + painter.rect_filled(cell, 0.0, WALL_COLOR); + } + + if state.validated.start == pos { + painter.circle_filled(cell.center(), CELL_SIZE / 2.0, START_COLOR); + } + + if state.validated.goal == pos { + painter.rect_filled(cell, 4.0, GOAL_COLOR); + } + } + } + + if state.show_path { + // can be used for both path length and generation to show + let elements_to_show = (state.path.len() as f32 / 100.0 * state.time_tick as f32).ceil() as usize; + for &pos in &state.path[..elements_to_show] { + if pos != state.validated.start && pos != state.validated.goal { + let cell = logical_pos_to_screen_rect(pos, min_x, min_y); + painter.rect_filled(cell, 6.0, PATH_COLOR); + } + } + + for generation in 0..(elements_to_show as u32) { + if let Some(positions) = state.explored.get(&generation) { + for &pos in positions { + let cell = logical_pos_to_screen_rect(pos, min_x, min_y); + painter.rect_filled(cell, 0.0, EXPLORED_COLOR); + } + } + } + } +} + +fn logical_pos_to_screen_rect(pos: Pos, min_x: f32, min_y: f32) -> Rect { + let Pos { x, y } = pos; + let screen_min_x = x as f32 * CELL_SIZE + min_x; + let screen_max_x = (x + 1) as f32 * CELL_SIZE + min_x; + let screen_min_y = (y as f32 * CELL_SIZE + min_y).ceil(); + let screen_max_y = ((y + 1) as f32 * CELL_SIZE + min_y).ceil(); + Rect::from_x_y_ranges(screen_min_x..=screen_max_x, screen_min_y..=screen_max_y) +} diff --git a/search_viz/src/dfs_ui.rs b/search_viz/src/dfs_ui.rs new file mode 100644 index 0000000..c9bf6d4 --- /dev/null +++ b/search_viz/src/dfs_ui.rs @@ -0,0 +1,18 @@ +use egui::RichText; + +use crate::TemplateApp; + +pub fn ui(state: &mut TemplateApp, ui: &mut egui::Ui) { + ui.collapsing( + RichText::new("Depth-First Search Graph Algorithm Demo").heading(), + |ui| { + ui.label("This demo allows you to play tic-tac-toe against the computer."); + ui.label("Computer will use depth-first search to select its moves."); + ui.horizontal(|ui| { + ui.label("You can find the source code for this example "); + ui.hyperlink_to("here", "https://github.com/lakret/gir"); + ui.label("."); + }); + }, + ); +} diff --git a/search_viz/src/main.rs b/search_viz/src/main.rs index fdf3ca5..22a0659 100644 --- a/search_viz/src/main.rs +++ b/search_viz/src/main.rs @@ -1,20 +1,13 @@ use std::collections::HashMap; use std::error::Error; -use egui::{vec2, Button, Color32, Frame, Margin, Rect, RichText, Sense, Slider, Stroke, TextEdit, Vec2}; +use egui::{vec2, Color32, Frame, Margin, RichText}; use instant::{Duration, Instant}; mod bfs; use bfs::*; - -const CELL_SIZE: f32 = 16.0; -const WALL_COLOR: Color32 = Color32::from_rgb(125, 0, 255); -const START_COLOR: Color32 = Color32::from_rgb(0, 0, 255); -const GOAL_COLOR: Color32 = Color32::from_rgb(0, 255, 0); -const PATH_COLOR: Color32 = Color32::from_rgb(255, 255, 0); -const EXPLORED_COLOR: Color32 = Color32::from_rgb(0, 50, 100); -const DFS_TAB_COLOR: Color32 = Color32::from_rgb(20, 100, 30); -const TAB_BTN_SIZE: Vec2 = vec2(200.0, 50.0); +mod bfs_ui; +mod dfs_ui; // when compiling natively #[cfg(not(target_arch = "wasm32"))] @@ -196,9 +189,9 @@ impl eframe::App for TemplateApp { }); if self.tab == Tabs::BFS { - self.bfs_ui(ui); + bfs_ui::ui(self, ui); } else { - self.dfs_ui(ui); + dfs_ui::ui(self, ui); } }); @@ -206,143 +199,3 @@ impl eframe::App for TemplateApp { ctx.request_repaint_after(Duration::from_millis(50)); } } - -impl TemplateApp { - fn bfs_ui(&mut self, ui: &mut egui::Ui) { - ui.collapsing( - RichText::new("Breadth-First Search Graph Algorithm Demo").heading(), - |ui| { - ui.label("This demo allows you to try out an example Advent of Code problem for BFS."); - ui.horizontal(|ui| { - ui.label("You can find the source code for this example "); - ui.hyperlink_to("here", "https://github.com/lakret/gir"); - ui.label("."); - }); - }, - ); - - ui.hyperlink_to("Tutorial Video", "https://youtu.be/ZDy3tqn-DKA"); - ui.hyperlink_to("Advent of Code 2016, Day 13", "https://adventofcode.com/2016/day/13"); - ui.add_space(15.0); - - ui.horizontal(|ui| { - ui.label("Favorite Number: "); - ui.add(TextEdit::singleline(&mut self.user_input.fav_number).margin(vec2(10.0, 6.0))); - - ui.add( - Slider::new(&mut self.user_input.levels, 1..=200) - .text("Levels to Draw") - .integer(), - ); - }); - ui.add_space(15.0); - - match self.user_input.validate() { - Ok(new_validated) => { - let (path, explored) = path_and_explored_by_generation_for_animation( - new_validated.fav_number, - new_validated.start, - new_validated.goal, - ); - self.path = path; - self.explored = explored; - self.validated = new_validated; - } - Err(msg) => { - ui.label(RichText::new(msg.to_string()).color(Color32::DARK_RED)); - } - } - - ui.horizontal(|ui| { - let animate_bfs_checkbox = ui.checkbox(&mut self.animate_bfs, "Play Animation"); - if animate_bfs_checkbox.changed() && self.animate_bfs { - self.time = Instant::now(); - } - - ui.add(Slider::new(&mut self.time_tick, 0..=100).text("Tick").integer()); - - ui.checkbox(&mut self.show_path, "Show Path"); - }); - ui.add_space(15.0); - - egui::ScrollArea::both().show(ui, |ui| self.draw_animated_grid(ui)); - } - - fn draw_animated_grid(&mut self, ui: &mut egui::Ui) { - if self.animate_bfs { - // each animation tick is 1/20 of a second = 50 milliseconds - self.time_tick = (self.time.elapsed().as_secs_f32() * 20.0).ceil() as u32 % 100; - } - - let (response, painter) = - ui.allocate_painter(Vec2::splat(self.validated.levels as f32 * CELL_SIZE), Sense::hover()); - - let rect = response.rect; - painter.rect_stroke(rect, 0.0, Stroke::new(1.0, WALL_COLOR)); - - let min_x = *rect.x_range().start(); - let min_y = *rect.y_range().start(); - for y in 0..self.validated.levels { - for x in 0..self.validated.levels { - let pos = bfs::Pos { x, y }; - let cell = logical_pos_to_screen_rect(pos, min_x, min_y); - - if !pos.is_open(self.validated.fav_number) { - painter.rect_filled(cell, 0.0, WALL_COLOR); - } - - if self.validated.start == pos { - painter.circle_filled(cell.center(), CELL_SIZE / 2.0, START_COLOR); - } - - if self.validated.goal == pos { - painter.rect_filled(cell, 4.0, GOAL_COLOR); - } - } - } - - if self.show_path { - // can be used for both path length and generation to show - let elements_to_show = (self.path.len() as f32 / 100.0 * self.time_tick as f32).ceil() as usize; - for &pos in &self.path[..elements_to_show] { - if pos != self.validated.start && pos != self.validated.goal { - let cell = logical_pos_to_screen_rect(pos, min_x, min_y); - painter.rect_filled(cell, 6.0, PATH_COLOR); - } - } - - for generation in 0..(elements_to_show as u32) { - if let Some(positions) = self.explored.get(&generation) { - for &pos in positions { - let cell = logical_pos_to_screen_rect(pos, min_x, min_y); - painter.rect_filled(cell, 0.0, EXPLORED_COLOR); - } - } - } - } - } - - fn dfs_ui(&mut self, ui: &mut egui::Ui) { - ui.collapsing( - RichText::new("Depth-First Search Graph Algorithm Demo").heading(), - |ui| { - ui.label("This demo allows you to play tic-tac-toe against the computer."); - ui.label("Computer will use depth-first search to select its moves."); - ui.horizontal(|ui| { - ui.label("You can find the source code for this example "); - ui.hyperlink_to("here", "https://github.com/lakret/gir"); - ui.label("."); - }); - }, - ); - } -} - -fn logical_pos_to_screen_rect(pos: Pos, min_x: f32, min_y: f32) -> Rect { - let Pos { x, y } = pos; - let screen_min_x = x as f32 * CELL_SIZE + min_x; - let screen_max_x = (x + 1) as f32 * CELL_SIZE + min_x; - let screen_min_y = (y as f32 * CELL_SIZE + min_y).ceil(); - let screen_max_y = ((y + 1) as f32 * CELL_SIZE + min_y).ceil(); - Rect::from_x_y_ranges(screen_min_x..=screen_max_x, screen_min_y..=screen_max_y) -} From 34b30fed23879a5c931cf65b9b8700b19612aa3d Mon Sep 17 00:00:00 2001 From: lakret Date: Fri, 7 Jul 2023 03:21:17 +0200 Subject: [PATCH 05/22] bfs_ui::State extraction --- search_viz/src/bfs_ui.rs | 258 ++++++++++++++++++++++++++------------- search_viz/src/main.rs | 77 +----------- 2 files changed, 176 insertions(+), 159 deletions(-) diff --git a/search_viz/src/bfs_ui.rs b/search_viz/src/bfs_ui.rs index f2175b9..fd39c67 100644 --- a/search_viz/src/bfs_ui.rs +++ b/search_viz/src/bfs_ui.rs @@ -1,9 +1,11 @@ +use std::collections::HashMap; +use std::error::Error; + use egui::Rect; use egui::{vec2, Color32, RichText, Sense, Slider, Stroke, TextEdit, Vec2}; use instant::Instant; use crate::bfs::*; -use crate::TemplateApp; const CELL_SIZE: f32 = 16.0; const WALL_COLOR: Color32 = Color32::from_rgb(125, 0, 255); @@ -12,113 +14,197 @@ const GOAL_COLOR: Color32 = Color32::from_rgb(0, 255, 0); const PATH_COLOR: Color32 = Color32::from_rgb(255, 255, 0); const EXPLORED_COLOR: Color32 = Color32::from_rgb(0, 50, 100); -pub fn ui(state: &mut TemplateApp, ui: &mut egui::Ui) { - ui.collapsing( - RichText::new("Breadth-First Search Graph Algorithm Demo").heading(), - |ui| { - ui.label("This demo allows you to try out an example Advent of Code problem for BFS."); - ui.horizontal(|ui| { - ui.label("You can find the source code for this example "); - ui.hyperlink_to("here", "https://github.com/lakret/gir"); - ui.label("."); - }); - }, - ); - - ui.hyperlink_to("Tutorial Video", "https://youtu.be/ZDy3tqn-DKA"); - ui.hyperlink_to("Advent of Code 2016, Day 13", "https://adventofcode.com/2016/day/13"); - ui.add_space(15.0); - - ui.horizontal(|ui| { - ui.label("Favorite Number: "); - ui.add(TextEdit::singleline(&mut state.user_input.fav_number).margin(vec2(10.0, 6.0))); - - ui.add( - Slider::new(&mut state.user_input.levels, 1..=200) - .text("Levels to Draw") - .integer(), - ); - }); - ui.add_space(15.0); - - match state.user_input.validate() { - Ok(new_validated) => { - let (path, explored) = path_and_explored_by_generation_for_animation( - new_validated.fav_number, - new_validated.start, - new_validated.goal, - ); - state.path = path; - state.explored = explored; - state.validated = new_validated; - } - Err(msg) => { - ui.label(RichText::new(msg.to_string()).color(Color32::DARK_RED)); - } - } +#[derive(Debug, Clone, PartialEq)] +pub struct State { + user_input: UserInput, + validated: Validated, + path: Vec, + explored: HashMap>, + time: Instant, + time_tick: u32, + animate_bfs: bool, + show_path: bool, +} - ui.horizontal(|ui| { - let animate_bfs_checkbox = ui.checkbox(&mut state.animate_bfs, "Play Animation"); - if animate_bfs_checkbox.changed() && state.animate_bfs { - state.time = Instant::now(); +impl Default for State { + fn default() -> Self { + let user_input = UserInput { + fav_number: "1350".to_string(), + levels: 50, + start_x: "1".to_string(), + start_y: "1".to_string(), + goal_x: "31".to_string(), + goal_y: "39".to_string(), + }; + let validated = user_input.validate().unwrap(); + let (path, explored) = + path_and_explored_by_generation_for_animation(validated.fav_number, validated.start, validated.goal); + + Self { + user_input, + validated, + path, + explored, + time: Instant::now(), + time_tick: 0, + animate_bfs: true, + show_path: true, } + } +} - ui.add(Slider::new(&mut state.time_tick, 0..=100).text("Tick").integer()); - - ui.checkbox(&mut state.show_path, "Show Path"); - }); - ui.add_space(15.0); +#[derive(Debug, Clone, PartialEq)] +pub struct UserInput { + fav_number: String, + levels: u32, + start_x: String, + start_y: String, + goal_x: String, + goal_y: String, +} - egui::ScrollArea::both().show(ui, |ui| draw_animated_grid(state, ui)); +#[derive(Debug, Clone, PartialEq)] +pub struct Validated { + fav_number: u32, + levels: u32, + start: Pos, + goal: Pos, } -fn draw_animated_grid(state: &mut TemplateApp, ui: &mut egui::Ui) { - if state.animate_bfs { - // each animation tick is 1/20 of a second = 50 milliseconds - state.time_tick = (state.time.elapsed().as_secs_f32() * 20.0).ceil() as u32 % 100; +impl UserInput { + fn validate(&self) -> Result> { + let fav_number = self + .fav_number + .parse::() + .map_err(|err| format!("Incorrect value for Favorite Number: {}", err.to_string()))?; + let start = Pos { + x: self.start_x.parse::().map_err(|err| err.to_string())?, + y: self.start_y.parse::().map_err(|err| err.to_string())?, + }; + let goal = Pos { + x: self.goal_x.parse::().map_err(|err| err.to_string())?, + y: self.goal_y.parse::().map_err(|err| err.to_string())?, + }; + + Ok(Validated { + fav_number, + levels: self.levels, + start, + goal, + }) } +} - let (response, painter) = ui.allocate_painter(Vec2::splat(state.validated.levels as f32 * CELL_SIZE), Sense::hover()); +impl State { + pub fn ui(&mut self, ui: &mut egui::Ui) { + ui.collapsing( + RichText::new("Breadth-First Search Graph Algorithm Demo").heading(), + |ui| { + ui.label("This demo allows you to try out an example Advent of Code problem for BFS."); + ui.horizontal(|ui| { + ui.label("You can find the source code for this example "); + ui.hyperlink_to("here", "https://github.com/lakret/gir"); + ui.label("."); + }); + }, + ); - let rect = response.rect; - painter.rect_stroke(rect, 0.0, Stroke::new(1.0, WALL_COLOR)); + ui.hyperlink_to("Tutorial Video", "https://youtu.be/ZDy3tqn-DKA"); + ui.hyperlink_to("Advent of Code 2016, Day 13", "https://adventofcode.com/2016/day/13"); + ui.add_space(15.0); - let min_x = *rect.x_range().start(); - let min_y = *rect.y_range().start(); - for y in 0..state.validated.levels { - for x in 0..state.validated.levels { - let pos = Pos { x, y }; - let cell = logical_pos_to_screen_rect(pos, min_x, min_y); + ui.horizontal(|ui| { + ui.label("Favorite Number: "); + ui.add(TextEdit::singleline(&mut self.user_input.fav_number).margin(vec2(10.0, 6.0))); - if !pos.is_open(state.validated.fav_number) { - painter.rect_filled(cell, 0.0, WALL_COLOR); + ui.add( + Slider::new(&mut self.user_input.levels, 1..=200) + .text("Levels to Draw") + .integer(), + ); + }); + ui.add_space(15.0); + + match self.user_input.validate() { + Ok(new_validated) => { + let (path, explored) = path_and_explored_by_generation_for_animation( + new_validated.fav_number, + new_validated.start, + new_validated.goal, + ); + self.path = path; + self.explored = explored; + self.validated = new_validated; } - - if state.validated.start == pos { - painter.circle_filled(cell.center(), CELL_SIZE / 2.0, START_COLOR); + Err(msg) => { + ui.label(RichText::new(msg.to_string()).color(Color32::DARK_RED)); } + } - if state.validated.goal == pos { - painter.rect_filled(cell, 4.0, GOAL_COLOR); + ui.horizontal(|ui| { + let animate_bfs_checkbox = ui.checkbox(&mut self.animate_bfs, "Play Animation"); + if animate_bfs_checkbox.changed() && self.animate_bfs { + self.time = Instant::now(); } - } + + ui.add(Slider::new(&mut self.time_tick, 0..=100).text("Tick").integer()); + + ui.checkbox(&mut self.show_path, "Show Path"); + }); + ui.add_space(15.0); + + egui::ScrollArea::both().show(ui, |ui| self.draw_animated_grid(ui)); } - if state.show_path { - // can be used for both path length and generation to show - let elements_to_show = (state.path.len() as f32 / 100.0 * state.time_tick as f32).ceil() as usize; - for &pos in &state.path[..elements_to_show] { - if pos != state.validated.start && pos != state.validated.goal { + fn draw_animated_grid(&mut self, ui: &mut egui::Ui) { + if self.animate_bfs { + // each animation tick is 1/20 of a second = 50 milliseconds + self.time_tick = (self.time.elapsed().as_secs_f32() * 20.0).ceil() as u32 % 100; + } + + let (response, painter) = + ui.allocate_painter(Vec2::splat(self.validated.levels as f32 * CELL_SIZE), Sense::hover()); + + let rect = response.rect; + painter.rect_stroke(rect, 0.0, Stroke::new(1.0, WALL_COLOR)); + + let min_x = *rect.x_range().start(); + let min_y = *rect.y_range().start(); + for y in 0..self.validated.levels { + for x in 0..self.validated.levels { + let pos = Pos { x, y }; let cell = logical_pos_to_screen_rect(pos, min_x, min_y); - painter.rect_filled(cell, 6.0, PATH_COLOR); + + if !pos.is_open(self.validated.fav_number) { + painter.rect_filled(cell, 0.0, WALL_COLOR); + } + + if self.validated.start == pos { + painter.circle_filled(cell.center(), CELL_SIZE / 2.0, START_COLOR); + } + + if self.validated.goal == pos { + painter.rect_filled(cell, 4.0, GOAL_COLOR); + } } } - for generation in 0..(elements_to_show as u32) { - if let Some(positions) = state.explored.get(&generation) { - for &pos in positions { + if self.show_path { + // can be used for both path length and generation to show + let elements_to_show = (self.path.len() as f32 / 100.0 * self.time_tick as f32).ceil() as usize; + for &pos in &self.path[..elements_to_show] { + if pos != self.validated.start && pos != self.validated.goal { let cell = logical_pos_to_screen_rect(pos, min_x, min_y); - painter.rect_filled(cell, 0.0, EXPLORED_COLOR); + painter.rect_filled(cell, 6.0, PATH_COLOR); + } + } + + for generation in 0..(elements_to_show as u32) { + if let Some(positions) = self.explored.get(&generation) { + for &pos in positions { + let cell = logical_pos_to_screen_rect(pos, min_x, min_y); + painter.rect_filled(cell, 0.0, EXPLORED_COLOR); + } } } } diff --git a/search_viz/src/main.rs b/search_viz/src/main.rs index 22a0659..a6f9247 100644 --- a/search_viz/src/main.rs +++ b/search_viz/src/main.rs @@ -46,48 +46,6 @@ fn main() { }); } -#[derive(Debug, Clone, PartialEq)] -pub struct UserInput { - fav_number: String, - levels: u32, - start_x: String, - start_y: String, - goal_x: String, - goal_y: String, -} - -#[derive(Debug, Clone, PartialEq)] -pub struct Validated { - fav_number: u32, - levels: u32, - start: Pos, - goal: Pos, -} - -impl UserInput { - fn validate(&self) -> Result> { - let fav_number = self - .fav_number - .parse::() - .map_err(|err| format!("Incorrect value for Favorite Number: {}", err.to_string()))?; - let start = Pos { - x: self.start_x.parse::().map_err(|err| err.to_string())?, - y: self.start_y.parse::().map_err(|err| err.to_string())?, - }; - let goal = Pos { - x: self.goal_x.parse::().map_err(|err| err.to_string())?, - y: self.goal_y.parse::().map_err(|err| err.to_string())?, - }; - - Ok(Validated { - fav_number, - levels: self.levels, - start, - goal, - }) - } -} - #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Tabs { BFS, @@ -97,42 +55,15 @@ enum Tabs { #[derive(Debug, Clone, PartialEq)] pub struct TemplateApp { tab: Tabs, - // BFS - user_input: UserInput, - validated: Validated, - path: Vec, - explored: HashMap>, - time: Instant, - time_tick: u32, - animate_bfs: bool, - show_path: bool, + bfs_state: bfs_ui::State, } impl Default for TemplateApp { fn default() -> Self { - let user_input = UserInput { - fav_number: "1350".to_string(), - levels: 50, - start_x: "1".to_string(), - start_y: "1".to_string(), - goal_x: "31".to_string(), - goal_y: "39".to_string(), - }; - let validated = user_input.validate().unwrap(); - let (path, explored) = - path_and_explored_by_generation_for_animation(validated.fav_number, validated.start, validated.goal); - - Self { + TemplateApp { // TODO: return it to be the BFS default tab: Tabs::DFS, - user_input, - validated, - path, - explored, - time: Instant::now(), - time_tick: 0, - animate_bfs: true, - show_path: true, + bfs_state: bfs_ui::State::default(), } } } @@ -189,7 +120,7 @@ impl eframe::App for TemplateApp { }); if self.tab == Tabs::BFS { - bfs_ui::ui(self, ui); + self.bfs_state.ui(ui); } else { dfs_ui::ui(self, ui); } From fa7e855dcd04907ab3b77102f234032e59af4358 Mon Sep 17 00:00:00 2001 From: lakret Date: Fri, 7 Jul 2023 03:25:16 +0200 Subject: [PATCH 06/22] finalized the new module tree --- search_viz/src/dfs.rs | 0 search_viz/src/dfs_ui.rs | 31 +++++++++++++++++-------------- search_viz/src/main.rs | 11 +++++------ 3 files changed, 22 insertions(+), 20 deletions(-) create mode 100644 search_viz/src/dfs.rs diff --git a/search_viz/src/dfs.rs b/search_viz/src/dfs.rs new file mode 100644 index 0000000..e69de29 diff --git a/search_viz/src/dfs_ui.rs b/search_viz/src/dfs_ui.rs index c9bf6d4..3ee8a30 100644 --- a/search_viz/src/dfs_ui.rs +++ b/search_viz/src/dfs_ui.rs @@ -1,18 +1,21 @@ use egui::RichText; -use crate::TemplateApp; +#[derive(Debug, Clone, Default, PartialEq)] +pub struct State {} -pub fn ui(state: &mut TemplateApp, ui: &mut egui::Ui) { - ui.collapsing( - RichText::new("Depth-First Search Graph Algorithm Demo").heading(), - |ui| { - ui.label("This demo allows you to play tic-tac-toe against the computer."); - ui.label("Computer will use depth-first search to select its moves."); - ui.horizontal(|ui| { - ui.label("You can find the source code for this example "); - ui.hyperlink_to("here", "https://github.com/lakret/gir"); - ui.label("."); - }); - }, - ); +impl State { + pub fn ui(&mut self, ui: &mut egui::Ui) { + ui.collapsing( + RichText::new("Depth-First Search Graph Algorithm Demo").heading(), + |ui| { + ui.label("This demo allows you to play tic-tac-toe against the computer."); + ui.label("Computer will use depth-first search to select its moves."); + ui.horizontal(|ui| { + ui.label("You can find the source code for this example "); + ui.hyperlink_to("here", "https://github.com/lakret/gir"); + ui.label("."); + }); + }, + ); + } } diff --git a/search_viz/src/main.rs b/search_viz/src/main.rs index a6f9247..90d3968 100644 --- a/search_viz/src/main.rs +++ b/search_viz/src/main.rs @@ -1,12 +1,9 @@ -use std::collections::HashMap; -use std::error::Error; - use egui::{vec2, Color32, Frame, Margin, RichText}; -use instant::{Duration, Instant}; +use instant::Duration; mod bfs; -use bfs::*; mod bfs_ui; +mod dfs; mod dfs_ui; // when compiling natively @@ -56,6 +53,7 @@ enum Tabs { pub struct TemplateApp { tab: Tabs, bfs_state: bfs_ui::State, + dfs_state: dfs_ui::State, } impl Default for TemplateApp { @@ -64,6 +62,7 @@ impl Default for TemplateApp { // TODO: return it to be the BFS default tab: Tabs::DFS, bfs_state: bfs_ui::State::default(), + dfs_state: dfs_ui::State::default(), } } } @@ -122,7 +121,7 @@ impl eframe::App for TemplateApp { if self.tab == Tabs::BFS { self.bfs_state.ui(ui); } else { - dfs_ui::ui(self, ui); + self.dfs_state.ui(ui); } }); From 0ca9195771869425f6de1b3c55c65333c60fd59c Mon Sep 17 00:00:00 2001 From: lakret Date: Fri, 7 Jul 2023 03:37:29 +0200 Subject: [PATCH 07/22] game state as bitvector --- Cargo.lock | 40 ++++++++++++++++++++++++++++++++++++++++ search_viz/Cargo.toml | 2 ++ search_viz/src/dfs.rs | 3 +++ 3 files changed, 45 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 9c79d1d..7a905f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -118,6 +118,18 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "block" version = "0.1.6" @@ -649,6 +661,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "gethostname" version = "0.2.3" @@ -1293,6 +1311,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.5" @@ -1482,6 +1506,7 @@ dependencies = [ name = "search_viz" version = "0.1.0" dependencies = [ + "bitvec", "console_error_panic_hook", "eframe", "egui", @@ -1634,6 +1659,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "textwrap" version = "0.11.0" @@ -2288,6 +2319,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x11-dl" version = "2.21.0" diff --git a/search_viz/Cargo.toml b/search_viz/Cargo.toml index 2abd25e..d1c89f9 100644 --- a/search_viz/Cargo.toml +++ b/search_viz/Cargo.toml @@ -18,6 +18,8 @@ eframe = { version = "0.21.0", default-features = false, features = [ ] } # since std::time is not implemented for WASM instant = { version = "0.1", features = ["wasm-bindgen"] } +# for dfs example +bitvec = "^1.0" # native [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/search_viz/src/dfs.rs b/search_viz/src/dfs.rs index e69de29..84c3294 100644 --- a/search_viz/src/dfs.rs +++ b/search_viz/src/dfs.rs @@ -0,0 +1,3 @@ +use bitvec::prelude::*; + +pub struct Game(BitVec); From 6993e131518de3b455d6e9538421601af9701b3b Mon Sep 17 00:00:00 2001 From: lakret Date: Fri, 7 Jul 2023 03:48:45 +0200 Subject: [PATCH 08/22] let's better use BitArray --- search_viz/src/dfs.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/search_viz/src/dfs.rs b/search_viz/src/dfs.rs index 84c3294..7cccc19 100644 --- a/search_viz/src/dfs.rs +++ b/search_viz/src/dfs.rs @@ -1,3 +1,5 @@ use bitvec::prelude::*; -pub struct Game(BitVec); +#[derive(Debug, Default, Clone, Copy)] +// `BitArr!(...)` expands to `BitArray<[u16; ::bitvec::mem::elts::(9)], Msb0>` +pub struct Game(BitArr!(for 9, in u16, Msb0)); From e24c7913dd092752a51e94d8a10e221fecb93353 Mon Sep 17 00:00:00 2001 From: lakret Date: Fri, 7 Jul 2023 04:18:02 +0200 Subject: [PATCH 09/22] better abstraction --- Cargo.lock | 40 -------------------------------------- search_viz/Cargo.toml | 2 -- search_viz/src/dfs.rs | 45 +++++++++++++++++++++++++++++++++++++++---- 3 files changed, 41 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7a905f1..9c79d1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -118,18 +118,6 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - [[package]] name = "block" version = "0.1.6" @@ -661,12 +649,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - [[package]] name = "gethostname" version = "0.2.3" @@ -1311,12 +1293,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - [[package]] name = "rand" version = "0.8.5" @@ -1506,7 +1482,6 @@ dependencies = [ name = "search_viz" version = "0.1.0" dependencies = [ - "bitvec", "console_error_panic_hook", "eframe", "egui", @@ -1659,12 +1634,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - [[package]] name = "textwrap" version = "0.11.0" @@ -2319,15 +2288,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - [[package]] name = "x11-dl" version = "2.21.0" diff --git a/search_viz/Cargo.toml b/search_viz/Cargo.toml index d1c89f9..2abd25e 100644 --- a/search_viz/Cargo.toml +++ b/search_viz/Cargo.toml @@ -18,8 +18,6 @@ eframe = { version = "0.21.0", default-features = false, features = [ ] } # since std::time is not implemented for WASM instant = { version = "0.1", features = ["wasm-bindgen"] } -# for dfs example -bitvec = "^1.0" # native [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/search_viz/src/dfs.rs b/search_viz/src/dfs.rs index 7cccc19..58111a2 100644 --- a/search_viz/src/dfs.rs +++ b/search_viz/src/dfs.rs @@ -1,5 +1,42 @@ -use bitvec::prelude::*; +#[derive(Debug, Clone, Copy)] +pub enum Mark { + Cross, + Circle, +} -#[derive(Debug, Default, Clone, Copy)] -// `BitArr!(...)` expands to `BitArray<[u16; ::bitvec::mem::elts::(9)], Msb0>` -pub struct Game(BitArr!(for 9, in u16, Msb0)); +#[derive(Debug, Default, Clone)] +pub struct Game { + state: [Option; 9], + circle_turn: bool, +} + +impl Game { + pub fn is_cross_turn(&self) -> bool { + !self.circle_turn + } + + pub fn is_circle_turn(&self) -> bool { + self.circle_turn + } + + pub fn do_move(&mut self, pos: usize) { + assert!(pos < 9); + + let mark = if self.is_cross_turn() { + Mark::Cross + } else { + Mark::Circle + }; + + self.state[pos] = Some(mark); + } + + pub fn next_moves(&self) -> impl Iterator { + todo!(); + vec![].into_iter() + } + + pub fn is_won(&self) -> bool { + todo!() + } +} From 2c65cbb77508a31fca194c2c9737fc8e103e495b Mon Sep 17 00:00:00 2001 From: lakret Date: Fri, 7 Jul 2023 04:29:00 +0200 Subject: [PATCH 10/22] tictactoe game methods --- search_viz/src/dfs.rs | 45 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/search_viz/src/dfs.rs b/search_viz/src/dfs.rs index 58111a2..3b6ce18 100644 --- a/search_viz/src/dfs.rs +++ b/search_viz/src/dfs.rs @@ -1,4 +1,4 @@ -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Mark { Cross, Circle, @@ -10,6 +10,20 @@ pub struct Game { circle_turn: bool, } +const LINES: [[usize; 3]; 8] = [ + // rows + [0, 1, 2], + [3, 4, 5], + [6, 7, 8], + // columns + [0, 3, 6], + [1, 4, 7], + [2, 5, 8], + // diagonals + [0, 4, 8], + [2, 4, 6], +]; + impl Game { pub fn is_cross_turn(&self) -> bool { !self.circle_turn @@ -29,14 +43,33 @@ impl Game { }; self.state[pos] = Some(mark); + self.circle_turn = !self.circle_turn; } - pub fn next_moves(&self) -> impl Iterator { - todo!(); - vec![].into_iter() + pub fn next_moves(&self) -> Vec { + let mut moves = vec![]; + + for pos in 0..9 { + if self.state[pos].is_none() { + let mut new_game = self.clone(); + new_game.do_move(pos); + + moves.push(new_game); + } + } + + moves } - pub fn is_won(&self) -> bool { - todo!() + pub fn is_won(&self) -> Option { + for line in LINES { + if let Some(mark) = self.state[line[0]] { + if self.state[line[1]] == Some(mark) && self.state[line[2]] == Some(mark) { + return Some(mark); + } + } + } + + None } } From 0cd898882fe34ce50b8c2e0a8ff7b5a277b47f84 Mon Sep 17 00:00:00 2001 From: lakret Date: Fri, 7 Jul 2023 05:01:14 +0200 Subject: [PATCH 11/22] playble person-to-person mode --- search_viz/src/dfs.rs | 40 +++++++++++++++++++++++++-- search_viz/src/dfs_ui.rs | 59 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 95 insertions(+), 4 deletions(-) diff --git a/search_viz/src/dfs.rs b/search_viz/src/dfs.rs index 3b6ce18..575b806 100644 --- a/search_viz/src/dfs.rs +++ b/search_viz/src/dfs.rs @@ -4,8 +4,9 @@ pub enum Mark { Circle, } -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default, Clone, PartialEq)] pub struct Game { + // TODO: remove temp state: [Option; 9], circle_turn: bool, } @@ -25,6 +26,22 @@ const LINES: [[usize; 3]; 8] = [ ]; impl Game { + pub fn mark_at(&self, row: usize, col: usize) -> Option { + if let Some(pos) = row_col_to_pos(row, col) { + return self.state[pos]; + } + + None + } + + pub fn is_occupied(&self, row: usize, col: usize) -> bool { + if let Some(pos) = row_col_to_pos(row, col) { + return self.state[pos].is_some(); + } + + false + } + pub fn is_cross_turn(&self) -> bool { !self.circle_turn } @@ -33,6 +50,11 @@ impl Game { self.circle_turn } + pub fn do_move_row_col(&mut self, row: usize, col: usize) { + let pos = row_col_to_pos(row, col).expect("invalid row or col"); + self.do_move(pos); + } + pub fn do_move(&mut self, pos: usize) { assert!(pos < 9); @@ -61,7 +83,11 @@ impl Game { moves } - pub fn is_won(&self) -> Option { + pub fn is_won(&self) -> bool { + self.winning_mark().is_some() + } + + pub fn winning_mark(&self) -> Option { for line in LINES { if let Some(mark) = self.state[line[0]] { if self.state[line[1]] == Some(mark) && self.state[line[2]] == Some(mark) { @@ -73,3 +99,13 @@ impl Game { None } } + +fn row_col_to_pos(row: usize, col: usize) -> Option { + let pos = row * 3 + col; + + if pos < 9 { + Some(pos) + } else { + None + } +} diff --git a/search_viz/src/dfs_ui.rs b/search_viz/src/dfs_ui.rs index 3ee8a30..12cb02e 100644 --- a/search_viz/src/dfs_ui.rs +++ b/search_viz/src/dfs_ui.rs @@ -1,7 +1,11 @@ -use egui::RichText; +use egui::{vec2, Button, Grid, RichText}; + +use crate::dfs::{Game, Mark}; #[derive(Debug, Clone, Default, PartialEq)] -pub struct State {} +pub struct State { + game: Game, +} impl State { pub fn ui(&mut self, ui: &mut egui::Ui) { @@ -17,5 +21,56 @@ impl State { }); }, ); + + Grid::new("game_grid") + .num_columns(3) + .spacing([8.0, 8.0]) + .show(ui, |ui| { + for row in 0..3 { + for col in 0..3 { + let mark = self.game.mark_at(row, col); + let label = mark.map_or("", mark_image); + + let button = Button::new(label) + .fill(match mark { + Some(Mark::Cross) => egui::Color32::from_rgb(150, 0, 150), + Some(Mark::Circle) => egui::Color32::from_rgb(0, 100, 200), + None => egui::Color32::from_gray(200), + }) + .min_size(vec2(64.0, 64.0)); + + let button = ui.add(button); + if button.clicked() && !self.game.is_occupied(row, col) && !self.game.is_won() { + self.game.do_move_row_col(row, col); + } + } + + ui.end_row(); + } + }); + + match self.game.winning_mark() { + None => { + ui.label(format!( + "{:?} turn.", + // TODO: maybe we should store Mark as the "next_turn" field in Game struct directly? + if self.game.is_cross_turn() { + Mark::Cross + } else { + Mark::Circle + } + )); + } + Some(winning_mark) => { + ui.label(format!("{:?} won.", winning_mark)); + } + } + } +} + +fn mark_image(mark: Mark) -> &'static str { + match mark { + Mark::Cross => "X", + Mark::Circle => "O", } } From 3285eaeb9c696483e6572f8c3b5e4fab60146ae9 Mon Sep 17 00:00:00 2001 From: lakret Date: Fri, 7 Jul 2023 05:05:53 +0200 Subject: [PATCH 12/22] visuals --- search_viz/src/main.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/search_viz/src/main.rs b/search_viz/src/main.rs index 90d3968..1f42adf 100644 --- a/search_viz/src/main.rs +++ b/search_viz/src/main.rs @@ -98,12 +98,14 @@ impl eframe::App for TemplateApp { /// Called each time the UI needs repainting, which may be many times per second. /// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`. fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + let tab_bg_fill = if self.tab == Tabs::BFS { + Color32::from_rgb(20, 0, 50) + } else { + Color32::from_rgb(0, 30, 30) + }; + egui::CentralPanel::default() - .frame( - Frame::default() - .fill(Color32::from_rgb(20, 0, 50)) - .inner_margin(Margin::same(20.0)), - ) + .frame(Frame::default().fill(tab_bg_fill).inner_margin(Margin::same(20.0))) .show(ctx, |ui| { ui.horizontal_top(|ui| { ui.selectable_value( From 23a87f9e780b151720618827e4c5ac78d1f3e318 Mon Sep 17 00:00:00 2001 From: lakret Date: Fri, 7 Jul 2023 22:12:43 +0200 Subject: [PATCH 13/22] next --- search_viz/src/dfs.rs | 16 +++++++++++++++- search_viz/src/dfs_ui.rs | 13 ++++++++++--- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/search_viz/src/dfs.rs b/search_viz/src/dfs.rs index 575b806..eb9c75d 100644 --- a/search_viz/src/dfs.rs +++ b/search_viz/src/dfs.rs @@ -6,7 +6,6 @@ pub enum Mark { #[derive(Debug, Default, Clone, PartialEq)] pub struct Game { - // TODO: remove temp state: [Option; 9], circle_turn: bool, } @@ -98,6 +97,13 @@ impl Game { None } + + // computer always plays as circles + pub fn select_next_move(&self) -> Option { + // TODO: run dfs and figure out which turn to take + todo!(); + None + } } fn row_col_to_pos(row: usize, col: usize) -> Option { @@ -109,3 +115,11 @@ fn row_col_to_pos(row: usize, col: usize) -> Option { None } } + +#[cfg(test)] +mod tests { + #[test] + fn next_moves_test() { + // TODO: check next moves + } +} diff --git a/search_viz/src/dfs_ui.rs b/search_viz/src/dfs_ui.rs index 12cb02e..26c5e6e 100644 --- a/search_viz/src/dfs_ui.rs +++ b/search_viz/src/dfs_ui.rs @@ -5,6 +5,7 @@ use crate::dfs::{Game, Mark}; #[derive(Debug, Clone, Default, PartialEq)] pub struct State { game: Game, + manual_mode: bool, } impl State { @@ -22,6 +23,12 @@ impl State { }, ); + ui.horizontal(|ui| { + ui.label("Play against:"); + ui.radio_value(&mut self.manual_mode, false, "Computer"); + ui.radio_value(&mut self.manual_mode, true, "Person"); + }); + Grid::new("game_grid") .num_columns(3) .spacing([8.0, 8.0]) @@ -31,16 +38,16 @@ impl State { let mark = self.game.mark_at(row, col); let label = mark.map_or("", mark_image); - let button = Button::new(label) + let cell = Button::new(RichText::new(label).size(46.0)) .fill(match mark { Some(Mark::Cross) => egui::Color32::from_rgb(150, 0, 150), Some(Mark::Circle) => egui::Color32::from_rgb(0, 100, 200), None => egui::Color32::from_gray(200), }) .min_size(vec2(64.0, 64.0)); + let cell = ui.add(cell); - let button = ui.add(button); - if button.clicked() && !self.game.is_occupied(row, col) && !self.game.is_won() { + if cell.clicked() && !self.game.is_occupied(row, col) && !self.game.is_won() { self.game.do_move_row_col(row, col); } } From 481b5d923016bd9c4e3299c4559f104f4c1fbb0d Mon Sep 17 00:00:00 2001 From: Lakret Date: Sat, 8 Jul 2023 02:52:27 +0200 Subject: [PATCH 14/22] Display for Game and test I --- search_viz/src/dfs.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/search_viz/src/dfs.rs b/search_viz/src/dfs.rs index eb9c75d..dfb00d2 100644 --- a/search_viz/src/dfs.rs +++ b/search_viz/src/dfs.rs @@ -1,3 +1,5 @@ +use std::fmt::Display; + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Mark { Cross, @@ -10,6 +12,24 @@ pub struct Game { circle_turn: bool, } +impl Display for Game { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for row in 0..3 { + for col in 0..3 { + let ch = match self.mark_at(row, col) { + None => "口", + Some(Mark::Cross) => "X", + Some(Mark::Circle) => "⚪", + }; + write!(f, "{} ", ch)?; + } + writeln!(f)?; + } + + Ok(()) + } +} + const LINES: [[usize; 3]; 8] = [ // rows [0, 1, 2], @@ -118,8 +138,15 @@ fn row_col_to_pos(row: usize, col: usize) -> Option { #[cfg(test)] mod tests { + use super::*; + #[test] fn next_moves_test() { // TODO: check next moves + let mut game = Game::default(); + // cross in the center + game.do_move_row_col(1, 1); + game.do_move_row_col(0, 0); + println!("{}", game); } } From a3a2ad0be197104e13200655d428ec98e625ae9c Mon Sep 17 00:00:00 2001 From: lakret Date: Sun, 16 Jul 2023 17:09:35 +0200 Subject: [PATCH 15/22] faulty scoring function --- search_viz/src/dfs.rs | 88 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 76 insertions(+), 12 deletions(-) diff --git a/search_viz/src/dfs.rs b/search_viz/src/dfs.rs index dfb00d2..25ff091 100644 --- a/search_viz/src/dfs.rs +++ b/search_viz/src/dfs.rs @@ -1,12 +1,15 @@ -use std::fmt::Display; +use std::{ + collections::{HashMap, HashSet}, + fmt::Display, +}; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Mark { Cross, Circle, } -#[derive(Debug, Default, Clone, PartialEq)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] pub struct Game { state: [Option; 9], circle_turn: bool, @@ -87,7 +90,7 @@ impl Game { self.circle_turn = !self.circle_turn; } - pub fn next_moves(&self) -> Vec { + pub fn next_moves_with_pos(&self) -> Vec<(Game, usize)> { let mut moves = vec![]; for pos in 0..9 { @@ -95,7 +98,7 @@ impl Game { let mut new_game = self.clone(); new_game.do_move(pos); - moves.push(new_game); + moves.push((new_game, pos)); } } @@ -106,6 +109,10 @@ impl Game { self.winning_mark().is_some() } + pub fn is_draw(&self) -> bool { + self.state.iter().all(|cell| cell.is_some()) && !self.is_won() + } + pub fn winning_mark(&self) -> Option { for line in LINES { if let Some(mark) = self.state[line[0]] { @@ -118,11 +125,54 @@ impl Game { None } - // computer always plays as circles - pub fn select_next_move(&self) -> Option { - // TODO: run dfs and figure out which turn to take - todo!(); - None + pub fn score_next_moves(&self, player_mark: Mark) -> HashMap { + let mut scores = HashMap::new(); + let mut is_first_turn = true; + + let mut stack = vec![(*self, 0, 1)]; + let mut seen = HashSet::new(); + + while let Some((game, first_turn_pos, depth)) = stack.pop() { + if !seen.contains(&game) { + seen.insert(game); + + for (next_move_game, pos) in game.next_moves_with_pos() { + // if it's the first turn the player makes on this branch, record the position for scoring; + // if it's deeper in the tree than the first turn, we just preserve the first turn position + let first_turn_pos = if is_first_turn { pos } else { first_turn_pos }; + + match next_move_game.winning_mark() { + None => stack.push((next_move_game, first_turn_pos, depth + 1)), + Some(next_game_win_mark) => { + if next_game_win_mark == player_mark { + scores + .entry(first_turn_pos) + .and_modify(|score| *score += 1.0 * (1.0 / depth as f32)) + .or_insert(1.0); + } else { + scores + .entry(first_turn_pos) + .and_modify(|score| *score -= 1.0 * (1.0 / depth as f32)) + .or_insert(-1.0); + } + } + } + } + + is_first_turn = false; + } + } + + scores + } + + pub fn select_next_move(&self, mark: Mark) -> Option { + let scores = self.score_next_moves(mark); + scores + .iter() + // TODO: unwrap + .max_by(|(_pos1, score1), (_pos2, score2)| score1.partial_cmp(score2).unwrap()) + .map(|(pos, _score)| *pos) } } @@ -141,12 +191,26 @@ mod tests { use super::*; #[test] - fn next_moves_test() { - // TODO: check next moves + fn select_next_moves_test() { let mut game = Game::default(); // cross in the center game.do_move_row_col(1, 1); + println!("{}", game); + + let scores = game.score_next_moves(Mark::Circle); + dbg!(scores); + + let pos = game.select_next_move(Mark::Circle); + game.do_move(pos.unwrap()); + println!("{}", game); + game.do_move_row_col(0, 0); println!("{}", game); + + let scores = game.score_next_moves(Mark::Circle); + dbg!(scores); + let pos = game.select_next_move(Mark::Circle); + game.do_move(pos.unwrap()); + println!("{}", game); } } From 44eaede465b199c7d85845c4d3edff0c01cbf8a9 Mon Sep 17 00:00:00 2001 From: lakret Date: Sun, 16 Jul 2023 18:40:37 +0200 Subject: [PATCH 16/22] change of strat is in order here --- search_viz/src/dfs.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/search_viz/src/dfs.rs b/search_viz/src/dfs.rs index 25ff091..8d86708 100644 --- a/search_viz/src/dfs.rs +++ b/search_viz/src/dfs.rs @@ -125,6 +125,7 @@ impl Game { None } + // TODO: BFS is much better here, because we can adjust score based on the depth of a level directly pub fn score_next_moves(&self, player_mark: Mark) -> HashMap { let mut scores = HashMap::new(); let mut is_first_turn = true; @@ -145,14 +146,15 @@ impl Game { None => stack.push((next_move_game, first_turn_pos, depth + 1)), Some(next_game_win_mark) => { if next_game_win_mark == player_mark { + // TODO: make depth cliff more profound: neg inf or something for the depth==2 lose scores .entry(first_turn_pos) - .and_modify(|score| *score += 1.0 * (1.0 / depth as f32)) + .and_modify(|score| *score += 1.0 * (1.0 / (depth as f32 * 10.0))) .or_insert(1.0); } else { scores .entry(first_turn_pos) - .and_modify(|score| *score -= 1.0 * (1.0 / depth as f32)) + .and_modify(|score| *score -= 1.0 * (1.0 / (depth as f32 * 10.0))) .or_insert(-1.0); } } From 85d596f06468dc7fd874e2aa9c214ed4c0ebb932 Mon Sep 17 00:00:00 2001 From: lakret Date: Sun, 16 Jul 2023 19:49:38 +0200 Subject: [PATCH 17/22] change of direction --- search_viz/src/dfs.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/search_viz/src/dfs.rs b/search_viz/src/dfs.rs index 8d86708..38a98ac 100644 --- a/search_viz/src/dfs.rs +++ b/search_viz/src/dfs.rs @@ -126,6 +126,7 @@ impl Game { } // TODO: BFS is much better here, because we can adjust score based on the depth of a level directly + // thus => switch to minimax pub fn score_next_moves(&self, player_mark: Mark) -> HashMap { let mut scores = HashMap::new(); let mut is_first_turn = true; From 6f87d1e9d55926db3a41e903afe8a1f76809722f Mon Sep 17 00:00:00 2001 From: lakret Date: Sun, 16 Jul 2023 22:32:28 +0200 Subject: [PATCH 18/22] basic minimax --- search_viz/src/dfs.rs | 44 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/search_viz/src/dfs.rs b/search_viz/src/dfs.rs index 38a98ac..863b1c1 100644 --- a/search_viz/src/dfs.rs +++ b/search_viz/src/dfs.rs @@ -9,6 +9,15 @@ pub enum Mark { Circle, } +impl Mark { + fn opponent(self) -> Mark { + match self { + Mark::Cross => Mark::Circle, + Mark::Circle => Mark::Cross, + } + } +} + #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] pub struct Game { state: [Option; 9], @@ -125,8 +134,36 @@ impl Game { None } - // TODO: BFS is much better here, because we can adjust score based on the depth of a level directly - // thus => switch to minimax + // TODO: optional max_depth: usize + pub fn minimax(&self, maximizing_player: Mark) -> f64 { + if let Some(winner) = self.winning_mark() { + if winner == maximizing_player { + return 1.0; + } else { + return -1.0; + } + }; + + if self.is_circle_turn() && maximizing_player == Mark::Circle + || self.is_cross_turn() && maximizing_player == Mark::Cross + { + // maximizing player + let mut value = -f64::NEG_INFINITY; + for (next_move, _pos) in self.next_moves_with_pos() { + value = f64::max(value, next_move.minimax(maximizing_player.opponent())); + } + return value; + } else { + // minimizing player + let mut value = f64::INFINITY; + for (next_move, _pos) in self.next_moves_with_pos() { + value = f64::min(value, next_move.minimax(maximizing_player.opponent())); + } + return value; + } + } + + // TODO: delete pub fn score_next_moves(&self, player_mark: Mark) -> HashMap { let mut scores = HashMap::new(); let mut is_first_turn = true; @@ -212,6 +249,9 @@ mod tests { let scores = game.score_next_moves(Mark::Circle); dbg!(scores); + + dbg!(game.minimax(Mark::Circle)); + let pos = game.select_next_move(Mark::Circle); game.do_move(pos.unwrap()); println!("{}", game); From 8f39e4e0aa1e7e5a8ff8182c0501cf39bf19aeae Mon Sep 17 00:00:00 2001 From: lakret Date: Mon, 17 Jul 2023 13:21:00 +0200 Subject: [PATCH 19/22] minimax auto-player --- search_viz/src/dfs.rs | 114 +++++++++++++++--------------------------- 1 file changed, 39 insertions(+), 75 deletions(-) diff --git a/search_viz/src/dfs.rs b/search_viz/src/dfs.rs index 863b1c1..96f3979 100644 --- a/search_viz/src/dfs.rs +++ b/search_viz/src/dfs.rs @@ -134,86 +134,44 @@ impl Game { None } - // TODO: optional max_depth: usize - pub fn minimax(&self, maximizing_player: Mark) -> f64 { - if let Some(winner) = self.winning_mark() { - if winner == maximizing_player { - return 1.0; + pub fn select_next_move(&self) -> Option { + self + .next_moves_with_pos() + .into_iter() + .map(|(game, pos)| (pos, game.minimax(false, 0))) + .max_by_key(|(_pos, score)| *score) + .map(|(best_pos, _best_score)| best_pos) + } + + pub fn minimax(&self, is_maximizing: bool, depth: i32) -> i32 { + if self.is_won() { + if is_maximizing { + return -10 + depth; } else { - return -1.0; + return 10 - depth; } }; - if self.is_circle_turn() && maximizing_player == Mark::Circle - || self.is_cross_turn() && maximizing_player == Mark::Cross - { + if self.is_draw() { + return 0; + } + + if is_maximizing { // maximizing player - let mut value = -f64::NEG_INFINITY; + let mut best_score = i32::MIN; for (next_move, _pos) in self.next_moves_with_pos() { - value = f64::max(value, next_move.minimax(maximizing_player.opponent())); + best_score = best_score.max(next_move.minimax(false, depth + 1)); } - return value; + return best_score; } else { // minimizing player - let mut value = f64::INFINITY; + let mut best_score = i32::MAX; for (next_move, _pos) in self.next_moves_with_pos() { - value = f64::min(value, next_move.minimax(maximizing_player.opponent())); + best_score = best_score.min(next_move.minimax(true, depth + 1)); } - return value; + return best_score; } } - - // TODO: delete - pub fn score_next_moves(&self, player_mark: Mark) -> HashMap { - let mut scores = HashMap::new(); - let mut is_first_turn = true; - - let mut stack = vec![(*self, 0, 1)]; - let mut seen = HashSet::new(); - - while let Some((game, first_turn_pos, depth)) = stack.pop() { - if !seen.contains(&game) { - seen.insert(game); - - for (next_move_game, pos) in game.next_moves_with_pos() { - // if it's the first turn the player makes on this branch, record the position for scoring; - // if it's deeper in the tree than the first turn, we just preserve the first turn position - let first_turn_pos = if is_first_turn { pos } else { first_turn_pos }; - - match next_move_game.winning_mark() { - None => stack.push((next_move_game, first_turn_pos, depth + 1)), - Some(next_game_win_mark) => { - if next_game_win_mark == player_mark { - // TODO: make depth cliff more profound: neg inf or something for the depth==2 lose - scores - .entry(first_turn_pos) - .and_modify(|score| *score += 1.0 * (1.0 / (depth as f32 * 10.0))) - .or_insert(1.0); - } else { - scores - .entry(first_turn_pos) - .and_modify(|score| *score -= 1.0 * (1.0 / (depth as f32 * 10.0))) - .or_insert(-1.0); - } - } - } - } - - is_first_turn = false; - } - } - - scores - } - - pub fn select_next_move(&self, mark: Mark) -> Option { - let scores = self.score_next_moves(mark); - scores - .iter() - // TODO: unwrap - .max_by(|(_pos1, score1), (_pos2, score2)| score1.partial_cmp(score2).unwrap()) - .map(|(pos, _score)| *pos) - } } fn row_col_to_pos(row: usize, col: usize) -> Option { @@ -237,22 +195,28 @@ mod tests { game.do_move_row_col(1, 1); println!("{}", game); - let scores = game.score_next_moves(Mark::Circle); - dbg!(scores); + let pos = game.select_next_move(); + game.do_move(pos.unwrap()); + println!("{}", game); - let pos = game.select_next_move(Mark::Circle); + game.do_move_row_col(0, 1); + println!("{}", game); + + let pos = game.select_next_move(); game.do_move(pos.unwrap()); println!("{}", game); - game.do_move_row_col(0, 0); + game.do_move_row_col(2, 0); println!("{}", game); - let scores = game.score_next_moves(Mark::Circle); - dbg!(scores); + let pos = game.select_next_move(); + game.do_move(pos.unwrap()); + println!("{}", game); - dbg!(game.minimax(Mark::Circle)); + game.do_move_row_col(1, 2); + println!("{}", game); - let pos = game.select_next_move(Mark::Circle); + let pos = game.select_next_move(); game.do_move(pos.unwrap()); println!("{}", game); } From cc3e8d89e98220ea21c5e547ca40bad44ce605b2 Mon Sep 17 00:00:00 2001 From: lakret Date: Mon, 17 Jul 2023 13:31:55 +0200 Subject: [PATCH 20/22] finished minimax example --- search_viz/src/main.rs | 18 +++++----- search_viz/src/{dfs.rs => minimax.rs} | 5 +-- search_viz/src/{dfs_ui.rs => minimax_ui.rs} | 40 +++++++++++++-------- 3 files changed, 36 insertions(+), 27 deletions(-) rename search_viz/src/{dfs.rs => minimax.rs} (98%) rename search_viz/src/{dfs_ui.rs => minimax_ui.rs} (65%) diff --git a/search_viz/src/main.rs b/search_viz/src/main.rs index 1f42adf..205c7ad 100644 --- a/search_viz/src/main.rs +++ b/search_viz/src/main.rs @@ -3,8 +3,8 @@ use instant::Duration; mod bfs; mod bfs_ui; -mod dfs; -mod dfs_ui; +mod minimax; +mod minimax_ui; // when compiling natively #[cfg(not(target_arch = "wasm32"))] @@ -46,23 +46,23 @@ fn main() { #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Tabs { BFS, - DFS, + Minimax, } #[derive(Debug, Clone, PartialEq)] pub struct TemplateApp { tab: Tabs, bfs_state: bfs_ui::State, - dfs_state: dfs_ui::State, + minimax_state: minimax_ui::State, } impl Default for TemplateApp { fn default() -> Self { TemplateApp { // TODO: return it to be the BFS default - tab: Tabs::DFS, + tab: Tabs::Minimax, bfs_state: bfs_ui::State::default(), - dfs_state: dfs_ui::State::default(), + minimax_state: minimax_ui::State::default(), } } } @@ -115,15 +115,15 @@ impl eframe::App for TemplateApp { ); ui.selectable_value( &mut self.tab, - Tabs::DFS, - RichText::new("Depth-First Search Example").size(26.0), + Tabs::Minimax, + RichText::new("Minimax Example").size(26.0), ); }); if self.tab == Tabs::BFS { self.bfs_state.ui(ui); } else { - self.dfs_state.ui(ui); + self.minimax_state.ui(ui); } }); diff --git a/search_viz/src/dfs.rs b/search_viz/src/minimax.rs similarity index 98% rename from search_viz/src/dfs.rs rename to search_viz/src/minimax.rs index 96f3979..20fed22 100644 --- a/search_viz/src/dfs.rs +++ b/search_viz/src/minimax.rs @@ -1,7 +1,4 @@ -use std::{ - collections::{HashMap, HashSet}, - fmt::Display, -}; +use std::fmt::Display; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Mark { diff --git a/search_viz/src/dfs_ui.rs b/search_viz/src/minimax_ui.rs similarity index 65% rename from search_viz/src/dfs_ui.rs rename to search_viz/src/minimax_ui.rs index 26c5e6e..66ad8a2 100644 --- a/search_viz/src/dfs_ui.rs +++ b/search_viz/src/minimax_ui.rs @@ -1,6 +1,6 @@ -use egui::{vec2, Button, Grid, RichText}; +use egui::{vec2, Button, Color32, Grid, RichText}; -use crate::dfs::{Game, Mark}; +use crate::minimax::{Game, Mark}; #[derive(Debug, Clone, Default, PartialEq)] pub struct State { @@ -10,24 +10,22 @@ pub struct State { impl State { pub fn ui(&mut self, ui: &mut egui::Ui) { - ui.collapsing( - RichText::new("Depth-First Search Graph Algorithm Demo").heading(), - |ui| { - ui.label("This demo allows you to play tic-tac-toe against the computer."); - ui.label("Computer will use depth-first search to select its moves."); - ui.horizontal(|ui| { - ui.label("You can find the source code for this example "); - ui.hyperlink_to("here", "https://github.com/lakret/gir"); - ui.label("."); - }); - }, - ); + ui.collapsing(RichText::new("Minimax Algorithm Demo").heading(), |ui| { + ui.label("This demo allows you to play tic-tac-toe against the computer."); + ui.label("Computer will use minimax to select its moves."); + ui.horizontal(|ui| { + ui.label("You can find the source code for this example "); + ui.hyperlink_to("here", "https://github.com/lakret/gir"); + ui.label("."); + }); + }); ui.horizontal(|ui| { ui.label("Play against:"); ui.radio_value(&mut self.manual_mode, false, "Computer"); ui.radio_value(&mut self.manual_mode, true, "Person"); }); + ui.add_space(8.0); Grid::new("game_grid") .num_columns(3) @@ -49,6 +47,13 @@ impl State { if cell.clicked() && !self.game.is_occupied(row, col) && !self.game.is_won() { self.game.do_move_row_col(row, col); + + // do computer turn if computer mode is enabled + if !self.manual_mode { + if let Some(pos) = self.game.select_next_move() { + self.game.do_move(pos); + } + } } } @@ -72,6 +77,13 @@ impl State { ui.label(format!("{:?} won.", winning_mark)); } } + + if ui + .add(Button::new(RichText::new("New Game").size(26.0)).fill(Color32::DARK_GREEN)) + .clicked() + { + self.game = Game::default(); + } } } From 741cad391976383e1603f367aa4d88b4eddc74ba Mon Sep 17 00:00:00 2001 From: lakret Date: Mon, 17 Jul 2023 13:35:43 +0200 Subject: [PATCH 21/22] support draws in the UI --- search_viz/src/minimax_ui.rs | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/search_viz/src/minimax_ui.rs b/search_viz/src/minimax_ui.rs index 66ad8a2..a7f7290 100644 --- a/search_viz/src/minimax_ui.rs +++ b/search_viz/src/minimax_ui.rs @@ -63,15 +63,18 @@ impl State { match self.game.winning_mark() { None => { - ui.label(format!( - "{:?} turn.", - // TODO: maybe we should store Mark as the "next_turn" field in Game struct directly? - if self.game.is_cross_turn() { - Mark::Cross - } else { - Mark::Circle - } - )); + if self.game.is_draw() { + ui.label("It's a draw.") + } else { + ui.label(format!( + "{:?}'s turn.", + if self.game.is_cross_turn() { + Mark::Cross + } else { + Mark::Circle + } + )); + } } Some(winning_mark) => { ui.label(format!("{:?} won.", winning_mark)); From e06ac5bf8e2b953e0d8a2b1880774f6a8c84acdf Mon Sep 17 00:00:00 2001 From: lakret Date: Mon, 17 Jul 2023 13:37:43 +0200 Subject: [PATCH 22/22] tic-tac-toe --- search_viz/src/minimax_ui.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/search_viz/src/minimax_ui.rs b/search_viz/src/minimax_ui.rs index a7f7290..ff7da9e 100644 --- a/search_viz/src/minimax_ui.rs +++ b/search_viz/src/minimax_ui.rs @@ -60,11 +60,12 @@ impl State { ui.end_row(); } }); + ui.add_space(8.0); match self.game.winning_mark() { None => { if self.game.is_draw() { - ui.label("It's a draw.") + ui.label("It's a draw."); } else { ui.label(format!( "{:?}'s turn.", @@ -80,6 +81,7 @@ impl State { ui.label(format!("{:?} won.", winning_mark)); } } + ui.add_space(8.0); if ui .add(Button::new(RichText::new("New Game").size(26.0)).fill(Color32::DARK_GREEN))