A Pen by Dan Brickley on CodePen.
Created
January 15, 2026 21:27
-
-
Save danbri/a6b0d5b4457b9eb76d728430dd8d9c96 to your computer and use it in GitHub Desktop.
mamichess! π
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Mamichess</title> | |
| <style> | |
| body { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| padding-top: 20px; | |
| min-height: 100vh; | |
| margin: 0; | |
| background-color: #333; | |
| color: #eee; | |
| font-family: 'Segoe UI Emoji', 'Apple Color Emoji', 'Segoe UI Symbol', 'Noto Color Emoji', Arial, sans-serif; | |
| user-select: none; | |
| overflow: hidden; /* Prevent scrollbars during full-screen animation */ | |
| } | |
| h1 { margin-bottom: 15px; color: #fff; text-transform: uppercase; letter-spacing: 1px; } | |
| #game-container { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| position: relative; | |
| z-index: 1; | |
| } | |
| #game-board { | |
| display: grid; | |
| grid-template-columns: repeat(4, 75px); | |
| grid-template-rows: repeat(3, 75px); | |
| border: 5px solid #555; | |
| box-shadow: 0 0 15px rgba(0,0,0,0.3); | |
| border-radius: 5px; | |
| position: relative; | |
| } | |
| .cell { | |
| width: 75px; | |
| height: 75px; | |
| box-sizing: border-box; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| cursor: pointer; | |
| position: relative; | |
| transition: background-color 0.2s ease-out, box-shadow 0.2s ease-out; | |
| } | |
| .cell.light-square { background-color: #e0c4a0; } | |
| .cell.dark-square { background-color: #a07c54; } | |
| .piece-emoji { | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: opacity 0.1s, transform 0.1s; | |
| font-size: 68px; | |
| line-height: 1; | |
| } | |
| .piece-emoji[data-piece="king"], | |
| .piece-emoji[data-piece="queen"] { | |
| font-size: 102px; /* 68px * 1.5 */ | |
| } | |
| .piece-emoji.dragging { opacity: 0.3; } | |
| .piece-emoji.active-piece-animation { | |
| animation: gentle-throb 1.2s infinite ease-in-out alternate; | |
| } | |
| @keyframes gentle-throb { | |
| from { transform: scale(1); opacity: 1; } | |
| to { transform: scale(1.08); opacity: 0.9; } | |
| } | |
| #controls { margin-top: 20px; display: flex; flex-direction: column; align-items: center; } | |
| #message { font-size: 1.1em; font-weight: bold; color: #f0f0f0; text-align: center; margin-bottom:12px; min-height: 1.3em;} | |
| #subtle-move-message { font-size: 0.9em; color: #a0a0ff; text-align: center; min-height: 1em; margin-bottom: 5px;} | |
| #buttons-panel { display: flex; gap: 10px; } | |
| .control-button, #help-toggle-label { /* Keeping #help-toggle-label for potential future use if button becomes a label */ | |
| padding: 10px 15px; font-size: 1.2em; cursor: pointer; border: 1px solid #777; | |
| background-color: #555; color: #fff; border-radius: 5px; | |
| transition: background-color 0.2s, box-shadow 0.2s; | |
| display: inline-flex; align-items: center; justify-content: center; min-width: 50px; | |
| } | |
| .control-button:hover, #help-toggle-label:hover { background-color: #666; box-shadow: 0 0 5px rgba(255,255,255,0.2); } | |
| #play-again { display: none; } | |
| /* #help-toggle { display: none; } /* This was for a checkbox, not used. */ | |
| #explanation-btn.active { | |
| background-color: #779977; border-color: #99bb99; | |
| } | |
| #explanation-text { | |
| display:none; | |
| margin-top:15px; | |
| padding:12px; | |
| border:1px solid #777; | |
| background-color:#444; | |
| max-width: 320px; /* Constrain width */ | |
| text-align:left; | |
| font-size: 1em; | |
| line-height: 1.6; | |
| border-radius: 5px; | |
| box-shadow: 0 2px 5px rgba(0,0,0,0.2); | |
| color: #ddd; | |
| /* === FIX FOR SCROLLABILITY === */ | |
| max-height: 35vh; /* Limit height (e.g., 35% of viewport height or a fixed px value like 250px) */ | |
| overflow-y: auto; /* Add scrollbar if content exceeds max-height */ | |
| position: relative; /* Ensure it layers correctly if needed, though likely not critical here */ | |
| z-index: 10; /* Same as above */ | |
| } | |
| #explanation-text a { color: #8af; text-decoration: none; } | |
| #explanation-text a:hover { text-decoration: underline; } | |
| #explanation-text ul { | |
| list-style: disc; | |
| padding-left: 25px; | |
| margin-top: 8px; | |
| margin-bottom: 0; | |
| } | |
| #explanation-text li { | |
| margin-bottom: 8px; | |
| } | |
| #explanation-text li:last-child { | |
| margin-bottom: 0; | |
| } | |
| /* --- Skull End Game Animation Styles --- */ | |
| #skull-animation-overlay { | |
| display: none; | |
| position: fixed; | |
| top: 0; left: 0; | |
| width: 100vw; height: 100vh; | |
| background-color: rgba(0,0,0,0); | |
| z-index: 2000; | |
| pointer-events: none; | |
| } | |
| #animated-skull { | |
| position: absolute; | |
| opacity: 0; | |
| color: #fff; | |
| text-align: center; | |
| will-change: transform, opacity, font-size, left, top; | |
| transform-origin: center center; | |
| } | |
| .king-on-skull-fading { | |
| animation: fadeOutBoardPiece 0.5s ease-out forwards; | |
| } | |
| @keyframes fadeOutBoardPiece { | |
| from { opacity: 1; transform: scale(1); } | |
| to { opacity: 0; transform: scale(0.7); } | |
| } | |
| #animated-skull.trigger-skull-win-animation { | |
| animation: | |
| skullInitialFadeIn 0.3s forwards, | |
| skullGlowRed 1.5s linear infinite 0.1s, | |
| skullGrowAndCenter2x 2.0s cubic-bezier(0.4, 0, 0.2, 1) 0.1s forwards; | |
| } | |
| @keyframes skullInitialFadeIn { | |
| to { opacity: 1; } | |
| } | |
| @keyframes skullGlowRed { | |
| 0%, 100% { text-shadow: 0 0 10px #ff0000, 0 0 20px #ff0000, 0 0 30px #ff0000; } | |
| 50% { text-shadow: 0 0 15px #ff3333, 0 0 25px #ff3333, 0 0 35px #ff3333; } | |
| } | |
| @keyframes skullGrowAndCenter2x { | |
| 0% { | |
| transform: translate(0,0) scale(1); | |
| opacity: 1; | |
| } | |
| 40% { | |
| left: 50vw; | |
| top: 50vh; | |
| transform: translate(-50%, -50%) scale(1.5); | |
| opacity: 1; | |
| } | |
| 100% { | |
| left: 50vw; | |
| top: 50vh; | |
| transform: translate(-50%, -50%) scale(2); | |
| opacity: 0.9; | |
| } | |
| } | |
| #skull-animation-overlay.fading-to-black { | |
| animation: fadeToBlackBG 1s forwards; | |
| } | |
| @keyframes fadeToBlackBG { | |
| to { background-color: rgba(0,0,0,1); } | |
| } | |
| #skull-animation-overlay.fading-out-from-black { | |
| animation: fadeOutBlackBG 1s forwards; | |
| } | |
| @keyframes fadeOutBlackBG { | |
| from { background-color: rgba(0,0,0,1); opacity: 1; } | |
| to { background-color: rgba(0,0,0,0); opacity: 0; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="game-container"> | |
| <div id="game-board"></div> | |
| <div id="controls"> | |
| <p id="message"></p> | |
| <p id="subtle-move-message"></p> | |
| <div id="buttons-panel"> | |
| <button id="explanation-btn" class="control-button" title="Help/Rules" aria-controls="explanation-text" aria-expanded="false">?</button> | |
| <button id="play-again" class="control-button" title="Play Again">π</button> | |
| </div> | |
| <div id="explanation-text"> <!-- Restored to original position --> | |
| <strong>Mamichess Puzzle Prime</strong><br> | |
| The goal for the Queen (β) is to checkmate the opposing King (β) by forcing it onto the "grave" square (π, d3: the top-right square). Queen moves first. The King will try its best to avoid the grave or capture the Queen. Queen has a forced win. | |
| <br><br> | |
| <strong>Learn More:</strong> | |
| <ul> | |
| <li>About the creator: <a href="https://en.wikipedia.org/wiki/Mamikon_Mnatsakanian" target="_blank" rel="noopener noreferrer">Mamikon Mnatsakanian</a> (Wikipedia)</li> | |
| <li>Mamikon's work (including this puzzle): <a href="https://web.archive.org/web/20080307094905/https:/home.earthlink.net/~mamikara/Start.html" target="_blank" rel="noopener noreferrer">Mamikon's Visual Calculus Page</a></li> | |
| <li>Background and solutions: <a href="https://web.archive.org/web/20080406051404/http://www.mamikon.com/GARDNER/MamikonMiniChess.html" target="_blank" rel="noopener noreferrer">Mamikon Mini-Chess page</a>.</li> | |
| </ul> | |
| <p></p> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="skull-animation-overlay"> | |
| <span id="animated-skull" class="piece-emoji">π</span> | |
| </div> | |
| <script> | |
| const BOARD_SIZE = { rows: 3, cols: 4 }; | |
| const PIECE_EMOJI = { WHITE_QUEEN: 'β', BLACK_KING: 'β', SKULL: 'π' }; | |
| const INITIAL_KING_POS = { row: 0, col: 0 }; | |
| const INITIAL_QUEEN_POS = { row: 1, col: 2 }; | |
| const SKULL_TARGET = { row: 0, col: 3 }; | |
| let kingPos, queenPos; | |
| let currentTurn = 'white'; | |
| let gameOver = false; | |
| let queenPossibleMoves = []; | |
| let cellElements = []; | |
| let draggedPieceElement = null; | |
| let originalQueenPos = null; | |
| let isAnimatingMove = false; | |
| const ANIMATION_DURATION_MS = 350; | |
| const boardElement = document.getElementById('game-board'); | |
| const messageElement = document.getElementById('message'); | |
| const subtleMoveMessageElement = document.getElementById('subtle-move-message'); | |
| const playAgainButton = document.getElementById('play-again'); | |
| const explanationBtn = document.getElementById('explanation-btn'); | |
| const explanationText = document.getElementById('explanation-text'); // Restored | |
| const skullAnimationOverlay = document.getElementById('skull-animation-overlay'); | |
| const animatedSkullElement = document.getElementById('animated-skull'); | |
| function createBoard() { | |
| boardElement.innerHTML = ''; | |
| cellElements = []; | |
| for (let r = 0; r < BOARD_SIZE.rows; r++) { | |
| for (let c = 0; c < BOARD_SIZE.cols; c++) { | |
| const cell = document.createElement('div'); | |
| cell.classList.add('cell'); | |
| cell.dataset.row = r; | |
| cell.dataset.col = c; | |
| if ((r + c) % 2 === 0) cell.classList.add('light-square'); | |
| else cell.classList.add('dark-square'); | |
| cell.addEventListener('click', handleCellClick); | |
| cell.addEventListener('dragover', handleDragOver); | |
| cell.addEventListener('dragenter', handleDragEnterCell); | |
| cell.addEventListener('dragleave', handleDragLeaveCell); | |
| cell.addEventListener('drop', handleDrop); | |
| boardElement.appendChild(cell); | |
| cellElements.push(cell); | |
| } | |
| } | |
| } | |
| function getCellElement(row, col) { | |
| if (row < 0 || row >= BOARD_SIZE.rows || col < 0 || col >= BOARD_SIZE.cols) return null; | |
| return cellElements[row * BOARD_SIZE.cols + col]; | |
| } | |
| function drawPiecesAndHighlights() { | |
| if (currentTurn === 'white' && !gameOver && !isAnimatingMove) { | |
| queenPossibleMoves = getValidMovesForPiece(queenPos, 'queen'); | |
| } else { | |
| queenPossibleMoves = []; | |
| } | |
| subtleMoveMessageElement.textContent = ''; | |
| cellElements.forEach(cell => { | |
| cell.innerHTML = ''; | |
| const r = parseInt(cell.dataset.row); | |
| const c = parseInt(cell.dataset.col); | |
| let pieceSpan = null; | |
| if (r === SKULL_TARGET.row && c === SKULL_TARGET.col) { | |
| const isSkullCoveredByPiece = (kingPos.row === r && kingPos.col === c) || (queenPos.row === r && queenPos.col === c); | |
| if (!isSkullCoveredByPiece) { | |
| pieceSpan = document.createElement('span'); | |
| pieceSpan.classList.add('piece-emoji'); | |
| pieceSpan.textContent = PIECE_EMOJI.SKULL; | |
| } | |
| } | |
| if (r === queenPos.row && c === queenPos.col) { | |
| pieceSpan = document.createElement('span'); | |
| pieceSpan.classList.add('piece-emoji'); | |
| pieceSpan.textContent = PIECE_EMOJI.WHITE_QUEEN; | |
| pieceSpan.dataset.piece = 'queen'; | |
| if (currentTurn === 'white' && !gameOver) { | |
| pieceSpan.classList.add('active-piece-animation'); | |
| if (!isAnimatingMove) { | |
| pieceSpan.draggable = true; | |
| pieceSpan.addEventListener('dragstart', handleDragStartQueen); | |
| pieceSpan.addEventListener('dragend', handleDragEndQueen); | |
| } | |
| } | |
| } | |
| else if (r === kingPos.row && c === kingPos.col) { | |
| pieceSpan = document.createElement('span'); | |
| pieceSpan.classList.add('piece-emoji'); | |
| pieceSpan.textContent = PIECE_EMOJI.BLACK_KING; | |
| pieceSpan.dataset.piece = 'king'; | |
| if (currentTurn === 'black' && !gameOver) { | |
| pieceSpan.classList.add('active-piece-animation'); | |
| } | |
| } | |
| if (pieceSpan) { | |
| cell.appendChild(pieceSpan); | |
| } | |
| }); | |
| } | |
| function animatePieceMove(pieceEmojiText, oldLogicPos, newLogicPos, onAnimationComplete) { | |
| isAnimatingMove = true; | |
| const boardRect = boardElement.getBoundingClientRect(); | |
| const oldCellElement = getCellElement(oldLogicPos.row, oldLogicPos.col); | |
| const newCellElement = getCellElement(newLogicPos.row, newLogicPos.col); | |
| if (!oldCellElement || !newCellElement) { | |
| console.error("Animation cells not found:", oldLogicPos, newLogicPos); | |
| isAnimatingMove = false; | |
| onAnimationComplete(); | |
| return; | |
| } | |
| const oldCellRect = oldCellElement.getBoundingClientRect(); | |
| const newCellRect = newCellElement.getBoundingClientRect(); | |
| const ghostPiece = document.createElement('span'); | |
| ghostPiece.textContent = pieceEmojiText; | |
| ghostPiece.classList.add('piece-emoji'); | |
| if (pieceEmojiText === PIECE_EMOJI.WHITE_QUEEN) ghostPiece.dataset.piece = 'queen'; | |
| if (pieceEmojiText === PIECE_EMOJI.BLACK_KING) ghostPiece.dataset.piece = 'king'; | |
| ghostPiece.style.position = 'absolute'; | |
| ghostPiece.style.left = `${oldCellRect.left - boardRect.left}px`; | |
| ghostPiece.style.top = `${oldCellRect.top - boardRect.top}px`; | |
| ghostPiece.style.width = `${oldCellRect.width}px`; | |
| ghostPiece.style.height = `${oldCellRect.height}px`; | |
| ghostPiece.style.display = 'flex'; | |
| ghostPiece.style.alignItems = 'center'; | |
| ghostPiece.style.justifyContent = 'center'; | |
| ghostPiece.style.zIndex = '1000'; | |
| ghostPiece.style.pointerEvents = 'none'; | |
| const pieceInOldCell = oldCellElement.querySelector('.piece-emoji'); | |
| if (pieceInOldCell) { | |
| pieceInOldCell.style.visibility = 'hidden'; | |
| } | |
| boardElement.appendChild(ghostPiece); | |
| ghostPiece.getBoundingClientRect(); | |
| const deltaX = newCellRect.left - oldCellRect.left; | |
| const deltaY = newCellRect.top - oldCellRect.top; | |
| ghostPiece.style.transition = `transform ${ANIMATION_DURATION_MS / 1000}s ease-in-out`; | |
| ghostPiece.style.transform = `translate(${deltaX}px, ${deltaY}px)`; | |
| ghostPiece.addEventListener('transitionend', () => { | |
| if (ghostPiece.parentNode) { | |
| ghostPiece.parentNode.removeChild(ghostPiece); | |
| } | |
| isAnimatingMove = false; | |
| onAnimationComplete(); | |
| }, { once: true }); | |
| } | |
| function handleCellClick(event) { | |
| if (isAnimatingMove || gameOver || currentTurn !== 'white') return; | |
| const clickedRow = parseInt(event.currentTarget.dataset.row); | |
| const clickedCol = parseInt(event.currentTarget.dataset.col); | |
| const move = queenPossibleMoves.find(m => m.row === clickedRow && m.col === clickedCol); | |
| if (move) { | |
| makeQueenMove(clickedRow, clickedCol); | |
| } | |
| } | |
| function makeQueenMove(newRow, newCol) { | |
| if (isAnimatingMove || gameOver || currentTurn !== 'white') return; | |
| const oldPos = { ...queenPos }; | |
| const newPos = { row: newRow, col: newCol }; | |
| animatePieceMove(PIECE_EMOJI.WHITE_QUEEN, oldPos, newPos, () => { | |
| queenPos = newPos; | |
| if (navigator.vibrate) navigator.vibrate(20); | |
| checkWinConditions('white'); | |
| if (!gameOver) { | |
| currentTurn = 'black'; | |
| updateMessage(); | |
| drawPiecesAndHighlights(); | |
| setTimeout(computerMove, 700); | |
| } else if (!skullAnimationOverlay.classList.contains('fading-to-black') && skullAnimationOverlay.style.display !== 'block' ) { | |
| drawPiecesAndHighlights(); | |
| } | |
| }); | |
| } | |
| function getValidMovesForPiece(piecePos, pieceType, currentKingPos = kingPos) { | |
| const moves = []; | |
| const { row, col } = piecePos; | |
| if (pieceType === 'queen') { | |
| const directions = [ | |
| { dr: 0, dc: 1 }, { dr: 0, dc: -1 }, { dr: 1, dc: 0 }, { dr: -1, dc: 0 }, | |
| { dr: 1, dc: 1 }, { dr: 1, dc: -1 }, { dr: -1, dc: 1 }, { dr: -1, dc: -1 } | |
| ]; | |
| for (const dir of directions) { | |
| for (let i = 1; i < Math.max(BOARD_SIZE.rows, BOARD_SIZE.cols); i++) { | |
| const newR = row + dir.dr * i; const newC = col + dir.dc * i; | |
| if (!isValidBoardPos(newR, newC)) break; | |
| if (newR === currentKingPos.row && newC === currentKingPos.col) { | |
| moves.push({ row: newR, col: newC }); break; | |
| } | |
| moves.push({ row: newR, col: newC }); | |
| } | |
| } | |
| } else if (pieceType === 'king') { | |
| const KingOffsets = [ | |
| { dr: -1, dc: -1 }, { dr: -1, dc: 0 }, { dr: -1, dc: 1 }, { dr: 0, dc: -1 }, | |
| { dr: 0, dc: 1 }, { dr: 1, dc: -1 }, { dr: 1, dc: 0 }, { dr: 1, dc: 1 } | |
| ]; | |
| for (const offset of KingOffsets) { | |
| const newR = row + offset.dr; const newC = col + offset.dc; | |
| if (isValidBoardPos(newR, newC)) { | |
| if (newR === queenPos.row && newC === queenPos.col) { | |
| moves.push({ row: newR, col: newC }); | |
| } | |
| else if (!isSquareAttackedByQueen({row: newR, col: newC}, queenPos, {row:newR, col:newC})) { | |
| moves.push({ row: newR, col: newC }); | |
| } | |
| } | |
| } | |
| } | |
| return moves; | |
| } | |
| function isValidBoardPos(row, col) { | |
| return row >= 0 && row < BOARD_SIZE.rows && col >= 0 && col < BOARD_SIZE.cols; | |
| } | |
| function isSquareAttackedByQueen(targetPos, currentQueenPos, currentKingPosForLoS) { | |
| const { row: qR, col: qC } = currentQueenPos; | |
| const { row: tR, col: tC } = targetPos; | |
| if (tR === qR && tC === qC) return false; | |
| if (tR === qR || tC === qC || Math.abs(tR - qR) === Math.abs(tC - qC)) { | |
| const dr = Math.sign(tR - qR); | |
| const dc = Math.sign(tC - qC); | |
| let r = qR + dr; | |
| let c = qC + dc; | |
| while ((r !== tR || c !== tC) && isValidBoardPos(r,c)) { | |
| if (r === currentKingPosForLoS.row && c === currentKingPosForLoS.col && | |
| !(r === tR && c === tC) ) { | |
| return false; | |
| } | |
| r += dr; | |
| c += dc; | |
| } | |
| return (r === tR && c === tC); | |
| } | |
| return false; | |
| } | |
| function computerMove() { | |
| if (isAnimatingMove || gameOver || currentTurn !== 'black') return; | |
| const kingValidMoves = getValidMovesForPiece(kingPos, 'king'); | |
| let chosenMove = null; | |
| const oldKingPos = { ...kingPos }; | |
| const captureQueenMove = kingValidMoves.find(m => m.row === queenPos.row && m.col === queenPos.col); | |
| if (captureQueenMove) { | |
| chosenMove = captureQueenMove; | |
| } else { | |
| const nonSkullMoves = kingValidMoves.filter(m => !(m.row === SKULL_TARGET.row && m.col === SKULL_TARGET.col)); | |
| if (nonSkullMoves.length > 0) { | |
| nonSkullMoves.sort((a, b) => { | |
| const distA = Math.hypot(a.row - queenPos.row, a.col - queenPos.col); | |
| const distB = Math.hypot(b.row - queenPos.row, b.col - queenPos.col); | |
| return distB - distA; | |
| }); | |
| chosenMove = nonSkullMoves[0]; | |
| } else if (kingValidMoves.length > 0) { | |
| chosenMove = kingValidMoves[0]; | |
| } | |
| } | |
| if (chosenMove) { | |
| const newKingPos = chosenMove; | |
| animatePieceMove(PIECE_EMOJI.BLACK_KING, oldKingPos, newKingPos, () => { | |
| kingPos = newKingPos; | |
| checkWinConditions('black'); | |
| if (!gameOver) { | |
| currentTurn = 'white'; | |
| updateMessage(); | |
| drawPiecesAndHighlights(); | |
| } else if (!skullAnimationOverlay.classList.contains('fading-to-black') && skullAnimationOverlay.style.display !== 'block' ) { | |
| drawPiecesAndHighlights(); | |
| } | |
| }); | |
| } else { | |
| if (isSquareAttackedByQueen(kingPos, queenPos, kingPos)) { | |
| endGame(`${PIECE_EMOJI.WHITE_QUEEN} wins! Checkmate.`); | |
| } else { | |
| endGame(`Stalemate! ${PIECE_EMOJI.BLACK_KING} has no moves. White wins!`); | |
| } | |
| drawPiecesAndHighlights(); | |
| } | |
| } | |
| function checkWinConditions(whoMoved) { | |
| if (gameOver && !(whoMoved === 'black' && kingPos.row === SKULL_TARGET.row && kingPos.col === SKULL_TARGET.col)) { | |
| return; | |
| } | |
| if (whoMoved === 'white') { | |
| if (queenPos.row === kingPos.row && queenPos.col === kingPos.col) { | |
| endGame(`${PIECE_EMOJI.WHITE_QUEEN} wins! Captured ${PIECE_EMOJI.BLACK_KING} at ${coordsToAlg(kingPos)}.`); | |
| } | |
| } else if (whoMoved === 'black') { | |
| if (kingPos.row === queenPos.row && kingPos.col === queenPos.col) { | |
| endGame(`${PIECE_EMOJI.BLACK_KING} wins! Captured ${PIECE_EMOJI.WHITE_QUEEN} at ${coordsToAlg(queenPos)}!`); | |
| } else if (kingPos.row === SKULL_TARGET.row && kingPos.col === SKULL_TARGET.col) { | |
| const message = `${PIECE_EMOJI.WHITE_QUEEN} wins! ${PIECE_EMOJI.BLACK_KING} forced to ${PIECE_EMOJI.SKULL} at ${coordsToAlg(SKULL_TARGET)}!`; | |
| playKingHitsSkullAnimation(message, () => { | |
| drawPiecesAndHighlights(); | |
| }); | |
| return; | |
| } | |
| } | |
| if (!gameOver && currentTurn === 'white') { | |
| const kingMoves = getValidMovesForPiece(kingPos, 'king'); | |
| if (kingMoves.length === 0) { | |
| if (isSquareAttackedByQueen(kingPos, queenPos, kingPos)) { | |
| endGame(`${PIECE_EMOJI.WHITE_QUEEN} wins! Checkmate.`); | |
| } else { | |
| endGame(`Stalemate! ${PIECE_EMOJI.BLACK_KING} has no moves. White wins!`); | |
| } | |
| } | |
| } | |
| } | |
| function playKingHitsSkullAnimation(endGameMessage, onAnimationEndCallback) { | |
| gameOver = true; | |
| isAnimatingMove = true; | |
| messageElement.textContent = "β οΈ The King meets his fate... β οΈ"; | |
| subtleMoveMessageElement.textContent = ''; | |
| playAgainButton.style.display = 'none'; | |
| const kingCellElement = getCellElement(kingPos.row, kingPos.col); | |
| const kingEmojiOnBoard = kingCellElement ? kingCellElement.querySelector('.piece-emoji[data-piece="king"]') : null; | |
| if (!kingEmojiOnBoard || !skullAnimationOverlay || !animatedSkullElement) { | |
| console.error("Skull animation elements missing."); | |
| endGame(endGameMessage); | |
| if (onAnimationEndCallback) onAnimationEndCallback(); | |
| isAnimatingMove = false; | |
| return; | |
| } | |
| kingEmojiOnBoard.classList.add('king-on-skull-fading'); | |
| const kingRect = kingEmojiOnBoard.getBoundingClientRect(); | |
| const kingComputedStyle = window.getComputedStyle(kingEmojiOnBoard); | |
| const initialSkullFontSize = kingComputedStyle.fontSize; | |
| animatedSkullElement.style.left = `${kingRect.left}px`; | |
| animatedSkullElement.style.top = `${kingRect.top}px`; | |
| animatedSkullElement.style.width = `${kingRect.width}px`; | |
| animatedSkullElement.style.height = `${kingRect.height}px`; | |
| animatedSkullElement.style.fontSize = initialSkullFontSize; | |
| animatedSkullElement.style.transform = 'translate(0,0) scale(1)'; | |
| animatedSkullElement.style.opacity = '0'; | |
| skullAnimationOverlay.style.backgroundColor = 'rgba(0,0,0,0)'; | |
| skullAnimationOverlay.style.display = 'block'; | |
| animatedSkullElement.getBoundingClientRect(); | |
| animatedSkullElement.classList.add('trigger-skull-win-animation'); | |
| const growAnimationTotalTime = 2100; | |
| setTimeout(() => { | |
| skullAnimationOverlay.classList.add('fading-to-black'); | |
| setTimeout(() => { | |
| skullAnimationOverlay.classList.remove('fading-to-black'); | |
| skullAnimationOverlay.classList.add('fading-out-from-black'); | |
| setTimeout(() => { | |
| skullAnimationOverlay.style.display = 'none'; | |
| skullAnimationOverlay.classList.remove('fading-out-from-black'); | |
| animatedSkullElement.classList.remove('trigger-skull-win-animation'); | |
| animatedSkullElement.style.opacity = '0'; | |
| if (kingEmojiOnBoard) kingEmojiOnBoard.classList.remove('king-on-skull-fading'); | |
| endGame(endGameMessage); | |
| if (onAnimationEndCallback) onAnimationEndCallback(); | |
| isAnimatingMove = false; | |
| playAgainButton.style.display = 'inline-flex'; | |
| }, 1000); | |
| }, 1000); | |
| }, growAnimationTotalTime); | |
| } | |
| function updateMessage() { | |
| if (gameOver) return; | |
| if (currentTurn === 'white') messageElement.textContent = `${PIECE_EMOJI.WHITE_QUEEN} to move`; | |
| else messageElement.textContent = `${PIECE_EMOJI.BLACK_KING} thinking...`; | |
| } | |
| function endGame(msg) { | |
| gameOver = true; | |
| messageElement.textContent = msg; | |
| subtleMoveMessageElement.textContent = ''; | |
| if (!skullAnimationOverlay.classList.contains('fading-to-black') && skullAnimationOverlay.style.display !== 'block' ) { | |
| playAgainButton.style.display = 'inline-flex'; | |
| } | |
| queenPossibleMoves = []; | |
| } | |
| function coordsToAlg(pos) { | |
| const colChar = String.fromCharCode(97 + pos.col); | |
| const rowNum = BOARD_SIZE.rows - pos.row; | |
| return `${colChar}${rowNum}`; | |
| } | |
| function resetGame() { | |
| isAnimatingMove = false; | |
| kingPos = { ...INITIAL_KING_POS }; | |
| queenPos = { ...INITIAL_QUEEN_POS }; | |
| currentTurn = 'white'; | |
| gameOver = false; | |
| playAgainButton.style.display = 'none'; | |
| explanationText.style.display = 'none'; // Hide help text | |
| explanationBtn.classList.remove('active'); | |
| explanationBtn.setAttribute('aria-expanded', 'false'); | |
| skullAnimationOverlay.style.display = 'none'; | |
| skullAnimationOverlay.classList.remove('fading-to-black', 'fading-out-from-black'); | |
| animatedSkullElement.classList.remove('trigger-skull-win-animation'); | |
| animatedSkullElement.style.opacity = '0'; | |
| updateMessage(); | |
| subtleMoveMessageElement.textContent = ''; | |
| drawPiecesAndHighlights(); | |
| } | |
| // --- Drag and Drop Handlers --- | |
| function handleDragStartQueen(event) { | |
| if (isAnimatingMove || currentTurn !== 'white' || gameOver) { | |
| event.preventDefault(); return; | |
| } | |
| draggedPieceElement = event.target; | |
| originalQueenPos = { ...queenPos }; | |
| event.dataTransfer.effectAllowed = 'move'; | |
| setTimeout(() => { if (draggedPieceElement) draggedPieceElement.classList.add('dragging'); }, 0); | |
| } | |
| function handleDragOver(event) { | |
| if (isAnimatingMove) return; | |
| event.preventDefault(); | |
| event.dataTransfer.dropEffect = 'move'; | |
| } | |
| function handleDragEnterCell(event) { /* No visual feedback */ } | |
| function handleDragLeaveCell(event) { /* No visual feedback */ } | |
| function handleDrop(event) { | |
| event.preventDefault(); | |
| if (isAnimatingMove || currentTurn !== 'white' || gameOver || !originalQueenPos) return; | |
| const cell = event.currentTarget; | |
| const r = parseInt(cell.dataset.row); | |
| const c = parseInt(cell.dataset.col); | |
| if (queenPossibleMoves.length === 0 && currentTurn === 'white' && !gameOver) { | |
| queenPossibleMoves = getValidMovesForPiece(queenPos, 'queen'); | |
| } | |
| const move = queenPossibleMoves.find(m => m.row === r && m.col === c); | |
| if (move) { | |
| makeQueenMove(r, c); | |
| } | |
| } | |
| function handleDragEndQueen(event) { | |
| if (draggedPieceElement) { | |
| draggedPieceElement.classList.remove('dragging'); | |
| } | |
| draggedPieceElement = null; | |
| originalQueenPos = null; | |
| if (!isAnimatingMove) { | |
| drawPiecesAndHighlights(); | |
| } | |
| } | |
| playAgainButton.addEventListener('click', resetGame); | |
| // Original simple toggle for explanation text | |
| explanationBtn.addEventListener('click', () => { | |
| const currentlyVisible = explanationText.style.display === 'block'; | |
| explanationText.style.display = currentlyVisible ? 'none' : 'block'; | |
| explanationBtn.classList.toggle('active', !currentlyVisible); | |
| explanationBtn.setAttribute('aria-expanded', String(!currentlyVisible)); | |
| }); | |
| createBoard(); | |
| resetGame(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment