diff --git a/Cargo.toml b/Cargo.toml index ba96a52..a2c9592 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,9 @@ gio = "0.20.12" glib = "0.20.12" wasm4 = "0.2.0" wasm4-sys = "0.1.3" +strum = "0.27.1" +strum_macros = "0.27.1" +heapless = "0.8.0" #[patch.crates-io] #cairo-rs = { git = "https://github.com/gtk-rs/gtk-rs-core.git", package = "cairo-rs", tag="0.20.12" } diff --git a/rdraught-ui/Cargo.toml b/rdraught-ui/Cargo.toml index 8b317d5..4ce4f05 100644 --- a/rdraught-ui/Cargo.toml +++ b/rdraught-ui/Cargo.toml @@ -11,12 +11,17 @@ version.workspace = true [dependencies] gtk4.workspace = true gdk4.workspace = true -rdraught.workspace = true +rdraught = {workspace = true, features = ["std"]} librsvg.workspace = true cairo-rs.workspace = true gio.workspace = true glib.workspace = true -# [[bin]] -# name = "dialog_test" -# path = "src/greeting_dialog.rs" +[lib] +name = "rdraught_ui" +crate-type = ["lib"] +bench = false + +[[bin]] +name = "rdraught-ui" +path = "src/main.rs" diff --git a/rdraught-ui/examples/ai_debugger.rs b/rdraught-ui/examples/ai_debugger.rs new file mode 100644 index 0000000..93a1c86 --- /dev/null +++ b/rdraught-ui/examples/ai_debugger.rs @@ -0,0 +1,19 @@ +use glib::ExitCode; +use rdraught::draughts::DraughtsGame; +use rdraught::{draughts::Piece, draughts::Player, position::Position}; +use rdraught_ui::run; +use std::collections::HashMap; + +fn main() -> ExitCode { + let mut pieces = HashMap::::new(); + pieces.insert(Position::new(2, 4), Piece::CrownedRedPawn); + pieces.insert(Position::new(5, 5), Piece::CrownedWhitePawn); + let game = DraughtsGame::new( + |p| match pieces.get(&p) { + None => Piece::NoPiece, + Some(piece) => *piece, + }, + Player::Red, + ); + run(game) +} diff --git a/rdraught-ui/src/final_dialog.rs b/rdraught-ui/src/final_dialog.rs new file mode 100644 index 0000000..c1f012b --- /dev/null +++ b/rdraught-ui/src/final_dialog.rs @@ -0,0 +1,12 @@ +use gtk4::{AlertDialog, Window, prelude::IsA}; +use rdraught::draughts::Player; + +pub(crate) async fn create_dialog>(window: W, winner: Player) { + let msg = match winner { + Player::Red => String::from("Red player wins"), + Player::White => String::from("White player wins"), + }; + let info_dialog = AlertDialog::builder().modal(true).message(msg).build(); + + info_dialog.show(Some(&window)); +} diff --git a/rdraught-ui/src/lib.rs b/rdraught-ui/src/lib.rs new file mode 100644 index 0000000..84cca54 --- /dev/null +++ b/rdraught-ui/src/lib.rs @@ -0,0 +1,8 @@ +mod geo2d; + +mod final_dialog; +mod greeting_dialog; +mod rdraught_application; +mod types; + +pub use rdraught_application::run; diff --git a/rdraught-ui/src/main.rs b/rdraught-ui/src/main.rs index dfe1191..396d7dc 100644 --- a/rdraught-ui/src/main.rs +++ b/rdraught-ui/src/main.rs @@ -1,511 +1,15 @@ -use gdk4::cairo::{Context as CairoContext, Matrix, Rectangle}; -use gtk4::cairo::Error; -use gtk4::glib::{MainContext, Propagation}; -use gtk4::{self as gtk, gdk::ffi::GDK_BUTTON_PRIMARY}; -use gtk4::{Application, DrawingArea, prelude::*}; -use rdraught::draughts::{DraughtsBoard, DraughtsGame, Move, Piece, Player}; -use rdraught::position::Position; +use glib::ExitCode; +use rdraught; +use rdraught::draughts::DraughtsGame; +use rdraught_ui; mod geo2d; -use core::f64::consts::PI; -use geo2d::Point; -use rsvg::SvgHandle; -use std::thread; -const SQUARE_SIZE: f64 = 1.0; mod final_dialog; mod greeting_dialog; +mod rdraught_application; mod types; -use types::{SharedMutable, SharedMutableRef, new_shared_mut, new_shared_mut_ref}; - -const CROWN_RED: &[u8] = include_bytes!("crown_red.svg"); -const CROWN_WHITE: &[u8] = include_bytes!("crown_white.svg"); - -fn transform_point(p: &Point, m: &Matrix) -> Point { - let (x, y) = m.transform_point(p.x(), p.y()); - Point::new(x, y) -} - -trait AugmentedRect { - fn from_points(tl: Point, br: Point) -> Rectangle; - fn center(&self) -> Point; - fn tl(&self) -> Point; - fn br(&self) -> Point; - fn contains(&self, p: &Point) -> bool; -} - -impl AugmentedRect for Rectangle { - fn center(&self) -> Point { - Point::new( - self.x() + self.width() / 2.0, - self.y() + self.height() / 2.0, - ) - } - - fn tl(&self) -> Point { - Point::new(self.x(), self.y()) - } - - fn br(&self) -> Point { - Point::new(self.x() + self.width(), self.y() + self.height()) - } - - fn from_points(tl: Point, br: Point) -> Rectangle { - Rectangle::new(tl.x(), tl.y(), br.x() - tl.x(), br.y() - tl.y()) - } - - fn contains(&self, p: &Point) -> bool { - self.x() < p.x() - && self.x() + self.width() > p.x() - && self.y() < p.y() - && self.y() + self.height() > p.y() - } -} - -fn draw_piece( - cr: &CairoContext, - square: &Rectangle, - piece: Piece, - crown_red: &SvgHandle, - crown_white: &SvgHandle, -) -> Result<(), Error> { - if let Piece::NoPiece = piece { - Ok(()) - } else { - cr.save()?; - let center = square.center(); - let outer_radius = square.width() * 0.3; - let vertical_scale_factor = 0.8; - let matrix = { - let mut m1 = Matrix::identity(); - m1.translate(0.0, -(center.y() - outer_radius)); - let mut m2 = Matrix::identity(); - m2.scale(1.0, vertical_scale_factor); - let mut m3 = Matrix::identity(); - m3.translate(0.0, center.y() - outer_radius * vertical_scale_factor); - Matrix::multiply(&Matrix::multiply(&m1, &m2), &m3) - }; - cr.set_matrix(Matrix::multiply(&matrix, &cr.matrix())); - cr.set_source_rgb(0.0, 0.0, 0.0); - let thickness = outer_radius * 0.3; - cr.arc( - center.x(), - center.y() + thickness / 2.0, - outer_radius, - 0.0, - 2.0 * PI, - ); - cr.rectangle( - center.x() - outer_radius, - center.y() - thickness / 2.0, - outer_radius * 2.0, - thickness, - ); - cr.arc( - center.x(), - center.y() - thickness / 2.0, - outer_radius, - 0.0, - 2.0 * PI, - ); - cr.fill().unwrap(); - let (color, crowned) = match piece { - Piece::NoPiece => return Ok(()), - Piece::SimpleRedPawn => ((1.0, 0.0, 0.0), false), - Piece::SimpleWhitePawn => ((1.0, 1.0, 1.0), false), - Piece::CrownedRedPawn => ((1.0, 0.0, 0.0), true), - Piece::CrownedWhitePawn => ((1.0, 1.0, 1.0), true), - }; - let radius = square.width() * 0.275; - cr.set_source_rgb(color.0, color.1, color.2); - cr.arc( - center.x(), - center.y() - thickness / 2.0, - radius, - 0.0, - 2.0 * PI, - ); - cr.fill()?; - cr.restore()?; - if crowned { - let renderer = match piece.player() { - Some(Player::Red) => rsvg::CairoRenderer::new(crown_red), - Some(Player::White) => rsvg::CairoRenderer::new(crown_white), - None => panic!("This should never happen"), - }; - let m4 = { - let mut m1 = Matrix::identity(); - m1.translate(-center.x(), -(center.y() - thickness / 1.0)); - let mut m2 = Matrix::identity(); - m2.scale(0.5, 0.5); - let mut m3 = Matrix::identity(); - m3.translate(center.x(), center.y() - thickness / 1.0); - Matrix::multiply(&Matrix::multiply(&m1, &m2), &m3) - }; - cr.set_matrix(Matrix::multiply( - &Matrix::multiply(&matrix, &m4), - &cr.matrix(), - )); - renderer - .render_document( - cr, - &cairo::Rectangle::new( - square.tl().x(), - square.tl().y(), - square.width(), - square.height(), - ), - ) - .unwrap(); - } - Ok(()) - } -} - -fn draw_score_bar( - cr: &CairoContext, - board: &Rectangle, - draughts_game: &DraughtsGame, - current_player: Player, -) { - fn modulate_score(relative_score: f64) -> f64 { - let x = relative_score; - f64::atan(8.0 * x - 4.0) / f64::atan(4.0) / 2.0 + 0.5 - } - let score_bar = Rectangle::new( - board.tl().x() - board.width() / 10.0, - board.tl().y(), - board.width() / 16.0, - board.height(), - ); - let score_percentage = modulate_score(draughts_game.relative_score(current_player) as f64); - let tl = score_bar.tl(); - cr.save().unwrap(); - match current_player { - Player::White => cr.set_source_rgb(1.0, 0.0, 0.0), - Player::Red => cr.set_source_rgb(1.0, 1.0, 1.0), - } - cr.rectangle( - score_bar.tl().x(), - score_bar.tl().y(), - score_bar.width(), - score_bar.height() * (1.0 - score_percentage), - ); - cr.fill().unwrap(); - match current_player { - Player::White => cr.set_source_rgb(1.0, 1.0, 1.0), - Player::Red => cr.set_source_rgb(1.0, 0.0, 0.0), - } - cr.rectangle( - tl.x(), - tl.y() + score_bar.height() * (1.0 - score_percentage), - score_bar.width(), - score_bar.height() * score_percentage, - ); - cr.fill().unwrap(); - cr.restore().unwrap(); -} - -fn create_game_window(application: &Application, current_player: Player) { - // Create a new window. - let window = gtk::ApplicationWindow::builder() - .application(application) - .title("Rdraught") - .default_width(800) - .default_height(800) - .build(); - - // Create a DrawingArea widget where we will draw the chessboard. - let drawing_area = DrawingArea::new(); - // Add the drawing area to the window. - window.set_child(Some(&drawing_area)); - - let draughts_game = new_shared_mut_ref(DraughtsGame::default()); - let selected_piece: SharedMutable> = new_shared_mut(None); - let available_moves = new_shared_mut_ref(Vec::::new()); - // Get the allocation information for the widget. - let board_width = SQUARE_SIZE * DraughtsBoard::rows() as f64; - let board_height = SQUARE_SIZE * DraughtsBoard::columns() as f64; - let board = Rectangle::from_points(Point::new(0.0, 0.0), Point::new(board_width, board_height)); - let board_with_bar = Rectangle::from_points( - Point::new(-board_width / 10.0, 0.0), - Point::new(board_width, board_height), - ); - let board_clone = board; - let crown_red_handle = { - let stream = gio::MemoryInputStream::from_bytes(&glib::Bytes::from_static(CROWN_RED)); - - rsvg::Loader::new() - .read_stream( - &stream, - None::<&gio::File>, // no base file as this document has no references - None::<&gio::Cancellable>, // no cancellable - ) - .unwrap() - }; - let crown_white_handle = { - let stream = gio::MemoryInputStream::from_bytes(&glib::Bytes::from_static(CROWN_WHITE)); - - rsvg::Loader::new() - .read_stream( - &stream, - None::<&gio::File>, // no base file as this document has no references - None::<&gio::Cancellable>, // no cancellable - ) - .unwrap() - }; - let xform = new_shared_mut_ref(Matrix::identity()); - // Set the "draw" function of the drawing area. This callback is called - // whenever GTK needs to redraw this widget (for example, on first display or when resized). - { - let draughts_game = draughts_game.clone(); - let xform = xform.clone(); - let selected_piece = selected_piece.clone(); - let available_moves = available_moves.clone(); - drawing_area.set_draw_func(move |_widget, cr, width, height| { - let screen = Rectangle::from_points( - Point::new(0.0, 0.0), - Point::new(width as f64, height as f64), - ); - let f = f64::min( - screen.width() / board_with_bar.width(), - screen.height() / board_with_bar.height(), - ); - let screen_center = screen.center(); - let board_center = board_with_bar.center(); - let mut xform = xform.borrow_mut(); - *xform = Matrix::multiply( - &Matrix::multiply( - &Matrix::new(1.0, 0.0, 0.0, 1.0, -board_center.x(), -board_center.y()), - &Matrix::new(f, 0.0, 0.0, f, 0.0, 0.0), - ), - &Matrix::new(1.0, 0.0, 0.0, 1.0, screen_center.x(), screen_center.y()), - ); - cr.set_matrix(*xform); - - // Loop over rows and columns to draw each chessboard cell. - for row in 0..DraughtsBoard::rows() { - for col in 0..DraughtsBoard::columns() { - let position = match current_player { - Player::White => Position::new((8 - row - 1) as u8, col as u8), - Player::Red => Position::new(row as u8, col as u8), - }; - let square = Rectangle::new( - col as f64 * SQUARE_SIZE, - row as f64 * SQUARE_SIZE, - SQUARE_SIZE, - SQUARE_SIZE, - ); - cr.save().unwrap(); - // Alternate colors based on the sum of row and column indices. - if (row + col) % 2 == 0 { - cr.set_source_rgb(0.8, 0.8, 0.6); - } else { - cr.set_source_rgb(0.4, 0.4, 0.2); - } - - // Draw and fill the square. - cr.rectangle( - square.tl().x(), - square.tl().y(), - square.width(), - square.height(), - ); - cr.fill().unwrap(); - draw_piece( - cr, - &square, - draughts_game.borrow().piece_at(position), - &crown_red_handle, - &crown_white_handle, - ) - .unwrap(); - cr.restore().unwrap(); - } - } - if let Some(selected_position) = selected_piece.get() { - let screen_position = match current_player { - Player::White => { - Position::new(8 - 1 - selected_position.row(), selected_position.col()) - } - Player::Red => selected_position, - }; - let square = Rectangle::new( - screen_position.col() as f64 * SQUARE_SIZE, - screen_position.row() as f64 * SQUARE_SIZE, - SQUARE_SIZE, - SQUARE_SIZE, - ); - cr.save().unwrap(); - cr.new_path(); - cr.move_to(square.tl().x(), square.tl().y()); - cr.line_to(square.tl().x(), square.br().y()); - cr.line_to(square.br().x(), square.br().y()); - cr.line_to(square.br().x(), square.tl().y()); - cr.line_to(square.tl().x(), square.tl().y()); - cr.clip(); - cr.new_path(); - cr.set_source_rgb(0.0, 0.0, 1.0); - cr.set_line_width((square.width() + square.height()) * 0.05); - cr.move_to(square.tl().x(), square.tl().y()); - cr.line_to(square.tl().x(), square.br().y()); - cr.line_to(square.br().x(), square.br().y()); - cr.line_to(square.br().x(), square.tl().y()); - cr.line_to(square.tl().x(), square.tl().y()); - cr.stroke().unwrap(); - cr.restore().unwrap(); - } - - let am = available_moves.borrow(); - if !am.is_empty() { - for mv in am.iter() { - let end_pos = mv.get_end_position(); - let screen_position = match current_player { - Player::White => Position::new(8 - 1 - end_pos.row(), end_pos.col()), - Player::Red => end_pos, - }; - let square = Rectangle::new( - screen_position.col() as f64 * SQUARE_SIZE, - screen_position.row() as f64 * SQUARE_SIZE, - SQUARE_SIZE, - SQUARE_SIZE, - ); - cr.save().unwrap(); - cr.new_path(); - cr.move_to(square.tl().x(), square.tl().y()); - cr.line_to(square.tl().x(), square.br().y()); - cr.line_to(square.br().x(), square.br().y()); - cr.line_to(square.br().x(), square.tl().y()); - cr.line_to(square.tl().x(), square.tl().y()); - cr.clip(); - cr.new_path(); - cr.set_source_rgb(0.0, 1.0, 0.0); - cr.set_line_width((square.width() + square.height()) * 0.05); - cr.move_to(square.tl().x(), square.tl().y()); - cr.line_to(square.tl().x(), square.br().y()); - cr.line_to(square.br().x(), square.br().y()); - cr.line_to(square.br().x(), square.tl().y()); - cr.line_to(square.tl().x(), square.tl().y()); - cr.stroke().unwrap(); - cr.restore().unwrap(); - } - } - draw_score_bar(cr, &board, &draughts_game.borrow(), current_player); - }); - } - let gesture = gtk::GestureClick::new(); - - // Set the gestures button to the right mouse button (=3) - gesture.set_button(GDK_BUTTON_PRIMARY as u32); - - // Assign your handler to an event of the gesture (e.g. the `pressed` event) - { - let drawing_area = drawing_area.clone(); - let available_moves = available_moves.clone(); - let window = window.clone(); - gesture.connect_pressed(move |gesture, _, x, y| { - gesture.set_state(gtk::EventSequenceState::Claimed); - if let Some(winner) = draughts_game.borrow().winner() { - MainContext::default() - .spawn_local(final_dialog::create_dialog(window.clone(), winner)); - } else { - let xform = xform.borrow(); - let inverse = { - let mut m = *xform; - m.invert(); - m - }; - let p = transform_point(&Point::new(x, y), &inverse); - if board_clone.contains(&p) { - let p = &p - &board_clone.tl(); - // println!("Point: {:?}", p); - let position = match current_player { - Player::White => Position::new( - (8.0 - (p.y() / SQUARE_SIZE)) as u8, - (p.x() / SQUARE_SIZE) as u8, - ), - Player::Red => { - Position::new((p.y() / SQUARE_SIZE) as u8, (p.x() / SQUARE_SIZE) as u8) - } - }; - // println!("Selected position: {:?}", position); - let mut draughts_game = draughts_game.borrow_mut(); - let piece = draughts_game.piece_at(position); - // println!("Selected piece: {:?}", piece); - let mut am = available_moves.borrow_mut(); - let mut move_applied = false; - if !am.is_empty() { - for mv in am.iter() { - if mv.get_end_position() == position { - draughts_game.apply_move(mv).unwrap(); - let game_copy = draughts_game.clone(); - // thread::spawn(move || { - // if let (Some(mv), analyzed_moves) = game_copy.get_best_move(10) { - // println!( - // "Next best move: {:?}, analyzed moves: {}", - // mv, analyzed_moves - // ); - // } - // }); - move_applied = true; - break; - } - } - if move_applied { - selected_piece.set(None); - am.clear(); - } - } - if !move_applied { - match piece.player() { - Some(Player::Red) => selected_piece.set(Some(position)), - Some(Player::White) => selected_piece.set(Some(position)), - None => selected_piece.set(None), - } - am.clear(); - if piece.player().is_none() { - selected_piece.set(None) - } else { - selected_piece.set(Some(position)); - for mv in draughts_game.moves_for_piece(position) { - am.push(mv); - } - } - } - } else { - selected_piece.set(None); - } - drawing_area.queue_draw(); - } - }); - } - // Assign the gesture to the treeview - drawing_area.add_controller(gesture); - window.present(); -} - -fn on_activate(application: &Application) { - // Initialize GTK before using any GTK functions. - if gtk::init().is_err() { - panic!("Failed to initialize GTK."); - } - - let current_player = new_shared_mut(Player::Red); - let dialog = greeting_dialog::create(application, current_player.clone()); - let application = application.clone(); - dialog.connect_close_request(move |w| { - application.remove_window(w); - create_game_window(&application, current_player.get()); - Propagation::Proceed - }); -} - -fn main() { - // Create a new application with the builder pattern - let app = Application::builder() - .application_id("net.woggioni.rdraught") - .build(); - app.connect_activate(on_activate); - // Run the application - app.run(); +fn main() -> ExitCode { + let game = DraughtsGame::default(); + rdraught_ui::run(game) } diff --git a/rdraught-ui/src/rdraught_application.rs b/rdraught-ui/src/rdraught_application.rs new file mode 100644 index 0000000..4d8920b --- /dev/null +++ b/rdraught-ui/src/rdraught_application.rs @@ -0,0 +1,572 @@ +use super::geo2d::Point; +use core::f64::consts::PI; +use gdk4::cairo::{Context as CairoContext, Matrix, Rectangle}; +use glib::ExitCode; +use gtk4::glib::{MainContext, Propagation}; +use gtk4::{self as gtk, gdk::ffi::GDK_BUTTON_PRIMARY}; +use gtk4::{Application, DrawingArea, prelude::*}; +use rdraught::draughts::{DraughtsBoard, DraughtsGame, Error, Move, Piece, Player}; +use rdraught::position::Position; +use rsvg::SvgHandle; +use std::thread; +const SQUARE_SIZE: f64 = 1.0; + +use super::final_dialog; +use super::greeting_dialog; +use super::types; + +use types::{SharedMutable, SharedMutableRef, new_shared_mut, new_shared_mut_ref}; + +const CROWN_RED: &[u8] = include_bytes!("crown_red.svg"); +const CROWN_WHITE: &[u8] = include_bytes!("crown_white.svg"); + +fn transform_point(p: &Point, m: &Matrix) -> Point { + let (x, y) = m.transform_point(p.x(), p.y()); + Point::new(x, y) +} + +trait AugmentedRect { + fn from_points(tl: Point, br: Point) -> Rectangle; + fn center(&self) -> Point; + fn tl(&self) -> Point; + fn br(&self) -> Point; + fn contains(&self, p: &Point) -> bool; +} + +impl AugmentedRect for Rectangle { + fn center(&self) -> Point { + Point::new( + self.x() + self.width() / 2.0, + self.y() + self.height() / 2.0, + ) + } + + fn tl(&self) -> Point { + Point::new(self.x(), self.y()) + } + + fn br(&self) -> Point { + Point::new(self.x() + self.width(), self.y() + self.height()) + } + + fn from_points(tl: Point, br: Point) -> Rectangle { + Rectangle::new(tl.x(), tl.y(), br.x() - tl.x(), br.y() - tl.y()) + } + + fn contains(&self, p: &Point) -> bool { + self.x() < p.x() + && self.x() + self.width() > p.x() + && self.y() < p.y() + && self.y() + self.height() > p.y() + } +} + +fn draw_piece( + cr: &CairoContext, + square: &Rectangle, + piece: Piece, + crown_red: &SvgHandle, + crown_white: &SvgHandle, +) -> Result<(), cairo::Error> { + if let Piece::NoPiece = piece { + Ok(()) + } else { + cr.save()?; + let center = square.center(); + let outer_radius = square.width() * 0.3; + let vertical_scale_factor = 0.8; + let matrix = { + let mut m1 = Matrix::identity(); + m1.translate(0.0, -(center.y() - outer_radius)); + let mut m2 = Matrix::identity(); + m2.scale(1.0, vertical_scale_factor); + let mut m3 = Matrix::identity(); + m3.translate(0.0, center.y() - outer_radius * vertical_scale_factor); + Matrix::multiply(&Matrix::multiply(&m1, &m2), &m3) + }; + cr.set_matrix(Matrix::multiply(&matrix, &cr.matrix())); + cr.set_source_rgb(0.0, 0.0, 0.0); + let thickness = outer_radius * 0.3; + cr.arc( + center.x(), + center.y() + thickness / 2.0, + outer_radius, + 0.0, + 2.0 * PI, + ); + cr.rectangle( + center.x() - outer_radius, + center.y() - thickness / 2.0, + outer_radius * 2.0, + thickness, + ); + cr.arc( + center.x(), + center.y() - thickness / 2.0, + outer_radius, + 0.0, + 2.0 * PI, + ); + cr.fill().unwrap(); + let (color, crowned) = match piece { + Piece::NoPiece => return Ok(()), + Piece::SimpleRedPawn => ((1.0, 0.0, 0.0), false), + Piece::SimpleWhitePawn => ((1.0, 1.0, 1.0), false), + Piece::CrownedRedPawn => ((1.0, 0.0, 0.0), true), + Piece::CrownedWhitePawn => ((1.0, 1.0, 1.0), true), + }; + let radius = square.width() * 0.275; + cr.set_source_rgb(color.0, color.1, color.2); + cr.arc( + center.x(), + center.y() - thickness / 2.0, + radius, + 0.0, + 2.0 * PI, + ); + cr.fill()?; + cr.restore()?; + if crowned { + let renderer = match piece.player() { + Some(Player::Red) => rsvg::CairoRenderer::new(crown_red), + Some(Player::White) => rsvg::CairoRenderer::new(crown_white), + None => panic!("This should never happen"), + }; + let m4 = { + let mut m1 = Matrix::identity(); + m1.translate(-center.x(), -(center.y() - thickness / 1.0)); + let mut m2 = Matrix::identity(); + m2.scale(0.5, 0.5); + let mut m3 = Matrix::identity(); + m3.translate(center.x(), center.y() - thickness / 1.0); + Matrix::multiply(&Matrix::multiply(&m1, &m2), &m3) + }; + cr.set_matrix(Matrix::multiply( + &Matrix::multiply(&matrix, &m4), + &cr.matrix(), + )); + renderer + .render_document( + cr, + &cairo::Rectangle::new( + square.tl().x(), + square.tl().y(), + square.width(), + square.height(), + ), + ) + .unwrap(); + } + Ok(()) + } +} + +fn draw_score_bar( + cr: &CairoContext, + board: &Rectangle, + draughts_game: &DraughtsGame, + current_player: Player, +) { + fn modulate_score(relative_score: f64) -> f64 { + let x = relative_score; + f64::atan(8.0 * x - 4.0) / f64::atan(4.0) / 2.0 + 0.5 + } + let score_bar = Rectangle::new( + board.tl().x() - board.width() / 10.0, + board.tl().y(), + board.width() / 16.0, + board.height(), + ); + let score_percentage = modulate_score(draughts_game.relative_score(current_player) as f64); + let tl = score_bar.tl(); + cr.save().unwrap(); + match current_player { + Player::White => cr.set_source_rgb(1.0, 0.0, 0.0), + Player::Red => cr.set_source_rgb(1.0, 1.0, 1.0), + } + cr.rectangle( + score_bar.tl().x(), + score_bar.tl().y(), + score_bar.width(), + score_bar.height() * (1.0 - score_percentage), + ); + cr.fill().unwrap(); + match current_player { + Player::White => cr.set_source_rgb(1.0, 1.0, 1.0), + Player::Red => cr.set_source_rgb(1.0, 0.0, 0.0), + } + cr.rectangle( + tl.x(), + tl.y() + score_bar.height() * (1.0 - score_percentage), + score_bar.width(), + score_bar.height() * score_percentage, + ); + cr.fill().unwrap(); + cr.restore().unwrap(); +} + +fn create_game_window( + rd: SharedMutableRef, + application: &Application, + current_player: Player, +) { + // Create a new window. + let window = gtk::ApplicationWindow::builder() + .application(application) + .title("Rdraught") + .default_width(800) + .default_height(800) + .build(); + + // Create a DrawingArea widget where we will draw the chessboard. + let drawing_area = DrawingArea::new(); + // Add the drawing area to the window. + window.set_child(Some(&drawing_area)); + + let selected_piece: SharedMutable> = new_shared_mut(None); + let available_moves = new_shared_mut_ref(Vec::::new()); + // Get the allocation information for the widget. + let board_width = SQUARE_SIZE * DraughtsBoard::rows() as f64; + let board_height = SQUARE_SIZE * DraughtsBoard::columns() as f64; + let board = Rectangle::from_points(Point::new(0.0, 0.0), Point::new(board_width, board_height)); + let board_with_bar = Rectangle::from_points( + Point::new(-board_width / 10.0, 0.0), + Point::new(board_width, board_height), + ); + let board_clone = board; + let crown_red_handle = { + let stream = gio::MemoryInputStream::from_bytes(&glib::Bytes::from_static(CROWN_RED)); + rsvg::Loader::new() + .read_stream( + &stream, + None::<&gio::File>, // no base file as this document has no references + None::<&gio::Cancellable>, // no cancellable + ) + .unwrap() + }; + let crown_white_handle = { + let stream = gio::MemoryInputStream::from_bytes(&glib::Bytes::from_static(CROWN_WHITE)); + + rsvg::Loader::new() + .read_stream( + &stream, + None::<&gio::File>, // no base file as this document has no references + None::<&gio::Cancellable>, // no cancellable + ) + .unwrap() + }; + let xform = new_shared_mut_ref(Matrix::identity()); + // Set the "draw" function of the drawing area. This callback is called + // whenever GTK needs to redraw this widget (for example, on first display or when resized). + { + let rd = rd.clone(); + let xform = xform.clone(); + let selected_piece = selected_piece.clone(); + let available_moves = available_moves.clone(); + drawing_area.set_draw_func(move |_widget, cr, width, height| { + let screen = Rectangle::from_points( + Point::new(0.0, 0.0), + Point::new(width as f64, height as f64), + ); + let f = f64::min( + screen.width() / board_with_bar.width(), + screen.height() / board_with_bar.height(), + ); + let screen_center = screen.center(); + let board_center = board_with_bar.center(); + let mut xform = xform.borrow_mut(); + *xform = Matrix::multiply( + &Matrix::multiply( + &Matrix::new(1.0, 0.0, 0.0, 1.0, -board_center.x(), -board_center.y()), + &Matrix::new(f, 0.0, 0.0, f, 0.0, 0.0), + ), + &Matrix::new(1.0, 0.0, 0.0, 1.0, screen_center.x(), screen_center.y()), + ); + cr.set_matrix(*xform); + + // Loop over rows and columns to draw each chessboard cell. + for row in 0..DraughtsBoard::rows() { + for col in 0..DraughtsBoard::columns() { + let position = match current_player { + Player::Red => Position::new(row as u8, (8 - 1 - col) as u8), + Player::White => Position::new((8 - 1 - row) as u8, col as u8), + }; + let square = Rectangle::new( + col as f64 * SQUARE_SIZE, + row as f64 * SQUARE_SIZE, + SQUARE_SIZE, + SQUARE_SIZE, + ); + cr.save().unwrap(); + // Alternate colors based on the sum of row and column indices. + if (row + col) % 2 == 0 { + cr.set_source_rgb(0.8, 0.8, 0.6); + } else { + cr.set_source_rgb(0.4, 0.4, 0.2); + } + + // Draw and fill the square. + cr.rectangle( + square.tl().x(), + square.tl().y(), + square.width(), + square.height(), + ); + cr.fill().unwrap(); + draw_piece( + cr, + &square, + rd.borrow().game.piece_at(position), + &crown_red_handle, + &crown_white_handle, + ) + .unwrap(); + cr.restore().unwrap(); + } + } + if let Some(selected_position) = selected_piece.get() { + let screen_position = match current_player { + Player::White => { + Position::new(8 - 1 - selected_position.row(), selected_position.col()) + } + Player::Red => { + Position::new(selected_position.row(), 8 - 1 - selected_position.col()) + } + }; + let square = Rectangle::new( + screen_position.col() as f64 * SQUARE_SIZE, + screen_position.row() as f64 * SQUARE_SIZE, + SQUARE_SIZE, + SQUARE_SIZE, + ); + cr.save().unwrap(); + cr.new_path(); + cr.move_to(square.tl().x(), square.tl().y()); + cr.line_to(square.tl().x(), square.br().y()); + cr.line_to(square.br().x(), square.br().y()); + cr.line_to(square.br().x(), square.tl().y()); + cr.line_to(square.tl().x(), square.tl().y()); + cr.clip(); + cr.new_path(); + cr.set_source_rgb(0.0, 0.0, 1.0); + cr.set_line_width((square.width() + square.height()) * 0.05); + cr.move_to(square.tl().x(), square.tl().y()); + cr.line_to(square.tl().x(), square.br().y()); + cr.line_to(square.br().x(), square.br().y()); + cr.line_to(square.br().x(), square.tl().y()); + cr.line_to(square.tl().x(), square.tl().y()); + cr.stroke().unwrap(); + cr.restore().unwrap(); + } + + let am = available_moves.borrow(); + if !am.is_empty() { + for mv in am.iter() { + let end_pos = mv.get_end_position(); + let screen_position = match current_player { + Player::White => Position::new(8 - 1 - end_pos.row(), end_pos.col()), + Player::Red => Position::new(end_pos.row(), 8 - 1 - end_pos.col()), + }; + let square = Rectangle::new( + screen_position.col() as f64 * SQUARE_SIZE, + screen_position.row() as f64 * SQUARE_SIZE, + SQUARE_SIZE, + SQUARE_SIZE, + ); + cr.save().unwrap(); + cr.new_path(); + cr.move_to(square.tl().x(), square.tl().y()); + cr.line_to(square.tl().x(), square.br().y()); + cr.line_to(square.br().x(), square.br().y()); + cr.line_to(square.br().x(), square.tl().y()); + cr.line_to(square.tl().x(), square.tl().y()); + cr.clip(); + cr.new_path(); + cr.set_source_rgb(0.0, 1.0, 0.0); + cr.set_line_width((square.width() + square.height()) * 0.05); + cr.move_to(square.tl().x(), square.tl().y()); + cr.line_to(square.tl().x(), square.br().y()); + cr.line_to(square.br().x(), square.br().y()); + cr.line_to(square.br().x(), square.tl().y()); + cr.line_to(square.tl().x(), square.tl().y()); + cr.stroke().unwrap(); + cr.restore().unwrap(); + } + } + draw_score_bar(cr, &board, &rd.borrow().game, current_player); + }); + } + let gesture = gtk::GestureClick::new(); + + // Set the gestures button to the right mouse button (=3) + gesture.set_button(GDK_BUTTON_PRIMARY as u32); + + // Assign your handler to an event of the gesture (e.g. the `pressed` event) + { + let drawing_area = drawing_area.clone(); + let available_moves = available_moves.clone(); + let window = window.clone(); + gesture.connect_pressed(move |gesture, _, x, y| { + gesture.set_state(gtk::EventSequenceState::Claimed); + if let Some(winner) = rd.borrow().game.winner() { + MainContext::default() + .spawn_local(final_dialog::create_dialog(window.clone(), winner)); + } else { + let xform = xform.borrow(); + let inverse = { + let mut m = *xform; + m.invert(); + m + }; + let p = transform_point(&Point::new(x, y), &inverse); + if board_clone.contains(&p) { + let p = &p - &board_clone.tl(); + // println!("Point: {:?}", p); + let position = match current_player { + Player::White => Position::new( + (8.0 - (p.y() / SQUARE_SIZE)) as u8, + (p.x() / SQUARE_SIZE) as u8, + ), + Player::Red => Position::new( + (p.y() / SQUARE_SIZE) as u8, + (8.0 - p.x() / SQUARE_SIZE) as u8, + ), + }; + // println!("Selected position: {:?}", position); + let piece = { + let draughts_game = &rd.borrow().game; + draughts_game.piece_at(position) + // println!("Selected piece: {:?}", piece); + }; + let am = available_moves.replace(Vec::new()); + let mut move_applied = false; + if !am.is_empty() { + for mv in am.into_iter() { + if mv.get_end_position() == position { + let mut rd_app = rd.borrow_mut(); + println!("Applied move: {:?}", mv); + rd_app.apply_move(mv).unwrap(); + let game_copy = rd_app.game.clone(); + thread::spawn(move || { + if let (Some(mv), analyzed_moves) = game_copy.get_best_move(10) + { + println!( + "Next best move: {:?}, analyzed moves: {}", + mv, analyzed_moves + ); + } + }); + move_applied = true; + break; + } + } + if move_applied { + selected_piece.set(None); + } + } + if !move_applied { + match piece.player() { + Some(Player::Red) => selected_piece.set(Some(position)), + Some(Player::White) => selected_piece.set(Some(position)), + None => selected_piece.set(None), + } + if piece.player().is_none() { + selected_piece.set(None) + } else { + let mut am = available_moves.borrow_mut(); + selected_piece.set(Some(position)); + for mv in rd.borrow().game.moves_for_piece(position) { + am.push(mv); + } + } + } + } else { + selected_piece.set(None); + } + drawing_area.queue_draw(); + } + }); + } + // Assign the gesture to the treeview + drawing_area.add_controller(gesture); + window.present(); +} +struct RDraughtApplication { + initial_state: DraughtsGame, + game: DraughtsGame, + moves: Vec, + cursor: usize, +} + +fn on_activate(application: &Application, game: DraughtsGame) { + // Initialize GTK before using any GTK functions. + if gtk::init().is_err() { + panic!("Failed to initialize GTK."); + } + + let current_player = new_shared_mut(Player::Red); + let dialog = greeting_dialog::create(application, current_player.clone()); + let application = application.clone(); + dialog.connect_close_request(move |w| { + application.remove_window(w); + let rd = new_shared_mut_ref(RDraughtApplication::new(game.clone())); + create_game_window(rd.clone(), &application, current_player.get()); + Propagation::Proceed + }); +} + +impl RDraughtApplication { + fn new(game: DraughtsGame) -> RDraughtApplication { + RDraughtApplication { + initial_state: game.clone(), + game, + moves: Vec::::new(), + cursor: 0usize, + } + } + + fn undo(&mut self) -> Result<(), Error> { + let mut new_state = self.initial_state.clone(); + if self.cursor > 0 && !self.moves.is_empty() { + self.cursor -= 1; + for mv in &self.moves[0..self.cursor] { + new_state.apply_move(mv)?; + } + self.game = new_state; + Ok(()) + } else { + Err(Error::InvalidMove) + } + } + + fn redo(&mut self) -> Result<(), Error> { + let mut new_state = self.initial_state.clone(); + if self.cursor < self.moves.len() { + for mv in &self.moves[0..self.cursor] { + new_state.apply_move(mv)?; + } + self.game = new_state; + self.cursor += 1; + Ok(()) + } else { + Err(Error::InvalidMove) + } + } + + fn apply_move(&mut self, mv: Move) -> Result<(), Error> { + self.game.apply_move(&mv)?; + self.moves.truncate(self.cursor); + self.moves.push(mv); + self.cursor += 1; + Ok(()) + } +} + +pub fn run(game: DraughtsGame) -> ExitCode { + // Create a new application with the builder pattern + let app = Application::builder() + .application_id("net.woggioni.rdraught") + .build(); + app.connect_activate(move |it| on_activate(it, game.clone())); + app.run() +} diff --git a/rdraught/Cargo.toml b/rdraught/Cargo.toml index fdd75a4..4905624 100644 --- a/rdraught/Cargo.toml +++ b/rdraught/Cargo.toml @@ -13,7 +13,9 @@ name = "rdraught" crate-type = ["lib"] [dependencies] -heapless = "0.8" +heapless.workspace = true +strum.workspace = true +strum_macros.workspace = true -[dev-dependencies] -rand = "0.9" +[features] +std = [] diff --git a/rdraught/src/board.rs b/rdraught/src/board.rs index b714ae4..68eb525 100644 --- a/rdraught/src/board.rs +++ b/rdraught/src/board.rs @@ -10,7 +10,7 @@ pub trait RectangularBoard { fn columns() -> usize; } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Eq, PartialEq)] pub(crate) struct Board { data: [[TYPE; COLUMNS]; ROWS], } diff --git a/rdraught/src/draughts.rs b/rdraught/src/draughts.rs index 1258c0f..1150daa 100644 --- a/rdraught/src/draughts.rs +++ b/rdraught/src/draughts.rs @@ -1,12 +1,11 @@ use super::board::Board; use super::board::RectangularBoard; +use super::position::Position; use core::ops::Index; use core::ops::IndexMut; -use core::result; use heapless::BinaryHeap; use heapless::Vec; - -use super::position::Position; +use strum_macros::FromRepr; #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum Player { @@ -14,7 +13,7 @@ pub enum Player { Red = 1, } -#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[derive(Debug, PartialEq, Eq, Clone, Copy, FromRepr)] pub enum Piece { NoPiece = 0, SimpleRedPawn = 1, @@ -47,7 +46,7 @@ impl Piece { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Eq, PartialEq)] pub struct DraughtsBoard(Board); impl Default for DraughtsBoard { @@ -97,6 +96,15 @@ impl RectangularBoard for DraughtsBoard { } impl DraughtsBoard { + pub fn new(mut cb: T) -> DraughtsBoard + where + T: FnMut(Position) -> Piece, + { + DraughtsBoard(Board::::new(|(i, j)| { + cb(Position::new(i as u8, j as u8)) + })) + } + pub fn rows() -> usize { Board::::rows() } @@ -238,12 +246,18 @@ impl DraughtsBoard { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Eq, PartialEq)] pub struct DraughtsGame { board: DraughtsBoard, next_move: Player, } +#[derive(Clone)] +pub struct SerializedDraughtsGame { + data: Vec, + next_move: Player, +} + #[derive(Debug)] pub enum Error { WrongPlayer, @@ -269,8 +283,11 @@ type MoveRanking = BinaryHeap; impl DraughtsGame { - pub fn new() -> DraughtsGame { - DraughtsGame::default() + pub fn new Piece>(cb: T, next_move: Player) -> DraughtsGame { + DraughtsGame { + board: DraughtsBoard::new(cb), + next_move, + } } pub fn piece_at(&self, p: Position) -> Piece { @@ -504,6 +521,64 @@ impl DraughtsGame { } } } + +impl SerializedDraughtsGame { + fn serialize(position: Position, piece: Piece) -> u8 { + let p = position.row() * 4 + position.col() / 2; + p << 3 | piece as u8 + } + + fn deserialize(byte: u8) -> (Position, Piece) { + let index = byte >> 3; + let row = index >> 2; + let col = ((index - (row << 2)) % 4) * 2 + (row % 2); + let pos = Position::new(row, col); + (pos, Piece::from_repr((byte & 7) as usize).unwrap()) + } + + fn from(DraughtsGame { board, next_move }: &DraughtsGame) -> SerializedDraughtsGame { + let mut data = Vec::::new(); + for i in 0..DraughtsBoard::rows() { + for j in 0..DraughtsBoard::columns() { + let p = Position::new(i as u8, j as u8); + let piece = board.get_piece(&p); + if piece != Piece::NoPiece { + data.push(Self::serialize(p, piece)).unwrap(); + } + } + } + SerializedDraughtsGame { + data, + next_move: next_move.clone(), + } + } +} + +impl From for SerializedDraughtsGame { + fn from(DraughtsGame { board, next_move }: DraughtsGame) -> Self { + let mut data = Vec::::new(); + for i in 0..DraughtsBoard::rows() { + for j in 0..DraughtsBoard::columns() { + let p = Position::new(i as u8, j as u8); + let piece = board.get_piece(&p); + if piece != Piece::NoPiece { + data.push(Self::serialize(p, piece)).unwrap(); + } + } + } + SerializedDraughtsGame { + data, + next_move: next_move.clone(), + } + } +} + +impl Into for SerializedDraughtsGame { + fn into(self) -> DraughtsGame { + todo!() + } +} + #[derive(Debug, PartialEq, Eq)] struct MoveHeapEntry { mv: Move, @@ -666,3 +741,111 @@ impl<'a> Iterator for MoveIterator<'a> { None } } + +#[cfg(feature = "std")] +mod std { + extern crate std; + use super::Position; + use std::hash::{Hash, Hasher}; + + impl Hash for Position { + fn hash(&self, state: &mut H) { + state.write_u8(self.row()); + state.write_u8(self.col()); + } + } +} + +#[cfg(feature = "std")] +pub use std::*; + +#[cfg(test)] +mod tests { + extern crate std; + use super::{ + DraughtsBoard, DraughtsGame, Move, MoveDirection, Piece, Player, Position, + SerializedDraughtsGame, + }; + use std::collections::HashMap; + use std::hash::{Hash, Hasher}; + + #[test] + fn test_serialize_deserialize_piece() { + for (pos, piece) in [ + (Position::new(4u8, 2u8), Piece::SimpleRedPawn), + (Position::new(0u8, 6u8), Piece::CrownedRedPawn), + (Position::new(1u8, 7u8), Piece::SimpleWhitePawn), + (Position::new(2u8, 4u8), Piece::CrownedWhitePawn), + (Position::new(3u8, 5u8), Piece::SimpleRedPawn), + (Position::new(6u8, 2u8), Piece::CrownedRedPawn), + (Position::new(7u8, 7u8), Piece::SimpleWhitePawn), + (Position::new(7u8, 1u8), Piece::CrownedWhitePawn), + (Position::new(6u8, 6u8), Piece::SimpleRedPawn), + (Position::new(4u8, 6u8), Piece::CrownedRedPawn), + (Position::new(6u8, 6u8), Piece::SimpleWhitePawn), + (Position::new(4u8, 6u8), Piece::CrownedWhitePawn), + ] { + let serialized = SerializedDraughtsGame::serialize(pos, piece); + let (deserialized_pos, deserialized_piece) = + SerializedDraughtsGame::deserialize(serialized); + assert_eq!(piece, deserialized_piece); + assert_eq!(pos, deserialized_pos); + } + } +} + +#[cfg(test)] +#[cfg(feature = "std")] +mod ai_tests { + extern crate std; + use super::{DraughtsGame, Move, MoveDirection, Piece, Player, Position}; + use std::collections::HashMap; + + #[test] + fn ai_test_simple() { + let mut pieces = HashMap::::new(); + pieces.insert(Position::new(2, 4), Piece::CrownedRedPawn); + pieces.insert(Position::new(7, 3), Piece::CrownedWhitePawn); + let game = DraughtsGame::new( + |p| match pieces.get(&p) { + None => Piece::NoPiece, + Some(piece) => *piece, + }, + Player::Red, + ); + + let (best_move, _) = game.get_best_move(8); + assert_eq!( + Some(Move::Movement { + start: Position::new(2, 4), + direction: MoveDirection::NE + }), + best_move + ); + } + + #[test] + fn ai_test_multiple_capture() { + let mut pieces = HashMap::::new(); + pieces.insert(Position::new(4, 4), Piece::SimpleWhitePawn); + pieces.insert(Position::new(4, 0), Piece::SimpleWhitePawn); + pieces.insert(Position::new(6, 2), Piece::SimpleRedPawn); + pieces.insert(Position::new(7, 1), Piece::SimpleRedPawn); + let game = DraughtsGame::new( + |p| match pieces.get(&p) { + None => Piece::NoPiece, + Some(piece) => *piece, + }, + Player::Red, + ); + + let (best_move, _) = game.get_best_move(5); + assert_eq!( + Some(Move::Movement { + start: Position::new(6, 2), + direction: MoveDirection::SW + }), + best_move + ); + } +} diff --git a/rdraught/src/lib.rs b/rdraught/src/lib.rs index 9aa9b57..0c81326 100644 --- a/rdraught/src/lib.rs +++ b/rdraught/src/lib.rs @@ -1,4 +1,7 @@ #![no_std] +#[macro_use] +extern crate strum; + mod board; pub mod draughts; pub mod position;