From edab13f8e4d8daebc2943b33fbf88cc80a9a3607 Mon Sep 17 00:00:00 2001 From: jheaps Date: Thu, 25 Sep 2025 10:46:24 -0600 Subject: [PATCH 1/2] Javascript cleanup --- JoshHeaps.Net/Pages/Chess.cshtml | 9 +- .../wwwroot/js/ChessScripts/ChessAPI.js | 108 +++++++++++++++++ .../wwwroot/js/ChessScripts/ChessBoard.js | 112 ++++++++++++++++++ .../js/ChessScripts/ChessInteractions.js | 59 +++++++++ .../wwwroot/js/ChessScripts/ChessModals.js | 29 +++++ .../wwwroot/js/ChessScripts/ChessSignalR.js | 59 +++++++++ .../wwwroot/js/ChessScripts/ChessUtils.js | 44 +++++++ .../wwwroot/js/ChessScripts/GameState.js | 50 ++++++++ .../wwwroot/js/ChessScripts/chessMain.js | 82 +++++++++++++ 9 files changed, 551 insertions(+), 1 deletion(-) create mode 100644 JoshHeaps.Net/wwwroot/js/ChessScripts/ChessAPI.js create mode 100644 JoshHeaps.Net/wwwroot/js/ChessScripts/ChessBoard.js create mode 100644 JoshHeaps.Net/wwwroot/js/ChessScripts/ChessInteractions.js create mode 100644 JoshHeaps.Net/wwwroot/js/ChessScripts/ChessModals.js create mode 100644 JoshHeaps.Net/wwwroot/js/ChessScripts/ChessSignalR.js create mode 100644 JoshHeaps.Net/wwwroot/js/ChessScripts/ChessUtils.js create mode 100644 JoshHeaps.Net/wwwroot/js/ChessScripts/GameState.js create mode 100644 JoshHeaps.Net/wwwroot/js/ChessScripts/chessMain.js 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 From 95b4ae63ea5f3edffa3f1047f0d1cf98c1cbcbee Mon Sep 17 00:00:00 2001 From: jheaps Date: Thu, 2 Oct 2025 10:16:05 -0600 Subject: [PATCH 2/2] Anniversary page <3 --- .claude/settings.local.json | 3 +- JoshHeaps.Net/Pages/Index.cshtml | 324 ++++++++++++++---- JoshHeaps.Net/wwwroot/css/home/site.css | 4 +- .../wwwroot/images/Wife Images/45053.jpeg | Bin 0 -> 52678 bytes .../wwwroot/images/Wife Images/45054.jpeg | Bin 0 -> 100029 bytes JoshHeaps.Net/wwwroot/js/site.js | 2 +- 6 files changed, 257 insertions(+), 76 deletions(-) create mode 100644 JoshHeaps.Net/wwwroot/images/Wife Images/45053.jpeg create mode 100644 JoshHeaps.Net/wwwroot/images/Wife Images/45054.jpeg diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 84a6040..5efc564 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,7 +1,8 @@ { "permissions": { "allow": [ - "Bash(dotnet test:*)" + "Bash(dotnet test:*)", + "Bash(dotnet build)" ], "deny": [], "ask": [] diff --git a/JoshHeaps.Net/Pages/Index.cshtml b/JoshHeaps.Net/Pages/Index.cshtml index 0beccd0..f3ce34d 100644 --- a/JoshHeaps.Net/Pages/Index.cshtml +++ b/JoshHeaps.Net/Pages/Index.cshtml @@ -1,90 +1,270 @@ @page @model JoshHeaps.Net.Pages.IndexModel @{ - ViewData["Title"] = "JoshHeaps.Net"; + ViewData["Title"] = "Happy 5th Anniversary, Morgan!"; Layout = "_Layout"; } - + + +
+

Happy 5th Anniversary, Morgan!

+ +
+ +
💖 💕 💗
+ +