fix frontend board mapping, game-over handling, and WASM-4

- fix Xform yx and add a shared view-aware board_position helper
- add GTK automatic game-over, restart, status, and multi-capture selection
- pin librsvg to the 2.60 series to match GTK4/glib 0.20
- add WASM orientation, undo/redo/new/flip controls, and winner overlay
- replace WASM crown images with canvas paths and remove dead files/deps
- make WASM-4 playable with mouse input, crowns, winner handling, and correct mapping
- move the WASM-4 release profile to the workspace root
- clean up frontend dead code and update the README
This commit is contained in:
woggioni committed 2026-10-19 20:38:32 +00:00
1 parent dd3b918561
commit f17d328ed5
20 files changed
+997 -1290

No files matched your search

+1 -3
View File
@@ -1,7 +1,5 @@
use glib::ExitCode;
use rdraught::{DraughtsBoard, DraughtsGame, Move, Piece, Player, Position};
use rdraught_gtk::run;
use std::collections::HashMap;
use rdraught::{DraughtsGame, Move};
use std::mem::size_of;
fn main() -> ExitCode {
+1 -1
View File
@@ -1,7 +1,7 @@
use gtk4::{AlertDialog, Window, prelude::IsA};
use rdraught::Player;
pub(crate) async fn create_dialog<W: IsA<Window>>(window: W, winner: Player) {
pub(crate) fn create_dialog<W: IsA<Window>>(window: W, winner: Player) {
let msg = match winner {
Player::Red => String::from("Red player wins"),
Player::White => String::from("White player wins"),
+3 -11
View File
@@ -4,7 +4,7 @@ use rdraught::Player;
use rdraught_ui_common::SharedMutable;
pub(crate) fn create(application: &Application, current_player: SharedMutable<Player>) -> Window {
let label = Label::builder().label("Main player:").build();
let label = Label::builder().label("Board orientation:").build();
let red_player_button = CheckButton::builder().label("Red").active(true).build();
let white_player_button = CheckButton::builder()
.label("White")
@@ -44,19 +44,11 @@ pub(crate) fn create(application: &Application, current_player: SharedMutable<Pl
layout.set_margin_bottom(10);
layout.set_margin_start(10);
layout.set_margin_end(10);
let player_select_dialog = Window::builder()
Window::builder()
.application(application)
.title("Rdraught")
.modal(true)
.child(&layout)
.visible(true)
.build();
player_select_dialog.connect_close_request(move |window| {
if let Some(application) = window.application() {
application.remove_window(window);
}
glib::Propagation::Proceed
});
player_select_dialog
.build()
}
+158 -113
View File
@@ -1,9 +1,9 @@
use core::f64::consts::PI;
use gdk4::cairo::{Context as CairoContext, Matrix, Rectangle};
use glib::ExitCode;
use gtk4::glib::{MainContext, Propagation};
use gtk4::glib::Propagation;
use gtk4::{self as gtk, gdk::ffi::GDK_BUTTON_PRIMARY};
use gtk4::{Application, Button, DrawingArea, HeaderBar, prelude::*};
use gtk4::{Application, Button, DrawingArea, HeaderBar, Label, prelude::*};
use rdraught::{
DraughtsBoard, DraughtsGame, Move, Piece, Player, Position, RDraughtApplication,
RectangularBoard,
@@ -14,7 +14,7 @@ const SQUARE_SIZE: f64 = 1.0;
use super::final_dialog;
use super::greeting_dialog;
use rdraught_ui_common::{
Point, SharedMutable, SharedMutableRef, new_shared_mut, new_shared_mut_ref,
Point, SharedMutable, SharedMutableRef, board_position, new_shared_mut, new_shared_mut_ref,
};
const CROWN_RED: &[u8] = include_bytes!("crown_red.svg");
@@ -25,6 +25,47 @@ fn transform_point(p: &Point, m: &Matrix) -> Point {
Point::new(x, y)
}
fn player_name(player: Player) -> &'static str {
match player {
Player::Red => "Red",
Player::White => "White",
}
}
fn refresh_game_state(
rd: &SharedMutableRef<RDraughtApplication>,
status: &Label,
undo_button: &Button,
redo_button: &Button,
) -> Option<Player> {
let rd = rd.borrow();
let game = rd.game();
let winner = game.winner();
let text = match winner {
Some(winner) => format!("{} wins", player_name(winner)),
None => format!("{} to move", player_name(game.next_move())),
};
status.set_text(&text);
undo_button.set_sensitive(winner.is_none() && rd.can_undo());
redo_button.set_sensitive(winner.is_none() && rd.can_redo());
winner
}
fn handle_game_over(
winner: Option<Player>,
finished: &SharedMutable<bool>,
drawing_area: &DrawingArea,
window: &gtk::ApplicationWindow,
) {
if let Some(winner) = winner {
if !finished.get() {
finished.set(true);
drawing_area.set_sensitive(false);
final_dialog::create_dialog(window.clone(), winner);
}
}
}
trait AugmentedRect {
fn from_points(tl: Point, br: Point) -> Rectangle;
fn center(&self) -> Point;
@@ -253,6 +294,10 @@ fn create_game_window(
redo_button.set_icon_name("edit-redo-symbolic");
redo_button.set_sensitive(false);
let finished: SharedMutable<bool> = new_shared_mut(false);
let status = Label::new(None);
let new_game_button = Button::with_label("New Game");
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.
@@ -261,6 +306,11 @@ fn create_game_window(
window.set_child(Some(&drawing_area));
header_bar.pack_start(&undo_button);
header_bar.pack_start(&redo_button);
header_bar.pack_start(&new_game_button);
header_bar.pack_end(&status);
let _ = refresh_game_state(&rd, &status, &undo_button, &redo_button);
{
let da = drawing_area.clone();
let rd = rd.clone();
@@ -268,19 +318,23 @@ fn create_game_window(
let redo_btn_clone = redo_button.clone();
let selected_piece = selected_piece.clone();
let available_moves = available_moves.clone();
let status = status.clone();
let finished = finished.clone();
let window = window.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();
{
let mut app = rd.borrow_mut();
if app.can_undo() {
app.undo().ok();
selected_piece.set(None);
available_moves.borrow_mut().clear();
}
}
let winner = refresh_game_state(&rd, &status, &undo_btn_clone, &redo_btn_clone);
handle_game_over(winner, &finished, &da, &window);
da.queue_draw();
});
}
header_bar.pack_start(&redo_button);
{
let da = drawing_area.clone();
let rd = rd.clone();
@@ -288,16 +342,40 @@ fn create_game_window(
let redo_btn_clone = redo_button.clone();
let selected_piece = selected_piece.clone();
let available_moves = available_moves.clone();
let status = status.clone();
let finished = finished.clone();
let window = window.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();
{
let mut app = rd.borrow_mut();
if app.can_redo() {
app.redo().ok();
selected_piece.set(None);
available_moves.borrow_mut().clear();
}
}
let winner = refresh_game_state(&rd, &status, &undo_btn_clone, &redo_btn_clone);
handle_game_over(winner, &finished, &da, &window);
da.queue_draw();
});
}
{
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();
let status = status.clone();
let finished = finished.clone();
new_game_button.connect_clicked(move |_| {
*rd.borrow_mut() = RDraughtApplication::new(DraughtsGame::default());
finished.set(false);
selected_piece.set(None);
available_moves.borrow_mut().clear();
da.set_sensitive(true);
let _ = refresh_game_state(&rd, &status, &undo_btn_clone, &redo_btn_clone);
da.queue_draw();
});
}
// Get the allocation information for the widget.
@@ -362,10 +440,7 @@ fn create_game_window(
// 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 position = board_position(current_player, row as u8, col as u8);
let square = Rectangle::new(
col as f64 * SQUARE_SIZE,
row as f64 * SQUARE_SIZE,
@@ -481,99 +556,74 @@ fn create_game_window(
let window = window.clone();
let undo_btn_clone = undo_button.clone();
let redo_btn_clone = redo_button.clone();
let selected_piece = selected_piece.clone();
let finished = finished.clone();
let status = status.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 rd = rd.borrow();
let draughts_game = rd.game();
position
.clone()
.map_or(Piece::NoPiece, |it| draughts_game.piece_at(it))
// println!("Selected piece: {:?}", piece);
};
let am = available_moves.replace(Vec::new());
let mut move_applied = false;
if !am.is_empty() {
if let Ok(pos) = position {
for mv in am.into_iter() {
if mv.get_end_position() == Some(pos) {
let mut rd_app = rd.borrow_mut();
// println!("Applied move: {:?}", mv);
if rd_app.apply_move(mv).is_err() {
continue;
}
// 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);
let rd = rd.borrow();
undo_btn_clone.set_sensitive(rd.can_undo());
redo_btn_clone.set_sensitive(rd.can_redo());
if finished.get() {
return;
}
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) {
selected_piece.set(None);
available_moves.borrow_mut().clear();
drawing_area.queue_draw();
return;
}
let p = p - board_clone.tl();
let screen_row = (p.y() / SQUARE_SIZE) as u8;
let screen_col = (p.x() / SQUARE_SIZE) as u8;
let position = board_position(current_player, screen_row, screen_col).ok();
let am = available_moves.replace(Vec::new());
let mut move_applied = false;
if let Some(pos) = position {
for mv in am {
if mv.get_end_position() == Some(pos) {
let mut rd_app = rd.borrow_mut();
if rd_app.apply_move(mv).is_err() {
continue;
}
move_applied = true;
break;
}
if !move_applied {
let position = position.ok();
match piece.player() {
Some(Player::Red) => selected_piece.set(position),
Some(Player::White) => selected_piece.set(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(position);
if let Some(position) = position {
for mv in rd.borrow().game().moves_for_piece(position) {
am.push(mv);
}
}
}
}
}
if move_applied {
if let Some(pending) = rd.borrow().game().pending_capture() {
selected_piece.set(Some(pending));
let mut moves = available_moves.borrow_mut();
moves.clear();
for mv in rd.borrow().game().moves_for_piece(pending) {
moves.push(mv);
}
} else {
selected_piece.set(None);
available_moves.borrow_mut().clear();
}
let winner = refresh_game_state(&rd, &status, &undo_btn_clone, &redo_btn_clone);
handle_game_over(winner, &finished, &drawing_area, &window);
} else {
let mut moves = available_moves.borrow_mut();
moves.clear();
selected_piece.set(None);
if let Some(position) = position {
for mv in rd.borrow().game().moves_for_piece(position) {
selected_piece.set(Some(position));
moves.push(mv);
}
}
drawing_area.queue_draw();
}
drawing_area.queue_draw();
});
}
// Assign the gesture to the treeview
@@ -582,11 +632,6 @@ fn create_game_window(
}
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();