| <!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 { |
| --board-bg: #e3c086; |
| --board-line: #5d4037; |
| --p1-color: #111; |
| --p2-color: #f0f0f0; |
| --highlight: #ff4757; |
| --primary-btn: #2ed573; |
| --primary-btn-hover: #26af61; |
| } |
| |
| body { |
| font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; |
| background-color: #f7f1e3; |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| justify-content: center; |
| min-height: 100vh; |
| margin: 0; |
| user-select: none; |
| } |
| |
| header { |
| margin-bottom: 20px; |
| text-align: center; |
| } |
| |
| h1 { |
| color: #2f3542; |
| margin: 0; |
| font-size: 2rem; |
| letter-spacing: 2px; |
| } |
| |
| .game-wrapper { |
| display: flex; |
| gap: 30px; |
| flex-wrap: wrap; |
| justify-content: center; |
| align-items: flex-start; |
| padding: 20px; |
| background: white; |
| border-radius: 12px; |
| box-shadow: 0 10px 25px rgba(0,0,0,0.1); |
| } |
| |
| |
| .board-container { |
| position: relative; |
| padding: 15px; |
| background-color: var(--board-bg); |
| border-radius: 4px; |
| box-shadow: inset 0 0 10px rgba(0,0,0,0.3), 5px 5px 15px rgba(0,0,0,0.2); |
| |
| background-image: linear-gradient(45deg, rgba(0,0,0,0.03) 25%, transparent 25%, transparent 75%, rgba(0,0,0,0.03) 75%, rgba(0,0,0,0.03)), |
| linear-gradient(45deg, rgba(0,0,0,0.03) 25%, transparent 25%, transparent 75%, rgba(0,0,0,0.03) 75%, rgba(0,0,0,0.03)); |
| background-size: 20px 20px; |
| background-position: 0 0, 10px 10px; |
| } |
| |
| .board { |
| display: grid; |
| grid-template-columns: repeat(15, 30px); |
| grid-template-rows: repeat(15, 30px); |
| gap: 0; |
| position: relative; |
| cursor: pointer; |
| } |
| |
| |
| .cell { |
| width: 30px; |
| height: 30px; |
| position: relative; |
| display: flex; |
| justify-content: center; |
| align-items: center; |
| } |
| |
| |
| .cell::before { |
| content: ''; |
| position: absolute; |
| top: 50%; |
| left: 0; |
| width: 100%; |
| height: 1px; |
| background-color: var(--board-line); |
| z-index: 0; |
| } |
| |
| .cell::after { |
| content: ''; |
| position: absolute; |
| left: 50%; |
| top: 0; |
| height: 100%; |
| width: 1px; |
| background-color: var(--board-line); |
| z-index: 0; |
| } |
| |
| |
| .cell.star-point::before { |
| width: 100%; |
| } |
| .cell.star-point .dot { |
| position: absolute; |
| width: 6px; |
| height: 6px; |
| background: var(--board-line); |
| border-radius: 50%; |
| z-index: 1; |
| } |
| |
| |
| .piece { |
| width: 24px; |
| height: 24px; |
| border-radius: 50%; |
| z-index: 2; |
| transform: scale(0); |
| transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); |
| box-shadow: 2px 2px 4px rgba(0,0,0,0.4); |
| } |
| |
| .piece.show { |
| transform: scale(1); |
| } |
| |
| |
| .piece.p1 { |
| background: radial-gradient(circle at 30% 30%, #555, #000); |
| } |
| |
| |
| .piece.p2 { |
| background: radial-gradient(circle at 30% 30%, #fff, #ddd); |
| border: 1px solid #ccc; |
| } |
| |
| |
| .piece.last-move::after { |
| content: ''; |
| position: absolute; |
| top: 50%; |
| left: 50%; |
| transform: translate(-50%, -50%); |
| width: 6px; |
| height: 6px; |
| background-color: var(--highlight); |
| border-radius: 50%; |
| box-shadow: 0 0 4px rgba(255, 71, 87, 0.8); |
| } |
| |
| |
| .cell:hover:not(.occupied) .preview { |
| display: block; |
| } |
| .preview { |
| display: none; |
| width: 24px; |
| height: 24px; |
| border-radius: 50%; |
| opacity: 0.4; |
| z-index: 1; |
| } |
| .preview.p1 { background-color: #000; } |
| .preview.p2 { background-color: #fff; } |
| |
| |
| .sidebar { |
| width: 200px; |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| gap: 20px; |
| } |
| |
| .status-card { |
| background: #f1f2f6; |
| padding: 20px; |
| border-radius: 8px; |
| width: 100%; |
| text-align: center; |
| box-shadow: 0 2px 5px rgba(0,0,0,0.05); |
| } |
| |
| .status-title { |
| font-size: 0.9rem; |
| color: #747d8c; |
| margin-bottom: 10px; |
| } |
| |
| .current-player-indicator { |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| gap: 10px; |
| font-size: 1.2rem; |
| font-weight: bold; |
| margin-bottom: 15px; |
| } |
| |
| .indicator-dot { |
| width: 20px; |
| height: 20px; |
| border-radius: 50%; |
| border: 1px solid #ccc; |
| } |
| |
| .message-area { |
| min-height: 40px; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| font-weight: bold; |
| color: #2f3542; |
| } |
| |
| .message-area.win { color: var(--primary-btn); } |
| .message-area.draw { color: #ffa502; } |
| |
| button { |
| background-color: var(--primary-btn); |
| color: white; |
| border: none; |
| padding: 12px 24px; |
| font-size: 1rem; |
| border-radius: 6px; |
| cursor: pointer; |
| transition: background 0.2s, transform 0.1s; |
| width: 100%; |
| font-weight: bold; |
| } |
| |
| button:hover { |
| background-color: var(--primary-btn-hover); |
| } |
| |
| button:active { |
| transform: scale(0.98); |
| } |
| |
| |
| @media (max-width: 600px) { |
| .game-wrapper { |
| flex-direction: column; |
| align-items: center; |
| } |
| .sidebar { |
| width: 100%; |
| flex-direction: row; |
| justify-content: space-between; |
| box-sizing: border-box; |
| padding: 10px; |
| } |
| .status-card { |
| flex: 1; |
| margin-right: 10px; |
| } |
| button { |
| width: auto; |
| padding: 10px 20px; |
| } |
| .board { |
| |
| grid-template-columns: repeat(15, 22px); |
| grid-template-rows: repeat(15, 22px); |
| } |
| .cell { |
| width: 22px; |
| height: 22px; |
| } |
| .piece { |
| width: 18px; |
| height: 18px; |
| } |
| .preview { |
| width: 18px; |
| height: 18px; |
| } |
| } |
| </style> |
| </head> |
| <body> |
|
|
| <header> |
| <h1>五子棋</h1> |
| </header> |
|
|
| <main class="game-wrapper"> |
| |
| <div class="board-container"> |
| <div class="board" id="board"></div> |
| </div> |
|
|
| |
| <aside class="sidebar"> |
| <div class="status-card"> |
| <div class="status-title">游戏状态</div> |
| <div class="current-player-indicator"> |
| <span id="playerDot" class="indicator-dot" style="background: #000;"></span> |
| <span id="statusText">游戏开始!玩家1先落子</span> |
| </div> |
| <div class="message-area" id="messageArea"> |
| 等待落子... |
| </div> |
| </div> |
| <button id="restartBtn" onclick="game.restart()">重新开始</button> |
| </aside> |
| </main> |
|
|
| <script> |
| |
| |
| |
| const game = { |
| size: 15, |
| boardData: [], |
| currentPlayer: 1, |
| isGameOver: false, |
| moveCount: 0, |
| maxMoves: 15 * 15, |
| lastMove: null, |
| |
| |
| boardEl: document.getElementById('board'), |
| statusTextEl: document.getElementById('statusText'), |
| messageAreaEl: document.getElementById('messageArea'), |
| playerDotEl: document.getElementById('playerDot'), |
| restartBtn: document.getElementById('restartBtn'), |
| |
| |
| init() { |
| this.createBoard(); |
| this.restart(); |
| }, |
| |
| |
| createBoard() { |
| this.boardEl.innerHTML = ''; |
| for (let r = 0; r < this.size; r++) { |
| for (let c = 0; c < this.size; c++) { |
| const cell = document.createElement('div'); |
| cell.classList.add('cell'); |
| cell.dataset.row = r; |
| cell.dataset.col = c; |
| |
| |
| if ((r === 3 || r === 11 || r === 7) && (c === 3 || c === 11 || c === 7)) { |
| const dot = document.createElement('div'); |
| dot.classList.add('dot'); |
| cell.classList.add('star-point'); |
| cell.appendChild(dot); |
| } |
| |
| |
| const preview = document.createElement('div'); |
| preview.classList.add('preview', `p${this.currentPlayer}`); |
| cell.appendChild(preview); |
| |
| |
| cell.addEventListener('click', () => this.handleMove(r, c)); |
| this.boardEl.appendChild(cell); |
| } |
| } |
| }, |
| |
| |
| restart() { |
| this.boardData = Array(this.size).fill(null).map(() => Array(this.size).fill(0)); |
| this.currentPlayer = 1; |
| this.isGameOver = false; |
| this.moveCount = 0; |
| this.lastMove = null; |
| |
| |
| const cells = document.querySelectorAll('.cell'); |
| cells.forEach(cell => { |
| |
| cell.classList.remove('occupied'); |
| const piece = cell.querySelector('.piece'); |
| if (piece) piece.remove(); |
| |
| |
| const preview = cell.querySelector('.preview'); |
| if(preview) { |
| preview.className = `preview p${this.currentPlayer}`; |
| } |
| }); |
| |
| this.updateUI(); |
| this.messageAreaEl.textContent = "游戏开始!玩家1先落子"; |
| this.messageAreaEl.className = "message-area"; |
| this.statusTextEl.textContent = "轮到玩家1"; |
| }, |
| |
| |
| handleMove(r, c) { |
| |
| if (this.isGameOver || this.boardData[r][c] !== 0) return; |
| |
| |
| this.boardData[r][c] = this.currentPlayer; |
| this.moveCount++; |
| this.lastMove = { r, c }; |
| |
| |
| const cell = this.getCell(r, c); |
| const preview = cell.querySelector('.preview'); |
| if (preview) preview.style.display = 'none'; |
| |
| const piece = document.createElement('div'); |
| piece.classList.add('piece', `p${this.currentPlayer}`, 'show'); |
| |
| |
| const prevLast = document.querySelector('.piece.last-move'); |
| if (prevLast) prevLast.classList.remove('last-move'); |
| piece.classList.add('last-move'); |
| |
| cell.appendChild(piece); |
| |
| |
| if (this.checkWin(r, c, this.currentPlayer)) { |
| this.endGame(true); |
| return; |
| } |
| |
| |
| if (this.moveCount === this.maxMoves) { |
| this.endGame(false); |
| return; |
| } |
| |
| |
| this.currentPlayer = this.currentPlayer === 1 ? 2 : 1; |
| |
| |
| this.updatePreviews(); |
| this.updateUI(); |
| }, |
| |
| |
| updateUI() { |
| this.statusTextEl.textContent = `轮到玩家${this.currentPlayer}`; |
| this.playerDotEl.style.background = this.currentPlayer === 1 ? '#000' : '#fff'; |
| this.playerDotEl.style.border = this.currentPlayer === 1 ? 'none' : '1px solid #ccc'; |
| }, |
| |
| |
| updatePreviews() { |
| const previews = document.querySelectorAll('.preview'); |
| previews.forEach(p => { |
| p.className = `preview p${this.currentPlayer}`; |
| }); |
| }, |
| |
| |
| getCell(r, c) { |
| |
| return this.boardEl.children[r * this.size + c]; |
| }, |
| |
| |
| checkWin(r, c, player) { |
| const directions = [ |
| [[0, 1], [0, -1]], |
| [[1, 0], [-1, 0]], |
| [[1, 1], [-1, -1]], |
| [[1, -1], [-1, 1]] |
| ]; |
| |
| for (let axis of directions) { |
| let count = 1; |
| |
| for (let dir of axis) { |
| let nr = r + dir[0]; |
| let nc = c + dir[1]; |
| |
| while ( |
| nr >= 0 && nr < this.size && |
| nc >= 0 && nc < this.size && |
| this.boardData[nr][nc] === player |
| ) { |
| count++; |
| nr += dir[0]; |
| nc += dir[1]; |
| } |
| } |
| |
| if (count >= 5) return true; |
| } |
| return false; |
| }, |
| |
| |
| endGame(hasWinner) { |
| this.isGameOver = true; |
| if (hasWinner) { |
| const winnerName = this.currentPlayer === 1 ? "玩家1 (黑)" : "玩家2 (白)"; |
| this.messageAreaEl.textContent = `${winnerName} 获胜!`; |
| this.messageAreaEl.className = "message-area win"; |
| this.statusTextEl.textContent = "游戏结束"; |
| } else { |
| this.messageAreaEl.textContent = "平局!"; |
| this.messageAreaEl.className = "message-area draw"; |
| this.statusTextEl.textContent = "游戏结束"; |
| } |
| } |
| }; |
| |
| |
| game.init(); |
| |
| </script> |
| </body> |
| </html> |