js 3 --project 3

24 阅读5分钟

两个玩家不能同时操作,玩家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');
}

四、函数职责划分(模块化思维):

  1. rollDice():核心驱动逻辑(状态 → 事件 → 结果)

    • 生成随机骰子;
    • 更新骰子图片;
    • 根据结果修改状态(current / activePlayer)。
  2. switchPlayer():封装切换逻辑

    • 更新 activePlayer;
    • 调用 updatePlayerActiveHtml() 同步 UI。
  3. updatePlayerActiveHtml():UI 同步函数

    • 根据 activePlayer 更新 .player--active 类;
    • 不需要传递activePlayer参数,因为 activePlayer 是全局单一真相。
  4. 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)就是游戏结束,当游戏结束:

  1. 游戏应该停下来(不能再掷骰子、不能再 hold)
  2. 胜利的玩家在界面上高亮显示(添加 .player--winner 类)
  3. 当前激活状态(.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);

最后,review重构