Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Tic-Tac-Toe AI</title> | |
| <style> | |
| :root { | |
| --primary: #0f172a; | |
| --secondary: #1e293b; | |
| --accent: #0ea5e9; | |
| --win: #10b981; | |
| --lose: #ef4444; | |
| --draw: #f59e0b; | |
| --text: #e2e8f0; | |
| --subtle: #64748b; | |
| --cell-size: min(25vw, 120px); | |
| --transition: all .3s cubic-bezier(.4,0,.2,1); | |
| } | |
| * { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Segoe UI', system-ui, sans-serif; } | |
| body { | |
| display: grid; | |
| place-content: center; | |
| min-height: 100vh; | |
| background: linear-gradient(135deg, var(--primary), var(--secondary)); | |
| color: var(--text); | |
| padding: 2rem; | |
| position: relative; | |
| } | |
| h1 { | |
| font-size: clamp(1.5rem, 4vw, 2.5rem); | |
| font-weight: 700; | |
| text-align: center; | |
| margin-bottom: 1rem; | |
| letter-spacing: .05em; | |
| } | |
| #status { | |
| text-align: center; | |
| font-size: 1.125rem; | |
| min-height: 1.5em; | |
| margin-bottom: 1.5rem; | |
| transition: var(--transition); | |
| } | |
| #board { | |
| display: grid; | |
| grid-template-columns: repeat(3, var(--cell-size)); | |
| grid-template-rows: repeat(3, var(--cell-size)); | |
| gap: 8px; | |
| margin: 0 auto 2rem; | |
| position: relative; | |
| } | |
| .cell { | |
| background: var(--secondary); | |
| border-radius: 12px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: calc(var(--cell-size) * .45); | |
| cursor: pointer; | |
| transition: var(--transition); | |
| user-select: none; | |
| } | |
| .cell:hover { background: #334155; } | |
| .cell.x { color: var(--accent); } | |
| .cell.o { color: var(--win); } | |
| .cell.disabled { pointer-events: none; } | |
| #score { | |
| display: flex; | |
| justify-content: space-evenly; | |
| gap: 1rem; | |
| margin-bottom: 1.5rem; | |
| font-size: 1rem; | |
| } | |
| .score-item { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: .25rem; | |
| color: var(--subtle); | |
| } | |
| .score-item span:last-child { | |
| font-size: 1.5rem; | |
| font-weight: 700; | |
| color: var(--text); | |
| } | |
| button { | |
| display: block; | |
| margin: 0 auto; | |
| padding: .75rem 2rem; | |
| border: none; | |
| border-radius: 999px; | |
| background: var(--accent); | |
| color: #fff; | |
| font-size: 1rem; | |
| cursor: pointer; | |
| transition: var(--transition); | |
| } | |
| button:hover { background: #0284c7; transform: translateY(-2px); } | |
| button:active { transform: translateY(0); } | |
| .win-line { | |
| position: absolute; | |
| background: var(--win); | |
| border-radius: 4px; | |
| transition: var(--transition); | |
| opacity: 0; | |
| } | |
| .win-line.show { opacity: 1; } | |
| .hf-link { | |
| position: fixed; | |
| bottom: 15px; | |
| left: 15px; | |
| background: rgba(14, 165, 233, 0.15); | |
| padding: 6px 12px; | |
| border-radius: 8px; | |
| font-size: 0.9rem; | |
| color: var(--accent); | |
| text-decoration: none; | |
| transition: var(--transition); | |
| } | |
| .hf-link:hover { | |
| background: var(--accent); | |
| color: #fff; | |
| } | |
| @media (max-width: 400px) { | |
| #board { | |
| grid-template-columns: repeat(3, 80px); | |
| grid-template-rows: repeat(3, 80px); | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <h1>Tic-Tac-Toe vs AI</h1> | |
| <div id="status">Your turn (X)</div> | |
| <div id="board"> | |
| <div class="cell" data-index="0"></div> | |
| <div class="cell" data-index="1"></div> | |
| <div class="cell" data-index="2"></div> | |
| <div class="cell" data-index="3"></div> | |
| <div class="cell" data-index="4"></div> | |
| <div class="cell" data-index="5"></div> | |
| <div class="cell" data-index="6"></div> | |
| <div class="cell" data-index="7"></div> | |
| <div class="cell" data-index="8"></div> | |
| <div class="win-line"></div> | |
| </div> | |
| <div id="score"> | |
| <div class="score-item"> | |
| <span>You</span> | |
| <span id="playerScore">0</span> | |
| </div> | |
| <div class="score-item"> | |
| <span>Draws</span> | |
| <span id="drawScore">0</span> | |
| </div> | |
| <div class="score-item"> | |
| <span>Bot</span> | |
| <span id="botScore">0</span> | |
| </div> | |
| </div> | |
| <button onclick="restartGame()">Restart Game</button> | |
| <!-- Hugging Face link --> | |
| <a href="https://huggingface.co/spaces/sudo-saidso/tic-tac-toe" target="_blank" class="hf-link"> | |
| 🤖 on HuggingFace | |
| </a> | |
| <script> | |
| const cells = document.querySelectorAll('.cell'); | |
| const statusEl = document.getElementById('status'); | |
| const winLine = document.querySelector('.win-line'); | |
| let board = Array(9).fill(null); | |
| let gameOver = false; | |
| let scores = { player: 0, draw: 0, bot: 0 }; | |
| const winPatterns = [ | |
| [0, 1, 2], [3, 4, 5], [6, 7, 8], | |
| [0, 3, 6], [1, 4, 7], [2, 5, 8], | |
| [0, 4, 8], [2, 4, 6] | |
| ]; | |
| cells.forEach(c => c.addEventListener('click', handlePlayerMove)); | |
| function handlePlayerMove(e) { | |
| const idx = e.target.dataset.index; | |
| if (board[idx] || gameOver) return; | |
| makeMove(idx, 'x'); | |
| if (!gameOver) { | |
| statusEl.textContent = 'Bot is thinking...'; | |
| setTimeout(() => makeMove(bestMove(), 'o'), 400); | |
| } | |
| } | |
| function makeMove(idx, player) { | |
| board[idx] = player; | |
| cells[idx].textContent = player === 'x' ? 'X' : 'O'; | |
| cells[idx].classList.add(player, 'disabled'); | |
| const winner = checkWinner(); | |
| if (winner) endGame(winner); | |
| else if (board.every(Boolean)) endGame('draw'); | |
| else statusEl.textContent = 'Your turn (X)'; | |
| } | |
| function checkWinner(b = board) { | |
| for (const [a, bIdx, c] of winPatterns) { | |
| if (b[a] && b[a] === b[bIdx] && b[a] === b[c]) return b[a]; | |
| } | |
| return null; | |
| } | |
| function endGame(result) { | |
| gameOver = true; | |
| cells.forEach(c => c.classList.add('disabled')); | |
| if (result === 'draw') { | |
| statusEl.textContent = 'It\'s a draw!'; | |
| scores.draw++; | |
| document.getElementById('drawScore').textContent = scores.draw; | |
| } else { | |
| if (result === 'x') { | |
| statusEl.textContent = 'You win!'; | |
| scores.player++; | |
| document.getElementById('playerScore').textContent = scores.player; | |
| } else { | |
| statusEl.textContent = 'Bot wins!'; | |
| scores.bot++; | |
| document.getElementById('botScore').textContent = scores.bot; | |
| } | |
| } | |
| } | |
| function bestMove() { | |
| return minimax(board, 'o').index; | |
| } | |
| function minimax(newBoard, player) { | |
| const availSpots = newBoard.map((v, i) => v === null ? i : null).filter(v => v !== null); | |
| if (checkWinner(newBoard) === 'x') return { score: -10 }; | |
| if (checkWinner(newBoard) === 'o') return { score: 10 }; | |
| if (availSpots.length === 0) return { score: 0 }; | |
| const moves = []; | |
| for (let i = 0; i < availSpots.length; i++) { | |
| const move = {}; | |
| move.index = availSpots[i]; | |
| newBoard[availSpots[i]] = player; | |
| move.score = (player === 'o') | |
| ? minimax(newBoard, 'x').score | |
| : minimax(newBoard, 'o').score; | |
| newBoard[availSpots[i]] = null; | |
| moves.push(move); | |
| } | |
| let bestMove; | |
| if (player === 'o') { | |
| let bestScore = -Infinity; | |
| for (let i = 0; i < moves.length; i++) { | |
| if (moves[i].score > bestScore) { | |
| bestScore = moves[i].score; | |
| bestMove = i; | |
| } | |
| } | |
| } else { | |
| let bestScore = Infinity; | |
| for (let i = 0; i < moves.length; i++) { | |
| if (moves[i].score < bestScore) { | |
| bestScore = moves[i].score; | |
| bestMove = i; | |
| } | |
| } | |
| } | |
| return moves[bestMove]; | |
| } | |
| function restartGame() { | |
| board = Array(9).fill(null); | |
| gameOver = false; | |
| cells.forEach(c => { | |
| c.textContent = ''; | |
| c.className = 'cell'; | |
| }); | |
| winLine.className = 'win-line'; | |
| statusEl.textContent = 'Your turn (X)'; | |
| } | |
| </script> | |
| </body> | |
| </html> | |