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>
{/* 棋盘容器 */}
<div
style={{
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];
}
}
// 没有赢家则返回null
return 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之间的间距 */
}