From 224ba7fe00408212dbbc03c0e3843ad940f2d601 Mon Sep 17 00:00:00 2001 From: amiller68 Date: Wed, 24 Jan 2024 13:15:45 -0500 Subject: [PATCH 1/2] feat: board movement --- src/api/games/make_move.rs | 0 src/api/models/api_board.rs | 95 ++++++++++++++++++++++++------------- static/js/board.js | 75 +++++++++++++++++++++++++++++ static/js/chess.js | 39 --------------- static/styles.css | 12 +++-- templates/board.html | 14 ++++-- 6 files changed, 154 insertions(+), 81 deletions(-) create mode 100644 src/api/games/make_move.rs create mode 100644 static/js/board.js delete mode 100644 static/js/chess.js diff --git a/src/api/games/make_move.rs b/src/api/games/make_move.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/api/models/api_board.rs b/src/api/models/api_board.rs index 172e620..dad2158 100644 --- a/src/api/models/api_board.rs +++ b/src/api/models/api_board.rs @@ -7,17 +7,16 @@ use pleco::core::Piece; use crate::database::models::PartialGameWithFen; pub struct ApiBoard { - id: String, - html: String, + game_id: String, + board_html: String, } impl ApiBoard { - pub fn id(&self) -> &str { - &self.id + pub fn game_id(&self) -> &str { + &self.game_id } - - pub fn html(&self) -> &str { - &self.html + pub fn board_html(&self) -> &str { + &self.board_html } } @@ -25,35 +24,63 @@ impl TryFrom for ApiBoard { type Error = ApiBoardError; fn try_from(game: PartialGameWithFen) -> Result { - let id = game.id().to_string(); - let html = render_html_board(game.current_fen())?; - Ok(Self { id, html }) + let game_id = game.id().to_string(); + let board_html = render_html_board(game.current_fen())?; + Ok(Self { + game_id, + board_html, + }) } } +/// Render a FEN formatted str into an HTML chess board fn render_html_board(fen: &str) -> Result { + // Read the FEN into a board let board = Board::from_fen(fen).map_err(|e| ApiBoardError::FenBuilder(format!("{:?}", e)))?; + // We'll just pass raw HTML to our template let mut html_board = String::new(); html_board.push_str(""); + // Iterate over ranks to fully construct the board -- we need to populate every cell + // with metadata at the moment for rank in (0..8).rev() { - html_board.push_str(""); + // New rank + html_board.push_str(""); for file in 0..8 { + // Read the piece at this square and populate the cell let square = Sq::from(rank * 8 + file); let piece = board.piece_at_sq(square); - let class = if square.on_light_square() { - "light-square" + let id = square.to_string(); + let color_class = if square.on_light_square() { + "light" } else { - "dark-square" + "dark" }; - let piece_string = render_html_piece(piece); - - html_board.push_str(&format!( - "", - class, piece_string - )); + // Metadata breakdown: + // - id: the square's readable id (e.g. "a1") + // - class: + // - chess-square-{light|dark}: the square's color + // - chess-piece-{piece_char}: the occupying piece, if any. e.g. "chess-piece-P" for a white pawn + match render_html_piece(piece) { + Some(piece_html) => { + // Note: Since we know `piece` is `Some`, we can call .character_lossy() here + html_board.push_str(&format!( + "", + id, + color_class, + piece.character_lossy(), + piece_html + )); + } + None => { + html_board.push_str(&format!( + "", + id, color_class + )); + } + } } html_board.push_str(""); } @@ -62,21 +89,21 @@ fn render_html_board(fen: &str) -> Result { Ok(html_board) } -fn render_html_piece(piece: Piece) -> String { +fn render_html_piece(piece: Piece) -> Option { match piece { - Piece::None => " ".to_string(), - Piece::WhitePawn => "♙".to_string(), - Piece::WhiteKnight => "♘".to_string(), - Piece::WhiteBishop => "♗".to_string(), - Piece::WhiteRook => "♖".to_string(), - Piece::WhiteQueen => "♕".to_string(), - Piece::WhiteKing => "♔".to_string(), - Piece::BlackPawn => "♟︎".to_string(), - Piece::BlackKnight => "♞".to_string(), - Piece::BlackBishop => "♝".to_string(), - Piece::BlackRook => "♜".to_string(), - Piece::BlackQueen => "♛".to_string(), - Piece::BlackKing => "♚".to_string(), + Piece::None => None, + Piece::WhitePawn => Some("♙".to_string()), + Piece::WhiteKnight => Some("♘".to_string()), + Piece::WhiteBishop => Some("♗".to_string()), + Piece::WhiteRook => Some("♖".to_string()), + Piece::WhiteQueen => Some("♕".to_string()), + Piece::WhiteKing => Some("♔".to_string()), + Piece::BlackPawn => Some("♟︎".to_string()), + Piece::BlackKnight => Some("♞".to_string()), + Piece::BlackBishop => Some("♝".to_string()), + Piece::BlackRook => Some("♜".to_string()), + Piece::BlackQueen => Some("♛".to_string()), + Piece::BlackKing => Some("♚".to_string()), } } diff --git a/static/js/board.js b/static/js/board.js new file mode 100644 index 0000000..f77fed5 --- /dev/null +++ b/static/js/board.js @@ -0,0 +1,75 @@ +document.addEventListener('DOMContentLoaded', () => { + let selectedPiece = null; + + // Add listeners to all squares + document.querySelectorAll('[class*="chess-square-"]').forEach(square => { + // On click + square.addEventListener('click', function() { + if (!selectedPiece && squareHasPiece(this) ) { + // Select the piece + selectedPiece = this; + this.classList.add('selected'); + } else if (selectedPiece) { + // Move the piece to the new square + movePiece(selectedPiece, this); + selectedPiece.classList.remove('selected'); + selectedPiece = null; + } + }); + }); + + // (Overly) Simple function to check if a square has a piece + function squareHasPiece(square) { + return square.innerHTML !== ''; + } + + // Logic for moving a piece + function movePiece(fromSquare, toSquare) { + // Get the identifying class name (e.g. `chess-piece-P` or `chess-piece-p`) of the piece + let fromPieceClass = fromSquare.classList[1]; + let fromPiece = fromPieceClass.split('-')[2]; + + // Get the relevant squares + let fromPosition = fromSquare.getAttribute('id'); + let toPosition = toSquare.getAttribute('id'); + let toRank = toPosition[1]; + + // Determine the uci formatted move + let promotionHtml = null; + let promotionClass = null; + uciMove = `${fromPosition}${toPosition}`; + // Check if a pawn is being promoted + if ((fromPiece === 'P' && toRank === '8') || (fromPiece === 'p' && toRank === '1')) { + // TODO: allow user to select piece to promote to piece of their choice + uciMove += 'q'; // Promote to queen + if (fromPiece === 'P') { + promotionHtml = '♕'; + promotionClass = 'chess-piece-Q'; + } else { + promotionHtml = '♛'; + promotionClass = 'chess-piece-q'; + } + } + + // Update the board + let toPieceHtml = promotionHtml ?? fromSquare.innerHTML; + let toPieceClass = promotionClass ?? fromPieceClass; + toSquare.innerHTML = toPieceHtml; // Add the piece to the new square + if (toSquare.classList.length === 1) { + toSquare.classList.add(toPieceClass); + } else { + toSquare.classList.replace(toSquare.classList[1], toPieceClass); // Update the class of the new square + } + fromSquare.innerHTML = ''; // Remove the piece from the current square + sendMove(uciMove); + } + + function sendMove(uciMove) { + console.log(uciMove); + // Write our move to the hidden input field + document.getElementById('uciMoveInput').value = uciMove; + // Make the button visible + document.getElementById('moveForm').style.display = 'block'; + } +}); + diff --git a/static/js/chess.js b/static/js/chess.js deleted file mode 100644 index e67bd56..0000000 --- a/static/js/chess.js +++ /dev/null @@ -1,39 +0,0 @@ -// TODO: not needed rn, but eventually if the board is to be interactive -// // chess.js -// document.addEventListener('DOMContentLoaded', () => { -// let selectedPiece = null; - -// document.querySelectorAll('.chess-square').forEach(square => { -// square.addEventListener('click', function() { -// if (this.classList.contains('chess-piece') && !selectedPiece) { -// // Select the piece -// selectedPiece = this; -// this.classList.add('selected'); -// } else if (selectedPiece) { -// // Move the piece to the new square -// movePiece(selectedPiece, this); -// selectedPiece.classList.remove('selected'); -// selectedPiece = null; -// } -// }); -// }); - -// function movePiece(fromSquare, toSquare) { -// const piece = fromSquare.innerHTML; -// fromSquare.innerHTML = ''; // Remove the piece from the current square -// toSquare.innerHTML = piece; // Place the piece on the new square - -// // Send the move to the server for validation and state update -// const fromPosition = fromSquare.getAttribute('data-position'); -// const toPosition = toSquare.getAttribute('data-position'); -// sendMoveToServer(fromPosition, toPosition); -// } - -// function sendMoveToServer(from, to) { -// // This function should be implemented to send the move to the server. -// // For demonstration purposes, this is a mock function. -// console.log(`Move sent to server: ${from} to ${to}`); -// // Implement AJAX request or HTMX trigger here -// } -// }); - diff --git a/static/styles.css b/static/styles.css index 49d18d5..5460a72 100644 --- a/static/styles.css +++ b/static/styles.css @@ -15,11 +15,11 @@ table, th, td { margin: auto; /* Center the board horizontally */ } -.chess-row { +.chess-rank { height: 50px; /* Adjust the height of rows */ } -.chess-cell { +[class^="chess-square-"] { width: 50px; /* Adjust the width of cells */ height: 50px; /* Ensure cells are square */ text-align: center; /* Center the piece character */ @@ -29,10 +29,14 @@ table, th, td { border: 1px solid #333; /* Adds a border to each cell */ } -.light-square { +.chess-square-light { background-color: #f0d9b5; /* Light color for light squares */ } -.dark-square { +.chess-square-dark { background-color: #b58863; /* Dark color for dark squares */ } + +.selected { + background-color: #ffff00; +} diff --git a/templates/board.html b/templates/board.html index 1fa2c8b..cf6e8b8 100644 --- a/templates/board.html +++ b/templates/board.html @@ -2,11 +2,17 @@ {% block content %}

Board

- -{% let board_html = api_board.html() %} -{% let board_id = api_board.id() %} -
+ +{% let board_html = api_board.board_html() %} +{% let game_id = api_board.game_id() %} +
{{ board_html|safe }}
+ + + + + + {% endblock %} \ No newline at end of file From 80c042ba479d86d0fb48014d98643f8e607cb078 Mon Sep 17 00:00:00 2001 From: amiller68 Date: Wed, 24 Jan 2024 13:16:35 -0500 Subject: [PATCH 2/2] fix: rm move stub --- src/api/games/make_move.rs | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/api/games/make_move.rs diff --git a/src/api/games/make_move.rs b/src/api/games/make_move.rs deleted file mode 100644 index e69de29..0000000
{}{}