从一张像素网格,理解康威生命游戏的核心机制

这是一篇基于本项目实际代码的技术笔记:从细胞规则出发,依次看状态更新、稀疏计算、交互状态机,以及并行和递归能带来什么。

图 1:本项目的实际界面。用户先摆放细胞,再确认布局并开始演化。

1. 一格细胞,只需要两个状态

每个格子不是活,就是死。它下一代的状态,只由自己和周围 8 个邻居决定。

当前状态 活邻居数 下一代
活 2 或 3 活
活 0、1,或者 4 及以上 死
死 恰好 3 活
死 其他数量 死

这四条规则常写作 B3/S23 :B 表示诞生,S 表示存活。

把表格翻译成一个判断,便得到了项目里的核心条件:

js 复制代码
// count 是活邻居数,living 是当前代的活细胞集合
const aliveNext = count === 3 || (count === 2 && living.has(cell));

当邻居数为 3 时,活细胞存活、死细胞新生;当邻居数为 2 时,只有原本活着的细胞能留下。

2. 「同时更新」不等于「同时开很多线程」

生命游戏的关键约束是:计算下一代时,每个格子都只能看到同一份旧状态。

图 2:三个横向活细胞会变成竖向;再前进一步,又变回横向。这种图案叫闪烁器。

如果算完一个格子就立刻改写原网格,后面的格子可能读到已经更新过的邻居。此时结果会依赖扫描顺序,规则也就变了。

本项目采用两份状态:

text 复制代码
只读:第 t 代 living
计算:所有候选格的邻居数
写入:全新的 next
完成:返回 next,作为第 t + 1 代

计算过程可以按顺序执行;只要始终读旧代、统一发布新代,规则上就是同时更新。

3. 不扫描整张地图:只统计活细胞附近

地图里大部分时候是空白。项目只保存活细胞坐标,例如:

js 复制代码
const living = new Set(['1,0', '2,1', '0,2']);

每个活细胞向周围 8 个坐标各贡献一次计数。随后,对计数表里的每个坐标应用规则:

js 复制代码
// 节选自 src/life.js
const key = `${x + dx},${y + dy}`;
neighbours.set(key, (neighbours.get(key) ?? 0) + 1);

// 完成所有计数后,写入新的集合
if (count === 3 || (count === 2 && living.has(cell))) {
  next.add(cell);
}

这里有两个容易忽略的细节:

  • 零邻居的活细胞不会出现在计数表中,因此不会进入下一代,正好对应孤独死亡。
  • 计数表包含活格和死格:死格若收到 3 次计数,也有机会诞生。

设当前有 M 个活细胞,每个细胞固定检查 8 个方向。假设坐标长度和哈希操作成本可视为固定,单代计算的平均时间与额外空间都可记为 O(M) ;候选坐标不超过 8M。

这适合稀疏世界。若画面长期非常密集,字符串和 Map 的额外开销可能让二维数组更有优势,不能简单说稀疏存储在所有情况下都更快。

4. 按钮背后,也是一个小状态机

这是对交互行为的概括;代码里使用 editing、running 两个布尔值表达这些状态,并没有额外定义一个状态机类。

状态 能做什么 如何离开
编辑布局 点击格子添加或移除细胞 点击"确认布局"
已确认 / 暂停 手动下一代,或开始自动运行 点击"重新编辑"或"自动运行"
自动运行 定时前进一步 点击"暂停运行"或"重新编辑"

"清空重来"会停止计时、清空活细胞、将代数归零并回到编辑状态。切换地图尺寸不会清空已有细胞。

5. 展示区固定,改变的是视野密度

这里的 30×30、60×60、100×100 是当前可见区域的格子数量,不是世界的硬边界。

显示区域宽度 可见格数 每格宽度
P 像素 30 格 P / 30
P 像素 60 格 P / 60
P 像素 100 格 P / 100

例如展示区宽 600 像素时,三种格宽分别是 20、10、6 像素。窗口尺寸不变时,切换地图规模只改变格子密度。

计算使用世界坐标,绘制时再减去视野起点:

text 复制代码
视野起点 = 视野中心 - floor(可见格数 / 2)
可见列号 = 世界 x 坐标 - 视野起点 x

细胞可以在当前画面外继续演化;拖动画布可以把它们带回视野。这里的"无限"指逻辑上没有固定地图边界,实际运行仍受内存与 JavaScript 数值范围限制。

6. 如果需要并行计算,该怎么拆?

当前版本没有使用 Web Worker,也没有并行计算。 图 6 只是规模变大时可以考虑的扩展方案。

一种方案是把旧代的活细胞分组,各个 Worker 分别生成局部邻居计数;所有结果返回后,将相同坐标的计数相加,再统一应用规则。

