If you're seeing this message, it means we're having trouble loading external resources on our website.

如果你被网页过滤器挡住,请确保域名*.kastatic.org*.kasandbox.org 没有被阻止.

主要内容

记忆游戏:绘制砖瓦的网格

玩“记忆”游戏的第一步是随机洗牌, 然后将它们排列成一个矩形网格,正面朝下,这样我们就看不到哪个图像在哪张卡片的另一面。

将卡片正面朝下

开始编写这个游戏,我们目前只需思考创建面朝下的卡片,之后再去想如何做出不同的图像。
“卡片”(tile)是“记忆”游戏中的一个非常重要的对象,所以我们将使用面向对象的原则来定义 Tile 对象,然后创建它的多个实例。然后我们就可以把属性(例如位置和图像)以及行为(例如绘制它们)与每个 Tile 相关联。
首先,我们将定义Tile构造函数。 因为我们还没有处理图像,所以我们只将xy参数传递给它。 我们还将记住该对象的属性中的图块大小(常数)。
var Tile = function(x, y) {
  this.x = x;
  this.y = y;
  this.size = 50;
};
现在我们已经定义了构造函数,我们可以在循环中使用它在适当的 x 和 y 位置创建卡片。事实上我们将使用两个 for 循环——嵌套的 for 循环——因为这让为网格生成坐标在概念上变得容易。
首先我们需要声明一个空的 tiles 数组,以存储所有这些卡片:
var tiles = [];
外层循环重复我们想要的任何数量的列,我们的内层循环每重复一行,都用与该行和列相对应的 x 和 y 初始化每个新的 Tile
var NUM_COLS = 5;
var NUM_ROWS = 4;
for (var i = 0; i < NUM_COLS; i++) {
  for (var j = 0; j < NUM_ROWS; j++) {
    var tileX = i * 54 + 5;
    var tileY = j * 54 + 40;
    tiles.push(new Tile(tileX, tileY));
  }
}
但是我们很难知道这些卡片看起来好不好,因为我们还没有任何用来绘制它们的代码!事实上,也许我们应该先写了这些代码。有时候在编程中很难知道先做什么,不是吗?现在,让我们向 Tile 对象添加一个方法,该方法在画布上绘制正面朝下的卡片。我们将在指定的位置绘制一个有一片可爱的可汗叶子的圆角矩形。
Tile.prototype.draw = function() {
  fill(214, 247, 202);
  strokeWeight(2);
  rect(this.x, this.y, this.size, this.size, 10);
  image(getImage("avatars/leaf-green"),
        this.x, this.y, this.size, this.size);
};
我们马上就能检查我们的卡片是什么样子了!让我们添加一个新的 for 循环,该循环遍历所有卡片并调用绘图方法:
for (var i = 0; i < tiles.length; i++) {
    tiles[i].draw();
}
下面是包含所有代码的程序的样子。尝试调整嵌套 for 循环中的不同数字,看看它是如何更改网格或更改其绘制方式的(可能是不同的标志?)

将卡片正面朝上

现在我们已经有了一个正面朝下的卡片网格,让我们来解决一个更棘手的问题:为每个网格分配一个图像,这样数组中的每个图像都变成了2个,全都随机分布。可能有很多方法可以做到这一点,但以下是我的建议:
  1. 利用 getImage 函数从库中选一个些可能图像,创建一个可能图像的数组。
  2. 我们只需要 10 个图像来做 20 张卡片的正面,所以接下来创建一个存放 10 个随机选择的来自第一个数组的图像的两份拷贝的新数组。
  3. 打乱选择的图像的数组,所以一对图像就不会在数组里紧挨着彼此了。
  4. 在创造卡片的嵌套 for 循环里,我们将给每一张卡片分配一个来自之前的数组的图像。
