go / templates /index.html
jah242's picture
Create templates/index.html
134659f verified
<!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; }
/* Inline color picker (replaces popup) */
.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>
<!-- Inline color picker (no popup) -->
<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>
<!-- Sign-in -->
<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 });
// DOM
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');
// Inline color picker
var colorPicker = document.getElementById('color-picker');
var pickBlack = document.getElementById('pick-black');
var pickWhite = document.getElementById('pick-white');
// Persistence
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; } }
// State
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;
// server convention: captured[color] = stones of that color captured
var captured = { black:0, white:0 };
var winsByUser = {}; // username -> wins
var scores = { black:0, white:0 };
// Sign in
(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 events
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');
// hide sign-in only after successful init (i.e., password accepted)
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');
// Place locally
board[x][y] = placed; lastMove = {row:x,col:y};
// ALWAYS remove captured stones locally so the board visuals match
var removed = localCapture(x,y,placed);
// Then sync capture COUNTS from server if provided
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.');
});
// Controls
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 }); } };
// Helpers
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(){
// black player's captures = captured.white; white player's captures = captured.black
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;
}
// Board rendering (draw grid in every cell => inner edges present)
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);
}
}
}
// Local capture helpers (visual update)
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;
}
// Boot
(function(){
renderBoard();
updateTurnUI();
updateWinsUI();
updateScoresUI();
})();
</script>
</body>
</html>