有两条边界不能漏掉:

  1. 跨分组的邻居必须合并。 一个格子的邻居可能来自不同分组,不能凭局部计数就决定生死。
  2. 旧任务不能覆盖新操作。 如果用户在计算途中清空或重新编辑,应取消旧任务或用版本号丢弃过期结果。

Worker 可以把计算移出浏览器主线程,但会引入数据传递和合并成本;小地图不一定更快。MDN:Web Workers

7. 逐代递推,是不是递归算法?

生命游戏有一个简洁的递推关系:

text 复制代码
第 1 代 = F(第 0 代)
第 2 代 = F(第 1 代)
第 t+1 代 = F(第 t 代)

这里的 F 就是"统计邻居,再按 B3/S23 生成下一代"。

但本项目的实现不是递归函数 :手动点击或定时器触发一次计算,得到新集合后等待下一次触发。用循环或事件反复应用 F,同样能够实现递推。

真正值得讨论递归的例子是 HashLife:它把区域组织成可复用的层级结构,并缓存重复区域的演化结果;对于时间或空间上高度重复的图案,可以大幅减少重复工作。

这是一种不同的实现路线,本项目没有实现 HashLife 。它需要处理区域重叠、边界与缓存管理,对当前交互式小规模需求并非必要。Gosper 原论文;Golly 的 HashLife 说明。

8. 实测:计算一代要多久?

**测试场景:**将方形区域全部填满活细胞,每次从同一份初始状态计算一代;预热 10 次,采样 40 次。

初始填满区域 初始活细胞 下一代活细胞 中位数 P90
30×30 900 116 0.54 ms 0.61 ms
60×60 3,600 236 2.06 ms 2.18 ms
100×100 10,000 396 5.93 ms 6.34 ms

环境:2026-11-06,Apple M2,macOS,Node.js v25.8.1。数据来自项目中的 nextGeneration,不包含浏览器绘图、点击响应、打包加载或内存测试,也不代表其他设备的速度。

在项目源码目录下可重新运行:

bash 复制代码
node tests/benchmark.mjs

查看本次原始采样数据。测试使用同一初始状态重复计算,并非连续演化 40 代;数值也会随机器、系统负载和运行环境变化。

9. 验证时,优先看什么?

项目的 tests/life.test.mjs 用了五个有代表性的检查:

检查 验证目的
孤立细胞 0 个邻居时死亡
四格方块 稳定图形保持不变
闪烁器 同时更新,且两代后回到原状
拥挤中心 邻居达到 4 个及以上时死亡
跨原点滑翔机 允许负坐标,并能跨越视野方向继续演化

运行方式:

bash 复制代码
node --test tests/life.test.mjs

自动运行使用 setInterval(..., 3000);浏览器忙碌或页面处于后台时,实际间隔可能长于 3 秒,并非精密时钟。MDN:setInterval

最后的三个工程取舍

  1. 先保证规则正确。 新旧状态分离,比是否使用多线程更基础。
  2. 按真实数据规模选择结构。 稀疏 Set 和 Map 实现简单;遇到密集大图再比较数组、分块或 Worker。
  3. 把世界和视野分开。 可见网格的大小属于显示设置,细胞是否存在属于模拟状态。

项目运行入口是自包含 HTML;文章中的源码路径相对于本地项目目录。源码、文章与配图保留在项目文件夹中,游戏压缩包只包含运行入口和使用说明。

相关推荐
张彦峰ZYF2 小时前
放行、排队与拒绝:限流设计的算法选型、数据结构与并发代价
后端·架构·滑动窗口·漏桶·令牌桶·高并发+限流
Trae_陈冀2 小时前
写详细设计总堆代码没逻辑?我做了个自动生成工具,3套模板+规范内置,输出直接过评审
架构·agent
Linye2 小时前
AI 越跑越偏?从 500 行臃肿规则到 80 行核心约束:多 Agent 项目中的「规则瘦身与防腐」四步排查法
人工智能·架构
Hespethorn3 小时前
112 个 Valgrind 错误,1 个根因:C++ 多线程服务的关机顺序复盘
后端·架构
HelloWorld0015 小时前
首屏加载慢如蜗牛?基于 Rust SWC + Module Federation + 资源预加载打造极致丝滑微前端架构实战
架构
用户094248568035 小时前
第22章:Redis 持久化 RDB 与 AOF:数据不丢的底线
redis·架构·源码
Decay__6 小时前
22_MySQL 运维自动化平台 —— 部署编排 + 监控 + 巡检 + 告警 + 工单闭环一体化
运维·数据库·sql·mysql·架构
Cosolar7 小时前
极简智库(MiniDocs):docsify、VitePress、Notion 等知识库搭建服务的完美替代
前端·后端·架构