diff --git a/rdraught-ui/src/rdraught_application.rs b/rdraught-ui/src/rdraught_application.rs index fcf92bc..c980f44 100644 --- a/rdraught-ui/src/rdraught_application.rs +++ b/rdraught-ui/src/rdraught_application.rs @@ -4,13 +4,12 @@ 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 gtk4::{Application, Button, DrawingArea, HeaderBar, prelude::*}; use rdraught::{ DraughtsBoard, DraughtsGame, Move, Piece, Player, Position, RDraughtApplication, RectangularBoard, }; use rsvg::SvgHandle; -use std::thread; const SQUARE_SIZE: f64 = 1.0; use super::final_dialog; @@ -244,14 +243,64 @@ fn create_game_window( .default_width(800) .default_height(800) .build(); + let header_bar = HeaderBar::new(); + window.set_titlebar(Some(&header_bar)); + let undo_button = Button::new(); + undo_button.set_icon_name("edit-undo-symbolic"); + undo_button.set_sensitive(false); + + let redo_button = Button::new(); + redo_button.set_icon_name("edit-redo-symbolic"); + redo_button.set_sensitive(false); + + let selected_piece: SharedMutable> = new_shared_mut(None); + let available_moves = new_shared_mut_ref(Vec::::new()); // 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()); + header_bar.pack_start(&undo_button); + { + let da = drawing_area.clone(); + let rd = rd.clone(); + let undo_btn_clone = undo_button.clone(); + let redo_btn_clone = redo_button.clone(); + let selected_piece = selected_piece.clone(); + let available_moves = available_moves.clone(); + undo_button.connect_clicked(move |_| { + let mut rd = rd.borrow_mut(); + if rd.can_undo() { + rd.undo().ok(); + redo_btn_clone.set_sensitive(rd.can_redo()); + undo_btn_clone.set_sensitive(rd.can_undo()); + selected_piece.set(None); + available_moves.borrow_mut().clear(); + da.queue_draw(); + } + }); + } + header_bar.pack_start(&redo_button); + { + let da = drawing_area.clone(); + let rd = rd.clone(); + let undo_btn_clone = undo_button.clone(); + let redo_btn_clone = redo_button.clone(); + let selected_piece = selected_piece.clone(); + let available_moves = available_moves.clone(); + redo_button.connect_clicked(move |_| { + let mut rd = rd.borrow_mut(); + if rd.can_redo() { + rd.redo().ok(); + redo_btn_clone.set_sensitive(rd.can_redo()); + undo_btn_clone.set_sensitive(rd.can_undo()); + selected_piece.set(None); + available_moves.borrow_mut().clear(); + da.queue_draw(); + } + }); + } // 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; @@ -429,6 +478,8 @@ fn create_game_window( let drawing_area = drawing_area.clone(); let available_moves = available_moves.clone(); let window = window.clone(); + let undo_btn_clone = undo_button.clone(); + let redo_btn_clone = redo_button.clone(); gesture.connect_pressed(move |gesture, _, x, y| { gesture.set_state(gtk::EventSequenceState::Claimed); if let Some(winner) = rd.borrow().game().winner() { @@ -455,7 +506,7 @@ fn create_game_window( (8.0 - p.x() / SQUARE_SIZE) as u8, ), }; - println!("Selected position: {:?}", position); + // println!("Selected position: {:?}", position); let piece = { let rd = rd.borrow(); let draughts_game = rd.game(); @@ -471,7 +522,7 @@ fn create_game_window( for mv in am.into_iter() { if mv.get_end_position() == pos { let mut rd_app = rd.borrow_mut(); - println!("Applied move: {:?}", mv); + // println!("Applied move: {:?}", mv); rd_app.apply_move(mv).unwrap(); // let game_copy = rd_app.game().clone(); // thread::spawn(move || { @@ -491,6 +542,9 @@ fn create_game_window( } if move_applied { selected_piece.set(None); + let rd = rd.borrow(); + undo_btn_clone.set_sensitive(rd.can_undo()); + redo_btn_clone.set_sensitive(rd.can_redo()); } } if !move_applied { diff --git a/rdraught/src/circular_buffer.rs b/rdraught/src/circular_buffer.rs new file mode 100644 index 0000000..0d1913a --- /dev/null +++ b/rdraught/src/circular_buffer.rs @@ -0,0 +1,297 @@ +use core::mem::MaybeUninit; +use core::result::Result; + +#[derive(Debug, PartialEq)] +pub enum Error { + BufferIsFull, + BufferIsEmpty, + BufferHasZeroSize, +} + +pub struct CircularBuffer { + buffer: [MaybeUninit; SIZE], + front: usize, + rear: usize, + fill: usize, +} + +impl CircularBuffer { + pub fn new() -> CircularBuffer { + CircularBuffer { + buffer: [const { MaybeUninit::uninit() }; SIZE], + front: 0, + rear: 0, + fill: 0, + } + } + + pub fn push(&mut self, element: T) -> Result<(), Error> { + if self.len() == SIZE { + Err(Error::BufferIsFull) + } else { + self.buffer[self.rear].write(element); + self.rear = (self.rear + 1) % SIZE; + self.fill += 1; + Ok(()) + } + } + + pub fn push_evict(&mut self, element: T) -> Option { + let result = if self.len() == SIZE { + self.pop_front() + } else { + None + }; + self.buffer[self.rear].write(element); + self.rear = (self.rear + 1) % SIZE; + self.fill += 1; + result + } + + pub fn pop_front(&mut self) -> Option { + if self.len() == 0 { + None + } else { + let mut result = MaybeUninit::uninit(); + core::mem::swap(&mut result, &mut self.buffer[self.front]); + self.front = (self.front + 1) % SIZE; + self.fill -= 1; + Some(unsafe { result.assume_init() }) + } + } + + pub fn pop_back(&mut self) -> Option { + if self.len() == 0 { + None + } else { + let mut result = MaybeUninit::uninit(); + core::mem::swap(&mut result, &mut self.buffer[self.rear - 1]); + self.rear = (self.rear - 1) % SIZE; + self.fill -= 1; + Some(unsafe { result.assume_init() }) + } + } + + pub fn len(&self) -> usize { + self.fill + } + + pub fn capacity(&self) -> usize { + SIZE + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn iter(&self) -> impl DoubleEndedIterator { + CircularBufferIteratorRef:: { + cb: self, + count: 0, + count_back: 0, + } + } + + pub fn iter_mut(&mut self) -> impl DoubleEndedIterator { + CircularBufferIteratorMutRef:: { + cb: self, + count: 0, + count_back: 0, + } + } + + pub fn first(&self) -> Option<&T> { + if self.len() > 0 { + unsafe { Some(self.buffer[self.front].assume_init_ref()) } + } else { + None + } + } + + pub fn last(&self) -> Option<&T> { + if self.len() > 0 { + unsafe { Some(self.buffer[self.rear - 1].assume_init_ref()) } + } else { + None + } + } +} + +pub struct CircularBufferIterator { + buffer: CircularBuffer, +} + +impl Iterator for CircularBufferIterator { + fn next(&mut self) -> Option { + self.buffer.pop_front() + } + type Item = T; +} + +impl IntoIterator for CircularBuffer { + fn into_iter(self) -> Self::IntoIter { + CircularBufferIterator { buffer: self } + } + + type IntoIter = CircularBufferIterator; + type Item = T; +} + +pub struct CircularBufferIteratorRef<'a, T: Sized, const SIZE: usize> { + cb: &'a CircularBuffer, + count: usize, + count_back: usize, +} + +impl<'a, T: Sized, const SIZE: usize> Iterator for CircularBufferIteratorRef<'a, T, SIZE> { + fn next(&mut self) -> Option { + if self.count + self.count_back == self.cb.len() { + None + } else { + let i = (self.cb.front + self.count) % SIZE; + let result = Some(unsafe { self.cb.buffer[i].assume_init_ref() }); + self.count += 1; + result + } + } + type Item = &'a T; +} + +impl<'a, T: Sized, const SIZE: usize> DoubleEndedIterator + for CircularBufferIteratorRef<'a, T, SIZE> +{ + fn next_back(&mut self) -> Option { + if self.count_back == self.cb.len() { + None + } else { + let i = (SIZE + self.cb.rear - 1 - self.count_back) % SIZE; + let result = Some(unsafe { self.cb.buffer[i].assume_init_ref() }); + self.count_back += 1; + result + } + } +} + +impl<'a, T: Sized, const SIZE: usize> IntoIterator for &'a CircularBuffer { + fn into_iter(self) -> Self::IntoIter { + CircularBufferIteratorRef { + cb: self, + count: 0, + count_back: 0, + } + } + + type IntoIter = CircularBufferIteratorRef<'a, T, SIZE>; + type Item = &'a T; +} + +pub struct CircularBufferIteratorMutRef<'a, T: Sized, const SIZE: usize> { + cb: &'a mut CircularBuffer, + count: usize, + count_back: usize, +} + +impl<'a, T: Sized, const SIZE: usize> Iterator for CircularBufferIteratorMutRef<'a, T, SIZE> { + fn next(&mut self) -> Option { + if self.count + self.count_back == self.cb.len() { + None + } else { + let result = unsafe { + let i = (self.cb.front + self.count) % SIZE; + Some(core::mem::transmute::<&mut T, &'a mut T>( + self.cb.buffer[i].assume_init_mut(), + )) + }; + self.count += 1; + result + } + } + + type Item = &'a mut T; +} + +impl<'a, T: Sized, const SIZE: usize> DoubleEndedIterator + for CircularBufferIteratorMutRef<'a, T, SIZE> +{ + fn next_back(&mut self) -> Option { + if self.count + self.count_back == self.cb.len() { + None + } else { + let result = unsafe { + let i = (SIZE + self.cb.rear - 1 - self.count_back) % SIZE; + Some(core::mem::transmute::<&mut T, &'a mut T>( + self.cb.buffer[i].assume_init_mut(), + )) + }; + self.count_back += 1; + result + } + } +} + +impl<'a, T: Sized, const SIZE: usize> IntoIterator for &'a mut CircularBuffer { + fn into_iter(self) -> Self::IntoIter { + CircularBufferIteratorMutRef { + cb: self, + count: 0, + count_back: 0, + } + } + + type IntoIter = CircularBufferIteratorMutRef<'a, T, SIZE>; + type Item = &'a mut T; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + const SZ: usize = 10; + let mut cb = CircularBuffer::::new(); + for i in 0..SZ { + let result = cb.push(i as i32); + assert_eq!(true, result.is_ok()); + } + + //Check the buffer is full + assert_eq!(SZ, cb.len()); + + //The buffer contains the elements that we've pushed + for (index, elem) in (&cb).into_iter().enumerate() { + assert_eq!(index as i32, *elem); + } + let result = cb.push(10); + //Cannot push in a full buffer + assert_eq!(true, result.is_err()); + + //Check push_evict returns the first element + let result = cb.push_evict(10); + assert_eq!(Some(0), result); + + //Check pop_front returns the second element + let second = cb.pop_front(); + assert_eq!(Some(1), second); + //Check the buffer is not full + assert_eq!(SZ - 1, cb.len()); + + //Check pop_front returns the third element + let second = cb.pop_front(); + + assert_eq!(Some(2), second); + //Check the buffer is not full + assert_eq!(SZ - 2, cb.len()); + + //Replace all the elements with their index + for (n, i) in cb.iter_mut().enumerate() { + *i = n as i32; + } + for (n, i) in cb.iter().enumerate() { + assert_eq!(n as i32, *i); + } + for (n, i) in cb.iter().rev().enumerate() { + assert_eq!((cb.len() - n - 1) as i32, *i); + } + } +} diff --git a/rdraught/src/draughts.rs b/rdraught/src/draughts.rs index f23c7da..15a2de3 100644 --- a/rdraught/src/draughts.rs +++ b/rdraught/src/draughts.rs @@ -1,9 +1,10 @@ +use super::circular_buffer::CircularBuffer; use super::constants::{BITS_PER_POSITION, POSITIONS, POSITIONS_PER_ROW}; use super::movement::{Move, MoveDirection}; use super::position::Position; use crate::piece::Piece; use crate::player::Player; -use heapless::{HistoryBuffer, Vec}; +use heapless::Vec; pub trait RectangularBoard { fn rows() -> usize; @@ -120,7 +121,50 @@ impl DraughtsBoard { } } - fn apply_move(&mut self, mv: &Move) -> Result<(), Error> { + fn undo_move(&mut self, mv: &Move) { + let end_pos = mv.get_end_position(); + let p = self.get(end_pos); + self.set(end_pos, Piece::NoPiece); + self.set(mv.start_position(), p); + if mv.is_capture() { + let captured_position = (mv.start_position() + mv.get_end_position()) / (2, 2); + let captured_piece = match p.player().unwrap() { + Player::Red => { + if mv.crowned_captured() { + Piece::CrownedWhitePawn + } else { + Piece::SimpleWhitePawn + } + } + Player::White => { + if mv.crowned_captured() { + Piece::CrownedRedPawn + } else { + Piece::SimpleRedPawn + } + } + }; + self.set(captured_position, captured_piece); + } + } + + fn apply_move(&mut self, mv: &Move) { + let start = mv.start_position(); + let piece = self.get_piece(&start); + if mv.is_movement() { + let end = mv.get_end_position(); + self.set(start, Piece::NoPiece); + self.set(end, piece); + } else { + let end = mv.get_end_position(); + let captured_pos = (start + end) / (2, 2); + self.set(start, Piece::NoPiece); + self.set(end, piece); + self.set(captured_pos, Piece::NoPiece); + }; + } + + fn check_and_apply_move(&mut self, mv: &Move) -> Result<(), Error> { let start = mv.start_position(); let piece = self.get_piece(&start); if let Piece::NoPiece = piece { @@ -129,7 +173,7 @@ impl DraughtsBoard { let player = piece.player().unwrap(); if mv.is_movement() { let end = mv.get_end_position(); - // Make sure the move ends in a vlid position + // Make sure the move ends in a valid position if !DraughtsBoard::is_position_valid(end) { return Err(Error::InvalidMove); } @@ -150,7 +194,8 @@ impl DraughtsBoard { let piece_at_destination = self.get(mv.get_end_position()); // Make sure there is no piece at destination if let Piece::NoPiece = piece_at_destination { - let captured = self.get((end + start) / (2, 2)); + let captured_pos = (start + end) / (2, 2); + let captured = self.get(captured_pos); // Make sure there is a piece to be captured if let Piece::NoPiece = captured { return Err(Error::InvalidMove); @@ -160,7 +205,7 @@ impl DraughtsBoard { if captured_piece_player != player { self.set(start, Piece::NoPiece); self.set(end, piece); - self.set((end + start) / (2, 2), Piece::NoPiece); + self.set(captured_pos, Piece::NoPiece); } else { return Err(Error::InvalidMove); } @@ -338,19 +383,28 @@ impl DraughtsGame { } } - pub fn apply_move(&mut self, mv: &Move) -> Result<(), Error> { + fn undo_move(&mut self, mv: &Move) { + self.board.undo_move(mv); + self.next_turn(); + } + fn redo_move(&mut self, mv: &Move) { + self.board.apply_move(mv); + self.next_turn(); + } + + pub fn check_and_apply_move(&mut self, mv: &Move) -> Result<(), Error> { let start = mv.start_position(); let piece = self.board.get_piece(&start); if let Some(player) = piece.player() { if mv.is_movement() { if self.next_move == player { - self.board.apply_move(mv)?; + self.board.check_and_apply_move(mv)?; self.next_turn(); } else { return Err(Error::WrongPlayer); } } else if self.next_move == player { - self.board.apply_move(mv)?; + self.board.check_and_apply_move(mv)?; // Check if more captures are available for the current piece if self .board @@ -432,7 +486,7 @@ impl DraughtsGame { depth: u32, analyzed_moves: &mut usize, ) -> f32 { - self.apply_move(mv).unwrap(); + self.check_and_apply_move(mv).unwrap(); if depth != 0 { let mut best_score = None; for mv in self.available_moves() { @@ -599,57 +653,77 @@ impl<'a> Iterator for MoveIterator<'a> { } pub struct RDraughtApplication { - initial_state: DraughtsGame, game: DraughtsGame, - moves: HistoryBuffer, + moves: CircularBuffer, cursor: usize, } impl RDraughtApplication { pub fn new(game: DraughtsGame) -> RDraughtApplication { RDraughtApplication { - initial_state: game.clone(), game, - moves: HistoryBuffer::::new(), + moves: CircularBuffer::::new(), cursor: 0usize, } } + pub fn can_undo(&self) -> bool { + self.moves.len() > self.cursor + } + + pub fn can_redo(&self) -> bool { + self.cursor > 0 + } + pub 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)?; + let mut it = self.moves.iter().rev(); + let mut count = 0; + loop { + let mv = it.next(); + if let Some(mv) = mv { + if count == self.cursor { + self.game.undo_move(&mv); + self.cursor += 1; + break; + } + count += 1; + } else { + return Err(Error::InvalidMove); } - self.game = new_state; - Ok(()) - } else { - Err(Error::InvalidMove) } + Ok(()) } pub 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 { + if self.cursor == 0 { Err(Error::InvalidMove) + } else { + let mut it = self.moves.iter().rev(); + let mut count = 0; + loop { + let mv = it.next(); + if let Some(mv) = mv { + if count + 1 == self.cursor { + self.game.redo_move(&mv); + self.cursor -= 1; + break; + } + count += 1; + } else { + return Err(Error::InvalidMove); + } + } + Ok(()) } } pub fn apply_move(&mut self, mv: Move) -> Result<(), Error> { - self.game.apply_move(&mv)?; - if self.moves.len() == self.moves.capacity() { - self.initial_state.apply_move(self.moves.first().unwrap())?; + while self.cursor > 0 { + self.moves.pop_back(); + self.cursor -= 1; } - self.moves.write(mv); - self.cursor += 1; + self.game.check_and_apply_move(&mv)?; + self.moves.push_evict(mv); Ok(()) } @@ -676,9 +750,44 @@ mod std { #[cfg(feature = "std")] mod tests { extern crate std; + use crate::{Move, MoveDirection}; + use super::{DraughtsBoard, Piece, Position}; use std::collections::HashMap; + #[test] + fn test_undo_redo() { + let pieces = [ + (Position::new(0, 4), Piece::CrownedWhitePawn), + (Position::new(1, 3), Piece::SimpleRedPawn), + (Position::new(5, 5), Piece::SimpleWhitePawn), + (Position::new(4, 4), Piece::SimpleRedPawn), + ]; + + let map = pieces + .into_iter() + .map(|(pos, piece)| (pos.unwrap(), piece)) + .collect::>(); + let board = DraughtsBoard::new(|p| match map.get(&p) { + None => Piece::NoPiece, + Some(piece) => *piece, + }); + + let moves = [ + Move::capture(Position::new(0, 4).unwrap(), MoveDirection::NW, false), + Move::movement(Position::new(0, 4).unwrap(), MoveDirection::NE), + Move::capture(Position::new(5, 5).unwrap(), MoveDirection::SW, false), + Move::movement(Position::new(4, 4).unwrap(), MoveDirection::NW), + ]; + for mv in moves { + let mut board_clone = board.clone(); + board_clone.apply_move(&mv); + assert_ne!(board, board_clone); + board_clone.undo_move(&mv); + assert_eq!(board, board_clone); + } + } + #[test] fn test_create() { let boards = [ diff --git a/rdraught/src/lib.rs b/rdraught/src/lib.rs index 5c6cd2d..07c8ffd 100644 --- a/rdraught/src/lib.rs +++ b/rdraught/src/lib.rs @@ -1,5 +1,6 @@ #![no_std] +mod circular_buffer; mod constants; pub mod draughts; mod movement; @@ -8,7 +9,7 @@ mod player; mod position; pub use draughts::{DraughtsBoard, DraughtsGame, Error, RDraughtApplication, RectangularBoard}; -pub use movement::Move; +pub use movement::{Move, MoveDirection}; pub use piece::Piece; pub use player::Player; pub use position::Position;