JavaScript 中的 Langton 蚂蚁

在看过康威的生命游戏之后,我一直在研究其他形式的元胞自动机。这让我发现了兰顿蚂蚁,这是一种不同类型的元胞自动机,其中一个代理(即蚂蚁)用于在它绕网格移动时打开或关闭方块。

兰顿蚂蚁的规则非常简单。蚂蚁在网格中移动时只遵循两个规则。

  • 在白色方块处,顺时针旋转 90°,翻转方块的颜色,向前移动一个单位。

  • 在黑色方块处,逆时针旋转 90°,翻转方块的颜色,向前移动一个单位。

这可以通过查看蚂蚁站立的正方形的当前状态然后执行一个动作来用代码表示。特定方向的移动是通过增加或减少 x 或 y 坐标来实现的,具体取决于蚂蚁指向的方向。

最近我一直在自学一些关于 JavaScript 的知识,所以我想使用 HTML 画布元素在 JavaScript 中创建一个兰顿蚂蚁。

要生成画布,我们只需启动一个包含以下内容的网页。

<canvas id="grid" width="500" height="500"></canvas>

与康威的生命游戏一样,我们可以轻松地将正方形网格表示为二维数组。我们网格中的每个单元格都可以表示为单元格类型的对象。这个对象有一个“活着”的属性和两个允许我们与这个属性交互的函数。

class Cell {

  alive = false;

 

  setAlive(alive) {

   this.alive= alive;

  }

 

  get isAlive() {

    return this.alive;

  }

}

蚂蚁本身可以用一个 Ant 类型的对象来表示。Ant 对象的属性是它在我们网格上的坐标以及它当前面向的方向。方向被定义为我们可以稍后定义的常数。

class Ant {

  x = 0;

  y = 0;

 

  direction = ANTUP;

}

单元格网格可以由 Grid 类型的对象表示。它有许多属性,包括单元格数组、蚂蚁以及网格本身的高度和宽度。然后包含一个初始化函数,以便我们可以用 Cell 对象预先填充网格的单元格,然后创建我们的 Ant 对象。

class Grid {

  cells = [];

  ant;

  height = 0;

  width = 0;

 

  constructor(height, width) {

   this.height= height;

   this.width= width;

  }

 

  init() {

    for (let x = 0; x < this.width; x++) {

      this.cells[x] = [];

      for (let y = 0; y < this.height; y++) {

        const cell = new Cell();

        this.cells[x][y] = cell;

      }

    }

   this.ant= new Ant();

    this.ant.x =this.width/ 2;

    this.ant.y =this.height/ 2;

  }

 

  move() {

     // 稍后填写

  }

}

有了所有这些,我们现在可以实例化 Grid 对象并开始运行我们的动画。下面的代码目前不会做太多事情,因为我们还没有定义和移动,但它会设置我们需要的所有东西。最后一次调用setInterval()将调用 'ant' 函数大约每秒 13 次。

window.onload = function() {

  canvas = document.getElementById('grid');

  ctx = canvas.getContext('2d');

  const grid = new Grid(canvas.width, canvas.height);

  grid.init();

  setInterval(moveAnt, 1000/13, grid);

}

 

function moveAnt(grid) {

  grid.move();

  ctx.stroke();

}

我们用一个方向定义了 Ant 对象,所以现在让我们定义这些方向。我们将指南针的每个轴设置为一个数字,这意味着为了顺时针旋转我们只需要增加和逆时针旋转我们只需要减少。

const ANTUP = 0;

const ANTRIGHT = 1;

const ANTDOWN = 2;

const ANTLEFT = 3;

有了它,我们可以用一些移动和旋转功能充实 Ant 对象。请注意,我们可以在这里使用简洁的模数数学来循环高度、宽度和方向值。这不仅意味着我们将从上到下和从左到右循环,而且还会循环,而不会有很多 if 语句使代码混乱。

class Ant {

  x = 0;

  y = 0;

 

  direction = ANTUP;

 

  moveForward(width, height) {

    switch (this.direction) {

      case ANTUP:

       this.x= ((this.x - 1) + width) % width;

        break;

      case ANTRIGHT:

       this.y= ((this.y + 1) + height) % height;

        break;

      case ANTDOWN:

       this.x= ((this.x + 1) + width) % width;

        break;

      case ANTLEFT:

       this.y= ((this.y - 1) + height) % height;

        break;

    }

  }

 

  rotateRight() {

   this.direction= ((this.direction + 1) + (ANTLEFT + 1)) % (ANTLEFT + 1);

  }

 

  rotateLeft() {

   this.direction= ((this.direction - 1) + (ANTLEFT + 1)) % (ANTLEFT + 1);

  }

}

