浏览器中的元胞自动机

图片

元胞自动机是一个系统,它由网格中具有数值的单元以及确定这些单元行为的规则组成。 与规则的可视化并行地将规则重复应用于每个网格单元,即使规则相对简单,也常常可以得到具有复杂复杂行为的某种进化生物的影响。

细胞自动机具有各种形状,类型和尺寸。 也许最著名的细胞自动机是康威的生命游戏(GOL)。 它由一个二维网格组成,其中每个单元格都包含一个二进制值(有效或无效)。 附带的规则基于相邻单元格的状态来确定该单元格是死亡还是存活。 规则说,如果周围有少于两个活细胞,则活细胞会死于孤独。 如果三个以上的相邻细胞还活着,她将因人口过剩而死亡。 换句话说,如果某个细胞周围有2或3个存活的相邻细胞,则该细胞“存活”。 要使一个细胞复活 ,它必须恰好有3个活着的相邻细胞,否则它将保持死状态。 下面显示了GoL机器迭代几个状态的示例。

生活游戏

元胞自动机的另一个著名版本是一维的。 它被称为基本元胞自动机(ECA)。 这就是我们在本文中实现的。

此自动机的每个状态都存储为一维布尔值数组,并且需要两个维来可视化GOL状态,但一个自动值足以满足该自动机的需要。 因此,我们可以使用二维(而不是动画)来可视化此自动机状态的整个历史记录。 与GOL情况一样,本机中单元的状态为0或1,但与GOL单元不同,该单元根据其8个邻居进行更新,而ECA单元则根据左邻居,右邻居及其自身的状态进行更新!

规则示例如下所示:顶部的三个单元格是规则的输入,底部的一个单元格是输出,黑色是1,白色是0。此外,我们可以看到当初始状态均为0时,它们各自生成的模式在中间的单元格中。


您可能想知道:为什么上面给出的规则用数字表示? 因为从0到255范围内的每个数字都直接对应于ECA规则,因此这些数字将用作规则的名称。 该对应关系如下所示:


从数字到规则

0到255之间的任何数字都可以用仅8位的二进制形式表示(上面的第一个箭头)。 此外,我们可以根据其位置(第二箭头)为每个数字提供索引。 自然地,这些索引在0到7的范围内,也就是说,它们只能用3位数字(第三个箭头)以二进制形式表示。 解释这3位数字作为输入,并将原始数字中的相应数字作为输出,我们得到了所需的三进制函数(第四个箭头)。

规则生成


让我们将上述解释实现为一个高阶函数get_rule ,该函数接收从0到255之间的数字作为输入,并返回与该数字相对应的ECA规则。

我们需要创建如下内容:

 const rule30 = get_rule(30); const output110 = rule30(1, 1, 0); 

在上面的示例中,起始rule30(1,1,0)所有三个二进制值组合为一个数字(110 = 6),并将在二进制表示形式30中的那个位置(6)返回一位。二进制表示形式的数字30为00011110,因此,该函数将返回0(我们从右边开始计数,并从0开始计数)。

知道三个二进制输入变量将被组合为一个数字,让我们从实现这样的combine函数开始。

 const combine = (b1, b2, b3) => (b1 << 2) + (b2 << 1) + (b3 << 0); 

将参数左移到相应的位置,然后将三个移位的数字相加,即可得到所需的组合。

get_rule函数的第二个重要部分是确定数字中特定位置的位值。 因此,让我们创建一个函数get_bit(num, pos) ,该函数可以在给定位置pos上以给定数字num返回位值。 例如,二进制数141为10001101,因此get_bit(2, 141)应该返回1 ,而get_bit(5, 141)应该返回0

可以通过首先将数字右移pos ,然后对数字1执行按位运算“ AND”,来实现get_bit(num,pos)函数。

 const get_bit = (num, pos) => (num >> pos) & 1; 

现在我们只需要结合这两个功能:

 const get_rule = num => (b1, b2, b3) => get_bit(num, combine(b1, b2, b3)); 

太好了! 因此,我们有一个函数,可以为间隔内的每个数字提供唯一的ECA规则,使我们可以执行任何操作。 下一步是在浏览器中呈现它们。

规则可视化


为了在浏览器中呈现自动机,我们将使用canvas元素。 可以如下创建canvas并将其添加到html主体中:

 window.onload = function() { const canvas = document.createElement('canvas'); canvas.width = 800; canvas.height = 800; document.body.appendChild(canvas); }; 

为了能够与canvas交互,我们需要上下文 。 上下文允许我们绘制形状和线条,为对象着色以及通常在canvas导航。 它是通过canvasgetContext方法提供给我们的。

 const context = canvas.getContext('2d'); 

参数'2d'是指在此示例中将使用的上下文类型。

接下来,我们将创建一个函数,该函数具有上下文,ECA规则以及有关像元大小和数量的一些信息,可在canvas上绘制该规则。 这个想法是逐行生成和绘制一个网格。 代码的主要部分如下所示:

 function draw_rule(ctx, rule, scale, width, height) { let row = initial_row(width); for (let i = 0; i < height; i++) { draw_row(ctx, row, scale); row = next_row(row, rule); } } 

我们从某种初始的单元格集开始,即当前行。 如上例所示,此行通常包含全零,中间单元格中只有一个单位,但它也可以包含完全随机的1和0行。我们绘制此行单元格,然后使用该规则根据当前行。 然后,我们只需重复绘图并计算新步骤,直到发现网格足够高。