这些步骤可能还没有展示出它们的意义——让我们把每一步都做出来,看看它们是什么样子。
第一步: 利用 getImage 函数从库中选一个些可能图像,创建一个可能图像的数组:
var faces = [
    getImage("avatars/leafers-seed"),
    getImage("avatars/leafers-seedling"),
    getImage("avatars/leafers-sapling"),
    getImage("avatars/leafers-tree"),
    getImage("avatars/leafers-ultimate"),
    getImage("avatars/marcimus"),
    getImage("avatars/mr-pants"),
    getImage("avatars/mr-pink"),
    getImage("avatars/old-spice-man"),
    getImage("avatars/robot_female_1"),
    getImage("avatars/piceratops-tree"),
    getImage("avatars/orange-juice-squid")
];
我挑选了一堆头像,但你可以选择你最喜欢的图像。重要的是要确保这个数组中至少有 10 张图像,这样我们 20 个卡片的图像就不会不够用。不过我们可以添加不止 10 张图片,让我们的游戏每次比赛都有更多的变化,因为我们将在下一步缩小列表范围。
第二步: 我们只需要 10 个图像来做 20 张卡片的正面,所以接下来创建一个存放 10 个随机选择的来自第一个数组的图像的两份拷贝的新数组。
为此,我们创建了一个重复 10 次的 for 循环。在每次重复中,我们从 faces 数组中随机选取一个索引,将该索引进栈到 selected 数组上两次,然后使用 splice 方法将其从 faces 数组中删除,这样我们就不会选择它两次。最后一步是非常重要的!
var selected = [];
for (var i = 0; i < 10; i++) {
    // Randomly pick one from the array of faces
    var randomInd = floor(random(faces.length));
    var face = faces[randomInd];
    // Push 2 copies onto array
    selected.push(face);
    selected.push(face);
    // Remove from faces array so we don't re-pick
    faces.splice(randomInd, 1);
}
第三步: 我们只需要 10 个图像来做 20 张卡片的正面,所以接下来创建一个存放 10 个随机选择的来自第一个数组的图像的两份拷贝的新数组。
你可能在生活中洗过牌,但你可曾在 JavaScript 中洗过数组?在任何编程语言中最流行的洗牌(数组)技术被称为 Fisher-Yates Shuffle,我们也将在这里使用它。
Fisher-Yates Shuffle从在数组中随机选取一个元素开始,然后把它和数组中的最后一个元素互换位置。下一步,在数组中随机选取一个 除了 最后一个元素的元素,然后把它和数组中的 倒数第二个 元素互换位置。持续这种操作,直到每一个元素都换过位置了为止。
你可以点击这个可视化来看看我说的是什么意思:
为了在 JavaScript 中实现这一点,创建一个 shuffleArray 函数,该函数接收一个数组并打乱其元素,更改原始数组:
var shuffleArray = function(array) {
    var counter = array.length;

    // While there are elements in the array
    while (counter > 0) {
        // Pick a random index
        var ind = Math.floor(Math.random() * counter);
        // Decrease counter by 1
        counter--;
        // And swap the last element with it
        var temp = array[counter];
        array[counter] = array[ind];
        array[ind] = temp;
    }
};
如果在一步步了解可视化并阅读代码后这个算法还是不太好理解,你可以用现实中的一张纸牌来尝试,或者看看 Adam Khoury 是如何做到的。
在定义了该函数之后,我们需要调用它:
shuffleArray(selected);
现在我们有一个有 10 对图像的数组,随机洗牌!
第四步: 在创造卡片的嵌套 for 循环里,我们将给每一张卡片分配一个来自之前的数组的图像。
selected 数组中有20张图像,重复 20 次,以便在网格中的位置实例化新的卡片。要为每个卡片选择一个随机图像,我们可以调用数组上的 pop 方法。该方法从数组中删除最后一个元素并将其返回,是确保我们分配所有图像但不会分配两次这些图像的最简单方法。
for (var i = 0; i < NUM_COLS; i++) {
  for (var j = 0; j < NUM_ROWS; j++) {
    var tileX = i * 54 + 5;
    var tileY = j * 54 + 40;
    var tileFace = selected.pop();
    var tile = new Tile(tileX, tileY, tileFace);
    tiles.push(tile);
  }
}
注意到该代码是如何将 tileFace 作为第三个参数传递给 Tile 构造函数的了吗?我们的构造函数最初只有2个参数,xy,但现在通过修改它,我们可以记住每个卡片上的图像,以及它是否正面朝上:
var Tile = function(x, y, face) {
    this.x = x;
    this.y = y;
    this.size = 70;
    this.face = face;
    this.isFaceUp = false;
};
我们现在理论上有图像分配给每个卡片,但我们还没有把它们显示出来!让我们修改 Tile.draw 方法,让它可以绘制正面朝上的卡片:
Tile.prototype.draw = function() {
    fill(214, 247, 202);
    strokeWeight(2);
    rect(this.x, this.y, this.size, this.size, 10);
    if (this.isFaceUp) {
        image(this.face, this.x, this.y,
              this.size, this.size);
    } else {
        image(getImage("avatars/leaf-green"),
              this.x, this.y, this.size, this.size);
    }
};
最后,为了测试它完全运行正常,我们可以更改我们的 for 循环,将每个卡片的 isFaceUp 属性设置为 true,然后再绘制它:
for (var i = 0; i < tiles.length; i++) {
  tiles[i].isFaceUp = true;
  tiles[i].draw();
}
这就是全部了。尝试重新启动它,看看卡片每次是怎么变化的。