在上面的代码中,Ant 的移动是通过 x 或 y 坐标的加减来实现的。例如,为了向上移动网格,我们将从 x 值中减去,因为画布中的 x 从左上角开始。

定义 Ant 后,我们现在可以在 Grid 对象中填充移动函数。canvas 元素非常适合这种细胞处理,因为可以使用该fillRect()函数绘制单个 1x1 像素的正方形 ,因此我们在这里利用了该函数。

move函数需要做以下事情:

  • 抓取 Ant 所在的当前 Cell 对象。

  • 根据单元格的当前状态对单元格做出决定。

    • 如果单元格处于活动状态,那么我们用白色填充正方形,向右旋转 Ant 并将 Ant 向前移动一个单元格。

    • 如果单元格处于非活动状态,那么我们用黑色填充正方形,向左旋转 Ant 并将 Ant 向前移动一个单元格。

要在画布上创建一个 1x1 像素的正方形,我们只需要设置画布的填充样式,然后调用该fillRect()函数。在下面的代码中,我在当前 Ant 位置上绘制了一个黑色方块。

ctx.fillStyle = 'black';

ctx.fillRect(this.ant.x, this.ant.y, 1, 1);

在该过程结束时,我们将 Ant 当前所在的单元格变为红色,这使我们能够看到 Ant 和它当前在网格上的位置。 

class Grid {

  /// .. snip .. ///

  move () {

    let cell = this.cells[this.ant.x][this.ant.y];

    if (cell.isAlive) {

     cell.alive= false;

     ctx.fillStyle= 'white';

      ctx.fillRect(this.ant.x, this.ant.y, 1, 1);

      this.ant.rotateRight();

      this.ant.moveForward(this.width, this.height);

    }

    else {

     cell.alive= true;

     ctx.fillStyle= 'black';

      ctx.fillRect(this.ant.x, this.ant.y, 1, 1);

      this.ant.rotateLeft();

      this.ant.moveForward(this.width, this.height);

    }

   ctx.fillStyle= 'red';

    ctx.fillRect(this.ant.x, this.ant.y, 1, 1);

  }

}

有了所有这些,我们可以运行模拟,我们应该看到类似的东西。

请注意,此结构在移动周期中经过 10,000 步后开始自行构建。不幸的是,以每秒 13 帧的速度到达这个结构大约需要 15 分钟。我们可以通过在move()函数中添加一个循环来大大加快速度。这意味着每次移动函数运行时,我们都会在 Ant 循环中处理 100 个 septs,并在该过程中有效地向前跳转。因此,在 Ant 取得一些进展之前,我们不需要刷新画布。

class Grid {

  /// .. snip .. ///

  move () {

    for (let i = 0; i < 100; i++) {

      let cell = this.cells[this.ant.x][this.ant.y];

      if (cell.isAlive) {

       cell.alive= false;

       ctx.fillStyle= 'white';

        ctx.fillRect(this.ant.x, this.ant.y, 1, 1);

        this.ant.rotateRight();

        this.ant.moveForward(this.width, this.height);

      }

      else {

       cell.alive= true;

       ctx.fillStyle= 'black';

        ctx.fillRect(this.ant.x, this.ant.y, 1, 1);

        this.ant.rotateLeft();

        this.ant.moveForward(this.width, this.height);

      }

     ctx.fillStyle= 'red';

      ctx.fillRect(this.ant.x, this.ant.y, 1, 1);

      this.moves++;

    }

  }

}

有了这个,不久我们就会开始看到这种东西被展示出来。

我认为只用几个简单的规则就可以创造出具有这种复杂性的东西,这真的很有趣。创建的图片随机开始,然后蚂蚁开始在这些高速公路上移动。

如果您想自己使用此代码,那么我已经创建了一个 Langton's Ant 代码笔,上面运行的所有代码都在运行。作为对上述代码的一个小改进,我还添加了一个计步器,以便您可以轻松查看 Ant 移动了多少步。这涉及向 Grid 对象添加一个移动属性,然后在每次运行该move()函数时增加它。然后通过将 move 的值注入到页面的 HTML 中,在画布下方添加一个正在运行的打印输出。

在自然界中很少发生的一件事是独自看到一只蚂蚁。我意识到应该可以创建多只蚂蚁的模拟,多亏了我在这里采用的面向对象的方法,将代码转换为同时模拟许多蚂蚁非常简单。如果您有兴趣查看结果,那么我还为多个蚂蚁创建了一个代码笔。

如果您想阅读更多内容,我建议您查看有关 Langton's Ant 的维基百科页面。这篇文章更多地讨论了 Ant 如何移动,还提到了人们使用不止一种颜色来改变 Ant 在网格上移动的方式的扩展。

以上是 JavaScript 中的 Langton 蚂蚁 的全部内容, 来源链接: utcz.com/z/347545.html

回到顶部