对于以上代码段,我们需要实现三个函数: initial_rowdraw_rownext_row

initial_row是一个简单的函数。 它创建一个零数组,并将数组中心的元素更改为一。

 function initial_row(length) { const initial_row = Array(length).fill(0); initial_row[Math.floor(length / 2)] = 1; return initial_row; } 

由于我们已经有一个规则函数,因此next_row函数可以由一行组成。 新行中每个单元格的值是将规则与最近的单元格的值一起应用的结果,而旧行用作输入。

 const next_row = (row, rule) => row.map((_, i) => rule(row[i - 1], row[i], row[i + 1])); 

您是否注意到我们在这条线上作弊了? 新行中的每个单元都需要来自其他三个单元的输入,但是该行边缘的两个单元仅从两个单元接收数据。 例如, next_row[0]试图从row[-1]获取next_row[0]值。 这仍然有效,因为当试图通过不在数组中的索引访问值时,javascript返回undefined ,并且碰巧(undefined >> [ ]) (来自combine函数)总是返回0。这意味着实际上,我们将数组外部的每个值都处理为0。

我知道这很丑陋,但是很快我们将在屏幕上创建一些漂亮的东西,以便我们可以原谅。

接下来是draw_row函数; 是她执行渲染!

 function draw_row(ctx, row, scale) { ctx.save(); row.forEach(cell => { ctx.fillStyle = cell === 1 ? '#000' : '#fff'; ctx.fillRect(0, 0, scale, scale); ctx.translate(scale, 0); }); ctx.restore(); ctx.translate(0, scale); } 

在这里,我们非常依赖于上下文对象,使用至少5种不同的方法。 这里是一个简短的清单以及如何使用它们。

  • fillStyle指示我们要如何填充形状。 它可以是颜色,例如"#f55" ,也可以是渐变或图案。 我们使用这种方法在视觉上将单元格0与单元格1分开。
  • fillRect(x, y, w, h)从点(x,y fillRect(x, y, w, h)绘制一个矩形,其宽度为w且高度为h,并根据fillStyle填充。 我们的矩形是简单的正方形,但是您可能会惊讶于所有矩形的起点都是原点。 之所以发生这种情况,是因为我们将此方法与translate结合使用。
  • translate(x, y)允许您移动整个坐标系。 位置已保存,因此该方法是跟踪元素的不同位置的绝佳替代方法。 例如,我们可以简单地绘制一个单元格,向右移动,绘制一个新的单元格,而不是计算每个单独的网格单元格的位置。
  • save()restore()translate和其他坐标转换方法结合使用。 我们使用它们在特定点保存当前坐标系,以便稍后我们可以返回到它(使用restore )。 在这种情况下,我们在绘制直线之前先保存坐标系,然后将其向右移动。 然后,当我们完成绘制线并一直向右移动时,坐标将恢复并返回到原始状态。 然后我们向下移动以准备绘制下一行。

现在我们有了draw_rule函数所需的所有部分。 在准备canvas之后,我们在window.onload使用此功能。 我们还将确定所需的参数。

 window.onload = function() { const width = 1000; // Width of the canvas const height = 500; // Height of the canvas const cells_across = 200; // Number of cells horizontally in the grid const cell_scale = width / cells_across; // Size of each cell const cells_down = height / cell_scale; // Number of cells vertically in the grid const rule = get_rule(30); // The rule to display const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; document.body.appendChild(canvas); const context = canvas.getContext('2d'); draw_rule(context, rule, cell_scale, cells_across, cells_down); }; 

我们提取canvas尺寸作为单独的变量以及水平单元格的数量。 然后我们计算cell_scale和'cells_down',以便网格填充整个canvas ,而单元格保持正方形。 因此,我们可以轻松地更改canvas剩余的网格的“分辨率”。


仅此而已! 完整的代码示例在githubcodepen上

继续前进


借助这一系统,我们将能够依次检查所有256条规则,这些规则可以迭代,更改代码或在每次页面加载时随机选择一个规则号。 尽管如此,在我们受控的环境中研究所有这些不可预测的结果非常令人兴奋。

您还可以使自动机的像元的初始状态随机,而不是使静态“实零和一个单位”成为随机状态。 因此,我们得到了更加不可预测的结果。 此版本的initial_row函数可以这样编写:

 function random_initial_row(width) { return Array.from(Array(width), _ => Math.floor(Math.random() * 2)); } 

在下面,您可以看到输出线变化对输出有很大的影响。


随机源字符串

这只是您可以更改的一个方面! 为什么只将自己限制在两个条件下? (从2个状态转换为3个状态会将规则数量从256个增加到7 625 597 484 987个!)为什么限于正方形? 为什么只有二维? 为什么一次只有一条规则?

下面显示了基于ECA的可视化示例,但具有另一个draw_rule函数,该函数draw_rule线条不是等角线而是等距图案,然后用颜色填充这些线定义的区域。 您甚至不必显示分隔线,而只需显示颜色。


如果走得更远,则可以添加轴向(中间行)和镜像(底部行)的对称性。


如果这些可视化对您来说很有趣,但是研究此交互式沙箱 ,或者甚至更好,请从我们创建的代码开始,并尝试提出自己的蜂窝自动机!

祝你好运

Source: https://habr.com/ru/post/zh-CN481672/


All Articles