import React, { useState, useEffect, useCallback, useRef } from 'react'; // Audio context for traditional sounds const createAudioContext = () => { if (typeof window !== 'undefined') { return new (window.AudioContext || window.webkitAudioContext)(); } return null; }; // Sound effects generator const useSounds = () => { const audioContextRef = useRef(null); const musicOscillatorsRef = useRef([]); const musicIntervalRef = useRef(null); const [isMusicPlaying, setIsMusicPlaying] = useState(false); const getContext = useCallback(() => { if (!audioContextRef.current) { audioContextRef.current = createAudioContext(); } return audioContextRef.current; }, []); const playMove = useCallback(() => { const ctx = getContext(); if (!ctx) return; const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); osc.frequency.setValueAtTime(800, ctx.currentTime); osc.frequency.exponentialRampToValueAtTime(400, ctx.currentTime + 0.1); gain.gain.setValueAtTime(0.3, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.15); osc.start(ctx.currentTime); osc.stop(ctx.currentTime + 0.15); }, [getContext]); const playCapture = useCallback(() => { const ctx = getContext(); if (!ctx) return; const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); osc.type = 'triangle'; osc.frequency.setValueAtTime(600, ctx.currentTime); osc.frequency.exponentialRampToValueAtTime(200, ctx.currentTime + 0.2); gain.gain.setValueAtTime(0.4, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.25); osc.start(ctx.currentTime); osc.stop(ctx.currentTime + 0.25); }, [getContext]); const playCheck = useCallback(() => { const ctx = getContext(); if (!ctx) return; [0, 0.15].forEach((delay) => { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); osc.frequency.setValueAtTime(1000, ctx.currentTime + delay); gain.gain.setValueAtTime(0.3, ctx.currentTime + delay); gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + delay + 0.1); osc.start(ctx.currentTime + delay); osc.stop(ctx.currentTime + delay + 0.1); }); }, [getContext]); const playGameOver = useCallback(() => { const ctx = getContext(); if (!ctx) return; const notes = [523, 659, 784, 1047]; notes.forEach((freq, i) => { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); osc.frequency.setValueAtTime(freq, ctx.currentTime + i * 0.2); gain.gain.setValueAtTime(0.3, ctx.currentTime + i * 0.2); gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + i * 0.2 + 0.3); osc.start(ctx.currentTime + i * 0.2); osc.stop(ctx.currentTime + i * 0.2 + 0.3); }); }, [getContext]); // Traditional Thai-inspired background music (pentatonic melody) const startMusic = useCallback(() => { const ctx = getContext(); if (!ctx || isMusicPlaying) return; setIsMusicPlaying(true); // Thai pentatonic scale frequencies (approximation) const scale = [261.63, 293.66, 329.63, 392.00, 440.00, 523.25, 587.33, 659.25]; let noteIndex = 0; const playNote = () => { const osc = ctx.createOscillator(); const gain = ctx.createGain(); const filter = ctx.createBiquadFilter(); osc.connect(filter); filter.connect(gain); gain.connect(ctx.destination); filter.type = 'lowpass'; filter.frequency.setValueAtTime(1500, ctx.currentTime); osc.type = 'sine'; const freq = scale[noteIndex % scale.length]; osc.frequency.setValueAtTime(freq, ctx.currentTime); gain.gain.setValueAtTime(0.08, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.8); osc.start(ctx.currentTime); osc.stop(ctx.currentTime + 0.9); musicOscillatorsRef.current.push(osc); // Random walk through scale noteIndex += Math.floor(Math.random() * 3) - 1; if (noteIndex < 0) noteIndex = 0; if (noteIndex >= scale.length) noteIndex = scale.length - 1; }; playNote(); musicIntervalRef.current = setInterval(playNote, 1000); }, [getContext, isMusicPlaying]); const stopMusic = useCallback(() => { if (musicIntervalRef.current) { clearInterval(musicIntervalRef.current); musicIntervalRef.current = null; } setIsMusicPlaying(false); }, []); return { playMove, playCapture, playCheck, playGameOver, startMusic, stopMusic, isMusicPlaying }; }; // Piece definitions - more traditional Makruk representations const PIECES = { K: { name: 'Khun', english: 'King', symbol: '▲' }, // Tall spired shape like Thai temple M: { name: 'Met', english: 'Queen', symbol: '●' }, // Small rounded piece R: { name: 'Rua', english: 'Boat', symbol: '☗' }, // Boat shape B: { name: 'Khon', english: 'Noble', symbol: '◆' }, // Diamond/pointed shape for masked dancer N: { name: 'Ma', english: 'Horse', symbol: '♞' }, // Horse (same as Western) P: { name: 'Bia', english: 'Shell', symbol: '⬤' }, // Cowrie shell (round) }; // Initial board setup const createInitialBoard = () => { const board = Array(8).fill(null).map(() => Array(8).fill(null)); // White pieces (bottom) board[7] = [ { type: 'R', color: 'white' }, { type: 'N', color: 'white' }, { type: 'B', color: 'white' }, { type: 'M', color: 'white' }, { type: 'K', color: 'white' }, { type: 'B', color: 'white' }, { type: 'N', color: 'white' }, { type: 'R', color: 'white' }, ]; // White pawns on rank 6 (index 5) for (let i = 0; i < 8; i++) { board[5][i] = { type: 'P', color: 'white' }; } // Black pieces (top) board[0] = [ { type: 'R', color: 'black' }, { type: 'N', color: 'black' }, { type: 'B', color: 'black' }, { type: 'K', color: 'black' }, { type: 'M', color: 'black' }, { type: 'B', color: 'black' }, { type: 'N', color: 'black' }, { type: 'R', color: 'black' }, ]; // Black pawns on rank 3 (index 2) for (let i = 0; i < 8; i++) { board[2][i] = { type: 'P', color: 'black' }; } return board; }; // Game logic const isValidPosition = (row, col) => row >= 0 && row < 8 && col >= 0 && col < 8; const getMoves = (board, row, col, piece, checkKingSafety = true) => { const moves = []; const { type, color } = piece; const direction = color === 'white' ? -1 : 1; const addMove = (r, c) => { if (isValidPosition(r, c)) { const target = board[r][c]; if (!target || target.color !== color) { if (checkKingSafety) { // Verify move doesn't leave king in check const testBoard = board.map(row => row.map(cell => cell ? { ...cell } : null)); testBoard[r][c] = testBoard[row][col]; testBoard[row][col] = null; if (!isInCheck(testBoard, color)) { moves.push({ row: r, col: c }); } } else { moves.push({ row: r, col: c }); } } } }; switch (type) { case 'K': // King - moves 1 square any direction for (let dr = -1; dr <= 1; dr++) { for (let dc = -1; dc <= 1; dc++) { if (dr !== 0 || dc !== 0) addMove(row + dr, col + dc); } } break; case 'M': // Met (Queen) - moves 1 square diagonally for (let dr = -1; dr <= 1; dr += 2) { for (let dc = -1; dc <= 1; dc += 2) { addMove(row + dr, col + dc); } } break; case 'R': // Rua (Rook) - moves like chess rook for (const [dr, dc] of [[0, 1], [0, -1], [1, 0], [-1, 0]]) { for (let i = 1; i < 8; i++) { const r = row + dr * i; const c = col + dc * i; if (!isValidPosition(r, c)) break; const target = board[r][c]; if (target) { if (target.color !== color) addMove(r, c); break; } addMove(r, c); } } break; case 'B': // Khon (Bishop) - moves 1 diagonal or 1 forward // Diagonal moves for (let dr = -1; dr <= 1; dr += 2) { for (let dc = -1; dc <= 1; dc += 2) { addMove(row + dr, col + dc); } } // Forward move addMove(row + direction, col); break; case 'N': // Ma (Knight) - moves like chess knight for (const [dr, dc] of [[-2, -1], [-2, 1], [-1, -2], [-1, 2], [1, -2], [1, 2], [2, -1], [2, 1]]) { addMove(row + dr, col + dc); } break; case 'P': // Bia (Pawn) - moves 1 forward, captures diagonal // Forward move const forwardRow = row + direction; if (isValidPosition(forwardRow, col) && !board[forwardRow][col]) { if (checkKingSafety) { const testBoard = board.map(row => row.map(cell => cell ? { ...cell } : null)); testBoard[forwardRow][col] = testBoard[row][col]; testBoard[row][col] = null; if (!isInCheck(testBoard, color)) { moves.push({ row: forwardRow, col }); } } else { moves.push({ row: forwardRow, col }); } } // Diagonal captures for (const dc of [-1, 1]) { const captureCol = col + dc; if (isValidPosition(forwardRow, captureCol)) { const target = board[forwardRow][captureCol]; if (target && target.color !== color) { if (checkKingSafety) { const testBoard = board.map(row => row.map(cell => cell ? { ...cell } : null)); testBoard[forwardRow][captureCol] = testBoard[row][col]; testBoard[row][col] = null; if (!isInCheck(testBoard, color)) { moves.push({ row: forwardRow, col: captureCol }); } } else { moves.push({ row: forwardRow, col: captureCol }); } } } } break; } return moves; }; const findKing = (board, color) => { for (let r = 0; r < 8; r++) { for (let c = 0; c < 8; c++) { const piece = board[r][c]; if (piece && piece.type === 'K' && piece.color === color) { return { row: r, col: c }; } } } return null; }; const isInCheck = (board, color) => { const kingPos = findKing(board, color); if (!kingPos) return false; const opponentColor = color === 'white' ? 'black' : 'white'; for (let r = 0; r < 8; r++) { for (let c = 0; c < 8; c++) { const piece = board[r][c]; if (piece && piece.color === opponentColor) { const moves = getMoves(board, r, c, piece, false); if (moves.some(m => m.row === kingPos.row && m.col === kingPos.col)) { return true; } } } } return false; }; const getAllMoves = (board, color) => { const allMoves = []; for (let r = 0; r < 8; r++) { for (let c = 0; c < 8; c++) { const piece = board[r][c]; if (piece && piece.color === color) { const moves = getMoves(board, r, c, piece, true); moves.forEach(move => { allMoves.push({ from: { row: r, col: c }, to: move }); }); } } } return allMoves; }; const isCheckmate = (board, color) => { if (!isInCheck(board, color)) return false; return getAllMoves(board, color).length === 0; }; const isStalemate = (board, color) => { if (isInCheck(board, color)) return false; return getAllMoves(board, color).length === 0; }; // AI logic const pieceValues = { K: 10000, M: 150, R: 500, B: 200, N: 300, P: 100 }; const evaluateBoard = (board, aiColor) => { let score = 0; const playerColor = aiColor === 'white' ? 'black' : 'white'; for (let r = 0; r < 8; r++) { for (let c = 0; c < 8; c++) { const piece = board[r][c]; if (piece) { const value = pieceValues[piece.type]; const positionBonus = getPositionBonus(piece, r, c); if (piece.color === aiColor) { score += value + positionBonus; } else { score -= value + positionBonus; } } } } // Mobility bonus const aiMoves = getAllMoves(board, aiColor).length; const playerMoves = getAllMoves(board, playerColor).length; score += (aiMoves - playerMoves) * 5; // Check bonus if (isInCheck(board, playerColor)) score += 50; if (isCheckmate(board, playerColor)) score += 100000; return score; }; const getPositionBonus = (piece, row, col) => { const centerBonus = (3.5 - Math.abs(3.5 - col)) * 5 + (3.5 - Math.abs(3.5 - row)) * 5; if (piece.type === 'P') { // Pawns want to advance const advancement = piece.color === 'white' ? (5 - row) : (row - 2); return advancement * 10 + centerBonus * 0.5; } if (piece.type === 'N' || piece.type === 'B') { return centerBonus; } if (piece.type === 'K') { // King safety - stay back early game const backRank = piece.color === 'white' ? 7 : 0; return Math.abs(row - backRank) < 2 ? 20 : -10; } return centerBonus * 0.3; }; const minimax = (board, depth, alpha, beta, isMaximizing, aiColor) => { const playerColor = aiColor === 'white' ? 'black' : 'white'; if (depth === 0) { return evaluateBoard(board, aiColor); } const currentColor = isMaximizing ? aiColor : playerColor; const moves = getAllMoves(board, currentColor); if (moves.length === 0) { if (isInCheck(board, currentColor)) { return isMaximizing ? -100000 + depth : 100000 - depth; } return 0; // Stalemate } if (isMaximizing) { let maxEval = -Infinity; for (const move of moves) { const newBoard = board.map(row => row.map(cell => cell ? { ...cell } : null)); const piece = newBoard[move.from.row][move.from.col]; newBoard[move.to.row][move.to.col] = piece; newBoard[move.from.row][move.from.col] = null; // Handle promotion if (piece.type === 'P') { const promotionRank = piece.color === 'white' ? 2 : 5; if (move.to.row === promotionRank) { newBoard[move.to.row][move.to.col] = { type: 'M', color: piece.color }; } } const evalScore = minimax(newBoard, depth - 1, alpha, beta, false, aiColor); maxEval = Math.max(maxEval, evalScore); alpha = Math.max(alpha, evalScore); if (beta <= alpha) break; } return maxEval; } else { let minEval = Infinity; for (const move of moves) { const newBoard = board.map(row => row.map(cell => cell ? { ...cell } : null)); const piece = newBoard[move.from.row][move.from.col]; newBoard[move.to.row][move.to.col] = piece; newBoard[move.from.row][move.from.col] = null; // Handle promotion if (piece.type === 'P') { const promotionRank = piece.color === 'white' ? 2 : 5; if (move.to.row === promotionRank) { newBoard[move.to.row][move.to.col] = { type: 'M', color: piece.color }; } } const evalScore = minimax(newBoard, depth - 1, alpha, beta, true, aiColor); minEval = Math.min(minEval, evalScore); beta = Math.min(beta, evalScore); if (beta <= alpha) break; } return minEval; } }; const getAIMove = (board, difficulty) => { const aiColor = 'black'; const moves = getAllMoves(board, aiColor); if (moves.length === 0) return null; const depths = { easy: 1, medium: 2, hard: 3, expert: 4 }; const depth = depths[difficulty] || 2; let bestMove = null; let bestScore = -Infinity; // Add some randomness for easier difficulties const randomFactor = { easy: 0.3, medium: 0.15, hard: 0.05, expert: 0 }[difficulty] || 0.1; for (const move of moves) { const newBoard = board.map(row => row.map(cell => cell ? { ...cell } : null)); const piece = newBoard[move.from.row][move.from.col]; newBoard[move.to.row][move.to.col] = piece; newBoard[move.from.row][move.from.col] = null; // Handle promotion if (piece.type === 'P') { const promotionRank = 5; if (move.to.row === promotionRank) { newBoard[move.to.row][move.to.col] = { type: 'M', color: piece.color }; } } let score = minimax(newBoard, depth - 1, -Infinity, Infinity, false, aiColor); score += (Math.random() - 0.5) * randomFactor * 100; if (score > bestScore) { bestScore = score; bestMove = move; } } return bestMove; }; // Main component export default function MakrukGame() { const [board, setBoard] = useState(createInitialBoard); const [selectedSquare, setSelectedSquare] = useState(null); const [validMoves, setValidMoves] = useState([]); const [currentPlayer, setCurrentPlayer] = useState('white'); const [gameStatus, setGameStatus] = useState('playing'); const [difficulty, setDifficulty] = useState('medium'); const [showRules, setShowRules] = useState(true); const [isAIThinking, setIsAIThinking] = useState(false); const sounds = useSounds(); const resetGame = () => { setBoard(createInitialBoard()); setSelectedSquare(null); setValidMoves([]); setCurrentPlayer('white'); setGameStatus('playing'); setIsAIThinking(false); }; const handleSquareClick = (row, col) => { if (gameStatus !== 'playing' || currentPlayer !== 'white' || isAIThinking) return; const piece = board[row][col]; if (selectedSquare) { const isValidMove = validMoves.some(m => m.row === row && m.col === col); if (isValidMove) { makeMove(selectedSquare.row, selectedSquare.col, row, col); } else if (piece && piece.color === 'white') { setSelectedSquare({ row, col }); setValidMoves(getMoves(board, row, col, piece, true)); } else { setSelectedSquare(null); setValidMoves([]); } } else if (piece && piece.color === 'white') { setSelectedSquare({ row, col }); setValidMoves(getMoves(board, row, col, piece, true)); } }; const makeMove = (fromRow, fromCol, toRow, toCol) => { const newBoard = board.map(row => row.map(cell => cell ? { ...cell } : null)); const piece = newBoard[fromRow][fromCol]; const capturedPiece = newBoard[toRow][toCol]; newBoard[toRow][toCol] = piece; newBoard[fromRow][fromCol] = null; // Handle pawn promotion if (piece.type === 'P') { const promotionRank = piece.color === 'white' ? 2 : 5; if (toRow === promotionRank) { newBoard[toRow][toCol] = { type: 'M', color: piece.color }; } } // Play sounds if (capturedPiece) { sounds.playCapture(); } else { sounds.playMove(); } setBoard(newBoard); setSelectedSquare(null); setValidMoves([]); const nextPlayer = currentPlayer === 'white' ? 'black' : 'white'; // Check game status if (isInCheck(newBoard, nextPlayer)) { sounds.playCheck(); if (isCheckmate(newBoard, nextPlayer)) { sounds.playGameOver(); setGameStatus(`${currentPlayer} wins!`); return; } } else if (isStalemate(newBoard, nextPlayer)) { sounds.playGameOver(); setGameStatus('Stalemate!'); return; } setCurrentPlayer(nextPlayer); }; // AI move useEffect(() => { if (currentPlayer === 'black' && gameStatus === 'playing') { setIsAIThinking(true); setTimeout(() => { const aiMove = getAIMove(board, difficulty); if (aiMove) { makeMove(aiMove.from.row, aiMove.from.col, aiMove.to.row, aiMove.to.col); } setIsAIThinking(false); }, 500); } }, [currentPlayer, gameStatus, board, difficulty]); const rulesContent = [ { piece: 'K', moves: 'Moves 1 square in any direction' }, { piece: 'M', moves: 'Moves 1 square diagonally only' }, { piece: 'R', moves: 'Moves any number of squares horizontally or vertically' }, { piece: 'B', moves: 'Moves 1 square diagonally or 1 square forward' }, { piece: 'N', moves: 'Moves in an L-shape (2+1 squares), can jump' }, { piece: 'P', moves: 'Moves 1 forward, captures diagonally. Promotes to Met on 6th rank' }, ]; return (
{/* Decorative header */}

