/* * Canvas 渲染:木色棋盘、立体棋子、悬停预览、最后一手标记、获胜连线。 * 不持有游戏状态,每次 render(game, hover) 根据传入状态整体重绘。 */ (function () { const SIZE = 15; const STAR_POINTS = [[3, 3], [11, 3], [3, 11], [11, 11], [7, 7]]; function createRenderer(canvas) { const ctx = canvas.getContext('2d'); let cssSize = 600; let margin = 0; let cell = 0; function resize() { const rect = canvas.getBoundingClientRect(); cssSize = Math.max(200, Math.floor(rect.width)); const dpr = window.devicePixelRatio || 1; canvas.width = Math.round(cssSize * dpr); canvas.height = Math.round(cssSize * dpr); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); margin = cssSize / (SIZE + 1); cell = (cssSize - margin * 2) / (SIZE - 1); } function toPixel(x, y) { return { px: margin + x * cell, py: margin + y * cell }; } function drawBoard() { const bg = ctx.createLinearGradient(0, 0, cssSize, cssSize); bg.addColorStop(0, '#e3b473'); bg.addColorStop(1, '#d19a52'); ctx.fillStyle = bg; ctx.fillRect(0, 0, cssSize, cssSize); ctx.strokeStyle = '#7a5a2e'; ctx.lineWidth = 1; for (let i = 0; i < SIZE; i++) { const p = margin + i * cell; ctx.beginPath(); ctx.moveTo(margin, p); ctx.lineTo(cssSize - margin, p); ctx.moveTo(p, margin); ctx.lineTo(p, cssSize - margin); ctx.stroke(); } // 外框加粗一圈 ctx.lineWidth = 2; ctx.strokeRect(margin, margin, cell * (SIZE - 1), cell * (SIZE - 1)); ctx.fillStyle = '#5a3f1c'; for (const [sx, sy] of STAR_POINTS) { const { px, py } = toPixel(sx, sy); ctx.beginPath(); ctx.arc(px, py, Math.max(2.5, cell * 0.09), 0, Math.PI * 2); ctx.fill(); } } function drawStone(x, y, color, alpha) { const { px, py } = toPixel(x, y); const r = cell * 0.42; ctx.save(); ctx.globalAlpha = alpha; if (alpha === 1) { ctx.shadowColor = 'rgba(0, 0, 0, 0.35)'; ctx.shadowBlur = r * 0.35; ctx.shadowOffsetY = r * 0.12; } const g = ctx.createRadialGradient(px - r * 0.35, py - r * 0.35, r * 0.1, px, py, r); if (color === 1) { g.addColorStop(0, '#6a6a6a'); g.addColorStop(1, '#0d0d0d'); } else { g.addColorStop(0, '#ffffff'); g.addColorStop(1, '#c9c4ba'); } ctx.fillStyle = g; ctx.beginPath(); ctx.arc(px, py, r, 0, Math.PI * 2); ctx.fill(); ctx.restore(); } function render(game, hover) { drawBoard(); for (let y = 0; y < SIZE; y++) { for (let x = 0; x < SIZE; x++) { if (game.board[y][x] !== 0) drawStone(x, y, game.board[y][x], 1); } } // 最后一手红点标记(有获胜连线时不画,避免遮挡) const last = game.moveHistory[game.moveHistory.length - 1]; if (last && !game.winningLine) { const { px, py } = toPixel(last.x, last.y); ctx.fillStyle = '#e6493a'; ctx.beginPath(); ctx.arc(px, py, Math.max(2.5, cell * 0.1), 0, Math.PI * 2); ctx.fill(); } // 获胜连线 if (game.winningLine) { const a = toPixel(game.winningLine[0].x, game.winningLine[0].y); const b = toPixel( game.winningLine[game.winningLine.length - 1].x, game.winningLine[game.winningLine.length - 1].y ); ctx.save(); ctx.strokeStyle = 'rgba(230, 60, 45, 0.9)'; ctx.lineWidth = cell * 0.14; ctx.lineCap = 'round'; ctx.beginPath(); ctx.moveTo(a.px, a.py); ctx.lineTo(b.px, b.py); ctx.stroke(); ctx.restore(); } // 悬停预览:半透明显示当前行棋方棋子 if (hover && !game.gameOver && game.board[hover.y][hover.x] === 0) { drawStone(hover.x, hover.y, game.currentPlayer, 0.4); } } // 把鼠标事件换算成最近的交叉点,超出棋盘返回 null function cellFromEvent(evt) { const rect = canvas.getBoundingClientRect(); const x = Math.round((evt.clientX - rect.left - margin) / cell); const y = Math.round((evt.clientY - rect.top - margin) / cell); if (x < 0 || x >= SIZE || y < 0 || y >= SIZE) return null; return { x, y }; } resize(); return { render, resize, cellFromEvent }; } window.GomokuRenderer = { createRenderer }; })();