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:
2026-10-19 20:38:32 +00:00
parent dd3b918561
commit f17d328ed5
20 changed files with 997 additions and 1290 deletions
+70
View File
@@ -0,0 +1,70 @@
use rdraught::{DraughtsBoard, Error, Player, Position, RectangularBoard};
/// Maps a screen square to a board square for the given point of view.
///
/// Both views place pieces on screen squares where
/// `(screen_row + screen_col) % 2 == 1`.
pub fn board_position(view: Player, screen_row: u8, screen_col: u8) -> Result<Position, Error> {
let rows = DraughtsBoard::rows() as u8;
let columns = DraughtsBoard::columns() as u8;
if screen_row >= rows || screen_col >= columns {
return Err(Error::InvalidPosition);
}
match view {
Player::White => Position::new(rows - 1 - screen_row, screen_col),
Player::Red => Position::new(screen_row, columns - 1 - screen_col),
}
}
#[cfg(test)]
mod tests {
use super::board_position;
use rdraught::{Player, Position};
#[test]
fn white_view_mapping() {
assert_eq!(
Position::new(7, 1).unwrap(),
board_position(Player::White, 0, 1).unwrap()
);
assert_eq!(
Position::new(0, 0).unwrap(),
board_position(Player::White, 7, 0).unwrap()
);
assert!(board_position(Player::White, 0, 0).is_err());
}
#[test]
fn red_view_mapping() {
assert_eq!(
Position::new(0, 6).unwrap(),
board_position(Player::Red, 0, 1).unwrap()
);
assert_eq!(
Position::new(7, 7).unwrap(),
board_position(Player::Red, 7, 0).unwrap()
);
assert!(board_position(Player::Red, 0, 0).is_err());
}
#[test]
fn both_views_have_32_squares() {
for view in [Player::White, Player::Red] {
let mut count = 0;
for row in 0..8 {
for col in 0..8 {
if board_position(view, row, col).is_ok() {
count += 1;
}
}
}
assert_eq!(32, count);
}
}
#[test]
fn out_of_bounds_is_rejected() {
assert!(board_position(Player::White, 8, 0).is_err());
assert!(board_position(Player::Red, 0, 8).is_err());
}
}
+39 -1
View File
@@ -205,7 +205,7 @@ impl Xform {
}
pub fn yx(&self) -> f64 {
self.0[(1, 0)]
self.0[(0, 1)]
}
pub fn yy(&self) -> f64 {
@@ -259,3 +259,41 @@ impl Display for Xform {
self.0.fmt(f)
}
}
#[cfg(test)]
mod tests {
use super::{Point, Xform};
use core::f64::consts::PI;
fn close(actual: f64, expected: f64) {
assert!((actual - expected).abs() < 1e-9);
}
#[test]
fn rotation_uses_all_matrix_components() {
let rotation = Xform::rot(PI / 2.0);
close(rotation.yx(), 1.0);
close(rotation.xy(), -1.0);
let point = Point::new(1.0, 0.0) * &rotation;
close(point.x(), 0.0);
close(point.y(), 1.0);
}
#[test]
fn translation_and_scaling() {
let transform = Xform::scale(2.0, 3.0) * Xform::xlate(3.0, 4.0);
let point = Point::new(1.0, 2.0) * &transform;
close(point.x(), 5.0);
close(point.y(), 10.0);
}
#[test]
fn inversion_round_trips() {
let transform = Xform::xlate(3.0, 4.0) * Xform::rot(0.3) * Xform::scale(2.0, 0.5);
let point = Point::new(1.0, 2.0);
let result = (point * &transform) * &transform.invert();
close(result.x(), point.x());
close(result.y(), point.y());
}
}
+2
View File
@@ -1,5 +1,7 @@
mod board;
mod geo2d;
mod types;
pub use board::board_position;
pub use geo2d::{Point, Rect, Xform};
pub use types::{SharedMutable, SharedMutableRef, new_shared_mut, new_shared_mut_ref};