如何使用React构建一个掷骰子的小游戏
作者:练习两年半的工程师
这是一个用 React 构建的小游戏应用,名为 Tenzies,目标是掷骰子,直到所有骰子的值相同。玩家可以“冻结”某些骰子,使它们在后续掷骰中保持不变。

1. App.jsx
import Die from "../public/components/Die"
import { useState, useRef, useEffect } from "react"
import { nanoid } from "nanoid"
import Confetti from "react-confetti"- Die:自定义组件,用于显示单个骰子。
 - useState、useRef、useEffect:React hooks,用于管理状态、DOM引用和副作用。
 - nanoid:用于生成唯一 ID 的库,确保每个骰子有唯一标识符。
 - Confetti:库组件,用于显示游戏胜利的彩带特效。
 
初始化状态和引用
export default function App(){
  const [dice, setDice] = useState(() => generateAllNewDice())
  const buttonRef = useRef(null)dice:管理 10 个骰子的数组状态。初始值通过 generateAllNewDice 函数生成。
buttonRef:引用按钮 DOM 元素,用于在胜利时自动聚焦。
游戏胜利条件
const gameWon = dice.every(die => die.isHeld) && dice.every(die => die.value === dice[0].value)
游戏胜利的条件:
- 所有骰子的 isHeld 属性为 true(即冻结)。
 - 所有骰子的值相同。
 
处理副作用
useEffect(() => {
  if (gameWon) {
    buttonRef.current.focus()
  }
}, [gameWon])当 gameWon 为 true 时,自动让“新游戏”按钮获得焦点,提升可用性。
生成新的骰子数组
function generateAllNewDice() {
  return new Array(10).fill(0).map(() => ({
    value: Math.ceil(Math.random() * 6),
    isHeld: false,
    id: nanoid()
  }))
}创建 10 个骰子对象,每个骰子有:
value:1 到 6 的随机数。isHeld:初始为 false,表示未冻结。id:唯一标识符,使用 nanoid 生成。
Math.ceil(Math.random() * 6) 是 JavaScript 中生成随机整数的常见方法之一。下面逐步解释其工作原理:
Math.random()
- 作用:生成一个 0(包含)到 1(不包含) 的随机浮点数。
 - 范围:
[0, 1)。 
例如:可能的结果是 0.2345, 0.9876, 0.0012 等。
Math.random() * 6
- 作用:将生成的随机数放大至 0 到 6(不包含 6) 的范围。
 - 范围:
[0, 6)。 
示例: 如果 Math.random() 返回 0.2,则 0.2 * 6 = 1.2。
如果 Math.random() 返回 0.9,则 0.9 * 6 = 5.4。
Math.ceil()
作用:对数字向上取整,返回大于等于该数的最小整数。
- 例如: 
Math.ceil(1.2)返回2。 Math.ceil(5.4)返回6。Math.ceil(0)返回0。
综合步骤
Math.ceil(Math.random() * 6) 的完整过程:
- 调用 
Math.random()生成一个随机数,例如0.45。 - 将该随机数乘以 
6,结果是2.7。 - 用 
Math.ceil()对结果向上取整,得到3。 
返回结果
最终的返回值是一个 1 到 6 的随机整数:
- 范围:
[1, 6]。 - 为何能覆盖 1 到 6?
Math.random()取值为0时,Math.random() * 6 = 0,取整后为1。Math.random()接近1时,Math.random() * 6接近6,取整后为6。
 
掷骰子逻辑
function rollDice() {
  if (!gameWon) {
    setDice(oldDice => oldDice.map(die => 
      die.isHeld ?
        die :
        {...die, value: Math.ceil(Math.random() * 6)}
    ))
  } else {
    setDice(generateAllNewDice())
  }
}游戏未胜利时:
对未冻结的骰子重新生成随机值。
游戏胜利时:
重置游戏,生成新的骰子数组。
切换冻结状态
function hold(id) {
  setDice(oldDice => oldDice.map(die =>
    die.id === id ?
      {...die, isHeld: !die.isHeld} :
      die
  ))
}根据点击的骰子 id,切换对应骰子的 isHeld 状态。
显示骰子
const diceElements = dice.map(dieObj => 
  (<Die 
    key={dieObj.id} 
    value={dieObj.value} 
    isHeld={dieObj.isHeld}
    hold={() => hold(dieObj.id)}
  />)
)- 使用 map 遍历 dice 状态,为每个骰子生成一个 Die 组件。
 - hold 函数传递给每个骰子,用于处理点击事件。
 
渲染 UI
return (
  <main>
    {gameWon && <Confetti />}
    <div aria-live="polite" className="sr-only">
        {gameWon && <p>Congratulations! You won! Press "New Game" to start again.</p>}
    </div>
    <h1 className="title">Tenzies</h1>
    <p className="instructions">Roll until all dice are the same. Click each die to freeze it at its current value between rolls.</p>
    <div className="dice-container">
      {diceElements}
    </div>
    <button ref={buttonRef} className="roll-dice" onClick={rollDice}>
      {gameWon ? "New Game" : "Roll"}
    </button>
  </main>
)- 胜利时显示彩带效果:通过 Confetti 组件。
 - 无障碍支持:aria-live 提供游戏状态描述。
 - 骰子容器:动态显示所有 Die 组件。
 - 按钮文本:胜利时为“New Game”,否则为“Roll”。
 
代码的核心逻辑总结
- 初始状态下生成 10 个随机骰子。
 - 用户点击骰子时,可冻结当前骰子值。
 - 点击按钮:
– 若游戏未胜利,则重新掷未冻结的骰子。
– 若游戏胜利,则重置游戏。 - 实现游戏胜利条件检测和 UI 提示(如彩带效果、自动聚焦按钮)。
 
2. Die.jsx
export default function Die(props) {
    const styles = {
        backgroundColor: props.isHeld ? "#59E391" : "white"
    }styles 是一个动态样式对象,用于控制按钮的背景颜色:
- 如果 
props.isHeld为 true(表示该骰子被冻结),背景颜色为绿色 (#59E391)。 - 如果 
props.isHeld为 false(表示未冻结),背景颜色为白色。 
样式切换使用户能够直观地看到冻结状态。
return (
    <button 
        style={styles}
        onClick={props.hold}
        aria-pressed={props.isHeld}
        aria-label={`Die with value ${props.value}, 
        ${props.isHeld ? "held" : "not held"}`}
    >
        {props.value}
    </button>
)style={styles}
- 将动态样式对象 styles 应用到按钮的 style 属性,调整按钮的背景颜色。
 
onClick={props.hold}
- 定义按钮的点击事件处理函数。
 - props.hold 是从父组件传递的函数,当用户点击按钮时会触发这个函数。
 - 通常,props.hold 用于切换 isHeld 状态,冻结或解冻当前骰子。
 
aria-pressed={props.isHeld}
- 用于无障碍支持。
 - 指定按钮的按下状态:true:表示当前骰子已被“按下”或冻结。
 - false:表示当前骰子未被按下。帮助屏幕阅读器用户了解当前状态。
 
aria-label={Die with value ${props.value}, ${props.isHeld ? “held” : “not held”}}
- 用于描述按钮的详细信息,提升无障碍性。
 - 动态生成描述,例如:Die with value 4, held:骰子的值为 4,已冻结。Die with value 6, not held:骰子的值为 6,未冻结。
 - 代码的核心功能 动态显示骰子的值。
 - 提供交互功能,点击按钮会调用 props.hold,切换冻结状态。通过动态样式反映骰子的状态(冻结或未冻结)。提供无障碍支持,便于屏幕阅读器识别和描述按钮状态。
 
3. index.css
1. 通用选择器 *
* {
  box-sizing: border-box;
}- 作用:将所有元素的 
box-sizing设置为border-box。 box-sizing: border-box;:- 包括内容、内边距 (
padding) 和边框 (border) 的宽度和高度在width和height的计算中。 - 效果:简化布局调整,避免意外的尺寸增大。
 
- 包括内容、内边距 (
 
2. body 样式
body {
  font-family: Karla, sans-serif;
  margin: 0;
  background-color: #0B2434;
  padding: 20px;
  height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}- 字体:
font-family: Karla, sans-serif;使用Karla字体,不支持时回退到无衬线字体。 - 背景颜色:深蓝色背景 (
#0B2434)。 - 布局:
display: flex;:将body设为弹性容器。flex-direction: column;:子元素垂直排列。justify-content: center;:子元素在垂直方向上居中。align-items: center;:子元素在水平方向上居中。
 - 高度:
100vh使body占据整个视口高度。 - 内边距:
20px,用于给内容留出额外空间。 
3. div#root 样式
div#root {
  height: 100%;
  width: 100%;
  max-height: 400px;
  max-width: 400px;
}- 高度和宽度:默认占据父容器的全部空间。
 - 最大尺寸限制:
max-height和max-width分别限制为400px。- 效果:即使父容器较大,
#root的尺寸也不会超过 400 像素。 
 - 效果:即使父容器较大,
 
4. main 样式
main {
  background-color: #F5F5F5;
  height: 100%;
  border-radius: 5px;
  display: flex;
  flex-direction: column;
  justify-content: space-evenly;
  align-items: center;
}- 背景颜色:浅灰色 (
#F5F5F5)。 - 圆角:
border-radius: 5px;使容器的边角圆滑。 - 布局: 弹性布局,子元素垂直排列。
justify-content: space-evenly;:在主轴上均匀分布子元素,间隔相等。align-items: center;:子元素在交叉轴(水平方向)上居中。
 
5. .title 样式
.title {
  font-size: 40px;
  margin: 0;
}字体大小:40px。
边距:margin: 0; 去除外边距。
6. .instructions 样式
.instructions {
  font-family: 'Inter', sans-serif;
  font-weight: 400;
  margin-top: 0;
  text-align: center;
}字体:优先使用 Inter 字体。
字体粗细:普通粗细 (font-weight: 400)。
文本对齐:居中对齐 (text-align: center)。
7. .dice-container 样式
.dice-container {
  display: grid;
  grid-template: auto auto / repeat(5, 1fr);
  gap: 20px;
  margin-bottom: 40px;
}- 布局:CSS 网格布局。
grid-template:- 行模板:
auto auto(两行,每行高度自适应内容)。 - 列模板:
repeat(5, 1fr)(5 列,每列宽度相等)。 
- 行模板:
 gap: 20px;:网格单元之间的间距。
 - 底部边距:
margin-bottom: 40px;。 
8. 通用按钮样式
button {
  font-family: Karla, sans-serif;
  cursor: pointer;
}- 字体:
Karla。 - 鼠标样式:
cursor: pointer;鼠标悬停时显示手型图标。 
9. .dice-container button 样式
.dice-container button {
  height: 50px;
  width: 50px;
  box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.15);
  border-radius: 10px;
  border: none;
  background-color: white;
  font-size: 1.75rem;
  font-weight: bold;
}- 按钮尺寸:固定宽高 
50px。 - 阴影:
box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.15);添加轻微的阴影效果。 - 圆角:
border-radius: 10px;。 - 无边框:
border: none;。 - 背景颜色:白色。
 - 字体样式:
font-size: 1.75rem;大号字体。font-weight: bold;加粗。
 
10. .roll-dice 按钮样式
button.roll-dice {
  height: 50px;
  white-space: nowrap;
  width: auto;
  padding: 6px 21px;
  border: none;
  border-radius: 6px;
  background-color: #5035FF;
  color: white;
  font-size: 1.2rem;
}- 尺寸:高度固定为 
50px,宽度根据内容调整。 - 背景颜色:深蓝色 (
#5035FF)。 - 字体颜色:白色。
 - 内边距:
6px(垂直)和21px(水平)。无边框,并带有圆角。 
11. .sr-only 样式
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}- 作用:隐藏元素,但保留给屏幕阅读器使用。
 - 关键属性:
position: absolute;:从文档流中移除。width和height设置为1px。- 使用 
clip和overflow确保内容不可见。 - 提供无障碍支持,例如为视觉障碍用户提供额外的语音描述。
 
 
总结
这段 CSS 代码:
- 设计布局:通过弹性盒布局和网格布局组织内容。
 - 样式统一性:使用动态样式、字体和交互效果。
 - 无障碍支持:添加屏幕阅读器友好的隐藏元素(
.sr-only)。 - 视觉细节:通过颜色、圆角、阴影和间距提升用户体验。
 
到此这篇关于使用React构建一个掷骰子的小游戏的文章就介绍到这了,更多相关React掷骰子的小游戏内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
