Conway生命游戏

Posted on 2022-01-01  29 Views


生命游戏

计算机是一个非常有用的工具,我们可以使用他来建立各种各样的模型,赋予其不同的规则来模拟推算各种事物的演进和规律,以此推进了很多的研究,例如最近的例子,通过计算机来模拟新冠病毒感染效率,并以此推算疫情的发展形式和采取不同措施可能导致的后果。生命游戏也是这样的一种计算机模拟程序,只不过他的规则更为简单,更加适合作为入门程序进行学习。

什么是生命游戏

生命游戏是由英国数学家John Conway研究出来的细胞自动机,即网格上的一组彩色的细胞,根据定义相邻的细胞状态的一组规则来随着时间进行演进。

实际上这也并不是通俗意义上的游戏,他没有玩家的竞争,也没有畅快的操作,只是通过赋予不同的规则来观察我们所定义的细胞的演进发展。

在生命游戏中每个细胞可以处于ON(live)或OFF(death)状态。游戏从初始状态开始会为每一个细胞分配一个状态,数学规则决定其状态如何随时间而改变,只需要很简单的规则,系统就会演变出极其复杂的图案,仿佛他们真的是活的一样。这个游戏也有一定的哲学意义,他表明了复杂的结构可以通过简单的规则演进,不必遵循任何一种预设的模式。

下面是该项目包含的一些主要的概念:

  • 利用 matplotlib imshow 来展示数据的二维网络
  • 利用 matplotlib 生成动画
  • 使用 numpy 数组

工作原理

生命游戏以像素为细胞单位建立在一个二维表上,将以给定细胞作为中心,一个像素为长度画出九宫格,给定细胞周围的八个细胞就被称为他的邻居,在给定时间段,给定细胞的状态取决于前一段时间他们邻居的状态。
Conway生命游戏有四个规则:

  1. 如果一个细胞为ON,且邻居中少于两个为ON,则他将变为OFF
  2. 如果一个细胞为ON,且邻居中有两个或三个为ON,则他保持为ON
  3. 如果一个细胞为ON,且邻居中超过3个为ON,则他变为OFF
  4. 如果一个细胞为OFF,且邻居中恰好三个为ON,则他变为ON。

这些规则反映了一个生物群体随时间推移的遭遇:如果种群数量太多或者太少,都会杀死细胞,所以定义如果邻居的数量少于两个或多与三个都会将状态转变为OFF,如果种群数量适中则种群会开始繁殖以增加自己的数量,于是定义邻居的数量为两个或三个时细胞保持为ON,并将另一个细胞从OFF变为ON。

具体实现

所需模块

使用numpy数组来模拟整个生命游戏系统,使用matplotlib库来显示模拟的输出,用 matplotlib animation 模块更新模拟, 使用 matplotlib 的 imshow 函数来模拟细胞

表示网络

为了在网格上表示细胞的状态(ON 或者 OFF),分别使用255和0作为OFF和ON的值,采用matplotlib的imshow方法显示网格当前的状态,将一个数字矩阵表示为一张图像。下面简单介绍一下imshow方法

imshow()

热图(heatmap)是数据分析的常用方法,通过色差、亮度来展示数据的差异、易于理解。Python在Matplotlib库中,调用imshow()函数实现热图绘制。在本例子中使用255和0两个色差较大的值来模拟细胞的状态

初始条件

要开始模拟,首先要为二维表中的每个细胞设置一个初始状态,可以使用随机生成的方式或者设置特定的状态,看看整个系统将会如何发展。

如果希望采用随机的初始状态,可以使用numpy中的random模拟的choice方法

    numpy.random.choice()介绍
    可以从一个int数字或1维array里随机选取内容,并将选取结果放入n维array中返回。
    函数原型:numpy.random.choice(a, size=None, replace=True, p=None)
    函数作用:从一个给定的1维数组中随机取样。
    参数:
    a:如果a是一个一维数组,则就是从这个数组中进行随机取样,若a是一个数,则从numpy.arange(a)数组中取样,np.arange()用法请看我的另一篇blog:
    size:取样后输出的形状,例如size = (m, n),就需要采mn个样,并且输出为mn维的矩阵,缺省情况下返回一个数;
    replace:True或者是False,true时采样元素会有重复,false时不会有重复;
    p:一个一维数组,与a的元素一一对应,给出相应元素被采样的概率。

创建的代码很简单,只需要以下代码
np.random.choice([0,255],4*4,p=[0.1,0.9]).reshape(4,4)
因为返回的值是一个16个值的一维数组,所以使用reshape方法使他成为一个二维数组

如果希望建立特定的初始状态则可以通过在行列的特定位置增加图案的方式,具体代码如下

def addGlider(i,j,grid):
    glider = np.array([[0,0,255],[255,0,255],[0,255,255]])
    grid[i:i+3,j:j+3] = glider
grid = np.zeros(N*N).reshape(N*N)
addGlider(11,grid)

addGlider数组定义了一个滑翔机图案(一种生命游戏运行过程中出现的有趣图案,会在整个网格中进行平稳的运动)然后使用numpy的切片操作复制到指定位置。

边界处理

在这个二维表中,并不是所有的细胞都能拥有8个邻居,为了使整个系统都使用一套逻辑,我们选择将二维表上下边界定义为相邻左右边界定义为相邻,也就是将整个二维表定义为一个环面

我们可以通过使用取模运算来很简单的实现这个过程,例如二维数组 num[[0,1,2],[3,4,5],[6,7,8]] 中 第二行左边界num[1][0] 的值是3,右边界num[1][2] 的值是5,右边界+1再取模的值 num[1][(2+1)%3]等于左边界num[1][0] 也就是3。同理,其他边界也一样可以这样处理

规则实现

