Skip to content

Commit

Permalink
Merge pull request #4 from amiller68/board-movement
Browse files Browse the repository at this point in the history
Board movement
  • Loading branch information
amiller68 authored Jan 24, 2024
2 parents b69760a + 80c042b commit 724e6ba
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 81 deletions.
95 changes: 61 additions & 34 deletions src/api/models/api_board.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,53 +7,80 @@ 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
}
}

impl TryFrom<PartialGameWithFen> for ApiBoard {
type Error = ApiBoardError;

fn try_from(game: PartialGameWithFen) -> Result<Self, Self::Error> {
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<String, ApiBoardError> {
// 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("<table class='chess-board'>");

// 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("<tr class='chess-row'>");
// New rank
html_board.push_str("<tr class='chess-rank'>");
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!(
"<td class='chess-cell {}'>{}</td>",
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!(
"<td id='{}' class='chess-square-{} chess-piece-{}'>{}</td>",
id,
color_class,
piece.character_lossy(),
piece_html
));
}
None => {
html_board.push_str(&format!(
"<td id='{}' class='chess-square-{}'></td>",
id, color_class
));
}
}
}
html_board.push_str("</tr>");
}
Expand All @@ -62,21 +89,21 @@ fn render_html_board(fen: &str) -> Result<String, ApiBoardError> {
Ok(html_board)
}

fn render_html_piece(piece: Piece) -> String {
fn render_html_piece(piece: Piece) -> Option<String> {
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()),
}
}

Expand Down
75 changes: 75 additions & 0 deletions static/js/board.js
Original file line number Diff line number Diff line change
@@ -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';
}
});

39 changes: 0 additions & 39 deletions static/js/chess.js

This file was deleted.

12 changes: 8 additions & 4 deletions static/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -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;
}
14 changes: 10 additions & 4 deletions templates/board.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@

{% block content %}
<h1>Board</h1>

{% let board_html = api_board.html() %}
{% let board_id = api_board.id() %}
<div id="board-{{ board_id }}">
<script src="/static/js/board.js"></script>
{% let board_html = api_board.board_html() %}
{% let game_id = api_board.game_id() %}
<div id="board-{{ game_id }}">
{{ board_html|safe }}
</div>

<!-- Hidden form for submitting moves -->
<form id="moveForm" action="/games/{{ game_id }}" method="post" hx-target="#board-{{ game_id }}" hx-swap="innerHTML" style="display: none;">
<input type="hidden" id="uciMoveInput" name="uciMove">
<button type="submit" id="submitMove">Submit Move</button>
</form>

{% endblock %}

0 comments on commit 724e6ba

Please sign in to comment.