如何用JavaScript建立一个西蒙游戏

341 阅读8分钟

如何用JavaScript建立一个西蒙游戏

在浏览器中创建游戏是锻炼你的JavaScript技能并同时获得乐趣的一个好方法

在本教程中,我们将用JavaScript开发经典的西蒙游戏。游戏的目标是重复一系列由游戏创建的随机瓷砖点击。每一轮过后,这个序列会逐渐变长、变复杂,这使得它更难记住。

Simon Game

玩一个现场版本的游戏。

通常情况下,你会有四种不同的瓷砖,每一种都有独特的颜色和声音,在按下时就会激活。声音可以帮助玩家记住顺序,如果玩家错过了顺序中的一个步骤,游戏就会结束。

前提条件

本教程假定你有JavaScript和DOM的基本知识。

开始学习

GitHub上获取游戏的HTML和CSS文件。设置项目的说明在包含的README文件中。如果你愿意,你也可以使用JSFiddle来学习本教程。

JSFiddle

下面是JSFiddle上的起点

如前所述,当游戏以随机顺序激活一块或多块瓷砖时,一个回合就开始了,当玩家通过按下瓷砖重现该顺序时就结束了。在下一轮中,顺序中的瓷砖数量会增加一个。

让我们首先创建一个数组来记录原始的瓷砖点击顺序,并创建第二个数组来记录人类的顺序。

let sequence = [];
let humanSequence = [];

接下来,选择开始按钮并创建一个新的startGame() 函数,当这个按钮被点击时将被执行。

const startButton = document.querySelector('.js-start');

function startGame() {
  startButton.classList.add('hidden');
}

startButton.addEventListener('click', startGame);

这时,一旦开始按钮被按下,它将被隐藏。.info 元素也需要进入视野,因为那是显示状态信息的地方。它在HTML中默认应用了hidden 类,在CSS中已经用display: none; 样式,所以一旦游戏开始,这就是需要移除的东西。

<span class="info js-info hidden"></span>

更新你的JavaScript文件,如下所示。

const startButton = document.querySelector('.js-start');
const info = document.querySelector('.js-info');

function startGame() {
  startButton.classList.add('hidden');
  info.classList.remove('hidden');
  info.textContent = 'Wait for the computer';
}

现在,一旦开始按钮被点击,.info 元素就会显示一条信息,告诉用户等待序列的完成。

开始下一轮

humanSequence 下面创建一个新的level 变量。我们将用它来记录到目前为止已经进行的回合数。

let level = 0;

接下来,在startGame() 的正上方创建一个新的nextRound() 函数,如下面的片段所示。这个函数的目的是启动下一个点击瓷砖的序列。

function nextRound() {
  level += 1;

  // copy all the elements in the `sequence` array to `nextSequence`
  const nextSequence = [...sequence];
}

每次调用nextRound()level 变量就会递增1,并准备下一个序列。每一轮新的顺序都建立在前一轮的基础上,所以我们需要做的是复制现有的按钮按压顺序,并在其中加入一个新的随机顺序。我们已经在nextRound() 的最后一行完成了前者,所以现在让我们在这个序列中添加一个新的随机按钮。

nextRound() 的上方创建一个新的nextStep() 函数。

function nextStep() {
  const tiles = ['red', 'green', 'blue', 'yellow'];
  const random = tiles[Math.floor(Math.random() * tiles.length)];

  return random;
}

tiles 这个变量包含了游戏板上每个按钮的颜色。注意这些值是如何与HTML中的data-tile 属性的值相对应的。

<div class="tile tile-red" data-tile="red"></div>
<div class="tile tile-green" data-tile="green"></div>
<div class="tile tile-blue" data-tile="blue"></div>
<div class="tile tile-yellow" data-tile="yellow"></div>

我们需要在每次执行nextStep() 时从数组中获得一个随机值,我们可以通过使用Math.random() 函数和Math.floor() 来实现。前者返回一个范围在0到小于1之间的浮点数、伪随机数。

Math.random console demonstration

目前,这对我们来说不是很有用。它需要被转换为tiles 数组的有效索引(本例中为0、1、2或3),以便每次都能从数组中检索出一个随机值。将Math.random() 的值与tiles 的长度(4)相乘,可以确保随机数的范围现在是在0和小于4之间(而不是0和小于1)。

Math.random action in the console

但是,这些小数值不是有效的数组索引,所以Math.floor() ,将数字四舍五入为小于或等于给定值的最大整数。这样我们就得到了0到3之间的整数,可以用来从tiles 数组中检索一个随机值。

Math.floor demonstration in the console

让我们在nextRound() 中利用nextStep() 函数的返回值,如下所示。

function nextRound() {
  level += 1;

  const nextSequence = [...sequence];
  nextSequence.push(nextStep());
}

这里发生的情况是,当nextStep() 被执行时,它从tiles 数组中返回一个随机值("红"、"蓝"、"绿 "或 "黄"),该值被添加到nextSequence() 数组的末端,与前一轮的任何值一起。

