anycoder-a1e8029d / index.html
PHhTTPS's picture
Upload folder using huggingface_hub
ceddf7c verified
<!DOCTYPE html>
<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>