两个玩家不能同时操作,玩家0为首发玩家(所以该游戏的开局始终是玩家0先玩/被激活,虽然不太公平,但简化了逻辑) 玩家有以下3种操作:
1、用户投骰子 :生成随机的骰子数 --> 显示骰子数 -> 骰子数是否为1 --否-- > 把骰子数加到当前分数 --是--> 当前分数清零, 并更换玩家(即投、hold动作对另一边的当前分数和总分数生效,UI颜色变化)
2、用户hold分数:把当前分数加到总分数 --> 总分数是否 >= 100 --是--->当前玩家赢 --否--->当前分数清零,并更换玩家
3、用户重置游戏:把所有分数置0 ---> 玩家0重置为首发玩家
1、第一步,初始化页面
第一步,初始化页面: 把html两个总分数元素清零,隐藏骰子图像,添加player--active类到player--0(html文件本身就写了);
把指向html元素的变量加上后缀El
const score0El = document.getElementById('score--0'); //
const score1El = document.getElementById('score--1');
const diceEl = document.querySelector('.dice');
const initPage = function () {
score0El.textContent = 0;
score1El.textContent = 0;
diceEl.classList.add('hidden');
//html文件本身就在player--0里添加了player--activ,所以不需要再添加了
};
initPage();
2、第二步,投骰子函数
🎲【投骰子函数核心逻辑与工程思维解析】🎲
一、整体逻辑:
投骰子 -> 生成随机数(1~6) -> 显示对应骰子图片 -> 判断骰子是否为1: • 否:将骰子数累加到当前被激活玩家的“当前分数”; • 是:当前分数清零,并切换玩家。
二、核心状态设计:
我们需要明确“谁在玩”,以及“这个玩家当前分数是多少,所以需要变量去存储它们:
1️⃣ activePlayer —— 当前被激活的玩家(状态核心):
- 用数字 0 / 1 表示两位玩家;
- 初始化时设为 0(即玩家0先手);
- 当轮到另一位玩家时,通过 activePlayer = activePlayer === 1 ? 0 : 1 来切换。
2️⃣ current —— 当前分数数组(每位玩家的回合内得分):
- 结构:[0, 0]
- 通过 current[activePlayer] 精确指向当前玩家的“当前分数”;这就是为什么用0/1表示玩家
- 当玩家点击“Hold”时,会将 current[activePlayer] 加到总分(另一个数组中)。
⚠️ 工程思维要点: 程序不是在描述“故事”,而是在持续回答一个问题: “当前系统处于什么状态?接下来要做什么状态变更?”
换句话说,程序员不会说“如果投到1,就换人”, 而是说:“我维护一个状态变量 activePlayer,骰子结果为1时,状态切换。”
三、🧠 JS 驱动的正确思维:单一真相(Single Source of Truth)
🧠 JS 中的 activePlayer 是单一真相(Single Source of Truth)。
👁️ DOM 中的 .player--active 类,只是该状态的“视觉表现”。
因此:
✅ 永远由 JS 控制状态;
✅ DOM 仅作为状态的反映;
❌ 不依赖 DOM 的类名去判断逻辑。
在工程思维中,我们会说:
“所有逻辑状态(如:谁在玩、分数多少、游戏是否结束)都必须由 JS 变量控制。”
HTML 只是 JS 状态的“投影” ,DOM应仅作为状态的展示/反映,所以不应依赖DOM的类名去判断当前状态
也就是说:
let activePlayer = 0; // ← 真正的‘谁在玩’
if (activePlayer === 0) {
// 控制 UI,让玩家0高亮
player0El.classList.add('player--active');
player1El.classList.remove('player--active');
}
四、函数职责划分(模块化思维):
-
rollDice():核心驱动逻辑(状态 → 事件 → 结果)- 生成随机骰子;
- 更新骰子图片;
- 根据结果修改状态(current / activePlayer)。
-
switchPlayer():封装切换逻辑- 更新 activePlayer;
- 调用
updatePlayerActiveHtml()同步 UI。
-
updatePlayerActiveHtml():UI 同步函数- 根据 activePlayer 更新
.player--active类; - 不需要传递activePlayer参数,因为
activePlayer是全局单一真相。
- 根据 activePlayer 更新
-
updateCurrentScoresOfActivePlayerHtml():- 将
current[activePlayer]的变化反映到 UI 上。
- 将
✅ 优点:
- 各函数关注点单一;
- 状态逻辑集中统一;
- DOM 与逻辑分离,易于维护与扩展。 */
应该把所有需要的变量声明在开头,每次需要新的变量就声明在开头,但以下为了更好解释,暂在哪需要的就在哪声明了
let activePlayer = 0; // 当前被激活的玩家:0 表示玩家0,1 表示玩家1(初始玩家0先手)
let current = [0, 0]; // 记录两位玩家的当前分数
// UI同步函数:根据当前activePlayer更新Html玩家激活状态
const updatePlayerActiveHtml = function () {
// 不需要传递参数,直接使用全局状态 activePlayer
document
.querySelector(`.player--${activePlayer}`)
.classList.add('player--active');//知识点:该方法会自动判断是否含有hidden类,不需人工判断
const deactivePlayer = activePlayer === 1 ? 0 : 1;
document
.querySelector(`.player--${deactivePlayer}`)
.classList.remove('player--active');
};
// 生成1-6的随机骰子数
const createRandomDice = () => Math.trunc(Math.random() * 6) + 1;
// 切换玩家函数:切换玩家
const switchPlayer = function () {
activePlayer = activePlayer === 1 ? 0 : 1;
updatePlayerActiveHtml();
};
// UI同步函数: 根据当前current[activePlayer]更新html被激活玩家当前分数显示
const updateCurrentScoresOfActivePlayerHtml = function () {
document.querySelector(`#current--${activePlayer}`).textContent =
current[activePlayer];
};
// 投骰子逻辑主函数
const rollDice = function () {
if(playing){
// 1️⃣ 生成随机骰子数
const dice = createRandomDice();
// 2️⃣ 显示骰子图片(移除hidden类,并替换图片)
diceEl.classList.remove('hidden');//知识点:该方法会自动判断是否含有hidden类,不需人工判断
diceEl.src = `dice-${dice}.png`;
// 3️⃣ 状态逻辑判断
if (dice !== 1) {
// 骰子不为1:加到当前分数
current[activePlayer] += dice;
updateCurrentScoresOfActivePlayerHtml();
} else {
// 骰子为1:当前分数清零并切换玩家
current[activePlayer] = 0;
updateCurrentScoresOfActivePlayerHtml();
switchPlayer();
}
}
};
const btnRollDiceEl = document.querySelector('.btn--roll');
btnRollDiceEl.addEventListener('click', rollDice);
缺少测试
3、第三步:hold函数
把变量都声明在开头了
4、第四步:玩家赢了,游戏结束
某一方胜利(总分>=100)就是游戏结束,当游戏结束:
- 游戏应该停下来(不能再掷骰子、不能再 hold)
- 胜利的玩家在界面上高亮显示(添加 .player--winner 类)
- 当前激活状态(.player--active)要被移除(否则会有被激活对应的UI)。
所以我们还需要一个变量存储游戏状态表示游戏是否结束:
let playing = true; // 有玩家赢了就设置为false,只有playing为true,roll dice/hold score才能操作,所以给这两个函数加上if(playing)
/* hold socres */
const updateTotalScoreOfActivePlayerHtml = function () {
document.getElementById(`score--${activePlayer}`).textContent =
totalScore[activePlayer];
};
const gameWin = function () {
playing = false;
document
.querySelector(`.player--${activePlayer}`)
.classList.add('player--winner');
document
.querySelector(`.player--${activePlayer}`)
.classList.remove('player--active');
};
const holdScore = function () {
if (playing) {
//1.把被激活玩家的当前分数加到总分(也意味着当前分数要清零)
totalScore[activePlayer] += current[activePlayer];
current[activePlayer] = 0;
updateTotalScoreOfActivePlayerHtml();
updateCurrentSocresofActivePlayerHtml();
//2.1若总分超过100,则被激活玩家赢了,游戏结束
if (totalScore[activePlayer] >= 100) {
gameWin();
playing = false;
} //2.2否则更换玩家
else {
switchPlayer();
}
}
};
document.querySelector('.btn--hold').addEventListener('click', holdScore);
5、第五步:重开游戏
/* new game: 用户点击new game重新开始游戏:
我打算每次轮流首发,封装一个首发函数,若上一次activePlayer是0则这次就是1,别忘了
*/
const startPlayer = function () {
activePlayer = activePlayer === 1 ? 0 : 1;
document
.querySelector(`.player--${activePlayer}`)
.classList.remove('player--active');
};
const newGame = function () {
totalScore0El.textContent = 0;
totalScore1El.textContent = 0;
current0El.textContent = 0;
current1El.textContent = 0;
diceEl.classList.remove('hidden');
document
.querySelector(`.player--${activePlayer}`)
.classList.remove('player--winner');
startPlayer(); //首发函数
playing = true;
current = [0, 0];
totalScore = [0, 0];
};
document.querySelector('.btn--new').addEventListener('click', newGame);