生命游戏的规则基于相邻细胞的 ON 或者 OFF 数目,为了简化这些规则的应用,可以计算出周围 ON 状态的细胞总数,只需要将周围所有邻居细胞的和求出后除以255即可

def update(frameNum,img,grid,N):
    newGrid = grid.copy()
    for i in range(N):
        for j in range(N):
            total = int((grid[i, (j - 1) % N] + grid[i, (j + 1) % N] +
                         grid[(i - 1) % N, j] + grid[(i + 1) % N, j] +
                         grid[(i - 1) % N, (j - 1) % N] + grid[(i - 1) % N, (j + 1) % N] +
                         grid[(i + 1) % N, (j - 1) % N] + grid[(i + 1) % N, (j + 1) % N]) / 255)

            if grid[i, j] == ON:
                if (total < 2) or (total > 3):
                    newGrid[i, j] = OFF
            else:
                if total == 3:
                    newGrid[i, j] = ON

    img.set_data(newGrid)
    grid[:] = newGrid[:]
    return img,

到这里就可以开始写程序了,下面贴出完整的代码

完整代码

# Python code to implement Conway's Game Of Life
import argparse
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation

# setting up the values for the grid
ON = 255
OFF = 0
vals = [ON, OFF]

def randomGrid(N):
    """returns a grid of NxN random values"""
    return np.random.choice(vals, N * N, p=[0.2, 0.8]).reshape(N, N)

def addGlider(i, j, grid):
    """adds a glider with top left cell at (i, j)"""
    glider = np.array([[0, 0, 255],
                       [255, 0, 255],
                       [0, 255, 255]])
    grid[i:i + 3, j:j + 3] = glider

def addGosperGliderGun(i, j, grid):
    """adds a Gosper Glider Gun with top left
    cell at (i, j)"""
    gun = np.zeros(11 * 38).reshape(11, 38)

    gun[5][1] = gun[5][2] = 255
    gun[6][1] = gun[6][2] = 255

    gun[3][13] = gun[3][14] = 255
    gun[4][12] = gun[4][16] = 255
    gun[5][11] = gun[5][17] = 255
    gun[6][11] = gun[6][15] = gun[6][17] = gun[6][18] = 255
    gun[7][11] = gun[7][17] = 255
    gun[8][12] = gun[8][16] = 255
    gun[9][13] = gun[9][14] = 255

    gun[1][25] = 255
    gun[2][23] = gun[2][25] = 255
    gun[3][21] = gun[3][22] = 255
    gun[4][21] = gun[4][22] = 255
    gun[5][21] = gun[5][22] = 255
    gun[6][23] = gun[6][25] = 255
    gun[7][25] = 255

    gun[3][35] = gun[3][36] = 255
    gun[4][35] = gun[4][36] = 255

    grid[i:i + 11, j:j + 38] = gun

def update(frameNum, img, grid, N):
    # copy grid since we require 8 neighbors
    # for calculation and we go line by line
    newGrid = grid.copy()
    for i in range(N):
        for j in range(N):

            # compute 8-neighbor sum
            # using toroidal boundary conditions - x and y wrap around
            # so that the simulaton takes place on a toroidal surface.
            total = int((grid[i, (j - 1) % N] + grid[i, (j + 1) % N] +
                         grid[(i - 1) % N, j] + grid[(i + 1) % N, j] +
                         grid[(i - 1) % N, (j - 1) % N] + grid[(i - 1) % N, (j + 1) % N] +
                         grid[(i + 1) % N, (j - 1) % N] + grid[(i + 1) % N, (j + 1) % N]) / 255)

            # apply Conway's rules
            if grid[i, j] == ON:
                if (total < 2) or (total > 3):
                    newGrid[i, j] = OFF
            else:
                if total == 3:
                    newGrid[i, j] = ON

    # update data
    img.set_data(newGrid)
    grid[:] = newGrid[:]
    return img,

# main() function
def main():
    # Command line args are in sys.argv[1], sys.argv[2] ..
    # sys.argv[0] is the script name itself and can be ignored
    # parse arguments
    parser = argparse.ArgumentParser(description="Runs Conway's Game of Life simulation.")

    # add arguments
    parser.add_argument('--grid-size', dest='N', required=False)
    parser.add_argument('--mov-file', dest='movfile', required=False)
    parser.add_argument('--interval', dest='interval', required=False)
    parser.add_argument('--glider', action='store_true', required=False)
    parser.add_argument('--gosper', action='store_true', required=False)
    args = parser.parse_args()

    # set grid size
    N = 100
    if args.N and int(args.N) > 8:
        N = int(args.N)

    # set animation update interval
    updateInterval = 50
    if args.interval:
        updateInterval = int(args.interval)

    # declare grid
    grid = np.array([])

    # check if "glider" demo flag is specified
    if args.glider:
        grid = np.zeros(N * N).reshape(N, N)
        addGlider(1, 1, grid)
    elif args.gosper:
        grid = np.zeros(N * N).reshape(N, N)
        addGosperGliderGun(10, 10, grid)

    else:  # populate grid with random on/off -
        # more off than on
        grid = randomGrid(N)

    # set up animation
    fig, ax = plt.subplots()
    img = ax.imshow(grid, interpolation='nearest')
    ani = animation.FuncAnimation(fig, update, fargs=(img, grid, N,),
                                  frames=10,
                                  interval=updateInterval,
                                  save_count=50)

    # # of frames?
    # set output file
    if args.movfile:
        ani.save(args.movfile, fps=30, extra_args=['-vcodec', 'libx264'])

    plt.show()

# call main
if __name__ == '__main__':
    main()

运行结果


欢迎来到parafish的个人博客,这里是一个正在努力的ctfer

路虽远,行则必至