Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Board movement #4

Merged
merged 2 commits into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 %}
Loading