Add PGN export and a Copy PGN button for finished games
- Record moves in standard algebraic notation (GameState.SanHistory), built at
move time in ChessService (captures, disambiguation, castling, promotion,
en passant, and the check/mate suffix)
- Add PgnExporter.ToPgn (seven-tag header + movetext + result) and a
GET /api/chess/{gameId}/pgn endpoint
- Show a Copy PGN button when a game ends on both the play and watch pages,
prefetching the PGN so it works during the post-game cleanup window
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
d87cea572f
commit
b651683e63
@@ -14,6 +14,12 @@ const ChessAPI = {
|
||||
return await response.json();
|
||||
},
|
||||
|
||||
async getPgn(gameId) {
|
||||
const response = await fetch(`/api/chess/${gameId}/pgn`);
|
||||
if (!response.ok) throw new Error("PGN unavailable");
|
||||
return await response.text();
|
||||
},
|
||||
|
||||
async forfeit(gameId, playerId) {
|
||||
await fetch("/api/chess/forfeit", {
|
||||
method: "POST",
|
||||
@@ -115,6 +121,7 @@ const ChessAPI = {
|
||||
}
|
||||
|
||||
if (gameOver) {
|
||||
showCopyPgn(GameState.currentGameId);
|
||||
await ChessSignalR.leaveGame();
|
||||
}
|
||||
}, 500);
|
||||
|
||||
@@ -46,6 +46,7 @@ const ChessSignalR = {
|
||||
if (reason === "forfeit")
|
||||
alert(youWon ? "🏳️ Your opponent forfeited — you win!" : "🏳️ You forfeited this game.");
|
||||
|
||||
showCopyPgn(gameId);
|
||||
await this.leaveGame();
|
||||
},
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const Spectate = {
|
||||
connection: null,
|
||||
games: new Map(), // gameId -> { isVsComputer, isComputerVsComputer, result }
|
||||
pgns: new Map(), // gameId -> PGN text (prefetched when a game finishes)
|
||||
|
||||
pieceTypeNames: ["Pawn", "Rook", "Knight", "Bishop", "Queen", "King"],
|
||||
|
||||
@@ -106,6 +107,7 @@ const Spectate = {
|
||||
|
||||
async removeGame(gameId) {
|
||||
this.games.delete(gameId);
|
||||
this.pgns.delete(gameId);
|
||||
document.getElementById(`card-${gameId}`)?.remove();
|
||||
await this.connection.invoke("LeaveWebsocketGroup", gameId).catch(() => { });
|
||||
},
|
||||
@@ -207,6 +209,7 @@ const Spectate = {
|
||||
|
||||
if (!text) {
|
||||
banner?.remove();
|
||||
card.querySelector(".copyPgnBtn")?.remove();
|
||||
card.classList.remove("over");
|
||||
return;
|
||||
}
|
||||
@@ -219,6 +222,47 @@ const Spectate = {
|
||||
|
||||
banner.textContent = text;
|
||||
card.classList.add("over");
|
||||
this.addCopyPgn(gameId, card);
|
||||
},
|
||||
|
||||
addCopyPgn(gameId, card) {
|
||||
if (card.querySelector(".copyPgnBtn")) return;
|
||||
|
||||
const btn = document.createElement("button");
|
||||
btn.className = "copyPgnBtn";
|
||||
btn.textContent = "Copy PGN";
|
||||
btn.onclick = (event) => { event.stopPropagation(); this.copyPgn(gameId); };
|
||||
card.appendChild(btn);
|
||||
|
||||
// Prefetch now (while the game is still in memory) so copy works during the
|
||||
// brief window before the finished game is cleaned up.
|
||||
fetch(`/api/chess/${gameId}/pgn`)
|
||||
.then(r => r.ok ? r.text() : null)
|
||||
.then(t => { if (t) this.pgns.set(gameId, t); })
|
||||
.catch(() => { });
|
||||
},
|
||||
|
||||
async copyPgn(gameId) {
|
||||
let pgn = this.pgns.get(gameId);
|
||||
|
||||
if (!pgn) {
|
||||
try {
|
||||
const r = await fetch(`/api/chess/${gameId}/pgn`);
|
||||
if (r.ok) pgn = await r.text();
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
if (!pgn) {
|
||||
alert("PGN is no longer available for this game.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(pgn);
|
||||
alert("📋 PGN copied to clipboard!");
|
||||
} catch {
|
||||
alert("Couldn't access the clipboard. Here's the PGN:\n\n" + pgn);
|
||||
}
|
||||
},
|
||||
|
||||
enterFullscreen(gameId) {
|
||||
|
||||
@@ -1,3 +1,35 @@
|
||||
let lastPgn = null;
|
||||
|
||||
async function showCopyPgn(gameId) {
|
||||
try {
|
||||
lastPgn = await ChessAPI.getPgn(gameId);
|
||||
const btn = document.getElementById("copyPgnBtn");
|
||||
if (btn) btn.style.display = "";
|
||||
} catch (err) {
|
||||
console.warn("Could not load PGN.", err);
|
||||
}
|
||||
}
|
||||
|
||||
async function copyPgn() {
|
||||
if (!lastPgn) return;
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(lastPgn);
|
||||
alert("📋 PGN copied to clipboard!");
|
||||
} catch {
|
||||
alert("Couldn't access the clipboard. Here's the PGN:\n\n" + lastPgn);
|
||||
}
|
||||
}
|
||||
|
||||
function resetCopyPgn() {
|
||||
lastPgn = null;
|
||||
const btn = document.getElementById("copyPgnBtn");
|
||||
if (btn) btn.style.display = "none";
|
||||
}
|
||||
|
||||
window.copyPgn = copyPgn;
|
||||
window.showCopyPgn = showCopyPgn;
|
||||
|
||||
async function forfeitCurrentGame() {
|
||||
const gameId = ChessUtils.getCookie("chessGameId");
|
||||
const playerId = ChessUtils.getCookie("chessPlayerId");
|
||||
@@ -14,6 +46,7 @@ async function forfeitCurrentGame() {
|
||||
async function startNewGame() {
|
||||
await ChessSignalR.stopConnection();
|
||||
await forfeitCurrentGame();
|
||||
resetCopyPgn();
|
||||
|
||||
try {
|
||||
const gameData = await ChessAPI.joinGame();
|
||||
@@ -40,6 +73,7 @@ async function startNewGame() {
|
||||
async function startCPUGame() {
|
||||
await ChessSignalR.stopConnection();
|
||||
await forfeitCurrentGame();
|
||||
resetCopyPgn();
|
||||
|
||||
try {
|
||||
const difficulty = await ChessModals.promptDifficulty();
|
||||
|
||||
Reference in New Issue
Block a user