|
|
<!DOCTYPE html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="UTF-8" /> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> |
|
|
<title>Beautiful Go Game</title> |
|
|
<style> |
|
|
* { margin:0; padding:0; box-sizing:border-box; font-family:'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } |
|
|
:root { --cell-size: 40px; --board-size: 13; } |
|
|
|
|
|
body { background:linear-gradient(135deg,#1a2a6c,#b21f1f,#1a2a6c); min-height:100vh; display:flex; flex-direction:column; align-items:center; padding:20px; color:#fff; } |
|
|
.container { max-width:1200px; width:100%; display:flex; flex-direction:column; align-items:center; gap:20px; } |
|
|
|
|
|
header { text-align:center; padding:20px; width:100%; background:rgba(0,0,0,0.3); border-radius:15px; box-shadow:0 8px 32px rgba(0,0,0,0.3); backdrop-filter:blur(10px); border:1px solid rgba(255,255,255,0.1); } |
|
|
h1 { font-size:2.8rem; margin-bottom:10px; text-shadow:0 0 10px rgba(255,255,255,0.5); background:linear-gradient(to right,#ffd700,#ffffff); -webkit-background-clip:text; -webkit-text-fill-color:transparent; } |
|
|
.subtitle { font-size:1.2rem; opacity:0.9; margin-bottom:15px; } |
|
|
|
|
|
.game-info { display:flex; justify-content:space-between; width:100%; gap:20px; margin:10px 0; } |
|
|
.player-info { background:rgba(0,0,0,0.4); padding:15px; border-radius:10px; flex:1; text-align:center; box-shadow:0 4px 15px rgba(0,0,0,0.2); } |
|
|
.player-info.active { background:rgba(46,204,113,0.3); border:2px solid #2ecc71; transform:scale(1.05); transition:all .3s ease; } |
|
|
.player-name { font-size:1.5rem; font-weight:bold; margin-bottom:8px; word-break:break-word; } |
|
|
.player-stats { display:flex; justify-content:space-around; margin-top:10px; } |
|
|
.stat { text-align:center; } |
|
|
.stat-value { font-size:1.8rem; font-weight:bold; } |
|
|
.stat-label { font-size:0.9rem; opacity:0.8; } |
|
|
|
|
|
.game-controls { display:flex; gap:15px; margin:15px 0; flex-wrap:wrap; justify-content:center; } |
|
|
button { padding:12px 25px; font-size:1rem; font-weight:bold; border:none; border-radius:50px; cursor:pointer; background:linear-gradient(to right,#3498db,#2980b9); color:#fff; box-shadow:0 4px 10px rgba(0,0,0,0.3); transition:all .3s ease; } |
|
|
button:hover { transform:translateY(-3px); box-shadow:0 6px 15px rgba(0,0,0,0.4); } |
|
|
button:active { transform:translateY(1px); } |
|
|
#new-game { background:linear-gradient(to right,#2ecc71,#27ae60); } |
|
|
#pass-turn { background:linear-gradient(to right,#f39c12,#d35400); } |
|
|
#resign { background:linear-gradient(to right,#e74c3c,#c0392b); } |
|
|
|
|
|
.board-container { position:relative; background:#dcb35c; padding:20px; border-radius:10px; box-shadow:0 15px 35px rgba(0,0,0,0.5); border:8px solid #8b4513; } |
|
|
#go-board { --w: calc(var(--board-size) * var(--cell-size)); display:grid; grid-template-columns:repeat(var(--board-size),var(--cell-size)); grid-template-rows:repeat(var(--board-size),var(--cell-size)); width:var(--w); height:var(--w); gap:0; position:relative; } |
|
|
.cell { width:var(--cell-size); height:var(--cell-size); position:relative; display:flex; justify-content:center; align-items:center; } |
|
|
.grid-line { position:absolute; background:#000; pointer-events:none; } |
|
|
.horizontal { width:100%; height:1px; top:50%; } |
|
|
.vertical { width:1px; height:100%; left:50%; } |
|
|
|
|
|
.star-point { position:absolute; width:8px; height:8px; background:#000; border-radius:50%; z-index:1; } |
|
|
|
|
|
.stone { width:36px; height:36px; border-radius:50%; position:absolute; z-index:2; box-shadow:0 3px 5px rgba(0,0,0,0.5); transition:transform .2s ease; } |
|
|
.stone:hover { transform:scale(1.1); } |
|
|
.black { background:radial-gradient(circle at 30% 30%, #555, #000); border:1px solid #222; } |
|
|
.white { background:radial-gradient(circle at 30% 30%, #fff, #ddd); border:1px solid #999; } |
|
|
.last-move { box-shadow:0 0 0 3px #ff5722; } |
|
|
|
|
|
.game-status { background:rgba(0,0,0,0.4); padding:15px; border-radius:10px; text-align:center; font-size:1.2rem; width:100%; box-shadow:0 4px 15px rgba(0,0,0,0.2); } |
|
|
.score-display { display:flex; justify-content:center; gap:30px; margin-top:10px; } |
|
|
.score-item { display:flex; align-items:center; gap:8px; } |
|
|
.score-stone { width:20px; height:20px; border-radius:50%; } |
|
|
.score-stone.black { background:radial-gradient(circle at 30% 30%, #555, #000); } |
|
|
.score-stone.white { background:radial-gradient(circle at 30% 30%, #fff, #ddd); border:1px solid #999; } |
|
|
|
|
|
.instructions { background:rgba(0,0,0,0.3); padding:20px; border-radius:10px; max-width:800px; margin-top:20px; box-shadow:0 4px 15px rgba(0,0,0,0.2); } |
|
|
.instructions h2 { margin-bottom:15px; color:#ffd700; text-align:center; } |
|
|
.instructions ul { padding-left:20px; } |
|
|
.instructions li { margin-bottom:10px; line-height:1.5; } |
|
|
|
|
|
.winner-message { position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.8); display:flex; justify-content:center; align-items:center; z-index:100; opacity:0; pointer-events:none; transition:opacity .5s ease; } |
|
|
.winner-message.show { opacity:1; pointer-events:all; } |
|
|
.winner-content { background:linear-gradient(135deg,#1a2a6c,#b21f1f); padding:40px; border-radius:20px; text-align:center; max-width:500px; width:90%; box-shadow:0 0 50px rgba(255,215,0,0.5); border:3px solid gold; } |
|
|
.winner-content h2 { font-size:2.5rem; margin-bottom:20px; color:gold; } |
|
|
.winner-content p { font-size:1.5rem; margin-bottom:30px; } |
|
|
|
|
|
.signin-overlay { position:fixed; inset:0; background:rgba(0,0,0,0.85); display:flex; align-items:center; justify-content:center; z-index:200; } |
|
|
.card { background:linear-gradient(135deg,#1a2a6c,#b21f1f); padding:28px; border-radius:16px; width:90%; max-width:520px; border:2px solid rgba(255,255,255,0.2); box-shadow:0 10px 30px rgba(0,0,0,0.5); } |
|
|
.card h3 { margin-bottom:12px; text-align:center; } |
|
|
.row { display:flex; gap:10px; margin:10px 0 18px; flex-wrap:wrap; } |
|
|
.row input { flex:1; min-width:160px; padding:12px 14px; border-radius:8px; border:1px solid rgba(255,255,255,0.3); background:rgba(0,0,0,0.3); color:#fff; outline:none; } |
|
|
.row button { flex:0 0 auto; } |
|
|
|
|
|
|
|
|
.color-inline { display:none; align-items:center; justify-content:center; gap:12px; background:rgba(0,0,0,0.3); padding:10px 14px; border-radius:12px; } |
|
|
.color-inline h3 { margin-right:10px; font-size:1rem; font-weight:600; } |
|
|
.colors { display:flex; gap:12px; } |
|
|
.color-btn { flex:1; padding:12px; border-radius:10px; border:none; cursor:pointer; font-weight:700; } |
|
|
.color-black { background:#222; color:#fff; } |
|
|
.color-white { background:#eee; color:#111; } |
|
|
.disabled { opacity:0.5; pointer-events:none; } |
|
|
|
|
|
@media (max-width:768px) { |
|
|
.game-info { flex-direction:column; } |
|
|
:root { --cell-size:30px; } |
|
|
.stone { width:26px; height:26px; } |
|
|
h1 { font-size:2rem; } |
|
|
.color-inline { flex-direction:column; align-items:stretch; } |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div class="container"> |
|
|
<header> |
|
|
<h1>Beautiful Go Game</h1> |
|
|
<p class="subtitle">A strategic board game for two players</p> |
|
|
</header> |
|
|
|
|
|
<div class="game-info"> |
|
|
<div class="player-info" id="pBlack"> |
|
|
<div class="player-name" id="name-black">Black</div> |
|
|
<div class="player-stats"> |
|
|
<div class="stat"><div class="stat-value" id="wins-black">0</div><div class="stat-label">Wins</div></div> |
|
|
<div class="stat"><div class="stat-value" id="caps-black">0</div><div class="stat-label">Captures</div></div> |
|
|
</div> |
|
|
</div> |
|
|
<div class="player-info active" id="pWhite"> |
|
|
<div class="player-name" id="name-white">White</div> |
|
|
<div class="player-stats"> |
|
|
<div class="stat"><div class="stat-value" id="wins-white">0</div><div class="stat-label">Wins</div></div> |
|
|
<div class="stat"><div class="stat-value" id="caps-white">0</div><div class="stat-label">Captures</div></div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="game-controls"> |
|
|
<button id="new-game">New Game</button> |
|
|
<button id="pass-turn">Pass Turn</button> |
|
|
<button id="resign">Resign</button> |
|
|
<select id="board-size" title="Board size is controlled by the server"> |
|
|
<option value="9">9x9</option> |
|
|
<option value="13" selected>13x13</option> |
|
|
<option value="19">19x19</option> |
|
|
</select> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div id="color-picker" class="color-inline" aria-live="polite"> |
|
|
<h3>Pick your color</h3> |
|
|
<div class="colors"> |
|
|
<button id="pick-black" class="color-btn color-black">Play as Black</button> |
|
|
<button id="pick-white" class="color-btn color-white">Play as White</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="board-container"><div id="go-board"></div></div> |
|
|
|
|
|
<div class="game-status"> |
|
|
<div>Current Player: <span id="player-turn">Black</span></div> |
|
|
<div class="score-display"> |
|
|
<div class="score-item"><div class="score-stone black"></div><span id="score-black">0</span></div> |
|
|
<div class="score-item"><div class="score-stone white"></div><span id="score-white">0</span></div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="instructions"> |
|
|
<h2>How to Play Go</h2> |
|
|
<ul> |
|
|
<li><strong>Objective:</strong> Surround more territory than your opponent</li> |
|
|
<li><strong>Players:</strong> Black moves first, then White</li> |
|
|
<li><strong>Moves:</strong> Place stones on empty intersections</li> |
|
|
<li><strong>Capturing:</strong> Stones with no liberties are captured</li> |
|
|
<li><strong>Pass:</strong> Skip your turn when you have no beneficial moves</li> |
|
|
<li><strong>End:</strong> Game ends when both players pass consecutively</li> |
|
|
</ul> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="winner-message" id="winner-overlay"> |
|
|
<div class="winner-content"> |
|
|
<h2>Game Over!</h2> |
|
|
<p id="winner-text">Player wins by resignation!</p> |
|
|
<button id="play-again">Play Again</button> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="signin-overlay" id="signin"> |
|
|
<div class="card"> |
|
|
<h3>Sign in</h3> |
|
|
<div class="row"> |
|
|
<input id="username" placeholder="Enter your username" maxlength="24" /> |
|
|
<input id="password" type="password" placeholder="Enter the password" /> |
|
|
<button id="btn-signin">Continue</button> |
|
|
</div> |
|
|
<p style="opacity:.8;font-size:.9rem;text-align:center">Enter your username and the shared password to continue. After you click “New Game”, you’ll be asked to pick a color.</p> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<script src="https://cdn.socket.io/3.1.3/socket.io.min.js"></script> |
|
|
<script> |
|
|
var socket = io('/', { path:'/socket.io', transports:['polling'], upgrade:false }); |
|
|
|
|
|
|
|
|
var boardEl = document.getElementById('go-board'); |
|
|
var pBlackEl = document.getElementById('pBlack'); |
|
|
var pWhiteEl = document.getElementById('pWhite'); |
|
|
var nameBlackEl = document.getElementById('name-black'); |
|
|
var nameWhiteEl = document.getElementById('name-white'); |
|
|
var winsBlackEl = document.getElementById('wins-black'); |
|
|
var winsWhiteEl = document.getElementById('wins-white'); |
|
|
var capsBlackEl = document.getElementById('caps-black'); |
|
|
var capsWhiteEl = document.getElementById('caps-white'); |
|
|
var scoreBlackEl = document.getElementById('score-black'); |
|
|
var scoreWhiteEl = document.getElementById('score-white'); |
|
|
var playerTurnEl = document.getElementById('player-turn'); |
|
|
var winnerOverlay = document.getElementById('winner-overlay'); |
|
|
var winnerText = document.getElementById('winner-text'); |
|
|
var playAgainBtn = document.getElementById('play-again'); |
|
|
|
|
|
var boardSizeSelect = document.getElementById('board-size'); |
|
|
var btnNew = document.getElementById('new-game'); |
|
|
var btnPass = document.getElementById('pass-turn'); |
|
|
var btnResign = document.getElementById('resign'); |
|
|
|
|
|
var signin = document.getElementById('signin'); |
|
|
var usernameInput = document.getElementById('username'); |
|
|
var passwordInput = document.getElementById('password'); |
|
|
var btnSignin = document.getElementById('btn-signin'); |
|
|
|
|
|
|
|
|
var colorPicker = document.getElementById('color-picker'); |
|
|
var pickBlack = document.getElementById('pick-black'); |
|
|
var pickWhite = document.getElementById('pick-white'); |
|
|
|
|
|
|
|
|
var LS_USER = 'go_username'; |
|
|
var LS_COLOR = 'go_my_color'; |
|
|
function save(k,v){ try{ localStorage.setItem(k,v); }catch(e){} } |
|
|
function load(k){ try{ return localStorage.getItem(k); }catch(e){ return null; } } |
|
|
|
|
|
|
|
|
var username = ''; |
|
|
var colorsMap = { black:null, white:null }; |
|
|
var myColor = null; |
|
|
|
|
|
var boardSize = 13; |
|
|
boardEl.style.setProperty('--board-size', String(boardSize)); |
|
|
var board = Array(boardSize).fill().map(()=>Array(boardSize).fill(null)); |
|
|
var currentColor = 'black'; |
|
|
var lastMove = null; |
|
|
var gameOver = false; |
|
|
|
|
|
|
|
|
var captured = { black:0, white:0 }; |
|
|
var winsByUser = {}; |
|
|
var scores = { black:0, white:0 }; |
|
|
|
|
|
|
|
|
(function(){ var u = load(LS_USER); if (u) usernameInput.value = u; })(); |
|
|
btnSignin.onclick = function(){ |
|
|
var u = (usernameInput.value||'').trim(); |
|
|
var p = (passwordInput.value||'').trim(); |
|
|
if (!u){ alert('Enter a username'); return; } |
|
|
if (!p){ alert('Enter the password'); return; } |
|
|
username = u; save(LS_USER,u); |
|
|
socket.emit('join', { username: u, password: p }); |
|
|
}; |
|
|
|
|
|
function showColorPicker(){ |
|
|
pickBlack.classList.toggle('disabled', !!colorsMap.black); |
|
|
pickWhite.classList.toggle('disabled', !!colorsMap.white); |
|
|
colorPicker.style.display = 'flex'; |
|
|
} |
|
|
function hideColorPicker(){ colorPicker.style.display = 'none'; } |
|
|
pickBlack.onclick = function(){ |
|
|
if (!colorsMap.black) { socket.emit('claim_color', { username, color:'black' }); pickBlack.classList.add('disabled'); pickWhite.classList.add('disabled'); } |
|
|
}; |
|
|
pickWhite.onclick = function(){ |
|
|
if (!colorsMap.white) { socket.emit('claim_color', { username, color:'white' }); pickBlack.classList.add('disabled'); pickWhite.classList.add('disabled'); } |
|
|
}; |
|
|
|
|
|
|
|
|
socket.on('init', function(data){ |
|
|
var size = parseInt(data.board_size,10); |
|
|
if (!isNaN(size) && size>0){ |
|
|
boardSize = size; |
|
|
boardEl.style.setProperty('--board-size', String(boardSize)); |
|
|
board = Array(boardSize).fill().map(()=>Array(boardSize).fill(null)); |
|
|
} |
|
|
if (Array.isArray(data.board)){ |
|
|
for (var r=0;r<Math.min(boardSize,data.board.length);r++){ |
|
|
for (var c=0;c<Math.min(boardSize,data.board[r].length);c++){ |
|
|
var v = data.board[r][c]; |
|
|
board[r][c] = (v==='black'||v==='white')?v:null; |
|
|
} |
|
|
} |
|
|
} |
|
|
currentColor = data.current_player || 'black'; |
|
|
captured = data.captured || captured; |
|
|
scores = data.scores || scores; |
|
|
lastMove = data.last_move || null; |
|
|
gameOver = !!data.game_over; |
|
|
if (data.wins_by_user) winsByUser = data.wins_by_user; |
|
|
|
|
|
boardSizeSelect.value = String(boardSize); |
|
|
boardSizeSelect.disabled = true; |
|
|
|
|
|
refreshNamesUI(); |
|
|
refreshCapturesUI(); |
|
|
computeLiveScores(); |
|
|
updateWinsUI(); |
|
|
updateTurnUI(); |
|
|
renderBoard(); |
|
|
winnerOverlay.classList.remove('show'); |
|
|
|
|
|
|
|
|
signin.style.display = 'none'; |
|
|
}); |
|
|
|
|
|
socket.on('colors', function(map){ |
|
|
colorsMap = { black: map.black || null, white: map.white || null }; |
|
|
myColor = (colorsMap.black===username)?'black':(colorsMap.white===username)?'white':null; |
|
|
if (myColor) save(LS_COLOR,myColor); else save(LS_COLOR,''); |
|
|
|
|
|
if (!colorsMap.black && !colorsMap.white) showColorPicker(); |
|
|
else if (!myColor) showColorPicker(); |
|
|
else hideColorPicker(); |
|
|
|
|
|
refreshNamesUI(); |
|
|
updateWinsUI(); |
|
|
updateTurnUI(); |
|
|
}); |
|
|
|
|
|
socket.on('move', function(data){ |
|
|
var x = data.x, y = data.y; |
|
|
var placed = (data.player==='black'||data.player==='white') ? data.player |
|
|
: (data.next_player==='black'?'white':'black'); |
|
|
|
|
|
|
|
|
board[x][y] = placed; lastMove = {row:x,col:y}; |
|
|
|
|
|
|
|
|
var removed = localCapture(x,y,placed); |
|
|
|
|
|
|
|
|
if (data.captured) { |
|
|
captured = data.captured; |
|
|
} else if (removed > 0) { |
|
|
if (placed === 'black') captured.white = (captured.white||0) + removed; |
|
|
else captured.black = (captured.black||0) + removed; |
|
|
} |
|
|
refreshCapturesUI(); |
|
|
|
|
|
currentColor = data.next_player || (placed==='black'?'white':'black'); |
|
|
computeLiveScores(); |
|
|
updateTurnUI(); |
|
|
renderBoard(); |
|
|
}); |
|
|
|
|
|
socket.on('pass', function(data){ |
|
|
currentColor = data.next_player || (currentColor==='black'?'white':'black'); |
|
|
updateTurnUI(); |
|
|
}); |
|
|
|
|
|
socket.on('resign', function(data){ |
|
|
if (data.wins_by_user) winsByUser = data.wins_by_user; |
|
|
scores = data.scores || scores; |
|
|
updateWinsUI(); |
|
|
gameOver = true; |
|
|
var winner = data.winner; |
|
|
var winnerName = (winner==='black') ? (colorsMap.black||'Black') : (colorsMap.white||'White'); |
|
|
winnerText.textContent = winnerName + ' wins by resignation!'; |
|
|
winnerOverlay.classList.add('show'); |
|
|
}); |
|
|
|
|
|
socket.on('game_over', function(data){ |
|
|
scores = data.scores || scores; |
|
|
if (data.wins_by_user) winsByUser = data.wins_by_user; |
|
|
updateWinsUI(); |
|
|
updateScoresUI(); |
|
|
gameOver = true; |
|
|
var b=scores.black|0, w=scores.white|0; |
|
|
var winnerName = (b===w) ? "It's a draw!" : ((b>w)? (colorsMap.black||'Black')+' wins!' : (colorsMap.white||'White')+' wins!'); |
|
|
winnerText.textContent = winnerName; |
|
|
winnerOverlay.classList.add('show'); |
|
|
}); |
|
|
|
|
|
socket.on('error', function(err){ |
|
|
alert((err && err.message)? err.message : 'Server error.'); |
|
|
}); |
|
|
|
|
|
|
|
|
document.getElementById('new-game').onclick = function(){ |
|
|
if (!username){ alert('Sign in first.'); return; } |
|
|
showColorPicker(); |
|
|
socket.emit('new_game', { player: username }); |
|
|
}; |
|
|
document.getElementById('pass-turn').onclick = function(){ |
|
|
if (!username) return; |
|
|
if (!canPlayNow()){ alert('Not your turn.'); return; } |
|
|
socket.emit('pass', { player: username }); |
|
|
}; |
|
|
document.getElementById('resign').onclick = function(){ |
|
|
if (!username) return; |
|
|
socket.emit('resign', { player: username }); |
|
|
}; |
|
|
boardSizeSelect.onchange = function(e){ |
|
|
e.target.value = String(boardSize); |
|
|
alert('Board size is controlled by the server.'); |
|
|
}; |
|
|
playAgainBtn.onclick = function(){ if (username) { showColorPicker(); socket.emit('new_game', { player: username }); } }; |
|
|
|
|
|
|
|
|
function canPlayNow(){ |
|
|
if (gameOver) return false; |
|
|
if (!username) return false; |
|
|
if (currentColor === 'black') return colorsMap.black === username; |
|
|
if (currentColor === 'white') return colorsMap.white === username; |
|
|
return false; |
|
|
} |
|
|
|
|
|
function refreshNamesUI(){ |
|
|
nameBlackEl.textContent = colorsMap.black ? (colorsMap.black + (colorsMap.black===username?' (You)':'')) : 'Black'; |
|
|
nameWhiteEl.textContent = colorsMap.white ? (colorsMap.white + (colorsMap.white===username?' (You)':'')) : 'White'; |
|
|
} |
|
|
|
|
|
function updateTurnUI(){ |
|
|
var label = (currentColor==='black') ? (colorsMap.black||'Black') : (colorsMap.white||'White'); |
|
|
playerTurnEl.textContent = label; |
|
|
if (currentColor==='black'){ pBlackEl.classList.add('active'); pWhiteEl.classList.remove('active'); } |
|
|
else { pBlackEl.classList.remove('active'); pWhiteEl.classList.add('active'); } |
|
|
} |
|
|
|
|
|
function updateWinsUI(){ |
|
|
var bname = colorsMap.black, wname = colorsMap.white; |
|
|
winsBlackEl.textContent = bname && winsByUser[bname] != null ? winsByUser[bname] : 0; |
|
|
winsWhiteEl.textContent = wname && winsByUser[wname] != null ? winsByUser[wname] : 0; |
|
|
} |
|
|
|
|
|
function refreshCapturesUI(){ |
|
|
|
|
|
capsBlackEl.textContent = captured.white||0; |
|
|
capsWhiteEl.textContent = captured.black||0; |
|
|
} |
|
|
|
|
|
function computeLiveScores(){ |
|
|
var bs=0, ws=0; |
|
|
for (var r=0;r<boardSize;r++) for (var c=0;c<boardSize;c++){ |
|
|
if (board[r][c]==='black') bs++; else if (board[r][c]==='white') ws++; |
|
|
} |
|
|
scores.black = bs + (captured.white||0); |
|
|
scores.white = ws + (captured.black||0); |
|
|
updateScoresUI(); |
|
|
} |
|
|
|
|
|
function updateScoresUI(){ |
|
|
scoreBlackEl.textContent = scores.black||0; |
|
|
scoreWhiteEl.textContent = scores.white||0; |
|
|
} |
|
|
|
|
|
|
|
|
function renderBoard(){ |
|
|
boardEl.innerHTML = ''; |
|
|
for (var r = 0; r < boardSize; r++) { |
|
|
for (var c = 0; c < boardSize; c++) { |
|
|
(function(row, col){ |
|
|
var cell = document.createElement('div'); |
|
|
cell.className = 'cell'; |
|
|
|
|
|
var h = document.createElement('div'); h.className='grid-line horizontal'; cell.appendChild(h); |
|
|
var v = document.createElement('div'); v.className='grid-line vertical'; cell.appendChild(v); |
|
|
|
|
|
if (boardSize >= 13 && |
|
|
((row===3&&col===3) || (row===3&&col===boardSize-4) || |
|
|
(row===boardSize-4&&col===3) || (row===boardSize-4&&col===boardSize-4) || |
|
|
(row===Math.floor(boardSize/2)&&col===Math.floor(boardSize/2)) || |
|
|
(row===3&&col===Math.floor(boardSize/2)) || (row===boardSize-4&&col===Math.floor(boardSize/2)) || |
|
|
(row===Math.floor(boardSize/2)&&col===3) || (row===Math.floor(boardSize/2)&&col===boardSize-4))) { |
|
|
var star = document.createElement('div'); star.className='star-point'; cell.appendChild(star); |
|
|
} |
|
|
|
|
|
if (board[row][col]) { |
|
|
var stone = document.createElement('div'); |
|
|
stone.className = 'stone ' + board[row][col]; |
|
|
if (lastMove && lastMove.row===row && lastMove.col===col) stone.classList.add('last-move'); |
|
|
cell.appendChild(stone); |
|
|
} |
|
|
|
|
|
cell.addEventListener('click', function(){ |
|
|
if (!canPlayNow()) { alert('Not your turn / pick a color first.'); return; } |
|
|
if (board[row][col]) return; |
|
|
socket.emit('move', { x: row, y: col, player: username }); |
|
|
}); |
|
|
|
|
|
boardEl.appendChild(cell); |
|
|
})(r, c); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function hasLibertiesAt(row,col,color){ |
|
|
var vis = Array(boardSize).fill().map(()=>Array(boardSize).fill(false)); |
|
|
return dfs(row,col,color,vis); |
|
|
} |
|
|
function dfs(r,c,color,vis){ |
|
|
if (r<0||r>=boardSize||c<0||c>=boardSize) return false; |
|
|
if (vis[r][c]) return false; |
|
|
if (board[r][c]===null) return true; |
|
|
if (board[r][c]!==color) return false; |
|
|
vis[r][c]=true; |
|
|
return dfs(r-1,c,color,vis) || dfs(r+1,c,color,vis) || dfs(r,c-1,color,vis) || dfs(r,c+1,color,vis); |
|
|
} |
|
|
function floodRemove(r,c,color,vis){ |
|
|
if (r<0||r>=boardSize||c<0||c>=boardSize) return 0; |
|
|
if (vis[r][c] || board[r][c]!==color) return 0; |
|
|
vis[r][c]=true; board[r][c]=null; |
|
|
return 1 + floodRemove(r-1,c,color,vis) + floodRemove(r+1,c,color,vis) + floodRemove(r,c-1,color,vis) + floodRemove(r,c+1,color,vis); |
|
|
} |
|
|
function localCapture(r,c,placed){ |
|
|
var opp=(placed==='black')?'white':'black'; |
|
|
var removed=0; |
|
|
var dirs=[[-1,0],[1,0],[0,-1],[0,1]]; |
|
|
for (var i=0;i<dirs.length;i++){ |
|
|
var nr=r+dirs[i][0], nc=c+dirs[i][1]; |
|
|
if (nr>=0&&nr<boardSize&&nc>=0&&nc<boardSize&&board[nr][nc]===opp){ |
|
|
if (!hasLibertiesAt(nr,nc,opp)){ |
|
|
var vis=Array(boardSize).fill().map(()=>Array(boardSize).fill(false)); |
|
|
removed += floodRemove(nr,nc,opp,vis); |
|
|
} |
|
|
} |
|
|
} |
|
|
return removed; |
|
|
} |
|
|
|
|
|
|
|
|
(function(){ |
|
|
renderBoard(); |
|
|
updateTurnUI(); |
|
|
updateWinsUI(); |
|
|
updateScoresUI(); |
|
|
})(); |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
|