หมากรุก

Makruk - Thai Chess

{/* Controls */}
{/* Rules Panel */} {showRules && (

Piece Movement Guide

{rulesContent.map(({ piece, moves }) => (
{PIECES[piece].symbol} {PIECES[piece].name}
{moves}
))}

Pawns start on the 3rd rank. No castling, no en passant, no 2-square pawn moves.

)} {/* Game Status */}
{gameStatus !== 'playing' ? gameStatus : ( isAIThinking ? 'AI is thinking...' : `${currentPlayer === 'white' ? 'Your' : "AI's"} turn` )} {isInCheck(board, currentPlayer) && gameStatus === 'playing' && ( Check! )}
{/* Game Board */}
{/* Decorative corner patterns */}
{board.map((row, rowIndex) => row.map((piece, colIndex) => { const isLight = (rowIndex + colIndex) % 2 === 0; const isSelected = selectedSquare?.row === rowIndex && selectedSquare?.col === colIndex; const isValidMove = validMoves.some(m => m.row === rowIndex && m.col === colIndex); const isCapture = isValidMove && piece; return (
handleSquareClick(rowIndex, colIndex)} className="w-16 h-16 md:w-20 md:h-20 flex flex-col items-center justify-center cursor-pointer relative transition-all duration-150" style={{ background: isSelected ? 'linear-gradient(180deg, #7a9f35 0%, #5a7f25 100%)' : isLight ? 'linear-gradient(180deg, #f5edd8 0%, #e8dcc4 100%)' : 'linear-gradient(180deg, #6b8e50 0%, #4a6b38 100%)', boxShadow: isSelected ? 'inset 0 0 10px rgba(0,0,0,0.3)' : 'none' }} > {/* Valid move indicator */} {isValidMove && !piece && (
)} {/* Capture indicator */} {isCapture && (
)} {/* Piece */} {piece && ( {PIECES[piece.type].symbol} )}
); }) )}
{/* Legend */}

You play as White (bottom) • AI plays as Red (top)

Click a piece to see valid moves, then click destination

{/* Decorative footer */}
{['◆', '◇', '◆', '◇', '◆'].map((char, i) => ( {char} ))}
); }