diff --git a/Cargo.toml b/Cargo.toml index 0bb4b46..31733ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["rdraught", "rdraught-cli", "rdraught-pi", "rdraught-ui"] +members = ["rdraught", "rdraught-cli", "rdraught-pi", "rdraught-ui", "rdraught-w4"] resolver = "3" [workspace.package] @@ -20,6 +20,8 @@ gtk4 = "0.9" gdk4 = "0.9" gio = "0.20.12" glib = "0.20.12" +wasm4 = "0.2.0" +wasm4-sys = "0.1.3" #[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/src/main.rs b/rdraught-ui/src/main.rs index 45ea179..1191f38 100644 --- a/rdraught-ui/src/main.rs +++ b/rdraught-ui/src/main.rs @@ -63,10 +63,12 @@ fn draw_piece( piece: Piece, crown_red: &SvgHandle, crown_white: &SvgHandle, + current_player: Player, ) -> 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; @@ -79,8 +81,7 @@ fn draw_piece( m3.translate(0.0, center.y() - outer_radius * vertical_scale_factor); Matrix::multiply(&Matrix::multiply(&m1, &m2), &m3) }; - cr.save()?; - cr.set_matrix(matrix); + 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( @@ -115,12 +116,16 @@ fn draw_piece( cr.set_source_rgb(color.0, color.1, color.2); cr.arc( center.x(), - center.y() - thickness / 2.0, + match current_player { + Player::White => center.y() + thickness / 2.0, + Player::Red => 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), @@ -129,14 +134,27 @@ fn draw_piece( }; let m4 = { let mut m1 = Matrix::identity(); - m1.translate(-center.x(), -(center.y() - thickness / 1.0)); + match current_player { + Player::Red => m1.translate(-center.x(), -(center.y() - thickness / 1.0)), + Player::White => m1.translate(-center.x(), -(center.y() + thickness / 2.0)), + } let mut m2 = Matrix::identity(); m2.scale(0.5, 0.5); + if Player::White == current_player { + let m = Matrix::new(1.0, 0.0, 0.0, -1.0, 0.0, 0.0); + m2 = Matrix::multiply(&m2, &m); + } let mut m3 = Matrix::identity(); - m3.translate(center.x(), center.y() - thickness / 1.0); + match current_player { + Player::Red => m3.translate(center.x(), center.y() - thickness / 1.0), + Player::White => m3.translate(center.x(), center.y() + thickness / 2.0), + } Matrix::multiply(&Matrix::multiply(&m1, &m2), &m3) }; - cr.set_matrix(Matrix::multiply(&matrix, &m4)); + cr.set_matrix(Matrix::multiply( + &Matrix::multiply(&matrix, &m4), + &cr.matrix(), + )); renderer .render_document( cr, @@ -149,11 +167,54 @@ fn draw_piece( ) .unwrap(); } - cr.restore()?; Ok(()) } } +fn draw_score_bar( + cr: &CairoContext, + board: &Rectangle, + draughts_game: &DraughtsGame, + xform: &Matrix, +) { + let score_bar = Rectangle::new( + board.tl().x() - board.width() / 10.0, + board.tl().y(), + board.width() / 16.0, + board.height(), + ); + let score_percentage = draughts_game.relative_score(Player::White) as f64; + let tl = score_bar.tl(); + let br = score_bar.br(); + { + let (tlx, tly) = xform.transform_point(tl.x(), tl.y()); + let (brx, bry) = xform.transform_point(br.x(), br.y()); + println!( + "tl: ({}, {}), br: ({}, {}), score: {}", + tlx, tly, brx, bry, score_percentage + ); + } + cr.save().unwrap(); + //cr.set_matrix(*xform); + 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() * score_percentage, + ); + cr.fill().unwrap(); + cr.set_source_rgb(1.0, 0.0, 0.0); + cr.rectangle( + tl.x(), + tl.y() + score_bar.height() * score_percentage, + score_bar.width(), + score_bar.height(), + ); + cr.fill().unwrap(); + cr.restore().unwrap(); +} + fn on_activate(application: >k::Application) { // Initialize GTK before using any GTK functions. if gtk::init().is_err() { @@ -202,6 +263,7 @@ fn on_activate(application: >k::Application) { ) .unwrap() }; + let current_player = Rc::::new(Player::White); let xform = Rc::>::new(RefCell::new(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). @@ -209,26 +271,8 @@ fn on_activate(application: >k::Application) { let draughts_game = draughts_game.clone(); let xform = xform.clone(); let selected_piece = selected_piece.clone(); - let board_clone = board; let available_moves = available_moves.clone(); - let get_square_for_position = move |position: &Position, xform: &Matrix| -> Rectangle { - let square_size = SQUARE_SIZE; - - let p1 = Point::new( - (position.col() as f64) * square_size, - ((8 - 1 - position.row()) as f64) * square_size, - ); - let p2 = &p1 + &Point::new(square_size, square_size); - let square = Rectangle::from_points(&board_clone.tl() + &p1, &board_clone.tl() + &p2); - let tl = transform_point(&square.tl(), xform); - let br = transform_point(&square.br(), xform); - Rectangle::new( - f64::min(tl.x(), br.x()), - f64::min(tl.y(), br.y()), - f64::abs(tl.x() - br.x()), - f64::abs(tl.y() - br.y()), - ) - }; + let current_player = current_player.clone(); drawing_area .borrow_mut() .set_draw_func(move |_widget, cr, width, height| { @@ -245,17 +289,30 @@ fn on_activate(application: >k::Application) { 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::multiply( + &Matrix::new(1.0, 0.0, 0.0, 1.0, -board_center.x(), -board_center.y()), + &(match *current_player { + Player::White => Matrix::new(1.0, 0.0, 0.0, -1.0, 0.0, 0.0), + Player::Red => Matrix::new(1.0, 0.0, 0.0, 1.0, 0.0, 0.0), + }), + ), &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 = Position::new((8 - row - 1) as u8, col as u8); - let square = get_square_for_position(&position, &xform); + let square = Rectangle::new( + position.col() as f64 * SQUARE_SIZE, + position.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); @@ -277,12 +334,19 @@ fn on_activate(application: >k::Application) { draughts_game.borrow().piece_at(position), &crown_red_handle, &crown_white_handle, + *current_player, ) .unwrap(); + cr.restore().unwrap(); } } - if let Some(selected_postion) = selected_piece.get() { - let square = get_square_for_position(&selected_postion, &xform); + if let Some(selected_position) = selected_piece.get() { + let square = Rectangle::new( + selected_position.col() as f64 * SQUARE_SIZE, + selected_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()); @@ -306,7 +370,13 @@ fn on_activate(application: >k::Application) { let am = available_moves.borrow(); if !am.is_empty() { for mv in am.iter() { - let square = get_square_for_position(&mv.get_end_position(), &xform); + let end_pos = mv.get_end_position(); + let square = Rectangle::new( + end_pos.col() as f64 * SQUARE_SIZE, + end_pos.row() as f64 * SQUARE_SIZE, + SQUARE_SIZE, + SQUARE_SIZE, + ); cr.save().unwrap(); cr.new_path(); cr.move_to(square.tl().x(), square.tl().y()); @@ -327,6 +397,7 @@ fn on_activate(application: >k::Application) { cr.restore().unwrap(); } } + draw_score_bar(&cr, &board, &draughts_game.borrow(), &xform); }); } let gesture = gtk::GestureClick::new(); @@ -350,19 +421,21 @@ fn on_activate(application: >k::Application) { if board_clone.contains(&p) { let p = &p - &board_clone.tl(); // println!("Point: {:?}", p); - let position = Position::new( - 8 - 1 - (p.y() / SQUARE_SIZE) as u8, - (p.x() / SQUARE_SIZE) as u8, - ); + let position = + 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(); + if let Some(mv) = draughts_game.get_best_move(10) { + println!("Next best move: {:?}", mv); + } move_applied = true; break; } diff --git a/rdraught/src/draughts.rs b/rdraught/src/draughts.rs index 90000fa..b659b0d 100644 --- a/rdraught/src/draughts.rs +++ b/rdraught/src/draughts.rs @@ -1,9 +1,9 @@ use super::board::Board; -use super::board::BoardIteratorRef; use super::board::RectangularBoard; -use core::iter::{Filter, Map}; use core::ops::Index; use core::ops::IndexMut; +use core::result; +use heapless::BinaryHeap; use heapless::Vec; use super::position::Position; @@ -107,15 +107,12 @@ impl DraughtsBoard { pub fn pieces<'a, const PIECES: usize>( &'a self, - pieces: &'a Vec, - ) -> Map< - Filter, impl FnMut(&(usize, usize, Piece)) -> bool>, - impl FnMut((usize, usize, Piece)) -> Position, - > { + pieces: Vec, + ) -> impl Iterator { self.0 .iter() .filter(move |(_, _, piece)| { - for p in pieces { + for p in &pieces { if piece == p { return true; } @@ -268,6 +265,9 @@ impl Default for DraughtsGame { } } +type MoveRanking = + BinaryHeap; + impl DraughtsGame { pub fn new() -> DraughtsGame { DraughtsGame::default() @@ -294,7 +294,7 @@ impl DraughtsGame { } } let mut capture_available = false; - for pos in self.board.pieces(&needle) { + for pos in self.board.pieces(needle) { for _ in self.board.moves_for_piece(pos, true) { capture_available = true; } @@ -371,6 +371,164 @@ impl DraughtsGame { Err(Error::NoPiece) } } + + pub fn score_for_player(&self, p: Player) -> u32 { + let md = MoveDirection::NE; + let mut score = 0u32; + let pieces = { + let mut v = Vec::::new(); + match p { + Player::White => { + v.push(Piece::SimpleWhitePawn).unwrap(); + v.push(Piece::CrownedWhitePawn).unwrap(); + v + } + Player::Red => { + v.push(Piece::SimpleRedPawn).unwrap(); + v.push(Piece::CrownedRedPawn).unwrap(); + v + } + } + }; + for pos in self.board.pieces(pieces) { + let piece = self.board.get_piece(&pos); + if piece.is_crowned() { + score += 20; + } else { + score += 10; + if md.is_forward(piece).unwrap() { + score += pos.row() as u32; + } else { + score += DraughtsBoard::rows() as u32 - 1 - pos.row() as u32; + } + } + } + score + } + + pub fn relative_score(&self, p: Player) -> f32 { + let red = self.score_for_player(Player::Red); + let white = self.score_for_player(Player::White); + let subject = match p { + Player::White => white, + Player::Red => red, + }; + subject as f32 / (red + white) as f32 + } + + fn move_score(mut self, mv: &Move, player: Player, depth: u32) -> f32 { + let pos = mv.get_start_position(); + let piece = self.board.get_piece(&pos); + let moving_player = piece.player().unwrap(); + self.apply_move(&mv).unwrap(); + if depth != 0 { + let mut best_score = None; + for mv in self.available_moves() { + let clone = self.clone(); + let score = clone.move_score(&mv, player, depth - 1); + if best_score.is_none() { + best_score = Some(score); + } else { + if let Some(bs) = best_score { + if bs < score { + best_score = Some(bs); + } + } + } + } + best_score.unwrap_or_else(|| self.relative_score(player)) + } else { + self.relative_score(player) + } + } + + fn available_moves<'a>(&'a self) -> impl Iterator { + let mut pieces = Vec::::new(); + match self.next_move { + Player::White => { + pieces.push(Piece::SimpleWhitePawn).unwrap(); + pieces.push(Piece::CrownedWhitePawn).unwrap(); + } + Player::Red => { + pieces.push(Piece::SimpleRedPawn).unwrap(); + pieces.push(Piece::CrownedRedPawn).unwrap(); + } + } + self.board + .pieces(pieces) + .flat_map(|pos| self.moves_for_piece(pos)) + } + + fn find_best_move_rec( + &self, + ranking: &mut MoveRanking, + current_depth: u32, + max_depth: u32, + ) { + let mut pieces = Vec::::new(); + match self.next_move { + Player::White => { + pieces.push(Piece::SimpleWhitePawn).unwrap(); + pieces.push(Piece::CrownedWhitePawn).unwrap(); + } + Player::Red => { + pieces.push(Piece::SimpleRedPawn).unwrap(); + pieces.push(Piece::CrownedRedPawn).unwrap(); + } + } + self.board.pieces(pieces).for_each(|pos| { + self.moves_for_piece(pos).for_each(|mv| { + let mut game = self.clone(); + game.apply_move(&mv).unwrap(); + let new_score = game.score_for_player(self.next_move); + ranking.push(MoveHeapEntry { + mv, + score: new_score, + }); + }); + }); + } + + pub fn get_best_move(&self, max_depth: u32) -> Option { + let mut result: Option<(Move, f32)> = None; + let available_moves = self.available_moves().count(); + match available_moves { + 0 => None, + 1 => self.available_moves().next(), + _ => { + for mv in self.available_moves() { + let score = self.clone().move_score(&mv, self.next_move, max_depth - 1); + if result.is_none() { + result = Some((mv, score)); + } else { + if let Some((_, best_score)) = result { + if score > best_score { + result = Some((mv, score)); + } + } + } + } + result.map(|(mv, _)| mv) + } + } + } +} +#[derive(Debug, PartialEq, Eq)] +struct MoveHeapEntry { + mv: Move, + score: u32, +} + +impl PartialOrd for MoveHeapEntry { + fn partial_cmp(&self, other: &Self) -> Option { + self.score.partial_cmp(&other.score) + } +} + +impl Ord for MoveHeapEntry { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + self.score.cmp(&other.score) + } } #[derive(Debug, Copy, Clone, PartialEq, Eq)]