Create templates/index.html
Browse files- templates/index.html +527 -0
templates/index.html
ADDED
@@ -0,0 +1,527 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8" />
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
6 |
+
<title>Beautiful Go Game</title>
|
7 |
+
<style>
|
8 |
+
* { margin:0; padding:0; box-sizing:border-box; font-family:'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }
|
9 |
+
:root { --cell-size: 40px; --board-size: 13; }
|
10 |
+
|
11 |
+
body { background:linear-gradient(135deg,#1a2a6c,#b21f1f,#1a2a6c); min-height:100vh; display:flex; flex-direction:column; align-items:center; padding:20px; color:#fff; }
|
12 |
+
.container { max-width:1200px; width:100%; display:flex; flex-direction:column; align-items:center; gap:20px; }
|
13 |
+
|
14 |
+
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); }
|
15 |
+
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; }
|
16 |
+
.subtitle { font-size:1.2rem; opacity:0.9; margin-bottom:15px; }
|
17 |
+
|
18 |
+
.game-info { display:flex; justify-content:space-between; width:100%; gap:20px; margin:10px 0; }
|
19 |
+
.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); }
|
20 |
+
.player-info.active { background:rgba(46,204,113,0.3); border:2px solid #2ecc71; transform:scale(1.05); transition:all .3s ease; }
|
21 |
+
.player-name { font-size:1.5rem; font-weight:bold; margin-bottom:8px; word-break:break-word; }
|
22 |
+
.player-stats { display:flex; justify-content:space-around; margin-top:10px; }
|
23 |
+
.stat { text-align:center; }
|
24 |
+
.stat-value { font-size:1.8rem; font-weight:bold; }
|
25 |
+
.stat-label { font-size:0.9rem; opacity:0.8; }
|
26 |
+
|
27 |
+
.game-controls { display:flex; gap:15px; margin:15px 0; flex-wrap:wrap; justify-content:center; }
|
28 |
+
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; }
|
29 |
+
button:hover { transform:translateY(-3px); box-shadow:0 6px 15px rgba(0,0,0,0.4); }
|
30 |
+
button:active { transform:translateY(1px); }
|
31 |
+
#new-game { background:linear-gradient(to right,#2ecc71,#27ae60); }
|
32 |
+
#pass-turn { background:linear-gradient(to right,#f39c12,#d35400); }
|
33 |
+
#resign { background:linear-gradient(to right,#e74c3c,#c0392b); }
|
34 |
+
|
35 |
+
.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; }
|
36 |
+
#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; }
|
37 |
+
.cell { width:var(--cell-size); height:var(--cell-size); position:relative; display:flex; justify-content:center; align-items:center; }
|
38 |
+
.grid-line { position:absolute; background:#000; pointer-events:none; }
|
39 |
+
.horizontal { width:100%; height:1px; top:50%; }
|
40 |
+
.vertical { width:1px; height:100%; left:50%; }
|
41 |
+
|
42 |
+
.star-point { position:absolute; width:8px; height:8px; background:#000; border-radius:50%; z-index:1; }
|
43 |
+
|
44 |
+
.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; }
|
45 |
+
.stone:hover { transform:scale(1.1); }
|
46 |
+
.black { background:radial-gradient(circle at 30% 30%, #555, #000); border:1px solid #222; }
|
47 |
+
.white { background:radial-gradient(circle at 30% 30%, #fff, #ddd); border:1px solid #999; }
|
48 |
+
.last-move { box-shadow:0 0 0 3px #ff5722; }
|
49 |
+
|
50 |
+
.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); }
|
51 |
+
.score-display { display:flex; justify-content:center; gap:30px; margin-top:10px; }
|
52 |
+
.score-item { display:flex; align-items:center; gap:8px; }
|
53 |
+
.score-stone { width:20px; height:20px; border-radius:50%; }
|
54 |
+
.score-stone.black { background:radial-gradient(circle at 30% 30%, #555, #000); }
|
55 |
+
.score-stone.white { background:radial-gradient(circle at 30% 30%, #fff, #ddd); border:1px solid #999; }
|
56 |
+
|
57 |
+
.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); }
|
58 |
+
.instructions h2 { margin-bottom:15px; color:#ffd700; text-align:center; }
|
59 |
+
.instructions ul { padding-left:20px; }
|
60 |
+
.instructions li { margin-bottom:10px; line-height:1.5; }
|
61 |
+
|
62 |
+
.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; }
|
63 |
+
.winner-message.show { opacity:1; pointer-events:all; }
|
64 |
+
.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; }
|
65 |
+
.winner-content h2 { font-size:2.5rem; margin-bottom:20px; color:gold; }
|
66 |
+
.winner-content p { font-size:1.5rem; margin-bottom:30px; }
|
67 |
+
|
68 |
+
.signin-overlay { position:fixed; inset:0; background:rgba(0,0,0,0.85); display:flex; align-items:center; justify-content:center; z-index:200; }
|
69 |
+
.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); }
|
70 |
+
.card h3 { margin-bottom:12px; text-align:center; }
|
71 |
+
.row { display:flex; gap:10px; margin:10px 0 18px; flex-wrap:wrap; }
|
72 |
+
.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; }
|
73 |
+
.row button { flex:0 0 auto; }
|
74 |
+
|
75 |
+
/* Inline color picker (replaces popup) */
|
76 |
+
.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; }
|
77 |
+
.color-inline h3 { margin-right:10px; font-size:1rem; font-weight:600; }
|
78 |
+
.colors { display:flex; gap:12px; }
|
79 |
+
.color-btn { flex:1; padding:12px; border-radius:10px; border:none; cursor:pointer; font-weight:700; }
|
80 |
+
.color-black { background:#222; color:#fff; }
|
81 |
+
.color-white { background:#eee; color:#111; }
|
82 |
+
.disabled { opacity:0.5; pointer-events:none; }
|
83 |
+
|
84 |
+
@media (max-width:768px) {
|
85 |
+
.game-info { flex-direction:column; }
|
86 |
+
:root { --cell-size:30px; }
|
87 |
+
.stone { width:26px; height:26px; }
|
88 |
+
h1 { font-size:2rem; }
|
89 |
+
.color-inline { flex-direction:column; align-items:stretch; }
|
90 |
+
}
|
91 |
+
</style>
|
92 |
+
</head>
|
93 |
+
<body>
|
94 |
+
<div class="container">
|
95 |
+
<header>
|
96 |
+
<h1>Beautiful Go Game</h1>
|
97 |
+
<p class="subtitle">A strategic board game for two players</p>
|
98 |
+
</header>
|
99 |
+
|
100 |
+
<div class="game-info">
|
101 |
+
<div class="player-info" id="pBlack">
|
102 |
+
<div class="player-name" id="name-black">Black</div>
|
103 |
+
<div class="player-stats">
|
104 |
+
<div class="stat"><div class="stat-value" id="wins-black">0</div><div class="stat-label">Wins</div></div>
|
105 |
+
<div class="stat"><div class="stat-value" id="caps-black">0</div><div class="stat-label">Captures</div></div>
|
106 |
+
</div>
|
107 |
+
</div>
|
108 |
+
<div class="player-info active" id="pWhite">
|
109 |
+
<div class="player-name" id="name-white">White</div>
|
110 |
+
<div class="player-stats">
|
111 |
+
<div class="stat"><div class="stat-value" id="wins-white">0</div><div class="stat-label">Wins</div></div>
|
112 |
+
<div class="stat"><div class="stat-value" id="caps-white">0</div><div class="stat-label">Captures</div></div>
|
113 |
+
</div>
|
114 |
+
</div>
|
115 |
+
</div>
|
116 |
+
|
117 |
+
<div class="game-controls">
|
118 |
+
<button id="new-game">New Game</button>
|
119 |
+
<button id="pass-turn">Pass Turn</button>
|
120 |
+
<button id="resign">Resign</button>
|
121 |
+
<select id="board-size" title="Board size is controlled by the server">
|
122 |
+
<option value="9">9x9</option>
|
123 |
+
<option value="13" selected>13x13</option>
|
124 |
+
<option value="19">19x19</option>
|
125 |
+
</select>
|
126 |
+
</div>
|
127 |
+
|
128 |
+
<!-- Inline color picker (no popup) -->
|
129 |
+
<div id="color-picker" class="color-inline" aria-live="polite">
|
130 |
+
<h3>Pick your color</h3>
|
131 |
+
<div class="colors">
|
132 |
+
<button id="pick-black" class="color-btn color-black">Play as Black</button>
|
133 |
+
<button id="pick-white" class="color-btn color-white">Play as White</button>
|
134 |
+
</div>
|
135 |
+
</div>
|
136 |
+
|
137 |
+
<div class="board-container"><div id="go-board"></div></div>
|
138 |
+
|
139 |
+
<div class="game-status">
|
140 |
+
<div>Current Player: <span id="player-turn">Black</span></div>
|
141 |
+
<div class="score-display">
|
142 |
+
<div class="score-item"><div class="score-stone black"></div><span id="score-black">0</span></div>
|
143 |
+
<div class="score-item"><div class="score-stone white"></div><span id="score-white">0</span></div>
|
144 |
+
</div>
|
145 |
+
</div>
|
146 |
+
|
147 |
+
<div class="instructions">
|
148 |
+
<h2>How to Play Go</h2>
|
149 |
+
<ul>
|
150 |
+
<li><strong>Objective:</strong> Surround more territory than your opponent</li>
|
151 |
+
<li><strong>Players:</strong> Black moves first, then White</li>
|
152 |
+
<li><strong>Moves:</strong> Place stones on empty intersections</li>
|
153 |
+
<li><strong>Capturing:</strong> Stones with no liberties are captured</li>
|
154 |
+
<li><strong>Pass:</strong> Skip your turn when you have no beneficial moves</li>
|
155 |
+
<li><strong>End:</strong> Game ends when both players pass consecutively</li>
|
156 |
+
</ul>
|
157 |
+
</div>
|
158 |
+
</div>
|
159 |
+
|
160 |
+
<div class="winner-message" id="winner-overlay">
|
161 |
+
<div class="winner-content">
|
162 |
+
<h2>Game Over!</h2>
|
163 |
+
<p id="winner-text">Player wins by resignation!</p>
|
164 |
+
<button id="play-again">Play Again</button>
|
165 |
+
</div>
|
166 |
+
</div>
|
167 |
+
|
168 |
+
<!-- Sign-in -->
|
169 |
+
<div class="signin-overlay" id="signin">
|
170 |
+
<div class="card">
|
171 |
+
<h3>Sign in</h3>
|
172 |
+
<div class="row">
|
173 |
+
<input id="username" placeholder="Enter your username" maxlength="24" />
|
174 |
+
<input id="password" type="password" placeholder="Enter the password" />
|
175 |
+
<button id="btn-signin">Continue</button>
|
176 |
+
</div>
|
177 |
+
<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>
|
178 |
+
</div>
|
179 |
+
</div>
|
180 |
+
|
181 |
+
<script src="https://cdn.socket.io/3.1.3/socket.io.min.js"></script>
|
182 |
+
<script>
|
183 |
+
var socket = io('/', { path:'/socket.io', transports:['polling'], upgrade:false });
|
184 |
+
|
185 |
+
// DOM
|
186 |
+
var boardEl = document.getElementById('go-board');
|
187 |
+
var pBlackEl = document.getElementById('pBlack');
|
188 |
+
var pWhiteEl = document.getElementById('pWhite');
|
189 |
+
var nameBlackEl = document.getElementById('name-black');
|
190 |
+
var nameWhiteEl = document.getElementById('name-white');
|
191 |
+
var winsBlackEl = document.getElementById('wins-black');
|
192 |
+
var winsWhiteEl = document.getElementById('wins-white');
|
193 |
+
var capsBlackEl = document.getElementById('caps-black');
|
194 |
+
var capsWhiteEl = document.getElementById('caps-white');
|
195 |
+
var scoreBlackEl = document.getElementById('score-black');
|
196 |
+
var scoreWhiteEl = document.getElementById('score-white');
|
197 |
+
var playerTurnEl = document.getElementById('player-turn');
|
198 |
+
var winnerOverlay = document.getElementById('winner-overlay');
|
199 |
+
var winnerText = document.getElementById('winner-text');
|
200 |
+
var playAgainBtn = document.getElementById('play-again');
|
201 |
+
|
202 |
+
var boardSizeSelect = document.getElementById('board-size');
|
203 |
+
var btnNew = document.getElementById('new-game');
|
204 |
+
var btnPass = document.getElementById('pass-turn');
|
205 |
+
var btnResign = document.getElementById('resign');
|
206 |
+
|
207 |
+
var signin = document.getElementById('signin');
|
208 |
+
var usernameInput = document.getElementById('username');
|
209 |
+
var passwordInput = document.getElementById('password');
|
210 |
+
var btnSignin = document.getElementById('btn-signin');
|
211 |
+
|
212 |
+
// Inline color picker
|
213 |
+
var colorPicker = document.getElementById('color-picker');
|
214 |
+
var pickBlack = document.getElementById('pick-black');
|
215 |
+
var pickWhite = document.getElementById('pick-white');
|
216 |
+
|
217 |
+
// Persistence
|
218 |
+
var LS_USER = 'go_username';
|
219 |
+
var LS_COLOR = 'go_my_color';
|
220 |
+
function save(k,v){ try{ localStorage.setItem(k,v); }catch(e){} }
|
221 |
+
function load(k){ try{ return localStorage.getItem(k); }catch(e){ return null; } }
|
222 |
+
|
223 |
+
// State
|
224 |
+
var username = '';
|
225 |
+
var colorsMap = { black:null, white:null };
|
226 |
+
var myColor = null;
|
227 |
+
|
228 |
+
var boardSize = 13;
|
229 |
+
boardEl.style.setProperty('--board-size', String(boardSize));
|
230 |
+
var board = Array(boardSize).fill().map(()=>Array(boardSize).fill(null));
|
231 |
+
var currentColor = 'black';
|
232 |
+
var lastMove = null;
|
233 |
+
var gameOver = false;
|
234 |
+
|
235 |
+
// server convention: captured[color] = stones of that color captured
|
236 |
+
var captured = { black:0, white:0 };
|
237 |
+
var winsByUser = {}; // username -> wins
|
238 |
+
var scores = { black:0, white:0 };
|
239 |
+
|
240 |
+
// Sign in
|
241 |
+
(function(){ var u = load(LS_USER); if (u) usernameInput.value = u; })();
|
242 |
+
btnSignin.onclick = function(){
|
243 |
+
var u = (usernameInput.value||'').trim();
|
244 |
+
var p = (passwordInput.value||'').trim();
|
245 |
+
if (!u){ alert('Enter a username'); return; }
|
246 |
+
if (!p){ alert('Enter the password'); return; }
|
247 |
+
username = u; save(LS_USER,u);
|
248 |
+
socket.emit('join', { username: u, password: p });
|
249 |
+
};
|
250 |
+
|
251 |
+
function showColorPicker(){
|
252 |
+
pickBlack.classList.toggle('disabled', !!colorsMap.black);
|
253 |
+
pickWhite.classList.toggle('disabled', !!colorsMap.white);
|
254 |
+
colorPicker.style.display = 'flex';
|
255 |
+
}
|
256 |
+
function hideColorPicker(){ colorPicker.style.display = 'none'; }
|
257 |
+
pickBlack.onclick = function(){
|
258 |
+
if (!colorsMap.black) { socket.emit('claim_color', { username, color:'black' }); pickBlack.classList.add('disabled'); pickWhite.classList.add('disabled'); }
|
259 |
+
};
|
260 |
+
pickWhite.onclick = function(){
|
261 |
+
if (!colorsMap.white) { socket.emit('claim_color', { username, color:'white' }); pickBlack.classList.add('disabled'); pickWhite.classList.add('disabled'); }
|
262 |
+
};
|
263 |
+
|
264 |
+
// Socket events
|
265 |
+
socket.on('init', function(data){
|
266 |
+
var size = parseInt(data.board_size,10);
|
267 |
+
if (!isNaN(size) && size>0){
|
268 |
+
boardSize = size;
|
269 |
+
boardEl.style.setProperty('--board-size', String(boardSize));
|
270 |
+
board = Array(boardSize).fill().map(()=>Array(boardSize).fill(null));
|
271 |
+
}
|
272 |
+
if (Array.isArray(data.board)){
|
273 |
+
for (var r=0;r<Math.min(boardSize,data.board.length);r++){
|
274 |
+
for (var c=0;c<Math.min(boardSize,data.board[r].length);c++){
|
275 |
+
var v = data.board[r][c];
|
276 |
+
board[r][c] = (v==='black'||v==='white')?v:null;
|
277 |
+
}
|
278 |
+
}
|
279 |
+
}
|
280 |
+
currentColor = data.current_player || 'black';
|
281 |
+
captured = data.captured || captured;
|
282 |
+
scores = data.scores || scores;
|
283 |
+
lastMove = data.last_move || null;
|
284 |
+
gameOver = !!data.game_over;
|
285 |
+
if (data.wins_by_user) winsByUser = data.wins_by_user;
|
286 |
+
|
287 |
+
boardSizeSelect.value = String(boardSize);
|
288 |
+
boardSizeSelect.disabled = true;
|
289 |
+
|
290 |
+
refreshNamesUI();
|
291 |
+
refreshCapturesUI();
|
292 |
+
computeLiveScores();
|
293 |
+
updateWinsUI();
|
294 |
+
updateTurnUI();
|
295 |
+
renderBoard();
|
296 |
+
winnerOverlay.classList.remove('show');
|
297 |
+
|
298 |
+
// hide sign-in only after successful init (i.e., password accepted)
|
299 |
+
signin.style.display = 'none';
|
300 |
+
});
|
301 |
+
|
302 |
+
socket.on('colors', function(map){
|
303 |
+
colorsMap = { black: map.black || null, white: map.white || null };
|
304 |
+
myColor = (colorsMap.black===username)?'black':(colorsMap.white===username)?'white':null;
|
305 |
+
if (myColor) save(LS_COLOR,myColor); else save(LS_COLOR,'');
|
306 |
+
|
307 |
+
if (!colorsMap.black && !colorsMap.white) showColorPicker();
|
308 |
+
else if (!myColor) showColorPicker();
|
309 |
+
else hideColorPicker();
|
310 |
+
|
311 |
+
refreshNamesUI();
|
312 |
+
updateWinsUI();
|
313 |
+
updateTurnUI();
|
314 |
+
});
|
315 |
+
|
316 |
+
socket.on('move', function(data){
|
317 |
+
var x = data.x, y = data.y;
|
318 |
+
var placed = (data.player==='black'||data.player==='white') ? data.player
|
319 |
+
: (data.next_player==='black'?'white':'black');
|
320 |
+
|
321 |
+
// Place locally
|
322 |
+
board[x][y] = placed; lastMove = {row:x,col:y};
|
323 |
+
|
324 |
+
// ALWAYS remove captured stones locally so the board visuals match
|
325 |
+
var removed = localCapture(x,y,placed);
|
326 |
+
|
327 |
+
// Then sync capture COUNTS from server if provided
|
328 |
+
if (data.captured) {
|
329 |
+
captured = data.captured;
|
330 |
+
} else if (removed > 0) {
|
331 |
+
if (placed === 'black') captured.white = (captured.white||0) + removed;
|
332 |
+
else captured.black = (captured.black||0) + removed;
|
333 |
+
}
|
334 |
+
refreshCapturesUI();
|
335 |
+
|
336 |
+
currentColor = data.next_player || (placed==='black'?'white':'black');
|
337 |
+
computeLiveScores();
|
338 |
+
updateTurnUI();
|
339 |
+
renderBoard();
|
340 |
+
});
|
341 |
+
|
342 |
+
socket.on('pass', function(data){
|
343 |
+
currentColor = data.next_player || (currentColor==='black'?'white':'black');
|
344 |
+
updateTurnUI();
|
345 |
+
});
|
346 |
+
|
347 |
+
socket.on('resign', function(data){
|
348 |
+
if (data.wins_by_user) winsByUser = data.wins_by_user;
|
349 |
+
scores = data.scores || scores;
|
350 |
+
updateWinsUI();
|
351 |
+
gameOver = true;
|
352 |
+
var winner = data.winner;
|
353 |
+
var winnerName = (winner==='black') ? (colorsMap.black||'Black') : (colorsMap.white||'White');
|
354 |
+
winnerText.textContent = winnerName + ' wins by resignation!';
|
355 |
+
winnerOverlay.classList.add('show');
|
356 |
+
});
|
357 |
+
|
358 |
+
socket.on('game_over', function(data){
|
359 |
+
scores = data.scores || scores;
|
360 |
+
if (data.wins_by_user) winsByUser = data.wins_by_user;
|
361 |
+
updateWinsUI();
|
362 |
+
updateScoresUI();
|
363 |
+
gameOver = true;
|
364 |
+
var b=scores.black|0, w=scores.white|0;
|
365 |
+
var winnerName = (b===w) ? "It's a draw!" : ((b>w)? (colorsMap.black||'Black')+' wins!' : (colorsMap.white||'White')+' wins!');
|
366 |
+
winnerText.textContent = winnerName;
|
367 |
+
winnerOverlay.classList.add('show');
|
368 |
+
});
|
369 |
+
|
370 |
+
socket.on('error', function(err){
|
371 |
+
alert((err && err.message)? err.message : 'Server error.');
|
372 |
+
});
|
373 |
+
|
374 |
+
// Controls
|
375 |
+
document.getElementById('new-game').onclick = function(){
|
376 |
+
if (!username){ alert('Sign in first.'); return; }
|
377 |
+
showColorPicker();
|
378 |
+
socket.emit('new_game', { player: username });
|
379 |
+
};
|
380 |
+
document.getElementById('pass-turn').onclick = function(){
|
381 |
+
if (!username) return;
|
382 |
+
if (!canPlayNow()){ alert('Not your turn.'); return; }
|
383 |
+
socket.emit('pass', { player: username });
|
384 |
+
};
|
385 |
+
document.getElementById('resign').onclick = function(){
|
386 |
+
if (!username) return;
|
387 |
+
socket.emit('resign', { player: username });
|
388 |
+
};
|
389 |
+
boardSizeSelect.onchange = function(e){
|
390 |
+
e.target.value = String(boardSize);
|
391 |
+
alert('Board size is controlled by the server.');
|
392 |
+
};
|
393 |
+
playAgainBtn.onclick = function(){ if (username) { showColorPicker(); socket.emit('new_game', { player: username }); } };
|
394 |
+
|
395 |
+
// Helpers
|
396 |
+
function canPlayNow(){
|
397 |
+
if (gameOver) return false;
|
398 |
+
if (!username) return false;
|
399 |
+
if (currentColor === 'black') return colorsMap.black === username;
|
400 |
+
if (currentColor === 'white') return colorsMap.white === username;
|
401 |
+
return false;
|
402 |
+
}
|
403 |
+
|
404 |
+
function refreshNamesUI(){
|
405 |
+
nameBlackEl.textContent = colorsMap.black ? (colorsMap.black + (colorsMap.black===username?' (You)':'')) : 'Black';
|
406 |
+
nameWhiteEl.textContent = colorsMap.white ? (colorsMap.white + (colorsMap.white===username?' (You)':'')) : 'White';
|
407 |
+
}
|
408 |
+
|
409 |
+
function updateTurnUI(){
|
410 |
+
var label = (currentColor==='black') ? (colorsMap.black||'Black') : (colorsMap.white||'White');
|
411 |
+
playerTurnEl.textContent = label;
|
412 |
+
if (currentColor==='black'){ pBlackEl.classList.add('active'); pWhiteEl.classList.remove('active'); }
|
413 |
+
else { pBlackEl.classList.remove('active'); pWhiteEl.classList.add('active'); }
|
414 |
+
}
|
415 |
+
|
416 |
+
function updateWinsUI(){
|
417 |
+
var bname = colorsMap.black, wname = colorsMap.white;
|
418 |
+
winsBlackEl.textContent = bname && winsByUser[bname] != null ? winsByUser[bname] : 0;
|
419 |
+
winsWhiteEl.textContent = wname && winsByUser[wname] != null ? winsByUser[wname] : 0;
|
420 |
+
}
|
421 |
+
|
422 |
+
function refreshCapturesUI(){
|
423 |
+
// black player's captures = captured.white; white player's captures = captured.black
|
424 |
+
capsBlackEl.textContent = captured.white||0;
|
425 |
+
capsWhiteEl.textContent = captured.black||0;
|
426 |
+
}
|
427 |
+
|
428 |
+
function computeLiveScores(){
|
429 |
+
var bs=0, ws=0;
|
430 |
+
for (var r=0;r<boardSize;r++) for (var c=0;c<boardSize;c++){
|
431 |
+
if (board[r][c]==='black') bs++; else if (board[r][c]==='white') ws++;
|
432 |
+
}
|
433 |
+
scores.black = bs + (captured.white||0);
|
434 |
+
scores.white = ws + (captured.black||0);
|
435 |
+
updateScoresUI();
|
436 |
+
}
|
437 |
+
|
438 |
+
function updateScoresUI(){
|
439 |
+
scoreBlackEl.textContent = scores.black||0;
|
440 |
+
scoreWhiteEl.textContent = scores.white||0;
|
441 |
+
}
|
442 |
+
|
443 |
+
// Board rendering (draw grid in every cell => inner edges present)
|
444 |
+
function renderBoard(){
|
445 |
+
boardEl.innerHTML = '';
|
446 |
+
for (var r = 0; r < boardSize; r++) {
|
447 |
+
for (var c = 0; c < boardSize; c++) {
|
448 |
+
(function(row, col){
|
449 |
+
var cell = document.createElement('div');
|
450 |
+
cell.className = 'cell';
|
451 |
+
|
452 |
+
var h = document.createElement('div'); h.className='grid-line horizontal'; cell.appendChild(h);
|
453 |
+
var v = document.createElement('div'); v.className='grid-line vertical'; cell.appendChild(v);
|
454 |
+
|
455 |
+
if (boardSize >= 13 &&
|
456 |
+
((row===3&&col===3) || (row===3&&col===boardSize-4) ||
|
457 |
+
(row===boardSize-4&&col===3) || (row===boardSize-4&&col===boardSize-4) ||
|
458 |
+
(row===Math.floor(boardSize/2)&&col===Math.floor(boardSize/2)) ||
|
459 |
+
(row===3&&col===Math.floor(boardSize/2)) || (row===boardSize-4&&col===Math.floor(boardSize/2)) ||
|
460 |
+
(row===Math.floor(boardSize/2)&&col===3) || (row===Math.floor(boardSize/2)&&col===boardSize-4))) {
|
461 |
+
var star = document.createElement('div'); star.className='star-point'; cell.appendChild(star);
|
462 |
+
}
|
463 |
+
|
464 |
+
if (board[row][col]) {
|
465 |
+
var stone = document.createElement('div');
|
466 |
+
stone.className = 'stone ' + board[row][col];
|
467 |
+
if (lastMove && lastMove.row===row && lastMove.col===col) stone.classList.add('last-move');
|
468 |
+
cell.appendChild(stone);
|
469 |
+
}
|
470 |
+
|
471 |
+
cell.addEventListener('click', function(){
|
472 |
+
if (!canPlayNow()) { alert('Not your turn / pick a color first.'); return; }
|
473 |
+
if (board[row][col]) return;
|
474 |
+
socket.emit('move', { x: row, y: col, player: username });
|
475 |
+
});
|
476 |
+
|
477 |
+
boardEl.appendChild(cell);
|
478 |
+
})(r, c);
|
479 |
+
}
|
480 |
+
}
|
481 |
+
}
|
482 |
+
|
483 |
+
// Local capture helpers (visual update)
|
484 |
+
function hasLibertiesAt(row,col,color){
|
485 |
+
var vis = Array(boardSize).fill().map(()=>Array(boardSize).fill(false));
|
486 |
+
return dfs(row,col,color,vis);
|
487 |
+
}
|
488 |
+
function dfs(r,c,color,vis){
|
489 |
+
if (r<0||r>=boardSize||c<0||c>=boardSize) return false;
|
490 |
+
if (vis[r][c]) return false;
|
491 |
+
if (board[r][c]===null) return true;
|
492 |
+
if (board[r][c]!==color) return false;
|
493 |
+
vis[r][c]=true;
|
494 |
+
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);
|
495 |
+
}
|
496 |
+
function floodRemove(r,c,color,vis){
|
497 |
+
if (r<0||r>=boardSize||c<0||c>=boardSize) return 0;
|
498 |
+
if (vis[r][c] || board[r][c]!==color) return 0;
|
499 |
+
vis[r][c]=true; board[r][c]=null;
|
500 |
+
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);
|
501 |
+
}
|
502 |
+
function localCapture(r,c,placed){
|
503 |
+
var opp=(placed==='black')?'white':'black';
|
504 |
+
var removed=0;
|
505 |
+
var dirs=[[-1,0],[1,0],[0,-1],[0,1]];
|
506 |
+
for (var i=0;i<dirs.length;i++){
|
507 |
+
var nr=r+dirs[i][0], nc=c+dirs[i][1];
|
508 |
+
if (nr>=0&&nr<boardSize&&nc>=0&&nc<boardSize&&board[nr][nc]===opp){
|
509 |
+
if (!hasLibertiesAt(nr,nc,opp)){
|
510 |
+
var vis=Array(boardSize).fill().map(()=>Array(boardSize).fill(false));
|
511 |
+
removed += floodRemove(nr,nc,opp,vis);
|
512 |
+
}
|
513 |
+
}
|
514 |
+
}
|
515 |
+
return removed;
|
516 |
+
}
|
517 |
+
|
518 |
+
// Boot
|
519 |
+
(function(){
|
520 |
+
renderBoard();
|
521 |
+
updateTurnUI();
|
522 |
+
updateWinsUI();
|
523 |
+
updateScoresUI();
|
524 |
+
})();
|
525 |
+
</script>
|
526 |
+
</body>
|
527 |
+
</html>
|