-
- Share
- Explore
- Block Explorer (BSV)
- Blockchair
- WhatsOnChain
- ViaWallet
- BCHSVExplorer
这是可以长方形围棋的代码,可以自定义长宽、贴目、计时读秒、棋盘颜色。HTML文件,可在电脑上直接玩,草稿版,简陋请见谅,形势分析等复杂功能都没有。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>彩韵·自定义长方形围棋</title>
<style>
:root {
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--panel-bg: rgba(255, 255, 255, 0.85);
--text-main: #2d3748;
--text-light: #718096;
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
--shadow-glow: 0 0 20px rgba(118, 75, 162, 0.4);
}
body {
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
margin: 0;
padding: 0;
background: var(--primary-gradient);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
color: var(--text-main);
overflow-x: hidden;
}
/* 顶部标题 */
header {
text-align: center;
color: white;
padding: 20px 0;
text-shadow: 0 2px 4px rgba(0,0,0,0.2);
animation: fadeInDown 0.8s ease-out;
}
header h1 {
margin: 0;
font-size: 2.5rem;
letter-spacing: 2px;
font-weight: 300;
}
header p {
margin-top: 8px;
opacity: 0.9;
font-size: 1rem;
}
/* 主容器 */
.main-container {
display: flex;
flex-wrap: wrap;
gap: 30px;
justify-content: center;
align-items: flex-start;
width: 95%;
max-width: 1400px;
padding-bottom: 40px;
animation: fadeInUp 0.8s ease-out 0.2s backwards;
}
/* 控制面板 - 玻璃拟态 */
.control-panel {
background: var(--panel-bg);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-radius: 20px;
padding: 25px;
width: 320px;
box-shadow: var(--shadow-lg);
border: 1px solid rgba(255, 255, 255, 0.4);
display: flex;
flex-direction: column;
gap: 18px;
transition: transform 0.3s ease;
}
.control-panel:hover {
transform: translateY(-5px);
}
.panel-title {
font-size: 1.2rem;
font-weight: bold;
color: #4a5568;
border-bottom: 2px solid #e2e8f0;
padding-bottom: 10px;
margin-bottom: 5px;
display: flex;
align-items: center;
gap: 8px;
}
.input-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.input-row {
display: flex;
gap: 10px;
}
label {
font-size: 0.85rem;
color: var(--text-light);
font-weight: 600;
}
input[type=number], select {
padding: 10px;
border: 1px solid #cbd5e0;
border-radius: 8px;
background: white;
color: var(--text-main);
font-size: 0.95rem;
transition: all 0.2s;
width: 100%;
box-sizing: border-box;
}
input[type=number]:focus, select:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
}
input[type=color] {
width: 100%;
height: 40px;
border: none;
border-radius: 8px;
cursor: pointer;
background: none;
}
/* 按钮样式 */
.btn {
padding: 12px;
border: none;
border-radius: 10px;
font-weight: bold;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s;
text-transform: uppercase;
letter-spacing: 1px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(118, 75, 162, 0.3);
}
.btn-secondary {
background: #edf2f7;
color: #4a5568;
}
.btn-secondary:hover {
background: #e2e8f0;
}
.btn-danger {
background: #fc8181;
color: white;
}
.btn-danger:hover {
background: #f56565;
}
/* 游戏区域 */
.game-wrapper {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
background: var(--panel-bg);
padding: 20px;
border-radius: 20px;
box-shadow: var(--shadow-lg);
border: 1px solid rgba(255, 255, 255, 0.4);
backdrop-filter: blur(12px);
}
.status-bar {
display: flex;
justify-content: space-between;
width: 100%;
background: white;
padding: 15px 25px;
border-radius: 15px;
box-shadow: inset 0 2px 4px rgba(0,0,0,0.05);
box-sizing: border-box;
}
.player-card {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 16px;
border-radius: 12px;
transition: all 0.3s;
border: 2px solid transparent;
}
.player-card.active {
background: #ebf4ff;
border-color: #667eea;
transform: scale(1.05);
box-shadow: 0 4px 10px rgba(102, 126, 234, 0.2);
}
.stone-icon {
width: 24px;
height: 24px;
border-radius: 50%;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.stone-black { background: radial-gradient(circle at 30% 30%, #555, #000); }
.stone-white { background: radial-gradient(circle at 30% 30%, #fff, #ddd); border: 1px solid #ccc; }
.timer-display {
font-family: 'Courier New', monospace;
font-size: 1.4rem;
font-weight: bold;
color: #2d3748;
min-width: 80px;
text-align: right;
}
.timer-display.urgent {
color: #e53e3e;
animation: pulse 1s infinite;
}
canvas {
border-radius: 8px;
box-shadow: 0 15px 30px rgba(0,0,0,0.2);
cursor: crosshair;
transition: filter 0.3s;
max-width: 100%;
height: auto;
}
canvas:hover {
filter: brightness(1.02);
}
.log-panel {
width: 100%;
height: 120px;
background: #f7fafc;
border-radius: 10px;
padding: 10px;
overflow-y: auto;
font-size: 0.85rem;
color: #4a5568;
border: 1px solid #e2e8f0;
box-sizing: border-box;
}
.log-item {
margin-bottom: 4px;
border-bottom: 1px dashed #edf2f7;
padding-bottom: 2px;
}
/* 模态框 */
.modal-overlay {
display: none;
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(5px);
z-index: 1000;
justify-content: center;
align-items: center;
animation: fadeIn 0.3s;
}
.modal-content {
background: white;
padding: 40px;
border-radius: 20px;
text-align: center;
max-width: 500px;
box-shadow: 0 20px 40px rgba(0,0,0,0.3);
animation: slideIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.result-title {
font-size: 2rem;
margin-bottom: 10px;
background: var(--primary-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: 800;
}
.result-score {
font-size: 1.2rem;
color: #4a5568;
margin: 20px 0;
line-height: 1.6;
background: #f7fafc;
padding: 15px;
border-radius: 10px;
}
/* 动画 */
@keyframes fadeInDown { from { opacity: 0; transform: translateY(-20px); } to { opacity: 1; transform: translateY(0); } }
@keyframes fadeInUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes slideIn { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } }
@keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.6; } 100% { opacity: 1; } }
/* 滚动条美化 */
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 4px; }
::-webkit-scrollbar-thumb { background: #cbd5e0; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #a0aec0; }
</style>
</head>
<body>
<header>
<h1>自定义长方形围棋</h1>
<p>自定义长宽 | 自定义贴目 | 棋盘颜色个性化</p>
</header>
<div class="main-container">
<!-- 左侧控制面板 -->
<div class="control-panel">
<div class="panel-title">⚙️ 游戏设置</div>
<div class="input-row">
<div class="input-group" style="flex:1">
<label>宽度 (列)</label>
<input type="number" id="boardWidth" value="19" min="5" max="30">
</div>
<div class="input-group" style="flex:1">
<label>高度 (行)</label>
<input type="number" id="boardHeight" value="13" min="5" max="30">
</div>
</div>
<div class="input-group">
<label>贴目 (白方)</label>
<input type="number" id="komi" value="7.5" step="0.5">
</div>
<div class="input-row">
<div class="input-group" style="flex:1">
<label>基本时间 (分)</label>
<input type="number" id="mainTime" value="10" min="1">
</div>
<div class="input-group" style="flex:1">
<label>读秒次数</label>
<input type="number" id="byoyomiCount" value="5" min="0">
</div>
</div>
<div class="input-group">
<label>读秒时长 (秒/次)</label>
<input type="number" id="byoyomiTime" value="30" min="5">
</div>
<div class="input-group">
<label>棋盘底色</label>
<input type="color" id="boardColor" value="#EBCD96">
</div>
<div class="input-group">
<label>网格线颜色</label>
<input type="color" id="lineColor" value="#5D4037">
</div>
<button class="btn btn-primary" onclick="initGame()">🚀 开始新对局</button>
<button class="btn btn-secondary" onclick="passTurn()">⏭️ 停一手 (Pass)</button>
<button class="btn btn-danger" onclick="endGame()">🏁 结束并数子</button>
</div>
<!-- 右侧游戏区 -->
<div class="game-wrapper">
<div class="status-bar">
<div class="player-card active" id="blackCard">
<div class="stone-icon stone-black"></div>
<div>
<div style="font-weight:bold; font-size:0.9rem;">黑方 (Black)</div>
<div style="font-size:0.75rem; color:#718096;">先手</div>
</div>
<div class="timer-display" id="blackTimer">10:00</div>
</div>
<div style="font-weight:bold; color:#cbd5e0; font-size:1.2rem;">VS</div>
<div class="player-card" id="whiteCard">
<div class="stone-icon stone-white"></div>
<div>
<div style="font-weight:bold; font-size:0.9rem;">白方 (White)</div>
<div style="font-size:0.75rem; color:#718096;">后手 (+<span id="komiDisplay">7.5</span>)</div>
</div>
<div class="timer-display" id="whiteTimer">10:00</div>
</div>
</div>
<canvas id="goBoard"></canvas>
<div class="log-panel" id="gameLog">
<div class="log-item">👋 欢迎!请设置长宽后点击“开始新对局”。</div>
</div>
</div>
</div>
<!-- 结果弹窗 -->
<div id="resultModal" class="modal-overlay">
<div class="modal-content">
<h2 class="result-title" id="resultTitle">胜负已分</h2>
<div class="result-score" id="resultDetail"></div>
<button class="btn btn-primary" onclick="document.getElementById('resultModal').style.display='none'">查看棋盘</button>
<button class="btn btn-secondary" onclick="initGame()">再来一局</button>
</div>
</div>
<script>
/**
* 长方形围棋核心逻辑
*/
const canvas = document.getElementById('goBoard');
const ctx = canvas.getContext('2d');
// 游戏状态
let width = 19;
let height = 13;
let board = [];
let currentPlayer = 1; // 1: Black, 2: White
let komi = 7.5;
let gameOver = false;
let lastMove = null;
let capturedStones = {1: 0, 2: 0};
let moveCount = 0;
// 计时器
let timers = {};
let timerInterval = null;
// 配置
let config = {
boardColor: '#EBCD96',
lineColor: '#5D4037',
starRatio: 0.35 // 星位大致比例位置
};
function initGame() {
// 获取设置
width = parseInt(document.getElementById('boardWidth').value);
height = parseInt(document.getElementById('boardHeight').value);
komi = parseFloat(document.getElementById('komi').value);
const mainTimeMin = parseInt(document.getElementById('mainTime').value);
const byoyomiCount = parseInt(document.getElementById('byoyomiCount').value);
const byoyomiTime = parseInt(document.getElementById('byoyomiTime').value);
config.boardColor = document.getElementById('boardColor').value;
config.lineColor = document.getElementById('lineColor').value;
document.getElementById('komiDisplay').innerText = komi;
// 重置状态
board = Array(height).fill().map(() => Array(width).fill(0));
currentPlayer = 1;
gameOver = false;
lastMove = null;
capturedStones = {1: 0, 2: 0};
moveCount = 0;
// 重置计时器
const mainSeconds = mainTimeMin * 60;
timers = {
1: { main: mainSeconds, byoyomiLeft: byoyomiCount, byoyomiDuration: byoyomiTime, isByoyomi: false },
2: { main: mainSeconds, byoyomiLeft: byoyomiCount, byoyomiDuration: byoyomiTime, isByoyomi: false }
};
// UI 更新
document.getElementById('resultModal').style.display = 'none';
document.getElementById('gameLog').innerHTML = '<div class="log-item">🎲 新对局开始!棋盘尺寸:' + width + 'x' + height + '</div>';
updatePlayerUI();
resizeCanvas();
drawBoard();
if (timerInterval) clearInterval(timerInterval);
timerInterval = setInterval(gameTimerTick, 1000);
}
function resizeCanvas() {
// 计算合适的显示尺寸,保持纵横比
const maxWidth = window.innerWidth > 1000 ? window.innerWidth * 0.6 : window.innerWidth * 0.9;
const maxHeight = window.innerHeight * 0.6;
const aspectRatio = width / height;
let displayWidth = maxWidth;
let displayHeight = displayWidth / aspectRatio;
if (displayHeight > maxHeight) {
displayHeight = maxHeight;
displayWidth = displayHeight * aspectRatio;
}
// 设置 Canvas 实际分辨率 (高分屏优化)
const dpr = window.devicePixelRatio || 1;
canvas.style.width = `${displayWidth}px`;
canvas.style.height = `${displayHeight}px`;
canvas.width = displayWidth * dpr;
canvas.height = displayHeight * dpr;
ctx.scale(dpr, dpr);
// 存储逻辑绘图尺寸
canvas.logicalWidth = displayWidth;
canvas.logicalHeight = displayHeight;
drawBoard();
}
window.addEventListener('resize', () => {
if(!gameOver) resizeCanvas();
});
function drawBoard() {
const w = canvas.logicalWidth;
const h = canvas.logicalHeight;
// 计算网格参数
// 留边距为半个格子大小,确保边缘也有空间
const cellW = w / (width + 1);
const cellH = h / (height + 1);
const paddingX = cellW;
const paddingY = cellH;
ctx.clearRect(0, 0, w, h);
// 背景
ctx.fillStyle = config.boardColor;
// 绘制圆角矩形背景
ctx.beginPath();
ctx.roundRect(0, 0, w, h, 10);
ctx.fill();
// 阴影效果让棋盘更有质感
ctx.shadowColor = "rgba(0,0,0,0.2)";
ctx.shadowBlur = 10;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 5;
ctx.fill();
ctx.shadowColor = "transparent"; // 重置
// 网格线
ctx.strokeStyle = config.lineColor;
ctx.lineWidth = Math.max(1, Math.min(cellW, cellH) * 0.05);
ctx.beginPath();
for (let i = 0; i < height; i++) {
const y = paddingY + i * cellH;
ctx.moveTo(paddingX, y);
ctx.lineTo(w - paddingX, y);
}
for (let i = 0; i < width; i++) {
const x = paddingX + i * cellW;
ctx.moveTo(x, paddingY);
ctx.lineTo(x, h - paddingY);
}
ctx.stroke();
// 星位 (简单算法:在长宽的 1/4, 1/2, 3/4 处尝试放置)
ctx.fillStyle = config.lineColor;
const starPoints = [];
const xSteps = [0.25, 0.5, 0.75];
const ySteps = [0.25, 0.5, 0.75];
xSteps.forEach(xRatio => {
ySteps.forEach(yRatio => {
const gx = Math.round((width - 1) * xRatio);
const gy = Math.round((height - 1) * yRatio);
// 避免重复和边缘
if (!starPoints.some(p => p.x === gx && p.y === gy)) {
starPoints.push({x: gx, y: gy});
}
});
});
starPoints.forEach(pt => {
const cx = paddingX + pt.x * cellW;
const cy = paddingY + pt.y * cellH;
ctx.beginPath();
ctx.arc(cx, cy, Math.max(3, Math.min(cellW, cellH) * 0.15), 0, Math.PI * 2);
ctx.fill();
});
// 绘制棋子
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
if (board[y][x] !== 0) {
drawStone(x, y, board[y][x], cellW, cellH, paddingX, paddingY);
}
}
}
// 标记最后一步
if (lastMove) {
const cx = paddingX + lastMove.x * cellW;
const cy = paddingY + lastMove.y * cellH;
ctx.strokeStyle = currentPlayer === 1 ? "#FFF" : "#000"; // 反色标记
ctx.lineWidth = 2;
ctx.beginPath();
// 画一个小十字或圆圈
const markSize = Math.min(cellW, cellH) * 0.3;
ctx.arc(cx, cy, markSize, 0, Math.PI * 2);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(cx - 2, cy); ctx.lineTo(cx + 2, cy);
ctx.moveTo(cx, cy - 2); ctx.lineTo(cx, cy + 2);
ctx.stroke();
}
}
function drawStone(x, y, color, cellW, cellH, padX, padY) {
const cx = padX + x * cellW;
const cy = padY + y * cellH;
// 棋子半径略小于格子短边的一半
const radius = Math.min(cellW, cellH) * 0.45;
ctx.beginPath();
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
// 渐变光影
const grad = ctx.createRadialGradient(
cx - radius * 0.3, cy - radius * 0.3, radius * 0.1,
cx, cy, radius
);
if (color === 1) { // Black
grad.addColorStop(0, "#666");
grad.addColorStop(1, "#000");
} else { // White
grad.addColorStop(0, "#fff");
grad.addColorStop(1, "#dcdcdc");
}
ctx.fillStyle = grad;
// 棋子阴影
ctx.shadowColor = "rgba(0,0,0,0.4)";
ctx.shadowBlur = 6;
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
ctx.fill();
// 重置阴影
ctx.shadowColor = "transparent";
ctx.shadowBlur = 0;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
}
// 交互事件
canvas.addEventListener('click', (e) => {
if (gameOver) return;
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.logicalWidth / rect.width;
const scaleY = canvas.logicalHeight / rect.height;
const x = (e.clientX - rect.left) * scaleX;
const y = (e.clientY - rect.top) * scaleY;
const cellW = canvas.logicalWidth / (width + 1);
const cellH = canvas.logicalHeight / (height + 1);
const padX = cellW;
const padY = cellH;
// 计算最近的交叉点
const gridX = Math.round((x - padX) / cellW);
const gridY = Math.round((y - padY) / cellH);
if (gridX >= 0 && gridX < width && gridY >= 0 && gridY < height) {
placeStone(gridX, gridY);
}
});
function placeStone(x, y) {
if (board[y][x] !== 0) {
log("❌ 此处已有棋子。");
return;
}
const opponent = currentPlayer === 1 ? 2 : 1;
board[y][x] = currentPlayer;
// 检查提子
const captured = checkCaptures(x, y, opponent);
// 检查自杀
if (captured.length === 0 && !hasLiberties(x, y, currentPlayer)) {
board[y][x] = 0;
log("❌ 禁着点:不能自杀。");
return;
}
// 执行落子
capturedStones[currentPlayer] += captured.length;
lastMove = {x, y};
moveCount++;
// 切换
currentPlayer = opponent;
drawBoard();
const colorName = currentPlayer === 1 ? "黑方" : "白方";
const coord = `(${x+1}, ${y+1})`;
let msg = `第 ${moveCount} 手:${colorName} 落子 ${coord}`;
if (captured.length > 0) msg += ` ⚔️ 提掉 ${captured.length} 子`;
log(msg);
updatePlayerUI();
}
function checkCaptures(x, y, opponentColor) {
const directions = [[0,1], [0,-1], [1,0], [-1,0]];
let capturedList = [];
directions.forEach(([dx, dy]) => {
const nx = x + dx;
const ny = y + dy;
if (nx >= 0 && nx < width && ny >= 0 && ny < height && board[ny][nx] === opponentColor) {
if (!hasLiberties(nx, ny, opponentColor)) {
removeGroup(nx, ny, opponentColor, capturedList);
}
}
});
return capturedList;
}
function hasLiberties(startX, startY, color) {
const visited = new Set();
const stack = [[startX, startY]];
const directions = [[0,1], [0,-1], [1,0], [-1,0]];
while (stack.length > 0) {
const [x, y] = stack.pop();
const key = `${x},${y}`;
if (visited.has(key)) continue;
visited.add(key);
for (let [dx, dy] of directions) {
const nx = x + dx;
const ny = y + dy;
if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
if (board[ny][nx] === 0) return true;
if (board[ny][nx] === color && !visited.has(`${nx},${ny}`)) {
stack.push([nx, ny]);
}
}
}
}
return false;
}
function removeGroup(startX, startY, color, capturedList) {
const visited = new Set();
const stack = [[startX, startY]];
const directions = [[0,1], [0,-1], [1,0], [-1,0]];
while (stack.length > 0) {
const [x, y] = stack.pop();
const key = `${x},${y}`;
if (visited.has(key)) continue;
visited.add(key);
board[y][x] = 0;
capturedList.push({x, y});
for (let [dx, dy] of directions) {
const nx = x + dx;
const ny = y + dy;
if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
if (board[ny][nx] === color && !visited.has(`${nx},${ny}`)) {
stack.push([nx, ny]);
}
}
}
}
}
function passTurn() {
if (gameOver) return;
log(`⏭️ 第 ${moveCount + 1} 手:${currentPlayer === 1 ? "黑方" : "白方"} 停一手。`);
lastMove = null;
currentPlayer = currentPlayer === 1 ? 2 : 1;
moveCount++;
updatePlayerUI();
drawBoard();
}
// 计时逻辑
function gameTimerTick() {
if (gameOver) return;
const t = timers[currentPlayer];
if (t.isByoyomi) {
t.main--;
if (t.main <= 0) {
t.byoyomiLeft--;
if (t.byoyomiLeft < 0) {
endGameByTimeout();
return;
}
t.main = t.byoyomiDuration;
log(`⏰ ${currentPlayer===1?"黑方":"白方"} 进入下一次读秒,剩余 ${t.byoyomiLeft} 次`);
}
} else {
t.main--;
if (t.main <= 0) {
if (t.byoyomiLeft > 0) {
t.isByoyomi = true;
t.main = t.byoyomiDuration;
log(`⏰ ${currentPlayer===1?"黑方":"白方"} 基本时间用完,进入读秒!`);
} else {
endGameByTimeout();
return;
}
}
}
updateTimerDisplay();
}
function endGameByTimeout() {
gameOver = true;
clearInterval(timerInterval);
const winner = currentPlayer === 1 ? "白方" : "黑方";
showResult(`${winner}获胜!`, `${currentPlayer===1?"黑方":"白方"} 时间耗尽。`);
}
function updateTimerDisplay() {
for (let p of [1, 2]) {
const t = timers[p];
let timeStr = "";
let isUrgent = false;
if (t.isByoyomi) {
timeStr = `${t.byoyomiLeft}次 ${t.main}s`;
if (t.main <= 5) isUrgent = true;
} else {
const m = Math.floor(t.main / 60);
const s = t.main % 60;
timeStr = `${m}:${s.toString().padStart(2, '0')}`;
if (t.main <= 60) isUrgent = true;
}
const el = document.getElementById(p === 1 ? 'blackTimer' : 'whiteTimer');
el.innerText = timeStr;
if (isUrgent && p === currentPlayer && !gameOver) el.classList.add('urgent');
else el.classList.remove('urgent');
}
}
function updatePlayerUI() {
updateTimerDisplay();
document.getElementById('blackCard').classList.toggle('active', currentPlayer === 1 && !gameOver);
document.getElementById('whiteCard').classList.toggle('active', currentPlayer === 2 && !gameOver);
}
// 数子逻辑 (适配长方形)
function countScore() {
let blackScore = 0;
let whiteScore = 0;
const visited = Array(height).fill().map(() => Array(width).fill(false));
const directions = [[0,1], [0,-1], [1,0], [-1,0]];
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
if (board[y][x] !== 0) {
if (board[y][x] === 1) blackScore++;
else whiteScore++;
continue;
}
if (!visited[y][x]) {
let area = 0;
let owners = new Set();
let stack = [[x, y]];
visited[y][x] = true;
while(stack.length > 0) {
const [cx, cy] = stack.pop();
area++;
for (let [dx, dy] of directions) {
const nx = cx + dx;
const ny = cy + dy;
if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
if (board[ny][nx] === 0 && !visited[ny][nx]) {
visited[ny][nx] = true;
stack.push([nx, ny]);
} else if (board[ny][nx] !== 0) {
owners.add(board[ny][nx]);
}
}
}
}
if (owners.size === 1) {
if (owners.has(1)) blackScore += area;
else if (owners.has(2)) whiteScore += area;
}
}
}
}
return { black: blackScore, white: whiteScore + komi };
}
function endGame() {
if (gameOver) return;
if (!confirm("确定结束游戏?\n请确保死子已被提走,否则会影响数子结果。")) return;
gameOver = true;
clearInterval(timerInterval);
const scores = countScore();
const diff = scores.black - scores.white;
let title = "";
let detail = `<strong>黑方地盘:</strong> ${scores.black}<br>`;
detail += `<strong>白方地盘 (含贴目${komi}):</strong> ${scores.white}<br><hr>`;
if (diff > 0.01) {
title = "🏆 黑方胜!";
detail += `黑方胜出 ${diff.toFixed(1)} 子`;
} else if (diff < -0.01) {
title = "🏆 白方胜!";
detail += `白方胜出 ${Math.abs(diff).toFixed(1)} 子`;
} else {
title = "🤝 和棋 (Jigo)!";
detail += "双方地盘完全相同。";
}
showResult(title, detail);
}
function showResult(title, detail) {
document.getElementById('resultTitle').innerText = title;
document.getElementById('resultDetail').innerHTML = detail;
document.getElementById('resultModal').style.display = 'flex';
log("🏁 游戏结束。" + title);
}
function log(msg) {
const logEl = document.getElementById('gameLog');
const div = document.createElement('div');
div.className = 'log-item';
div.innerText = `[${new Date().toLocaleTimeString()}] ${msg}`;
logEl.prepend(div);
}
// 初始化
resizeCanvas();
// 预绘制一次空棋盘
ctx.fillStyle = config.boardColor;
ctx.fillRect(0,0, canvas.logicalWidth, canvas.logicalHeight);
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>彩韵·自定义长方形围棋</title>
<style>
:root {
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--panel-bg: rgba(255, 255, 255, 0.85);
--text-main: #2d3748;
--text-light: #718096;
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
--shadow-glow: 0 0 20px rgba(118, 75, 162, 0.4);
}
body {
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
margin: 0;
padding: 0;
background: var(--primary-gradient);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
color: var(--text-main);
overflow-x: hidden;
}
/* 顶部标题 */
header {
text-align: center;
color: white;
padding: 20px 0;
text-shadow: 0 2px 4px rgba(0,0,0,0.2);
animation: fadeInDown 0.8s ease-out;
}
header h1 {
margin: 0;
font-size: 2.5rem;
letter-spacing: 2px;
font-weight: 300;
}
header p {
margin-top: 8px;
opacity: 0.9;
font-size: 1rem;
}
/* 主容器 */
.main-container {
display: flex;
flex-wrap: wrap;
gap: 30px;
justify-content: center;
align-items: flex-start;
width: 95%;
max-width: 1400px;
padding-bottom: 40px;
animation: fadeInUp 0.8s ease-out 0.2s backwards;
}
/* 控制面板 - 玻璃拟态 */
.control-panel {
background: var(--panel-bg);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-radius: 20px;
padding: 25px;
width: 320px;
box-shadow: var(--shadow-lg);
border: 1px solid rgba(255, 255, 255, 0.4);
display: flex;
flex-direction: column;
gap: 18px;
transition: transform 0.3s ease;
}
.control-panel:hover {
transform: translateY(-5px);
}
.panel-title {
font-size: 1.2rem;
font-weight: bold;
color: #4a5568;
border-bottom: 2px solid #e2e8f0;
padding-bottom: 10px;
margin-bottom: 5px;
display: flex;
align-items: center;
gap: 8px;
}
.input-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.input-row {
display: flex;
gap: 10px;
}
label {
font-size: 0.85rem;
color: var(--text-light);
font-weight: 600;
}
input[type=number], select {
padding: 10px;
border: 1px solid #cbd5e0;
border-radius: 8px;
background: white;
color: var(--text-main);
font-size: 0.95rem;
transition: all 0.2s;
width: 100%;
box-sizing: border-box;
}
input[type=number]:focus, select:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
}
input[type=color] {
width: 100%;
height: 40px;
border: none;
border-radius: 8px;
cursor: pointer;
background: none;
}
/* 按钮样式 */
.btn {
padding: 12px;
border: none;
border-radius: 10px;
font-weight: bold;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s;
text-transform: uppercase;
letter-spacing: 1px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(118, 75, 162, 0.3);
}
.btn-secondary {
background: #edf2f7;
color: #4a5568;
}
.btn-secondary:hover {
background: #e2e8f0;
}
.btn-danger {
background: #fc8181;
color: white;
}
.btn-danger:hover {
background: #f56565;
}
/* 游戏区域 */
.game-wrapper {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
background: var(--panel-bg);
padding: 20px;
border-radius: 20px;
box-shadow: var(--shadow-lg);
border: 1px solid rgba(255, 255, 255, 0.4);
backdrop-filter: blur(12px);
}
.status-bar {
display: flex;
justify-content: space-between;
width: 100%;
background: white;
padding: 15px 25px;
border-radius: 15px;
box-shadow: inset 0 2px 4px rgba(0,0,0,0.05);
box-sizing: border-box;
}
.player-card {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 16px;
border-radius: 12px;
transition: all 0.3s;
border: 2px solid transparent;
}
.player-card.active {
background: #ebf4ff;
border-color: #667eea;
transform: scale(1.05);
box-shadow: 0 4px 10px rgba(102, 126, 234, 0.2);
}
.stone-icon {
width: 24px;
height: 24px;
border-radius: 50%;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.stone-black { background: radial-gradient(circle at 30% 30%, #555, #000); }
.stone-white { background: radial-gradient(circle at 30% 30%, #fff, #ddd); border: 1px solid #ccc; }
.timer-display {
font-family: 'Courier New', monospace;
font-size: 1.4rem;
font-weight: bold;
color: #2d3748;
min-width: 80px;
text-align: right;
}
.timer-display.urgent {
color: #e53e3e;
animation: pulse 1s infinite;
}
canvas {
border-radius: 8px;
box-shadow: 0 15px 30px rgba(0,0,0,0.2);
cursor: crosshair;
transition: filter 0.3s;
max-width: 100%;
height: auto;
}
canvas:hover {
filter: brightness(1.02);
}
.log-panel {
width: 100%;
height: 120px;
background: #f7fafc;
border-radius: 10px;
padding: 10px;
overflow-y: auto;
font-size: 0.85rem;
color: #4a5568;
border: 1px solid #e2e8f0;
box-sizing: border-box;
}
.log-item {
margin-bottom: 4px;
border-bottom: 1px dashed #edf2f7;
padding-bottom: 2px;
}
/* 模态框 */
.modal-overlay {
display: none;
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(5px);
z-index: 1000;
justify-content: center;
align-items: center;
animation: fadeIn 0.3s;
}
.modal-content {
background: white;
padding: 40px;
border-radius: 20px;
text-align: center;
max-width: 500px;
box-shadow: 0 20px 40px rgba(0,0,0,0.3);
animation: slideIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.result-title {
font-size: 2rem;
margin-bottom: 10px;
background: var(--primary-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
font-weight: 800;
}
.result-score {
font-size: 1.2rem;
color: #4a5568;
margin: 20px 0;
line-height: 1.6;
background: #f7fafc;
padding: 15px;
border-radius: 10px;
}
/* 动画 */
@keyframes fadeInDown { from { opacity: 0; transform: translateY(-20px); } to { opacity: 1; transform: translateY(0); } }
@keyframes fadeInUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes slideIn { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } }
@keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.6; } 100% { opacity: 1; } }
/* 滚动条美化 */
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 4px; }
::-webkit-scrollbar-thumb { background: #cbd5e0; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #a0aec0; }
</style>
</head>
<body>
<header>
<h1>自定义长方形围棋</h1>
<p>自定义长宽 | 自定义贴目 | 棋盘颜色个性化</p>
</header>
<div class="main-container">
<!-- 左侧控制面板 -->
<div class="control-panel">
<div class="panel-title">⚙️ 游戏设置</div>
<div class="input-row">
<div class="input-group" style="flex:1">
<label>宽度 (列)</label>
<input type="number" id="boardWidth" value="19" min="5" max="30">
</div>
<div class="input-group" style="flex:1">
<label>高度 (行)</label>
<input type="number" id="boardHeight" value="13" min="5" max="30">
</div>
</div>
<div class="input-group">
<label>贴目 (白方)</label>
<input type="number" id="komi" value="7.5" step="0.5">
</div>
<div class="input-row">
<div class="input-group" style="flex:1">
<label>基本时间 (分)</label>
<input type="number" id="mainTime" value="10" min="1">
</div>
<div class="input-group" style="flex:1">
<label>读秒次数</label>
<input type="number" id="byoyomiCount" value="5" min="0">
</div>
</div>
<div class="input-group">
<label>读秒时长 (秒/次)</label>
<input type="number" id="byoyomiTime" value="30" min="5">
</div>
<div class="input-group">
<label>棋盘底色</label>
<input type="color" id="boardColor" value="#EBCD96">
</div>
<div class="input-group">
<label>网格线颜色</label>
<input type="color" id="lineColor" value="#5D4037">
</div>
<button class="btn btn-primary" onclick="initGame()">🚀 开始新对局</button>
<button class="btn btn-secondary" onclick="passTurn()">⏭️ 停一手 (Pass)</button>
<button class="btn btn-danger" onclick="endGame()">🏁 结束并数子</button>
</div>
<!-- 右侧游戏区 -->
<div class="game-wrapper">
<div class="status-bar">
<div class="player-card active" id="blackCard">
<div class="stone-icon stone-black"></div>
<div>
<div style="font-weight:bold; font-size:0.9rem;">黑方 (Black)</div>
<div style="font-size:0.75rem; color:#718096;">先手</div>
</div>
<div class="timer-display" id="blackTimer">10:00</div>
</div>
<div style="font-weight:bold; color:#cbd5e0; font-size:1.2rem;">VS</div>
<div class="player-card" id="whiteCard">
<div class="stone-icon stone-white"></div>
<div>
<div style="font-weight:bold; font-size:0.9rem;">白方 (White)</div>
<div style="font-size:0.75rem; color:#718096;">后手 (+<span id="komiDisplay">7.5</span>)</div>
</div>
<div class="timer-display" id="whiteTimer">10:00</div>
</div>
</div>
<canvas id="goBoard"></canvas>
<div class="log-panel" id="gameLog">
<div class="log-item">👋 欢迎!请设置长宽后点击“开始新对局”。</div>
</div>
</div>
</div>
<!-- 结果弹窗 -->
<div id="resultModal" class="modal-overlay">
<div class="modal-content">
<h2 class="result-title" id="resultTitle">胜负已分</h2>
<div class="result-score" id="resultDetail"></div>
<button class="btn btn-primary" onclick="document.getElementById('resultModal').style.display='none'">查看棋盘</button>
<button class="btn btn-secondary" onclick="initGame()">再来一局</button>
</div>
</div>
<script>
/**
* 长方形围棋核心逻辑
*/
const canvas = document.getElementById('goBoard');
const ctx = canvas.getContext('2d');
// 游戏状态
let width = 19;
let height = 13;
let board = [];
let currentPlayer = 1; // 1: Black, 2: White
let komi = 7.5;
let gameOver = false;
let lastMove = null;
let capturedStones = {1: 0, 2: 0};
let moveCount = 0;
// 计时器
let timers = {};
let timerInterval = null;
// 配置
let config = {
boardColor: '#EBCD96',
lineColor: '#5D4037',
starRatio: 0.35 // 星位大致比例位置
};
function initGame() {
// 获取设置
width = parseInt(document.getElementById('boardWidth').value);
height = parseInt(document.getElementById('boardHeight').value);
komi = parseFloat(document.getElementById('komi').value);
const mainTimeMin = parseInt(document.getElementById('mainTime').value);
const byoyomiCount = parseInt(document.getElementById('byoyomiCount').value);
const byoyomiTime = parseInt(document.getElementById('byoyomiTime').value);
config.boardColor = document.getElementById('boardColor').value;
config.lineColor = document.getElementById('lineColor').value;
document.getElementById('komiDisplay').innerText = komi;
// 重置状态
board = Array(height).fill().map(() => Array(width).fill(0));
currentPlayer = 1;
gameOver = false;
lastMove = null;
capturedStones = {1: 0, 2: 0};
moveCount = 0;
// 重置计时器
const mainSeconds = mainTimeMin * 60;
timers = {
1: { main: mainSeconds, byoyomiLeft: byoyomiCount, byoyomiDuration: byoyomiTime, isByoyomi: false },
2: { main: mainSeconds, byoyomiLeft: byoyomiCount, byoyomiDuration: byoyomiTime, isByoyomi: false }
};
// UI 更新
document.getElementById('resultModal').style.display = 'none';
document.getElementById('gameLog').innerHTML = '<div class="log-item">🎲 新对局开始!棋盘尺寸:' + width + 'x' + height + '</div>';
updatePlayerUI();
resizeCanvas();
drawBoard();
if (timerInterval) clearInterval(timerInterval);
timerInterval = setInterval(gameTimerTick, 1000);
}
function resizeCanvas() {
// 计算合适的显示尺寸,保持纵横比
const maxWidth = window.innerWidth > 1000 ? window.innerWidth * 0.6 : window.innerWidth * 0.9;
const maxHeight = window.innerHeight * 0.6;
const aspectRatio = width / height;
let displayWidth = maxWidth;
let displayHeight = displayWidth / aspectRatio;
if (displayHeight > maxHeight) {
displayHeight = maxHeight;
displayWidth = displayHeight * aspectRatio;
}
// 设置 Canvas 实际分辨率 (高分屏优化)
const dpr = window.devicePixelRatio || 1;
canvas.style.width = `${displayWidth}px`;
canvas.style.height = `${displayHeight}px`;
canvas.width = displayWidth * dpr;
canvas.height = displayHeight * dpr;
ctx.scale(dpr, dpr);
// 存储逻辑绘图尺寸
canvas.logicalWidth = displayWidth;
canvas.logicalHeight = displayHeight;
drawBoard();
}
window.addEventListener('resize', () => {
if(!gameOver) resizeCanvas();
});
function drawBoard() {
const w = canvas.logicalWidth;
const h = canvas.logicalHeight;
// 计算网格参数
// 留边距为半个格子大小,确保边缘也有空间
const cellW = w / (width + 1);
const cellH = h / (height + 1);
const paddingX = cellW;
const paddingY = cellH;
ctx.clearRect(0, 0, w, h);
// 背景
ctx.fillStyle = config.boardColor;
// 绘制圆角矩形背景
ctx.beginPath();
ctx.roundRect(0, 0, w, h, 10);
ctx.fill();
// 阴影效果让棋盘更有质感
ctx.shadowColor = "rgba(0,0,0,0.2)";
ctx.shadowBlur = 10;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 5;
ctx.fill();
ctx.shadowColor = "transparent"; // 重置
// 网格线
ctx.strokeStyle = config.lineColor;
ctx.lineWidth = Math.max(1, Math.min(cellW, cellH) * 0.05);
ctx.beginPath();
for (let i = 0; i < height; i++) {
const y = paddingY + i * cellH;
ctx.moveTo(paddingX, y);
ctx.lineTo(w - paddingX, y);
}
for (let i = 0; i < width; i++) {
const x = paddingX + i * cellW;
ctx.moveTo(x, paddingY);
ctx.lineTo(x, h - paddingY);
}
ctx.stroke();
// 星位 (简单算法:在长宽的 1/4, 1/2, 3/4 处尝试放置)
ctx.fillStyle = config.lineColor;
const starPoints = [];
const xSteps = [0.25, 0.5, 0.75];
const ySteps = [0.25, 0.5, 0.75];
xSteps.forEach(xRatio => {
ySteps.forEach(yRatio => {
const gx = Math.round((width - 1) * xRatio);
const gy = Math.round((height - 1) * yRatio);
// 避免重复和边缘
if (!starPoints.some(p => p.x === gx && p.y === gy)) {
starPoints.push({x: gx, y: gy});
}
});
});
starPoints.forEach(pt => {
const cx = paddingX + pt.x * cellW;
const cy = paddingY + pt.y * cellH;
ctx.beginPath();
ctx.arc(cx, cy, Math.max(3, Math.min(cellW, cellH) * 0.15), 0, Math.PI * 2);
ctx.fill();
});
// 绘制棋子
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
if (board[y][x] !== 0) {
drawStone(x, y, board[y][x], cellW, cellH, paddingX, paddingY);
}
}
}
// 标记最后一步
if (lastMove) {
const cx = paddingX + lastMove.x * cellW;
const cy = paddingY + lastMove.y * cellH;
ctx.strokeStyle = currentPlayer === 1 ? "#FFF" : "#000"; // 反色标记
ctx.lineWidth = 2;
ctx.beginPath();
// 画一个小十字或圆圈
const markSize = Math.min(cellW, cellH) * 0.3;
ctx.arc(cx, cy, markSize, 0, Math.PI * 2);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(cx - 2, cy); ctx.lineTo(cx + 2, cy);
ctx.moveTo(cx, cy - 2); ctx.lineTo(cx, cy + 2);
ctx.stroke();
}
}
function drawStone(x, y, color, cellW, cellH, padX, padY) {
const cx = padX + x * cellW;
const cy = padY + y * cellH;
// 棋子半径略小于格子短边的一半
const radius = Math.min(cellW, cellH) * 0.45;
ctx.beginPath();
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
// 渐变光影
const grad = ctx.createRadialGradient(
cx - radius * 0.3, cy - radius * 0.3, radius * 0.1,
cx, cy, radius
);
if (color === 1) { // Black
grad.addColorStop(0, "#666");
grad.addColorStop(1, "#000");
} else { // White
grad.addColorStop(0, "#fff");
grad.addColorStop(1, "#dcdcdc");
}
ctx.fillStyle = grad;
// 棋子阴影
ctx.shadowColor = "rgba(0,0,0,0.4)";
ctx.shadowBlur = 6;
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
ctx.fill();
// 重置阴影
ctx.shadowColor = "transparent";
ctx.shadowBlur = 0;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
}
// 交互事件
canvas.addEventListener('click', (e) => {
if (gameOver) return;
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.logicalWidth / rect.width;
const scaleY = canvas.logicalHeight / rect.height;
const x = (e.clientX - rect.left) * scaleX;
const y = (e.clientY - rect.top) * scaleY;
const cellW = canvas.logicalWidth / (width + 1);
const cellH = canvas.logicalHeight / (height + 1);
const padX = cellW;
const padY = cellH;
// 计算最近的交叉点
const gridX = Math.round((x - padX) / cellW);
const gridY = Math.round((y - padY) / cellH);
if (gridX >= 0 && gridX < width && gridY >= 0 && gridY < height) {
placeStone(gridX, gridY);
}
});
function placeStone(x, y) {
if (board[y][x] !== 0) {
log("❌ 此处已有棋子。");
return;
}
const opponent = currentPlayer === 1 ? 2 : 1;
board[y][x] = currentPlayer;
// 检查提子
const captured = checkCaptures(x, y, opponent);
// 检查自杀
if (captured.length === 0 && !hasLiberties(x, y, currentPlayer)) {
board[y][x] = 0;
log("❌ 禁着点:不能自杀。");
return;
}
// 执行落子
capturedStones[currentPlayer] += captured.length;
lastMove = {x, y};
moveCount++;
// 切换
currentPlayer = opponent;
drawBoard();
const colorName = currentPlayer === 1 ? "黑方" : "白方";
const coord = `(${x+1}, ${y+1})`;
let msg = `第 ${moveCount} 手:${colorName} 落子 ${coord}`;
if (captured.length > 0) msg += ` ⚔️ 提掉 ${captured.length} 子`;
log(msg);
updatePlayerUI();
}
function checkCaptures(x, y, opponentColor) {
const directions = [[0,1], [0,-1], [1,0], [-1,0]];
let capturedList = [];
directions.forEach(([dx, dy]) => {
const nx = x + dx;
const ny = y + dy;
if (nx >= 0 && nx < width && ny >= 0 && ny < height && board[ny][nx] === opponentColor) {
if (!hasLiberties(nx, ny, opponentColor)) {
removeGroup(nx, ny, opponentColor, capturedList);
}
}
});
return capturedList;
}
function hasLiberties(startX, startY, color) {
const visited = new Set();
const stack = [[startX, startY]];
const directions = [[0,1], [0,-1], [1,0], [-1,0]];
while (stack.length > 0) {
const [x, y] = stack.pop();
const key = `${x},${y}`;
if (visited.has(key)) continue;
visited.add(key);
for (let [dx, dy] of directions) {
const nx = x + dx;
const ny = y + dy;
if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
if (board[ny][nx] === 0) return true;
if (board[ny][nx] === color && !visited.has(`${nx},${ny}`)) {
stack.push([nx, ny]);
}
}
}
}
return false;
}
function removeGroup(startX, startY, color, capturedList) {
const visited = new Set();
const stack = [[startX, startY]];
const directions = [[0,1], [0,-1], [1,0], [-1,0]];
while (stack.length > 0) {
const [x, y] = stack.pop();
const key = `${x},${y}`;
if (visited.has(key)) continue;
visited.add(key);
board[y][x] = 0;
capturedList.push({x, y});
for (let [dx, dy] of directions) {
const nx = x + dx;
const ny = y + dy;
if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
if (board[ny][nx] === color && !visited.has(`${nx},${ny}`)) {
stack.push([nx, ny]);
}
}
}
}
}
function passTurn() {
if (gameOver) return;
log(`⏭️ 第 ${moveCount + 1} 手:${currentPlayer === 1 ? "黑方" : "白方"} 停一手。`);
lastMove = null;
currentPlayer = currentPlayer === 1 ? 2 : 1;
moveCount++;
updatePlayerUI();
drawBoard();
}
// 计时逻辑
function gameTimerTick() {
if (gameOver) return;
const t = timers[currentPlayer];
if (t.isByoyomi) {
t.main--;
if (t.main <= 0) {
t.byoyomiLeft--;
if (t.byoyomiLeft < 0) {
endGameByTimeout();
return;
}
t.main = t.byoyomiDuration;
log(`⏰ ${currentPlayer===1?"黑方":"白方"} 进入下一次读秒,剩余 ${t.byoyomiLeft} 次`);
}
} else {
t.main--;
if (t.main <= 0) {
if (t.byoyomiLeft > 0) {
t.isByoyomi = true;
t.main = t.byoyomiDuration;
log(`⏰ ${currentPlayer===1?"黑方":"白方"} 基本时间用完,进入读秒!`);
} else {
endGameByTimeout();
return;
}
}
}
updateTimerDisplay();
}
function endGameByTimeout() {
gameOver = true;
clearInterval(timerInterval);
const winner = currentPlayer === 1 ? "白方" : "黑方";
showResult(`${winner}获胜!`, `${currentPlayer===1?"黑方":"白方"} 时间耗尽。`);
}
function updateTimerDisplay() {
for (let p of [1, 2]) {
const t = timers[p];
let timeStr = "";
let isUrgent = false;
if (t.isByoyomi) {
timeStr = `${t.byoyomiLeft}次 ${t.main}s`;
if (t.main <= 5) isUrgent = true;
} else {
const m = Math.floor(t.main / 60);
const s = t.main % 60;
timeStr = `${m}:${s.toString().padStart(2, '0')}`;
if (t.main <= 60) isUrgent = true;
}
const el = document.getElementById(p === 1 ? 'blackTimer' : 'whiteTimer');
el.innerText = timeStr;
if (isUrgent && p === currentPlayer && !gameOver) el.classList.add('urgent');
else el.classList.remove('urgent');
}
}
function updatePlayerUI() {
updateTimerDisplay();
document.getElementById('blackCard').classList.toggle('active', currentPlayer === 1 && !gameOver);
document.getElementById('whiteCard').classList.toggle('active', currentPlayer === 2 && !gameOver);
}
// 数子逻辑 (适配长方形)
function countScore() {
let blackScore = 0;
let whiteScore = 0;
const visited = Array(height).fill().map(() => Array(width).fill(false));
const directions = [[0,1], [0,-1], [1,0], [-1,0]];
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
if (board[y][x] !== 0) {
if (board[y][x] === 1) blackScore++;
else whiteScore++;
continue;
}
if (!visited[y][x]) {
let area = 0;
let owners = new Set();
let stack = [[x, y]];
visited[y][x] = true;
while(stack.length > 0) {
const [cx, cy] = stack.pop();
area++;
for (let [dx, dy] of directions) {
const nx = cx + dx;
const ny = cy + dy;
if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
if (board[ny][nx] === 0 && !visited[ny][nx]) {
visited[ny][nx] = true;
stack.push([nx, ny]);
} else if (board[ny][nx] !== 0) {
owners.add(board[ny][nx]);
}
}
}
}
if (owners.size === 1) {
if (owners.has(1)) blackScore += area;
else if (owners.has(2)) whiteScore += area;
}
}
}
}
return { black: blackScore, white: whiteScore + komi };
}
function endGame() {
if (gameOver) return;
if (!confirm("确定结束游戏?\n请确保死子已被提走,否则会影响数子结果。")) return;
gameOver = true;
clearInterval(timerInterval);
const scores = countScore();
const diff = scores.black - scores.white;
let title = "";
let detail = `<strong>黑方地盘:</strong> ${scores.black}<br>`;
detail += `<strong>白方地盘 (含贴目${komi}):</strong> ${scores.white}<br><hr>`;
if (diff > 0.01) {
title = "🏆 黑方胜!";
detail += `黑方胜出 ${diff.toFixed(1)} 子`;
} else if (diff < -0.01) {
title = "🏆 白方胜!";
detail += `白方胜出 ${Math.abs(diff).toFixed(1)} 子`;
} else {
title = "🤝 和棋 (Jigo)!";
detail += "双方地盘完全相同。";
}
showResult(title, detail);
}
function showResult(title, detail) {
document.getElementById('resultTitle').innerText = title;
document.getElementById('resultDetail').innerHTML = detail;
document.getElementById('resultModal').style.display = 'flex';
log("🏁 游戏结束。" + title);
}
function log(msg) {
const logEl = document.getElementById('gameLog');
const div = document.createElement('div');
div.className = 'log-item';
div.innerText = `[${new Date().toLocaleTimeString()}] ${msg}`;
logEl.prepend(div);
}
// 初始化
resizeCanvas();
// 预绘制一次空棋盘
ctx.fillStyle = config.boardColor;
ctx.fillRect(0,0, canvas.logicalWidth, canvas.logicalHeight);
</script>
</body>
</html>
