康威生命游戏 (英语: Conway's Game of Life ),又称康威生命棋,是英国数学家 约翰·何顿·康威 ( John Horton Conway )在1970年发明。
生命游戏是一个零玩家游戏。它是一个二维矩形世界,这个世界中的每个方格是一个活着或死了的细胞。一个细胞在下一个时刻生死取决于相邻八个方格中活着的或死了的细胞的数量。 在游戏的进行中,杂乱无序的细胞会逐渐演化出各种精致、有形的结构;这些结构往往有很好的对称性,而且每一代都在变化形状。而一些形状一经锁定,便不再逐代变化。有时,一些已经成形的结构会因为一些无序细胞的“入侵”而被破坏。但是形状和秩序经常能从杂乱中产生出来。
生命游戏是一个很具有“魔性”的数学游戏。 它通过几条极为简单的规则,就能演化出极具复杂性的“世界”。
发几个效果给你们感受一下(转自 维基百科) :
生命游戏在计算机上被实现过无数个版本,也给多个领域的后来者带来不少启发,甚至催生了一个全新的数学分支:细胞自动机。我当年在学编程的时候,也自己写过。
它 的发明者康威是普林斯顿大学教授、英国皇家学会院士,在多个数学领域均有所建树。然而不幸的是,在几天前,这位杰出的数学家因感染新冠肺炎而离世,终年82岁。
今天我们分享一篇生命游戏的Python实现,让更多人了解这个有趣而伟大的发明,也以此 作为对老爷子的纪念。
这次我们使用 Python 来实现生命游戏,这是一种简单的元胞自动机。 基于一定规则,程序可以自动从当前状态推演到下一状态。 制作的成品如下:
先来说说生命游戏的规则:
在生命游戏中,每个单元格有两种状态,生与死。在我们的实现中,黄色的单元格代表活着的细胞,红色单元格表示死亡的细胞。而每一个细胞的下一状态,是由该细胞及周围的八个细胞的当前状态决定的。
具体而言:
当前细胞为活细胞
周围有两个或者三个活细胞,下一世代,该细胞仍然活着。
周围少于两个活细胞,该细胞死于孤立。
周围多于三个活细胞,该细胞死于拥挤。
当前细胞为死细胞
所需模块 无需安装的标准库:
第三方库:
导入模块:
import argparsefrom enum import IntEnumimport matplotlib.pyplot as pltimport matplotlib.animation as animation # 制作动图 import numpy as np
编程要点 首先,我们要知道细胞的生存空间是 N * N 的方阵,每个细胞都有两种状态:on, off。on 为 255,off 为 0。我们使用 numpy 产生 N * N 的方阵。np.random.choice 是在 State.on 和 State.off ,等概率随机抽取元素构造 N * N 的方阵。
class State (IntEnum) : on = 255 off = 0 def random_data (length = 4 , seed = 420 ) -> np.array: np.random.seed(seed) return np.random.choice([State.off, State.on], size=(length, length), p=[0.5 , 0.5 ])
其次我们要明白如何计算细胞周围活细胞的个数,尤其是边界一圈的细胞。我们可以采用余数的方式,假设棋盘大小为 9 * 9,那么对于左右边界而言,左边界的左边一个元素的计算方式:- 1 % 9 = 8,自动折到右边界上。将细胞周围八个单元格的数值加起来,除以 255,就可以得到细胞周围活细胞的个数。
def _count (data, row, col) : shape = data.shape[0 ] up = (row - 1 ) % shape down = (row + 1 ) % shape right = (col + 1 ) % shape left = (col - 1 ) % shape return (data[up, right] + data[up, left] + data[down, right] + data[down, left] + data[row, right] + data[row, left] + data[up, col] + data[down, col]) // 255
接下来是对规则的翻译,即根据当前世代的状态,推演出下一世代,细胞的状态。initial 为当前世代的矩阵,data为下一世代的矩阵,我们根据 initial 的数值,计算出 data 的数值。total 为周围活细胞的个数,如果当前为活细胞,total 大于三或者小于二,下一世代就会死去。如果当前为死细胞,total 等于三,下一世代活细胞就会繁殖到该单元格上。
def count (initial, data, row, col) : total = _count(initial, row, col) if initial[row, col]: if (total 2) or (total > 3 ): data[row, col] = State.off else : if total == 3 : data[row, col] = State.on
接下来是制作动图的过程,前面几行是绘图的基本操作。之后,我们使用到了 matplotlib.animation 的方法。其中,FuncAnimation 接受的参数含义:fig 为图像句柄,generate 函数是我们更新每一帧数据的函数,下面会有介绍。fargs 为 genrate 函数的除去第一个参数的其他附加参数,而第一个参数由 FuncAnimation 指定的 framenum(帧数) 自动传给 generate 函数。frames 是帧数,interval 是更新图像的时间间隔,save_count 为从帧到缓存的值的数量。
如果指定保存路径(html),则保存为 html 动画。
def update (data, save_name) : update_interval = 50 fig, ax = plt.subplots() ax.set_xticks([]) ax.set_yticks([]) img = ax.imshow(data, cmap='autumn' , interpolation='nearest' ) ani = animation.FuncAnimation(fig, generate, fargs=(img, plt, data), frames=20 , interval=update_interval, save_count=50 ) if save_name: ani.save(save_name, fps=30 , extra_args=['-vcodec' , 'libx264' ]) plt.show()
下面我们来看 generate 函数,NUM 为当迭代次数,frame_num 接收来自 FuncAnimation 的帧数。通过嵌套的 for 循环,我们逐个地更新方阵中各元素的状态。
NUM = 0 def generate (frame_num, img, plt, initial) : global NUM NUM += 1 plt.title(f'{NUM} generation' ) data = initial.copy() rows, cols = data.shape for row in range(rows): for col in range(cols): count(initial, data, row, col) img.set_data(data) initial[:] = data[:] return img
最后,我们可以通过命令行参数,运行我们的程序:
-- size 参数为棋盘大小,--seed 为随机种子,用于产生不同的随机方阵。
python conway.py --size 50 --seed 18
高斯帕滑翔机(Gosper Glider Gun)
生命游戏中演化不少特殊的模式,高斯帕滑翔机就是其中之一:这种图形每15代产生一架“滑翔机”,无限延续。
我们在代码中也实现了它。
另外可将 --gosper 更改为 --glider 滑翔机。--save 为动图保存的地址。
python conway.py --size 80 --gosper --save gosper.html