React 教程:井字棋游戲
使用 React 實現一個交互式的井字棋游戲,并配上好看的樣式
// 導入必要的CSS樣式和React庫
import "./App.css";
import { useState } from "react";// Square組件 - 表示棋盤上的一個格子
function Square({ value, onSquareClick }) {return (<button className="square" onClick={onSquareClick}>{value}</button>);
}// App組件 - 游戲的主組件
export default function App() {// 使用useState鉤子管理游戲歷史狀態// 初始歷史是一個包含9個null的數組(表示空棋盤)const [history, setHistory] = useState([Array(9).fill(null)]);// 當前步驟的索引(表示當前顯示的是哪一步的棋盤狀態)const [currentMove, setCurrentMove] = useState(0);// 判斷當前玩家是否是"X"(?)// 根據當前步驟的奇偶性決定玩家順序const xIsNext = currentMove % 2 === 0;// 獲取當前棋盤的方格狀態const currentSquares = history[currentMove];// 處理玩家落子function handlePlay(nextSquares) {// 創建新的歷史記錄:// 1. 復制從第一步到當前步驟的歷史// 2. 添加新的棋盤狀態const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];// 更新歷史狀態setHistory(nextHistory);// 將當前步驟設置為最新的一步setCurrentMove(nextHistory.length - 1);}// 跳轉到指定步驟function jumpTo(nextMove) {setCurrentMove(nextMove);}// 生成歷史步驟列表const moves = history.map((squares, move) => {let description;// 如果是第一步,顯示"重新開始游戲",否則顯示"轉到第X步"description = move > 0 ? `轉到第 ${move} 步` : "重新開始游戲";return (<li key={move}><button onClick={() => jumpTo(move)}>{description}</button></li>);});// 渲染游戲界面return (<div className="game">{/* 游戲棋盤區域 */}<div className="game-board">{/* Board組件 - 渲染當前棋盤狀態 */}<Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} /></div>{/* 游戲信息區域(歷史記錄) */}<div className="game-info"><ol>{moves}</ol></div></div>);
}// Board組件 - 游戲棋盤
function Board({ xIsNext, squares, onPlay }) {// 處理格子點擊事件function handleClick(i) {// 如果該格子已有內容或已有贏家,則不做任何處理if (squares[i] || calculateWinner(squares)) return;// 創建新的棋盤狀態副本(React狀態不可變原則)const nextSquares = squares.slice();// 根據當前玩家設置格子的內容nextSquares[i] = xIsNext ? "?" : "?";// 調用父組件傳遞的onPlay函數更新游戲狀態onPlay(nextSquares);}// 計算當前是否有贏家const winner = calculateWinner(squares);// 設置狀態文本let status;if (winner) {status = "獲勝者: " + winner;} else {status = "下一位玩家: " + (xIsNext ? "?" : "?");}// 渲染棋盤return (<>{/* 顯示游戲狀態(贏家或下一步玩家) */}<div className="status">{status}</div>{/* 棋盤容器 */}<divstyle={{display: "flex",justifyContent: "center",alignItems: "center",padding: "20px",}}>{/* 棋盤主體 */}<div className="calculator-board">{/* 第一行 */}<div className="board-row"><Square value={squares[0]} onSquareClick={() => handleClick(0)} /><Square value={squares[1]} onSquareClick={() => handleClick(1)} /><Square value={squares[2]} onSquareClick={() => handleClick(2)} /></div>{/* 第二行 */}<div className="board-row"><Square value={squares[3]} onSquareClick={() => handleClick(3)} /><Square value={squares[4]} onSquareClick={() => handleClick(4)} /><Square value={squares[5]} onSquareClick={() => handleClick(5)} /></div>{/* 第三行 */}<div className="board-row"><Square value={squares[6]} onSquareClick={() => handleClick(6)} /><Square value={squares[7]} onSquareClick={() => handleClick(7)} /><Square value={squares[8]} onSquareClick={() => handleClick(8)} /></div></div></div></>);
}// 計算贏家函數
function calculateWinner(squares) {// 所有可能的獲勝線(水平、垂直、對角線)const lines = [[0, 1, 2], // 第一行[3, 4, 5], // 第二行[6, 7, 8], // 第三行[0, 3, 6], // 第一列[1, 4, 7], // 第二列[2, 5, 8], // 第三列[0, 4, 8], // 主對角線[2, 4, 6], // 副對角線];// 檢查所有可能的獲勝線for (let i = 0; i < lines.length; i++) {// 解構當前線的三個位置const [a, b, c] = lines[i];// 檢查三個格子是否相同且不為空if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {// 返回贏家("?"或"?")return squares[a];}}// 沒有贏家則返回nullreturn null;
}// 游戲結束注釋
#root {max-width: 1280px;margin: 0 auto;padding: 2rem;text-align: center;
}/* 九宮格容器 */
.calculator-board {display: flex;flex-direction: column;gap: 8px;background-color: #2c3e50;padding: 20px;border-radius: 12px;box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
}/* 按鈕行 */
.board-row {display: flex;gap: 8px;
}/* 按鈕樣式 */
.square {width: 80px;height: 80px;background-color: #3498db;border: none;border-radius: 8px;font-size: 36px;color: white;cursor: pointer;display: flex;justify-content: center;align-items: center;transition: all 0.2s ease;box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}/* 按鈕懸停效果 */
.square:hover {background-color: #2980b9;transform: translateY(-3px);box-shadow: 0 6px 8px rgba(0, 0, 0, 0.15);
}/* 按鈕點擊效果 */
.square:active {transform: translateY(1px);box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}.game-info ol {list-style-type: none; /* 移除默認的序列編號 */padding-left: 0; /* 移除默認的左內邊距 */margin: 0; /* 移除默認的外邊距 */
}.game-info button {background: #f0f0f0;border: 1px solid #ddd;border-radius: 4px;padding: 8px 12px;cursor: pointer;width: 240px;text-align: left;transition: background 0.2s;
}.game-info button:hover {background: #e0e0e0;
}.game-info button.active {background: #3498db;color: white;font-weight: bold;
}.game {display: flex;flex-direction: row;align-self: center;justify-self: center;gap: 20px; /* 設置兩個div之間的間距 */
}