diff --git a/JoshHeaps.Net/Pages/Chess.cshtml b/JoshHeaps.Net/Pages/Chess.cshtml
index 5b00402..df65f87 100644
--- a/JoshHeaps.Net/Pages/Chess.cshtml
+++ b/JoshHeaps.Net/Pages/Chess.cshtml
@@ -59,7 +59,14 @@
@section Scripts {
-
+
+
+
+
+
+
+
+
}
@section Styles {
diff --git a/JoshHeaps.Net/wwwroot/js/ChessScripts/ChessAPI.js b/JoshHeaps.Net/wwwroot/js/ChessScripts/ChessAPI.js
new file mode 100644
index 0000000..1d58820
--- /dev/null
+++ b/JoshHeaps.Net/wwwroot/js/ChessScripts/ChessAPI.js
@@ -0,0 +1,108 @@
+const ChessAPI = {
+ async joinGame() {
+ const response = await fetch('/api/chess/JoinGame');
+ return await response.json();
+ },
+
+ async createCPUGame(difficulty) {
+ const response = await fetch(`/api/chess/new/${difficulty}`);
+ return await response.json();
+ },
+
+ async getGameState(gameId) {
+ const response = await fetch(`/api/chess/${gameId}`);
+ return await response.json();
+ },
+
+ async getLegalMoves(pieceId) {
+ const response = await fetch(`/api/chess/${GameState.currentGameId}/legalMoves/${pieceId}`);
+ if (!response.ok) throw new Error("API failed");
+ return await response.json();
+ },
+
+ async makeMove(moveDto) {
+ const response = await fetch("/api/chess/move", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(moveDto)
+ });
+
+ const result = await response.json();
+
+ if (!response.ok || !result.success) {
+ throw new Error(result.message || "Invalid move or not your turn.");
+ }
+
+ return result;
+ },
+
+ async handleMove(targetRow, targetCol) {
+ if (!GameState.selectedPiece) return;
+
+ const isPawn = GameState.selectedPiece.type === 0;
+ const reachedEnd = (Number(GameState.selectedPiece.color) === 0 && Number(targetRow) === 0) ||
+ (Number(GameState.selectedPiece.color) === 1 && Number(targetRow) === 0);
+
+ let choice = null;
+
+ if (isPawn && reachedEnd) {
+ choice = await ChessModals.promptPromotion();
+ if (!choice) return;
+ }
+
+ [targetRow, targetCol] = ChessUtils.flipCoordinates(targetRow, targetCol);
+
+ const moveDto = {
+ GameId: GameState.currentGameId,
+ PlayerId: GameState.currentPlayerId,
+ PieceId: GameState.selectedPiece.id,
+ SourceRow: GameState.selectedPiece.row,
+ SourceCol: GameState.selectedPiece.col,
+ TargetRow: targetRow,
+ TargetCol: targetCol,
+ PromotionChoice: choice
+ };
+
+ GameState.setPreviousMove([GameState.selectedPiece.row, GameState.selectedPiece.col], [targetRow, targetCol]);
+
+ try {
+ const moveResult = await this.makeMove(moveDto);
+
+ const updatedGame = await this.getGameState(GameState.currentGameId);
+ ChessBoard.renderPieces(updatedGame.pieces);
+
+ GameState.clearSelection();
+ ChessInteractions.clearHighlights();
+
+ await ChessSignalR.notifyMoveMade(moveDto, moveResult);
+ this.alertGameStatusChange(moveResult);
+
+ } catch (error) {
+ alert("❌ " + error.message);
+ }
+ },
+
+ alertGameStatusChange(moveResult) {
+ setTimeout(async () => {
+ let gameOver = false;
+
+ if (moveResult.isCheck) {
+ console.log("🛑 Check!");
+ }
+
+ if (moveResult.isCheckmate) {
+ alert("♟️ Checkmate!");
+ gameOver = true;
+ } else if (moveResult.isStalemate) {
+ alert("🤝 Stalemate!");
+ gameOver = true;
+ }
+
+ if (gameOver) {
+ await ChessSignalR.leaveGame();
+ }
+ }, 500);
+ }
+};
+
+console.log("ChessAPI.js loaded");
\ No newline at end of file
diff --git a/JoshHeaps.Net/wwwroot/js/ChessScripts/ChessBoard.js b/JoshHeaps.Net/wwwroot/js/ChessScripts/ChessBoard.js
new file mode 100644
index 0000000..3f4ab77
--- /dev/null
+++ b/JoshHeaps.Net/wwwroot/js/ChessScripts/ChessBoard.js
@@ -0,0 +1,112 @@
+const ChessBoard = {
+ renderPieces(pieces) {
+ this.clearAllSquares();
+ this.placePieces(pieces);
+ this.setupSquareEventHandlers();
+ this.highlightPreviousMove();
+ },
+
+ clearAllSquares() {
+ for (let i = 0; i < 64; i++) {
+ const square = document.getElementById(`square-${i}`);
+ square.innerHTML = "";
+ }
+ },
+
+ placePieces(pieces) {
+ pieces.forEach(piece => {
+ const [r, c] = ChessUtils.flipCoordinates(piece.row, piece.col);
+ const index = r * 8 + c;
+ const square = document.getElementById(`square-${index}`);
+ if (!square) return;
+
+ const img = document.createElement("img");
+ img.src = ChessUtils.getPieceImageUrl(piece);
+ img.alt = piece.type;
+ img.classList.add("chessPiece");
+
+ this.setupPieceDragEvents(img, piece);
+ this.setupPieceClickEvent(img, piece);
+
+ square.appendChild(img);
+ });
+ },
+
+ setupPieceDragEvents(img, piece) {
+ img.draggable = true;
+ img.ondragstart = async (e) => {
+ GameState.setSelectedPiece(piece);
+ try {
+ const moves = await ChessAPI.getLegalMoves(piece.id);
+ GameState.setLegalMoves(moves);
+ ChessInteractions.highlightSelected(piece.row, piece.col);
+ ChessInteractions.highlightLegalMoves(moves);
+ } catch (err) {
+ console.error(err);
+ }
+
+ e.dataTransfer.setData("text/plain", JSON.stringify({
+ srcRow: piece.Row,
+ srcCol: piece.Col
+ }));
+ };
+
+ img.ondragend = () => ChessInteractions.clearHighlights();
+ },
+
+ setupPieceClickEvent(img, piece) {
+ img.onclick = (e) => {
+ e.stopPropagation();
+ ChessInteractions.handlePieceClick(piece);
+ console.log("Clicked on:", piece);
+ };
+ },
+
+ setupSquareEventHandlers() {
+ for (let i = 0; i < 64; i++) {
+ const square = document.getElementById(`square-${i}`);
+ square.classList.remove("previous-start", "previous-end");
+
+ if (!square.classList.contains("legal")) {
+ square.onclick = () => ChessInteractions.clearHighlights();
+ }
+ }
+ },
+
+ highlightPreviousMove() {
+ if (GameState.previousMoveStart && GameState.previousMoveEnd) {
+ const [startRow, startCol] = ChessUtils.flipCoordinates(...GameState.previousMoveStart);
+ const [endRow, endCol] = ChessUtils.flipCoordinates(...GameState.previousMoveEnd);
+
+ const startIndex = startRow * 8 + startCol;
+ const endIndex = endRow * 8 + endCol;
+
+ document.getElementById(`square-${startIndex}`)?.classList.add("previous-start");
+ document.getElementById(`square-${endIndex}`)?.classList.add("previous-end");
+ }
+ },
+
+ setupDragAndDrop() {
+ for (let i = 0; i < 64; i++) {
+ const square = document.getElementById(`square-${i}`);
+
+ square.ondragover = (e) => {
+ e.preventDefault();
+ };
+
+ square.ondrop = (e) => {
+ e.preventDefault();
+
+ if (!GameState.selectedPiece) return;
+
+ const index = parseInt(square.id.split('-')[1], 10);
+ const targetRow = Math.floor(index / 8);
+ const targetCol = index % 8;
+
+ ChessAPI.handleMove(targetRow, targetCol);
+ };
+ }
+ }
+};
+
+console.log("ChessBoard.js loaded");
\ No newline at end of file
diff --git a/JoshHeaps.Net/wwwroot/js/ChessScripts/ChessInteractions.js b/JoshHeaps.Net/wwwroot/js/ChessScripts/ChessInteractions.js
new file mode 100644
index 0000000..bbebbaa
--- /dev/null
+++ b/JoshHeaps.Net/wwwroot/js/ChessScripts/ChessInteractions.js
@@ -0,0 +1,59 @@
+const ChessInteractions = {
+ async handlePieceClick(piece) {
+ this.clearHighlights();
+ GameState.setSelectedPiece(piece);
+
+ try {
+ const moves = await ChessAPI.getLegalMoves(piece.id);
+ GameState.setLegalMoves(moves);
+ this.highlightSelected(piece.row, piece.col);
+ this.highlightLegalMoves(moves);
+ } catch (err) {
+ console.error("Error in handlePieceClick:", err);
+ }
+ },
+
+ highlightSelected(row, col) {
+ const [r, c] = ChessUtils.flipCoordinates(row, col);
+ const index = r * 8 + c;
+ document.getElementById(`square-${index}`).classList.add("selected");
+ },
+
+ highlightLegalMoves(moves) {
+ moves.forEach(move => {
+ const [r, c] = ChessUtils.flipCoordinates(move.row, move.col);
+ const index = r * 8 + c;
+ const square = document.getElementById(`square-${index}`);
+ square.classList.add("legal");
+
+ const img = square.querySelector("img");
+ if (img) img.onclick = null;
+
+ let overlay = document.createElement("div");
+ overlay.className = "legalOverlay";
+ square.appendChild(overlay);
+
+ square.onclick = () => ChessAPI.handleMove(r, c);
+ });
+ },
+
+ clearHighlights() {
+ for (let i = 0; i < 64; i++) {
+ const square = document.getElementById(`square-${i}`);
+
+ const overlay = square.querySelector(".legalOverlay");
+ if (overlay) square.removeChild(overlay);
+
+ if (square.classList.contains("legal")) {
+ square.classList.remove("legal");
+ square.onclick = null;
+ }
+
+ square.classList.remove("selected");
+ }
+
+ GameState.clearSelection();
+ }
+};
+
+console.log("ChessInteractions.js loaded");
\ No newline at end of file
diff --git a/JoshHeaps.Net/wwwroot/js/ChessScripts/ChessModals.js b/JoshHeaps.Net/wwwroot/js/ChessScripts/ChessModals.js
new file mode 100644
index 0000000..b8a78f2
--- /dev/null
+++ b/JoshHeaps.Net/wwwroot/js/ChessScripts/ChessModals.js
@@ -0,0 +1,29 @@
+const ChessModals = {
+ promptPromotion() {
+ return new Promise(resolve => {
+ let color = 1;
+ if (GameState.currentPlayerIsWhite)
+ color = 0;
+
+ ChessUtils.updatePromotionModalImages(color);
+
+ document.getElementById("promotionModal").style.display = "block";
+ window.selectPromotion = (piece) => {
+ document.getElementById("promotionModal").style.display = "none";
+ resolve(piece);
+ };
+ });
+ },
+
+ promptDifficulty() {
+ return new Promise(resolve => {
+ document.getElementById("difficultyModal").style.display = "block";
+ window.selectDifficulty = (difficulty) => {
+ document.getElementById("difficultyModal").style.display = "none";
+ resolve(difficulty);
+ };
+ });
+ }
+};
+
+console.log("ChessModals.js loaded");
\ No newline at end of file
diff --git a/JoshHeaps.Net/wwwroot/js/ChessScripts/ChessSignalR.js b/JoshHeaps.Net/wwwroot/js/ChessScripts/ChessSignalR.js
new file mode 100644
index 0000000..c12e53a
--- /dev/null
+++ b/JoshHeaps.Net/wwwroot/js/ChessScripts/ChessSignalR.js
@@ -0,0 +1,59 @@
+const ChessSignalR = {
+ connection: null,
+
+ async setupConnection() {
+ this.connection = new signalR.HubConnectionBuilder()
+ .withUrl("/chessHub")
+ .configureLogging(signalR.LogLevel.Information)
+ .build();
+
+ this.connection.onclose(err => {
+ console.error("❌ SignalR connection closed:", err?.message);
+ });
+
+ this.connection.on("ReceiveMoveUpdate", async (gameId, moveDto, moveResultDto) => {
+ await this.handleMoveUpdate(gameId, moveDto, moveResultDto);
+ });
+
+ try {
+ await this.connection.start();
+ console.log("✅ SignalR connected");
+ await this.connection.invoke("JoinWebsocketGroup", GameState.currentGameId);
+ } catch (err) {
+ console.error("❌ SignalR failed to start or join:", err);
+ }
+ },
+
+ async handleMoveUpdate(gameId, moveDto, moveResultDto) {
+ if (gameId !== GameState.currentGameId) return;
+
+ const gameState = await ChessAPI.getGameState(gameId);
+ GameState.setPreviousMove([moveDto.sourceRow, moveDto.sourceCol], [moveDto.targetRow, moveDto.targetCol]);
+ ChessBoard.renderPieces(gameState.pieces);
+
+ ChessAPI.alertGameStatusChange(moveResultDto);
+ },
+
+ async notifyMoveMade(moveDto, moveResult) {
+ if (this.connection) {
+ await this.connection.invoke("MoveMade", GameState.currentGameId, moveDto, moveResult);
+ }
+ },
+
+ async leaveGame() {
+ if (this.connection) {
+ await this.connection.invoke("LeaveWebsocketGroup", GameState.currentGameId);
+ await this.connection.stop();
+ this.connection = null;
+ }
+ },
+
+ async stopConnection() {
+ if (this.connection) {
+ await this.connection.stop();
+ this.connection = null;
+ }
+ }
+};
+
+console.log("ChessSignalR.js loaded");
\ No newline at end of file
diff --git a/JoshHeaps.Net/wwwroot/js/ChessScripts/ChessUtils.js b/JoshHeaps.Net/wwwroot/js/ChessScripts/ChessUtils.js
new file mode 100644
index 0000000..53afb41
--- /dev/null
+++ b/JoshHeaps.Net/wwwroot/js/ChessScripts/ChessUtils.js
@@ -0,0 +1,44 @@
+const ChessUtils = {
+ flipCoordinates(row, col) {
+ if (!GameState.currentPlayerIsWhite) {
+ return [7 - row, 7 - col];
+ }
+ return [row, col];
+ },
+
+ getCookie(name) {
+ const value = document.cookie.split('; ')
+ .find(row => row.startsWith(name + '='));
+ return value ? value.split('=')[1] : null;
+ },
+
+ setCookie(name, value, maxAge = 86400) {
+ document.cookie = `${name}=${value}; path=/; max-age=${maxAge}`;
+ },
+
+ getPieceImageUrl(piece) {
+ const basePath = "/images/Chess Images/";
+ const color = piece.color === 0 ? "White" : "Black";
+ const typeMap = {
+ 0: "Pawn",
+ 1: "Rook",
+ 2: "Knight",
+ 3: "Bishop",
+ 4: "Queen",
+ 5: "King"
+ };
+
+ return basePath + color + typeMap[piece.type] + ".svg";
+ },
+
+ updatePromotionModalImages(color) {
+ const pieceNames = ["Queen", "Rook", "Bishop", "Knight"];
+ const buttons = document.querySelectorAll("#promotionModal button img");
+
+ buttons.forEach((img, index) => {
+ img.src = `/images/Chess Images/${color === 0 ? "White" : "Black"}${pieceNames[index]}.svg`;
+ });
+ }
+};
+
+console.log("ChessUtils.js loaded");
\ No newline at end of file
diff --git a/JoshHeaps.Net/wwwroot/js/ChessScripts/GameState.js b/JoshHeaps.Net/wwwroot/js/ChessScripts/GameState.js
new file mode 100644
index 0000000..84416f9
--- /dev/null
+++ b/JoshHeaps.Net/wwwroot/js/ChessScripts/GameState.js
@@ -0,0 +1,50 @@
+const GameState = {
+ currentGameId: null,
+ currentPlayerId: null,
+ currentPlayerIsWhite: null,
+ selectedPiece: null,
+ legalMoves: [],
+ previousMoveStart: null,
+ previousMoveEnd: null,
+
+ reset() {
+ this.currentGameId = null;
+ this.currentPlayerId = null;
+ this.currentPlayerIsWhite = null;
+ this.selectedPiece = null;
+ this.legalMoves = [];
+ this.previousMoveStart = null;
+ this.previousMoveEnd = null;
+ },
+
+ setGameInfo(gameId, playerId, isWhite) {
+ this.currentGameId = gameId;
+ this.currentPlayerId = playerId;
+ this.currentPlayerIsWhite = isWhite;
+ },
+
+ setSelectedPiece(piece) {
+ this.selectedPiece = piece;
+ },
+
+ clearSelection() {
+ this.selectedPiece = null;
+ this.legalMoves = [];
+ },
+
+ setLegalMoves(moves) {
+ this.legalMoves = moves;
+ },
+
+ setPreviousMove(startPos, endPos) {
+ this.previousMoveStart = startPos;
+ this.previousMoveEnd = endPos;
+ },
+
+ clearPreviousMove() {
+ this.previousMoveStart = null;
+ this.previousMoveEnd = null;
+ }
+};
+
+console.log("GameState.js loaded");
\ No newline at end of file
diff --git a/JoshHeaps.Net/wwwroot/js/ChessScripts/chessMain.js b/JoshHeaps.Net/wwwroot/js/ChessScripts/chessMain.js
new file mode 100644
index 0000000..abae265
--- /dev/null
+++ b/JoshHeaps.Net/wwwroot/js/ChessScripts/chessMain.js
@@ -0,0 +1,82 @@
+async function startNewGame() {
+ await ChessSignalR.stopConnection();
+
+ try {
+ const gameData = await ChessAPI.joinGame();
+
+ GameState.setGameInfo(gameData.gameId, gameData.id, gameData.isWhite);
+ GameState.clearPreviousMove();
+
+ ChessUtils.setCookie('chessGameId', gameData.gameId);
+ ChessUtils.setCookie('chessPlayerId', gameData.id);
+ ChessUtils.setCookie('chessPlayerIsWhite', gameData.isWhite);
+
+ console.log("🆕 Game started:", gameData.gameId);
+
+ await ChessSignalR.setupConnection();
+
+ const gameState = await ChessAPI.getGameState(gameData.gameId);
+ ChessBoard.renderPieces(gameState.pieces);
+
+ } catch (error) {
+ console.error("Failed to start new game:", error);
+ }
+}
+
+async function startCPUGame() {
+ await ChessSignalR.stopConnection();
+
+ try {
+ const difficulty = await ChessModals.promptDifficulty();
+ const gameData = await ChessAPI.createCPUGame(difficulty);
+
+ GameState.setGameInfo(gameData.gameId, gameData.id, gameData.isWhite);
+ GameState.clearPreviousMove();
+
+ ChessUtils.setCookie('chessGameId', gameData.gameId);
+ ChessUtils.setCookie('chessPlayerId', gameData.id);
+ ChessUtils.setCookie('chessPlayerIsWhite', gameData.isWhite);
+
+ console.log("🆕 CPU Game started:", gameData.gameId);
+
+ await ChessSignalR.setupConnection();
+
+ const gameState = await ChessAPI.getGameState(gameData.gameId);
+ ChessBoard.renderPieces(gameState.pieces);
+
+ } catch (error) {
+ console.error("Failed to start CPU game:", error);
+ }
+}
+
+async function resumeSavedGame() {
+ const savedGameId = ChessUtils.getCookie("chessGameId");
+ const savedPlayerId = ChessUtils.getCookie("chessPlayerId");
+ const savedPlayerIsWhite = ChessUtils.getCookie("chessPlayerIsWhite");
+
+ if (savedGameId && savedPlayerId) {
+ try {
+ const gameState = await ChessAPI.getGameState(savedGameId);
+
+ console.log("🧠 Rejoining saved game...");
+ GameState.setGameInfo(savedGameId, savedPlayerId, savedPlayerIsWhite === "true");
+
+ await ChessSignalR.setupConnection();
+ ChessBoard.renderPieces(gameState.pieces);
+
+ } catch (err) {
+ console.warn("Saved game not found or expired.", err);
+ }
+ }
+}
+
+window.startNewGame = startNewGame;
+window.startCPUGame = startCPUGame;
+
+window.addEventListener('load', resumeSavedGame);
+
+document.addEventListener('DOMContentLoaded', () => {
+ ChessBoard.setupDragAndDrop();
+});
+
+console.log("chessMain.js loaded");
\ No newline at end of file