Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>GO - The Ancient Board Game</title> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| :root { | |
| --wood-dark: #5d4037; | |
| --wood-light: #8d6e63; | |
| --wood-surface: #a1887f; | |
| --board-border: #3e2723; | |
| --black-stone: #1a1a1a; | |
| --white-stone: #f5f5f5; | |
| --highlight: #ffd54f; | |
| --shadow: rgba(0, 0, 0, 0.3); | |
| } | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); | |
| min-height: 100vh; | |
| color: #fff; | |
| overflow-x: hidden; | |
| } | |
| /* Header */ | |
| .header { | |
| background: linear-gradient(90deg, var(--board-border) 0%, #2c1810 100%); | |
| padding: 1rem 2rem; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4); | |
| position: relative; | |
| z-index: 10; | |
| } | |
| .logo { | |
| display: flex; | |
| align-items: center; | |
| gap: 1rem; | |
| } | |
| .logo-icon { | |
| width: 50px; | |
| height: 50px; | |
| background: linear-gradient(145deg, #fff, #e0e0e0); | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 1.5rem; | |
| color: var(--board-border); | |
| font-weight: bold; | |
| box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3); | |
| } | |
| .logo-text { | |
| font-size: 2rem; | |
| font-weight: 700; | |
| background: linear-gradient(90deg, #ffd700, #ffecb3); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); | |
| } | |
| .header-links { | |
| display: flex; | |
| gap: 1.5rem; | |
| align-items: center; | |
| } | |
| .header-link { | |
| color: #ffd700; | |
| text-decoration: none; | |
| font-weight: 500; | |
| padding: 0.5rem 1rem; | |
| border-radius: 8px; | |
| transition: all 0.3s ease; | |
| border: 1px solid transparent; | |
| } | |
| .header-link:hover { | |
| background: rgba(255, 215, 0, 0.1); | |
| border-color: #ffd700; | |
| } | |
| /* Main Container */ | |
| .main-container { | |
| display: flex; | |
| justify-content: center; | |
| align-items: flex-start; | |
| padding: 2rem; | |
| gap: 2rem; | |
| flex-wrap: wrap; | |
| } | |
| /* Game Area */ | |
| .game-area { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| } | |
| .game-info { | |
| background: rgba(255, 255, 255, 0.1); | |
| backdrop-filter: blur(10px); | |
| border-radius: 16px; | |
| padding: 1.5rem 2rem; | |
| margin-bottom: 1.5rem; | |
| display: flex; | |
| gap: 2rem; | |
| align-items: center; | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| } | |
| .current-player { | |
| display: flex; | |
| align-items: center; | |
| gap: 1rem; | |
| } | |
| .player-indicator { | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 50%; | |
| box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); | |
| transition: all 0.3s ease; | |
| } | |
| .player-indicator.black { | |
| background: radial-gradient(circle at 30% 30%, #444, #000); | |
| } | |
| .player-indicator.white { | |
| background: radial-gradient(circle at 30% 30%, #fff, #ccc); | |
| } | |
| .player-label { | |
| font-size: 1.1rem; | |
| font-weight: 600; | |
| } | |
| .turn-indicator { | |
| animation: pulse 1.5s infinite; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { transform: scale(1); } | |
| 50% { transform: scale(1.1); } | |
| } | |
| /* Board */ | |
| .board-container { | |
| position: relative; | |
| padding: 2rem; | |
| background: linear-gradient(145deg, var(--wood-dark), var(--wood-light)); | |
| border-radius: 12px; | |
| box-shadow: | |
| 0 20px 60px rgba(0, 0, 0, 0.5), | |
| inset 0 2px 10px rgba(255, 255, 255, 0.1); | |
| border: 8px solid var(--board-border); | |
| } | |
| .board { | |
| position: relative; | |
| background: linear-gradient(145deg, #deb887, #d2a679); | |
| border-radius: 4px; | |
| box-shadow: inset 0 0 30px rgba(0, 0, 0, 0.2); | |
| } | |
| .grid-lines { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; | |
| } | |
| .grid-line { | |
| position: absolute; | |
| background: #4a3728; | |
| } | |
| .grid-line.horizontal { | |
| width: calc(100% - 2px); | |
| height: 1px; | |
| left: 1px; | |
| } | |
| .grid-line.vertical { | |
| width: 1px; | |
| height: calc(100% - 2px); | |
| top: 1px; | |
| } | |
| /* Star Points */ | |
| .star-point { | |
| position: absolute; | |
| width: 10px; | |
| height: 10px; | |
| background: #4a3728; | |
| border-radius: 50%; | |
| transform: translate(-50%, -50%); | |
| } | |
| /* Stones */ | |
| .stones-container { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| } | |
| .stone { | |
| position: absolute; | |
| width: 92%; | |
| height: 92%; | |
| border-radius: 50%; | |
| transform: translate(-50%, -50%); | |
| cursor: pointer; | |
| transition: transform 0.2s ease; | |
| box-shadow: | |
| 2px 4px 8px rgba(0, 0, 0, 0.4), | |
| inset 2px 4px 8px rgba(255, 255, 255, 0.2), | |
| inset -2px -4px 8px rgba(0, 0, 0, 0.3); | |
| } | |
| .stone:hover { | |
| transform: translate(-50%, -50%) scale(1.05); | |
| } | |
| .stone.black { | |
| background: radial-gradient(circle at 30% 30%, #555, #1a1a1a); | |
| } | |
| .stone.white { | |
| background: radial-gradient(circle at 30% 30%, #fff, #ccc); | |
| } | |
| .stone.last-move { | |
| box-shadow: | |
| 2px 4px 8px rgba(0, 0, 0, 0.4), | |
| inset 2px 4px 8px rgba(255, 255, 255, 0.2), | |
| inset -2px -4px 8px rgba(0, 0, 0, 0.3), | |
| 0 0 15px #ffd700; | |
| } | |
| /* Ghost Stone */ | |
| .ghost-stone { | |
| position: absolute; | |
| width: 92%; | |
| height: 92%; | |
| border-radius: 50%; | |
| transform: translate(-50%, -50%); | |
| pointer-events: none; | |
| opacity: 0; | |
| transition: opacity 0.2s ease; | |
| box-shadow: 0 0 20px rgba(255, 255, 255, 0.5); | |
| } | |
| .ghost-stone.visible { | |
| opacity: 0.5; | |
| } | |
| .ghost-stone.black { | |
| background: radial-gradient(circle at 30% 30%, #555, #1a1a1a); | |
| } | |
| .ghost-stone.white { | |
| background: radial-gradient(circle at 30% 30%, #fff, #ccc); | |
| } | |
| /* Capture Display */ | |
| .captures { | |
| display: flex; | |
| gap: 2rem; | |
| background: rgba(0, 0, 0, 0.2); | |
| padding: 1rem 1.5rem; | |
| border-radius: 12px; | |
| } | |
| .capture-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .capture-stone { | |
| width: 24px; | |
| height: 24px; | |
| border-radius: 50%; | |
| } | |
| .capture-stone.black { | |
| background: radial-gradient(circle at 30% 30%, #555, #1a1a1a); | |
| } | |
| .capture-stone.white { | |
| background: radial-gradient(circle at 30% 30%, #fff, #ccc); | |
| border: 1px solid #ccc; | |
| } | |
| /* Sidebar */ | |
| .sidebar { | |
| width: 320px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 1.5rem; | |
| } | |
| .panel { | |
| background: rgba(255, 255, 255, 0.1); | |
| backdrop-filter: blur(10px); | |
| border-radius: 16px; | |
| padding: 1.5rem; | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| } | |
| .panel-title { | |
| font-size: 1.2rem; | |
| font-weight: 600; | |
| margin-bottom: 1rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| color: #ffd700; | |
| } | |
| /* Game Controls */ | |
| .controls { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.75rem; | |
| } | |
| .btn { | |
| padding: 0.875rem 1.5rem; | |
| border: none; | |
| border-radius: 10px; | |
| font-size: 1rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 0.5rem; | |
| } | |
| .btn-primary { | |
| background: linear-gradient(135deg, #ffd700, #ffb300); | |
| color: #1a1a2e; | |
| box-shadow: 0 4px 15px rgba(255, 215, 0, 0.3); | |
| } | |
| .btn-primary:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 6px 20px rgba(255, 215, 0, 0.4); | |
| } | |
| .btn-secondary { | |
| background: rgba(255, 255, 255, 0.1); | |
| color: #fff; | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| } | |
| .btn-secondary:hover { | |
| background: rgba(255, 255, 255, 0.2); | |
| } | |
| /* Move History */ | |
| .move-history { | |
| max-height: 200px; | |
| overflow-y: auto; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.25rem; | |
| } | |
| .move-history::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| .move-history::-webkit-scrollbar-track { | |
| background: rgba(255, 255, 255, 0.1); | |
| border-radius: 3px; | |
| } | |
| .move-history::-webkit-scrollbar-thumb { | |
| background: rgba(255, 215, 0, 0.5); | |
| border-radius: 3px; | |
| } | |
| .move-entry { | |
| padding: 0.5rem 0.75rem; | |
| background: rgba(0, 0, 0, 0.2); | |
| border-radius: 6px; | |
| font-size: 0.9rem; | |
| display: flex; | |
| justify-content: space-between; | |
| transition: background 0.2s ease; | |
| } | |
| .move-entry:hover { | |
| background: rgba(0, 0, 0, 0.3); | |
| } | |
| .move-number { | |
| color: #ffd700; | |
| font-weight: 600; | |
| } | |
| /* Game Rules */ | |
| .rules-list { | |
| list-style: none; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 0.75rem; | |
| } | |
| .rules-list li { | |
| display: flex; | |
| align-items: flex-start; | |
| gap: 0.75rem; | |
| font-size: 0.95rem; | |
| line-height: 1.4; | |
| } | |
| .rules-list i { | |
| color: #ffd700; | |
| margin-top: 2px; | |
| } | |
| /* Modal */ | |
| .modal-overlay { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(0, 0, 0, 0.7); | |
| display: none; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 1000; | |
| backdrop-filter: blur(5px); | |
| } | |
| .modal-overlay.active { | |
| display: flex; | |
| } | |
| .modal { | |
| background: linear-gradient(145deg, #2c1810, #1a1a2e); | |
| border-radius: 20px; | |
| padding: 2.5rem; | |
| text-align: center; | |
| max-width: 400px; | |
| border: 2px solid #ffd700; | |
| box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5); | |
| animation: modalSlide 0.3s ease; | |
| } | |
| @keyframes modalSlide { | |
| from { | |
| transform: translateY(-50px); | |
| opacity: 0; | |
| } | |
| to { | |
| transform: translateY(0); | |
| opacity: 1; | |
| } | |
| } | |
| .modal h2 { | |
| font-size: 2rem; | |
| margin-bottom: 1rem; | |
| color: #ffd700; | |
| } | |
| .modal p { | |
| margin-bottom: 1.5rem; | |
| font-size: 1.1rem; | |
| line-height: 1.6; | |
| } | |
| .modal .btn-group { | |
| display: flex; | |
| gap: 1rem; | |
| justify-content: center; | |
| } | |
| /* Responsive */ | |
| @media (max-width: 900px) { | |
| .main-container { | |
| flex-direction: column; | |
| align-items: center; | |
| } | |
| .sidebar { | |
| width: 100%; | |
| max-width: 500px; | |
| flex-direction: row; | |
| flex-wrap: wrap; | |
| } | |
| .sidebar .panel { | |
| flex: 1; | |
| min-width: 200px; | |
| } | |
| } | |
| @media (max-width: 600px) { | |
| .header { | |
| flex-direction: column; | |
| gap: 1rem; | |
| } | |
| .header-links { | |
| flex-wrap: wrap; | |
| justify-content: center; | |
| } | |
| .game-info { | |
| flex-direction: column; | |
| gap: 1rem; | |
| } | |
| .sidebar { | |
| flex-direction: column; | |
| } | |
| } | |
| /* Animations */ | |
| @keyframes stonePlace { | |
| 0% { | |
| transform: translate(-50%, -50%) scale(0); | |
| opacity: 0; | |
| } | |
| 50% { | |
| transform: translate(-50%, -50%) scale(1.2); | |
| } | |
| 100% { | |
| transform: translate(-50%, -50%) scale(1); | |
| opacity: 1; | |
| } | |
| } | |
| .stone.new { | |
| animation: stonePlace 0.3s ease forwards; | |
| } | |
| @keyframes captureEffect { | |
| 0% { | |
| transform: translate(-50%, -50%) scale(1); | |
| opacity: 1; | |
| } | |
| 100% { | |
| transform: translate(-50%, -50%) scale(0); | |
| opacity: 0; | |
| } | |
| } | |
| .stone.captured { | |
| animation: captureEffect 0.5s ease forwards; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Header --> | |
| <header class="header"> | |
| <div class="logo"> | |
| <div class="logo-icon">囲</div> | |
| <span class="logo-text">GO</span> | |
| </div> | |
| <nav class="header-links"> | |
| <a href="#" class="header-link">Rules</a> | |
| <a href="#" class="header-link">History</a> | |
| <a href="#" class="header-link" onclick="openAbout()">About</a> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" class="header-link" target="_blank">Built with anycoder</a> | |
| </nav> | |
| </header> | |
| <!-- Main Container --> | |
| <main class="main-container"> | |
| <!-- Game Area --> | |
| <div class="game-area"> | |
| <div class="game-info"> | |
| <div class="current-player"> | |
| <div class="player-indicator black" id="playerIndicator"></div> | |
| <div class="player-label"> | |
| <span id="playerLabel">Black's Turn</span> | |
| </div> | |
| </div> | |
| <div class="captures"> | |
| <div class="capture-item"> | |
| <div class="capture-stone white"></div> | |
| <span id="whiteCaptures">0</span> | |
| </div> | |
| <div class="capture-item"> | |
| <div class="capture-stone black"></div> | |
| <span id="blackCaptures">0</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="board-container"> | |
| <div class="board" id="board"> | |
| <div class="grid-lines" id="gridLines"></div> | |
| <div class="star-points" id="starPoints"></div> | |
| <div class="stones-container" id="stonesContainer"> | |
| <div class="ghost-stone black" id="ghostStone"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Sidebar --> | |
| <aside class="sidebar"> | |
| <div class="panel"> | |
| <h3 class="panel-title"> | |
| <i class="fas fa-gamepad"></i> | |
| Game Controls | |
| </h3> | |
| <div class="controls"> | |
| <button class="btn btn-primary" onclick="newGame()"> | |
| <i class="fas fa-plus"></i> | |
| New Game | |
| </button> | |
| <button class="btn btn-secondary" onclick="passTurn()"> | |
| <i class="fas fa-hand"></i> | |
| Pass | |
| </button> | |
| <button class="btn btn-secondary" onclick="undoMove()"> | |
| <i class="fas fa-undo"></i> | |
| Undo | |
| </button> | |
| </div> | |
| </div> | |
| <div class="panel"> | |
| <h3 class="panel-title"> | |
| <i class="fas fa-history"></i> | |
| Move History | |
| </h3> | |
| <div class="move-history" id="moveHistory"> | |
| <div style="color: rgba(255,255,255,0.5); text-align: center;">No moves yet</div> | |
| </div> | |
| </div> | |
| <div class="panel"> | |
| <h3 class="panel-title"> | |
| <i class="fas fa-book"></i> | |
| How to Play | |
| </h3> | |
| <ul class="rules-list"> | |
| <li> | |
| <i class="fas fa-circle"></i> | |
| Players take turns placing stones on grid intersections | |
| </li> | |
| <li> | |
| <i class="fas fa-circle"></i> | |
| Capture stones by surrounding them (removing all liberties) | |
| </li> | |
| <li> | |
| <i class="fas fa-circle"></i> | |
| The goal is to control more territory than your opponent | |
| </li> | |
| <li> | |
| <i class="fas fa-circle"></i> | |
| Two consecutive passes end the game | |
| </li> | |
| </ul> | |
| </div> | |
| </aside> | |
| </main> | |
| <!-- Game Over Modal --> | |
| <div class="modal-overlay" id="gameOverModal"> | |
| <div class="modal"> | |
| <h2><i class="fas fa-trophy"></i> Game Over!</h2> | |
| <p id="gameResult"></p> | |
| <div class="btn-group"> | |
| <button class="btn btn-secondary" onclick="closeModal()">Close</button> | |
| <button class="btn btn-primary" onclick="newGame(); closeModal();">New Game</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- About Modal --> | |
| <div class="modal-overlay" id="aboutModal"> | |
| <div class="modal"> | |
| <h2><i class="fas fa-info-circle"></i> About Go</h2> | |
| <p>Go (囲碁) is an ancient board game originating in China over 2,500 years ago. It's considered one of the most complex and intellectually demanding strategy games in the world.</p> | |
| <p>The game is played on a 19×19 grid where two players take turns placing black and white stones. The objective is to control more territory than your opponent by surrounding empty points and capturing enemy stones.</p> | |
| <div class="btn-group"> | |
| <button class="btn btn-primary" onclick="closeAbout()">Got it!</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Game Configuration | |
| const BOARD_SIZE = 19; | |
| const CELL_SIZE = 30; | |
| const BOARD_PIXELS = (BOARD_SIZE - 1) * CELL_SIZE + 1; | |
| // Game State | |
| let board = []; | |
| let currentPlayer = 'black'; | |
| let moveHistory = []; | |
| let captures = { black: 0, white: 0 }; | |
| let passCount = 0; | |
| let gameOver = false; | |
| let lastMove = null; | |
| // DOM Elements | |
| const boardElement = document.getElementById('board'); | |
| const gridLinesElement = document.getElementById('gridLines'); | |
| const starPointsElement = document.getElementById('starPoints'); | |
| const stonesContainer = document.getElementById('stonesContainer'); | |
| const ghostStone = document.getElementById('ghostStone'); | |
| const playerIndicator = document.getElementById('playerIndicator'); | |
| const playerLabel = document.getElementById('playerLabel'); | |
| const moveHistoryElement = document.getElementById('moveHistory'); | |
| const whiteCapturesElement = document.getElementById('whiteCaptures'); | |
| const blackCapturesElement = document.getElementById('blackCaptures'); | |
| const gameOverModal = document.getElementById('gameOverModal'); | |
| const aboutModal = document.getElementById('aboutModal'); | |
| // Initialize Board | |
| function initBoard() { | |
| // Set board dimensions | |
| boardElement.style.width = BOARD_PIXELS + 'px'; | |
| boardElement.style.height = BOARD_PIXELS + 'px'; | |
| // Clear previous content | |
| gridLinesElement.innerHTML = ''; | |
| starPointsElement.innerHTML = ''; | |
| // Create grid lines | |
| for (let i = 0; i < BOARD_SIZE; i++) { | |
| // Horizontal lines | |
| const hLine = document.createElement('div'); | |
| hLine.className = 'grid-line horizontal'; | |
| hLine.style.top = (i * CELL_SIZE) + 'px'; | |
| gridLinesElement.appendChild(hLine); | |
| // Vertical lines | |
| const vLine = document.createElement('div'); | |
| vLine.className = 'grid-line vertical'; | |
| vLine.style.left = (i * CELL_SIZE) + 'px'; | |
| gridLinesElement.appendChild(vLine); | |
| } | |
| // Create star points (hoshi) - standard 19x19 board has 9 star points | |
| const starPositions = [3, 9, 15]; | |
| for (let row of starPositions) { | |
| for (let col of starPositions) { | |
| const starPoint = document.createElement('div'); | |
| starPoint.className = 'star-point'; | |
| starPoint.style.left = (col * CELL_SIZE) + 'px'; | |
| starPoint.style.top = (row * CELL_SIZE) + 'px'; | |
| starPointsElement.appendChild(starPoint); | |
| } | |
| } | |
| // Initialize internal board state | |
| resetGame(); | |
| } | |
| // Reset Game | |
| function resetGame() { | |
| board = Array(BOARD_SIZE).fill(null).map(() => Array(BOARD_SIZE).fill(null)); | |
| currentPlayer = 'black'; | |
| moveHistory = []; | |
| captures = { black: 0, white: 0 }; | |
| passCount = 0; | |
| gameOver = false; | |
| lastMove = null; | |
| // Clear stones (keep ghost stone) | |
| const stones = stonesContainer.querySelectorAll('.stone'); | |
| stones.forEach(stone => stone.remove()); | |
| updateUI(); | |
| updateMoveHistory(); | |
| } | |
| // Get Coordinates from Mouse Event | |
| function getCoordinates(e) { | |
| const rect = boardElement.getBoundingClientRect(); | |
| const x = e.clientX - rect.left; | |
| const y = e.clientY - rect.top; | |
| const col = Math.round(x / CELL_SIZE); | |
| const row = Math.round(y / CELL_SIZE); | |
| if (col >= 0 && col < BOARD_SIZE && row >= 0 && row < BOARD_SIZE) { | |
| return { row, col }; | |
| } | |
| return null; | |
| } | |
| // Place Stone | |
| function placeStone(row, col) { | |
| if (gameOver || board[row][col] !== null) return false; | |
| const newBoard = board.map(r => [...r]); | |
| newBoard[row][col] = currentPlayer; | |
| // Check for captures | |
| const opponent = currentPlayer === 'black' ? 'white' : 'black'; | |
| const capturedStones = []; | |
| // Check all four directions for opponent stones | |
| const directions = [[-1, 0], [1, 0], [0, -1], [0, 1]]; | |
| for (const [dr, dc] of directions) { | |
| const nr = row + dr; | |
| const nc = col + dc; | |
| if (nr >= 0 && nr < BOARD_SIZE && nc >= 0 && nc < BOARD_SIZE && newBoard[nr][nc] === opponent) { | |
| const group = getGroup(newBoard, nr, nc); | |
| if (getLiberties(newBoard, group) === 0 && !capturedStones.some(s => s.row === nr && s.col === nc)) { | |
| capturedStones.push(...group); | |
| } | |
| } | |
| } | |
| // Remove captured stones | |
| for (const stone of capturedStones) { | |
| newBoard[stone.row][stone.col] = null; | |
| } | |
| // Check for suicide (placing stone where it has no liberties after capture) | |
| const group = getGroup(newBoard, row, col); | |
| if (getLiberties(newBoard, group) === 0 && capturedStones.length === 0) { | |
| return false; | |
| } | |
| // Check for ko (simple version - can't recapture immediately) | |
| if (moveHistory.length > 0) { | |
| const lastState = moveHistory[moveHistory.length - 1].boardState; | |
| if (JSON.stringify(newBoard) === JSON.stringify(lastState)) { | |
| return false; | |
| } | |
| } | |
| // Valid move - update game state | |
| board = newBoard; | |
| captures[currentPlayer] += capturedStones.length; | |
| // Remove captured stones from DOM | |
| for (const stone of capturedStones) { | |
| const stoneElement = stonesContainer.querySelector(`[data-row="${stone.row}"][data-col="${stone.col}"]`); | |
| if (stoneElement) { | |
| stoneElement.classList.add('captured'); | |
| setTimeout(() => stoneElement.remove(), 500); | |
| } | |
| } | |
| // Add new stone | |
| addStoneVisual(row, col, currentPlayer); | |
| // Update last move indicator | |
| const prevLastMove = lastMove; | |
| lastMove = { row, col, player: currentPlayer }; | |
| updateLastMoveIndicator(prevLastMove); | |
| // Save move to history | |
| moveHistory.push({ | |
| boardState: board.map(r => [...r]), | |
| player: currentPlayer, | |
| position: { row, col }, | |
| captures: { ...captures } | |
| }); | |
| // Switch player | |
| currentPlayer = opponent; | |
| passCount = 0; | |
| updateUI(); | |
| updateMoveHistory(); | |
| return true; | |
| } | |
| // Get group of connected stones | |
| function getGroup(boardState, row, col) { | |
| const color = boardState[row][col]; | |
| if (!color) return []; | |
| const group = []; | |
| const visited = new Set(); | |
| const stack = [{ row, col }]; | |
| while (stack.length > 0) { | |
| const { row: r, col: c } = stack.pop(); | |
| const key = `${r},${c}`; | |
| if (visited.has(key)) continue; | |
| if (boardState[r][c] !== color) continue; | |
| visited.add(key); | |
| group.push({ row: r, col: c }); | |
| const directions = [[-1, 0], [1, 0], [0, -1], [0, 1]]; | |
| for (const [dr, dc] of directions) { | |
| const nr = r + dr; | |
| const nc = c + dc; | |
| if (nr >= 0 && nr < BOARD_SIZE && nc >= 0 && nc < BOARD_SIZE) { | |
| stack.push({ row: nr, col: nc }); | |
| } | |
| } | |
| } | |
| return group; | |
| } | |
| // Count liberties of a group | |
| function getLiberties(boardState, group) { | |
| const liberties = new Set(); | |
| for (const { row, col } of group) { | |
| const directions = [[-1, 0], [1, 0], [0, -1], [0, 1]]; | |
| for (const [dr, dc] of directions) { | |
| const nr = row + dr; | |
| const nc = col + dc; | |
| if (nr >= 0 && nr < BOARD_SIZE && nc >= 0 && nc < BOARD_SIZE) { | |
| if (boardState[nr][nc] === null) { | |
| liberties.add(`${nr},${nc}`); | |
| } | |
| } | |
| } | |
| } | |
| return liberties.size; | |
| } | |
| // Add Stone Visual | |
| function addStoneVisual(row, col, color) { | |
| const stone = document.createElement('div'); | |
| stone.className = `stone ${color} new`; | |
| stone.dataset.row = row; | |
| stone.dataset.col = col; | |
| stone.style.left = (col * CELL_SIZE) + 'px'; | |
| stone.style.top = (row * CELL_SIZE) + 'px'; | |
| stonesContainer.appendChild(stone); | |
| setTimeout(() => stone.classList.remove('new'), 300); | |
| } | |
| // Update Last Move Indicator | |
| function updateLastMoveIndicator(prevMove) { | |
| // Remove previous indicator | |
| const prevIndicator = stonesContainer.querySelector('.last-move'); | |
| if (prevIndicator) prevIndicator.classList.remove('last-move'); | |
| // Add new indicator | |
| if (lastMove) { | |
| const stone = stonesContainer.querySelector(`[data-row="${lastMove.row}"][data-col="${lastMove.col}"]`); | |
| if (stone) stone.classList.add('last-move'); | |
| } | |
| } | |
| // Update UI | |
| function updateUI() { | |
| // Update player indicator | |
| playerIndicator.className = `player-indicator ${currentPlayer} turn-indicator`; | |
| playerLabel.textContent = `${currentPlayer.charAt(0).toUpperCase() + currentPlayer.slice(1)}'s Turn`; | |
| // Update captures | |
| whiteCapturesElement.textContent = captures.white; | |
| blackCapturesElement.textContent = captures.black; | |
| // Update ghost stone | |
| ghostStone.className = `ghost-stone ${currentPlayer}`; | |
| } | |
| // Update Move History Display | |
| function updateMoveHistory() { | |
| if (moveHistory.length === 0) { | |
| moveHistoryElement.innerHTML = '<div style="color: rgba(255,255,255,0.5); text-align: center;">No moves yet</div>'; | |
| return; | |
| } | |
| const columns = 2; | |
| const movesPerColumn = Math.ceil(moveHistory.length / columns); | |
| let html = '<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.5rem;">'; | |
| for (let i = 0; i < moveHistory.length; i++) { | |
| const move = moveHistory[i]; | |
| const col = Math.floor(i / movesPerColumn) + 1; | |
| html += ` | |
| <div class="move-entry"> | |
| <span class="move-number">${i + 1}.</span> | |
| <span>${move.player.charAt(0).toUpperCase()} @ ${String.fromCharCode(65 + move.position.col)}${move.position.row + 1}</span> | |
| </div> | |
| `; | |
| } | |
| html += '</div>'; | |
| moveHistoryElement.innerHTML = html; | |
| } | |
| // Pass Turn | |
| function passTurn() { | |
| if (gameOver) return; | |
| passCount++; | |
| if (passCount >= 2) { | |
| endGame(); | |
| return; | |
| } | |
| moveHistory.push({ | |
| boardState: board.map(r => [...r]), | |
| player: currentPlayer, | |
| position: null, | |
| captures: { ...captures }, | |
| passed: true | |
| }); | |
| currentPlayer = currentPlayer === 'black' ? 'white' : 'black'; | |
| passCount++; | |
| lastMove = null; | |
| updateUI(); | |
| updateMoveHistory(); | |
| } | |
| // Undo Move | |
| function undoMove() { | |
| if (moveHistory.length === 0 || gameOver) return; | |
| moveHistory.pop(); | |
| if (moveHistory.length === 0) { | |
| resetGame(); | |
| return; | |
| } | |
| const lastState = moveHistory[moveHistory.length - 1]; | |
| board = lastState.boardState.map(r => [...r]); | |
| captures = { ...lastState.captures }; | |
| currentPlayer = lastState.player; | |
| passCount = 0; | |
| // Redraw all stones | |
| const stones = stonesContainer.querySelectorAll('.stone:not(#ghostStone)'); | |
| stones.forEach(stone => stone.remove()); | |
| for (let row = 0; row < BOARD_SIZE; row++) { | |
| for (let col = 0; col < BOARD_SIZE; col++) { | |
| if (board[row][col]) { | |
| addStoneVisual(row, col, board[row][col]); | |
| } | |
| } | |
| } | |
| lastMove = moveHistory.length > 0 ? moveHistory[moveHistory.length - 1] : null; | |
| updateLastMoveIndicator(null); | |
| updateUI(); | |
| updateMoveHistory(); | |
| } | |
| // End Game | |
| function endGame() { | |
| gameOver = true; | |
| // Simple scoring: territory + captures | |
| const blackScore = calculateTerritory('black') + captures.black; | |
| const whiteScore = calculateTerritory('white') + captures.white + 6.5; // Komi (handicap compensation) | |
| const winner = blackScore > whiteScore ? 'Black' : 'White'; | |
| const margin = Math.abs(blackScore - whiteScore).toFixed(1); | |
| document.getElementById('gameResult').innerHTML = ` | |
| <strong>${winner}</strong> wins by <strong>${margin} points</strong>!<br><br> | |
| <small>Black: ${blackScore.toFixed(1)} | White: ${whiteScore.toFixed(1)} (includes 6.5 komi)</small> | |
| `; | |
| gameOverModal.classList.add('active'); | |
| } | |
| // Calculate Territory | |
| function calculateTerritory(player) { | |
| const visited = new Set(); | |
| let territory = 0; | |
| for (let row = 0; row < BOARD_SIZE; row++) { | |
| for (let col = 0; col < BOARD_SIZE; col++) { | |
| if (board[row][col] === null && !visited.has(`${row},${col}`)) { | |
| const region = getEmptyRegion(row, col); | |
| const owners = new Set(); | |
| for (const { r, c } of region) { | |
| const directions = [[-1, 0], [1, 0], [0, -1], [0, 1]]; | |
| for (const [dr, dc] of directions) { | |
| const nr = r + dr; | |
| const nc = c + dc; | |
| if (nr >= 0 && nr < BOARD_SIZE && nc >= 0 && nc < BOARD_SIZE && board[nr][nc]) { | |
| owners.add(board[nr][nc]); | |
| } | |
| } | |
| } | |
| if (owners.size === 1 && owners.has(player)) { | |
| territory += region.length; | |
| } | |
| region.forEach(p => visited.add(`${p.r},${p.c}`)); | |
| } | |
| } | |
| } | |
| return territory; | |
| } | |
| // Get empty region | |
| function getEmptyRegion(row, col) { | |
| const region = []; | |
| const visited = new Set(); | |
| const stack = [{ r: row, c: col }]; | |
| while (stack.length > 0) { | |
| const { r, c } = stack.pop(); | |
| const key = `${r},${c}`; | |
| if (visited.has(key)) continue; | |
| if (board[r][c] !== null) continue; | |
| visited.add(key); | |
| region.push({ r, c }); | |
| const directions = [[-1, 0], [1, 0], [0, -1], [0, 1]]; | |
| for (const [dr, dc] of directions) { | |
| const nr = r + dr; | |
| const nc = c + dc; | |
| if (nr >= 0 && nr < BOARD_SIZE && nc >= 0 && nc < BOARD_SIZE) { | |
| stack.push({ r: nr, c: nc }); | |
| } | |
| } | |
| } | |
| return region; | |
| } | |
| // New Game | |
| function newGame() { | |
| resetGame(); | |
| } | |
| // Modal functions | |
| function closeModal() { | |
| gameOverModal.classList.remove('active'); | |
| } | |
| function openAbout() { | |
| aboutModal.classList.add('active'); | |
| } | |
| function closeAbout() { | |
| aboutModal.classList.remove('active'); | |
| } | |
| // Event Listeners | |
| boardElement.addEventListener('mousemove', (e) => { | |
| const coords = getCoordinates(e); | |
| if (coords && !gameOver) { | |
| ghostStone.style.left = (coords.col * CELL_SIZE) + 'px'; | |
| ghostStone.style.top = (coords.row * CELL_SIZE) + 'px'; | |
| ghostStone.classList.add('visible'); | |
| } else { | |
| ghostStone.classList.remove('visible'); | |
| } | |
| }); | |
| boardElement.addEventListener('mouseleave', () => { | |
| ghostStone.classList.remove('visible'); | |
| }); | |
| boardElement.addEventListener('click', (e) => { | |
| const coords = getCoordinates(e); | |
| if (coords) { | |
| placeStone(coords.row, coords.col); | |
| } | |
| }); | |
| // Close modals on outside click | |
| gameOverModal.addEventListener('click', (e) => { | |
| if (e.target === gameOverModal) closeModal(); | |
| }); | |
| aboutModal.addEventListener('click', (e) => { | |
| if (e.target === aboutModal) closeAbout(); | |
| }); | |
| // Keyboard shortcuts | |
| document.addEventListener('keydown', (e) => { | |
| if (e.key === 'n' || e.key === 'N') newGame(); | |
| if (e.key === 'p' || e.key === 'P') passTurn(); | |
| if (e.key === 'z' && (e.ctrlKey || e.metaKey)) { | |
| e.preventDefault(); | |
| undoMove(); | |
| } | |
| if (e.key === 'Escape') { | |
| closeModal(); | |
| closeAbout(); | |
| } | |
| }); | |
| // Initialize on load | |
| window.addEventListener('load', initBoard); | |
| // Handle window resize | |
| window.addEventListener('resize', () => { | |
| ghostStone.classList.remove('visible'); | |
| }); | |
| </script> | |
| </body> | |
| </html> |