播放下一回合

下一步是通过按正确的顺序激活屏幕上的瓷砖来实际进行下一轮游戏。在你的JavaScript文件中添加以下函数:nextStep()

function activateTile(color) {
  const tile = document.querySelector(`[data-tile='${color}']`);
  const sound = document.querySelector(`[data-sound='${color}']`);

  tile.classList.add('activated');
  sound.play();

  setTimeout(() => {
    tile.classList.remove('activated');
  }, 300);
}

function playRound(nextSequence) {
  nextSequence.forEach((color, index) => {
    setTimeout(() => {
      activateTile(color);
    }, (index + 1) * 600);
  });
}

playRound() 函数接收一个序列数组并对其进行迭代。然后,它使用setTimeout() 函数以600毫秒的间隔为序列中的每个值调用activateTile() 。之所以在这里使用setTimeout() ,是为了在每个按钮的按下之间增加一个人为的延迟。没有它,序列中的瓷砖将被一次性激活。

setTimeout() 函数中指定的毫秒数在每次迭代时都会改变。序列中的第一个按钮在600毫秒后被激活,下一个按钮在1200毫秒后(第一个按钮后600毫秒),第三个按钮在1800毫秒后,以此类推。

activateTile() 函数中,color 的值被用来选择适当的瓦片和音频元素。在HTML文件中,注意到audio 元素上的data-sound 属性是如何对应于按钮颜色的。

<audio src="https://s3.amazonaws.com/freecodecamp/simonSound1.mp3" data-sound="red" ></audio>
<audio src="https://s3.amazonaws.com/freecodecamp/simonSound2.mp3" data-sound="green" ></audio>
<audio src="https://s3.amazonaws.com/freecodecamp/simonSound3.mp3" data-sound="blue" ></audio>
<audio src="https://s3.amazonaws.com/freecodecamp/simonSound4.mp3" data-sound="yellow" ></audio>

activated 类被添加到选定的瓷砖上,play() 方法被触发到选定的音频元素上,导致src 属性中的链接的MP3文件被播放。300毫秒后,activated 类再次被移除。其效果是,每个瓦片被激活300毫秒,在序列中瓦片激活之间有300毫秒。

最后,在nextRound() 函数中调用playRound() ,在startGame() 函数中调用nextRound() ,如下图所示。

function nextRound() {
  level += 1;

  const nextSequence = [...sequence];
  nextSequence.push(nextStep());
  playRound(nextSequence);
}

function startGame() {
  startButton.classList.add('hidden');
  info.classList.remove('hidden');
  info.textContent = 'Wait for the computer';
  nextRound();
}

现在,一旦你点击开始按钮,第一轮就会开始,棋盘上会有一个随机按钮被激活。

Simon Game Next round demo

玩家的回合

一旦计算机通过激活下一序列的瓷砖开始一轮,玩家需要按照正确的顺序重复瓷砖激活的模式来结束这一轮。如果沿途错过了一个步骤,游戏就会结束并重新设置。

选择info 变量下面的标题和瓦片容器元素。

const heading = document.querySelector('.js-heading');
const tileContainer = document.querySelector('.js-container');

接下来,创建一个humanTurn 函数,表明计算机已经完成了这一轮,是时候让玩家重复这个顺序了。

function humanTurn(level) {
  tileContainer.classList.remove('unclickable');
  info.textContent = `Your turn: ${level} Tap${level > 1 ? 's' : ''}`;
}

第一步是将unclickable 类从瓦片容器中移除。这个类可以防止在游戏没有开始和人工智能没有完成按压顺序时按下按钮。

.unclickable {
  pointer-events: none;
}

在下一行中,info 元素的内容被改变,以表明玩家可以开始重复该序列。它还显示了需要输入多少次敲击。

humanTurn() 函数需要在计算机的序列结束后执行,所以我们不能立即调用它。我们需要添加一个人为的延迟,并计算出计算机何时完成敲击按钮的序列。

function nextRound() {
  level += 1;

  const nextSequence = [...sequence];
  nextSequence.push(nextStep());
  playRound(nextSequence);

  sequence = [...nextSequence];
  setTimeout(() => {
    humanTurn(level);
  }, level * 600 + 1000);
}

上面的setTimeout() 函数在序列中最后一个按钮被激活后一秒钟执行humanTurn() 。序列的总持续时间相当于当前级别乘以600ms,这是序列中每块瓷砖的持续时间。sequence 变量也被分配给更新的序列。

在对nextRound() 的下一次更新中,当回合开始时,unclickable 类被添加到瓦片容器中,并且infoheading 元素的内容被更新。

