added undo/redo functionality

This commit is contained in:
2025-07-02 15:28:26 +08:00
parent 60b65e3eec
commit dd0777ff9a
4 changed files with 504 additions and 43 deletions

View File

@@ -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<Option<Position>> = new_shared_mut(None);
let available_moves = new_shared_mut_ref(Vec::<Move>::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<Option<Position>> = new_shared_mut(None);
let available_moves = new_shared_mut_ref(Vec::<Move>::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 {

View File

@@ -0,0 +1,297 @@
use core::mem::MaybeUninit;
use core::result::Result;
#[derive(Debug, PartialEq)]
pub enum Error {
BufferIsFull,
BufferIsEmpty,
BufferHasZeroSize,
}
pub struct CircularBuffer<T: Sized, const SIZE: usize> {
buffer: [MaybeUninit<T>; SIZE],
front: usize,
rear: usize,
fill: usize,
}
impl<T: Sized, const SIZE: usize> CircularBuffer<T, SIZE> {
pub fn new() -> CircularBuffer<T, SIZE> {
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<T> {
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<T> {
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<T> {
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<Item = &T> {
CircularBufferIteratorRef::<T, SIZE> {
cb: self,
count: 0,
count_back: 0,
}
}
pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut T> {
CircularBufferIteratorMutRef::<T, SIZE> {
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<T: Sized, const SIZE: usize> {
buffer: CircularBuffer<T, SIZE>,
}
impl<T: Sized, const SIZE: usize> Iterator for CircularBufferIterator<T, SIZE> {
fn next(&mut self) -> Option<Self::Item> {
self.buffer.pop_front()
}
type Item = T;
}
impl<T: Sized, const SIZE: usize> IntoIterator for CircularBuffer<T, SIZE> {
fn into_iter(self) -> Self::IntoIter {
CircularBufferIterator { buffer: self }
}
type IntoIter = CircularBufferIterator<T, SIZE>;
type Item = T;
}
pub struct CircularBufferIteratorRef<'a, T: Sized, const SIZE: usize> {
cb: &'a CircularBuffer<T, SIZE>,
count: usize,
count_back: usize,
}
impl<'a, T: Sized, const SIZE: usize> Iterator for CircularBufferIteratorRef<'a, T, SIZE> {
fn next(&mut self) -> Option<Self::Item> {
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<Self::Item> {
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<T, SIZE> {
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<T, SIZE>,
count: usize,
count_back: usize,
}
impl<'a, T: Sized, const SIZE: usize> Iterator for CircularBufferIteratorMutRef<'a, T, SIZE> {
fn next(&mut self) -> Option<Self::Item> {
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<Self::Item> {
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<T, SIZE> {
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::<i32, SZ>::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);
}
}
}

View File

@@ -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<Move, 128>,
moves: CircularBuffer<Move, 128>,
cursor: usize,
}
impl RDraughtApplication {
pub fn new(game: DraughtsGame) -> RDraughtApplication {
RDraughtApplication {
initial_state: game.clone(),
game,
moves: HistoryBuffer::<Move, 128>::new(),
moves: CircularBuffer::<Move, 128>::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::<HashMap<Position, Piece>>();
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 = [

View File

@@ -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;