function nextRound() {
  level += 1;

  tileContainer.classList.add('unclickable');
  info.textContent = 'Wait for the computer';
  heading.textContent = `Level ${level} of 20`;

  const nextSequence = [...sequence];
  nextSequence.push(nextStep());
  playRound(nextSequence);

  sequence = [...nextSequence];
  setTimeout(() => {
    humanTurn(level);
  }, level * 600 + 1000);
}

Simon game demo

现在的标题反映了当前的水平

下一步是检测玩家对按钮的点击,并决定是否进入下一轮或结束游戏。添加以下事件监听器,就在startButton 的下面。

tileContainer.addEventListener('click', event => {
  const { tile } = event.target.dataset;

  if (tile) handleClick(tile);
});

在上面的事件监听器中,被点击的元素上data-tile 的值被访问并存储在tile 变量中。如果该值不是一个空字符串(对于没有data-tile 属性的元素),handleClick() 函数将被执行,其唯一的参数是tile 值。

如下图所示,在startGame() 的正上方创建handleClick() 函数。

function handleClick(tile) {
  const index = humanSequence.push(tile) - 1;
  const sound = document.querySelector(`[data-sound='${tile}']`);
  sound.play();

  const remainingTaps = sequence.length - humanSequence.length;

  if (humanSequence.length === sequence.length) {
    humanSequence = [];
    info.textContent = 'Success! Keep going!';
    setTimeout(() => {
      nextRound();
    }, 1000);
    return;
  }

  info.textContent = `Your turn: ${remainingTaps} Tap${
    remainingTaps > 1 ? 's' : ''
  }`;
}

这个函数将瓦片值推送到humanSequence 数组中,并将其索引存储在index 变量中。播放按钮对应的声音,计算出序列中剩余的步骤并在屏幕上更新。

if 块比较了humanSequence 数组和sequence 数组的长度。如果它们相等,就意味着这一轮已经结束,下一轮可以开始。这时,humanSequence 数组被重置,一秒钟后调用nextRound() 函数。延迟是为了让用户看到成功信息,否则,它根本就不会出现,因为它将立即被覆盖。

Simon game demo

喘口气,看看这一步结束时的完整代码

比较序列

我们需要比较玩家点击按钮的顺序和游戏生成的顺序。如果顺序不一致,游戏就会重设,并显示一条信息提醒玩家失败。

为此,在humanTurn() 上创建一个新的resetGame() 函数。

function resetGame(text) {
  alert(text);
  sequence = [];
  humanSequence = [];
  level = 0;
  startButton.classList.remove('hidden');
  heading.textContent = 'Simon Game';
  info.classList.add('hidden');
  tileContainer.classList.add('unclickable');
}

这个函数显示一个警告,并将游戏恢复到原来的状态。让我们在handleClick 中使用它,如下图所示。

function handleClick(tile) {
  const index = humanSequence.push(tile) - 1;
  const sound = document.querySelector(`[data-sound='${tile}']`);
  sound.play();

  const remainingTaps = sequence.length - humanSequence.length;

  if (humanSequence[index] !== sequence[index]) {
    resetGame('Oops! Game over, you pressed the wrong tile');
    return;
  }

  if (humanSequence.length === sequence.length) {
    humanSequence = [];
    info.textContent = 'Success! Keep going!';
    setTimeout(() => {
      nextRound();
    }, 1000);
    return;
  }

  info.textContent = `Your turn: ${remainingTaps} Tap${
    remainingTaps > 1 ? 's' : ''
  }`;
}

如果sequencehumanSequence 两个数组中由index 检索到的元素的值不匹配,这意味着玩家转错了弯。在这一点上,会显示一个警告,游戏也会重置。

准备一个结束状态

游戏大部分时间都在工作,但我们需要引入一个结束状态,让玩家赢得游戏。我选了20轮,但你可以使用你喜欢的任何数字。经典的西蒙游戏在35轮后结束。

如果用户达到并完成了第20轮,这里是结束游戏的部分。

function handleClick(tile) {
  const index = humanSequence.push(tile) - 1;
  const sound = document.querySelector(`[data-sound='${tile}']`);
  sound.play();

  const remainingTaps = sequence.length - humanSequence.length;

  if (humanSequence[index] !== sequence[index]) {
    resetGame('Oops! Game over, you pressed the wrong tile');
    return;
  }

  if (humanSequence.length === sequence.length) {
    if (humanSequence.length === 20) {
      resetGame('Congrats! You completed all the levels');
      return
    }

    humanSequence = [];
    info.textContent = 'Success! Keep going!';
    setTimeout(() => {
      nextRound();
    }, 1000);
    return;
  }

  info.textContent = `Your turn: ${remainingTaps} Tap${
    remainingTaps > 1 ? 's' : ''
  }`;
}

一旦完成第20轮,就会显示一条祝贺信息,游戏就会重新设置。一定要试一下,看看你是否能达到20级而不失败。

总结

在本教程中,我们用JavaScript开发了一个正常运行的西蒙游戏。我希望你在制作它时有很多乐趣。

谢谢你的阅读,并祝你编码愉快!