news 2026/9/7 7:15:15

用vim宏编程实现康威生命游戏:寄存器、vimscript与逐代演化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用vim宏编程实现康威生命游戏:寄存器、vimscript与逐代演化

如果你经常在 Linux 服务器上做事,一定遇到过这种场景:某个重复性文本操作要点几十次甚至上百次,手都酸了,还是要老老实实一批批改。之前我折腾 vim 宏编程时,一直觉得“录制按键再回放”这个能力非常神奇,于是就有了一个大胆的想法:能不能用 vim 宏来跑一遍经典的康威生命游戏(Conway's Game of Life)?

答案是可以,而且效果很有意思。这篇文章就是一份“手元向”的自编解说,我会从 vim 宏编程的基本概念开始,逐步讲解如何把 vim 寄存器宏与简单的 vimscript 函数结合起来,让生命游戏在一个文本缓冲区里一代一代演化下去。顺便也会聊聊传统 vi 环境下的兼容方案。

如果你是 vim 新手,可以把它当作宏编程入门;如果你已经熟悉 vim,可以直接跳到实战案例部分,看代码、照抄、自己跑一轮。

1. 背景与核心概念

1.1 vim 宏编程到底解决什么问题

vim 宏(macro)的本质,就是把你在普通模式、可视模式或命令行模式下输入的一连串按键记录下来,保存到一个寄存器中,之后用一个键就可以重复播放整套操作。

举个例子:你有一批行需要把行首的TODO改成DONE,还要在行尾加一个分号。手动操作会重复很多次,但录一个宏之后,按一次@a就能处理一行,按100@a就能处理 100 行。

宏编程解决的核心痛点,就是重复性文本编辑。它并不是编程语言,但它是 vim 操作体系里的“自动化脚本”,尤其适合:

  • 批量给代码加注释或去注释
  • 重复性格式化文本
  • 多行数据预处理
  • 用 vim 本身实现一些小动画、小游戏

宏编程之所以强大,是因为它不只是记录普通模式按键,还能记录:开头的 ex 命令、可视模式操作、跨文件切换等。录制的时候,vim 会把你的按键原封不动塞进寄存器,回放时相当于把这些按键重新按一遍。

1.2 生命游戏规则回顾

生命游戏是英国数学家约翰·康威(John Horton Conway)在 1970 年提出的元胞自动机,不是传统意义的“游戏”,而是一个按规则自动演化的二维格子世界

每个格子有两个状态:存活(alive)或死亡(dead)。在本文中,我们用#表示存活细胞,用.表示死亡细胞。每一代演化只依赖当前代的状态,规则总共四条:

  • 一个存活细胞,如果周围 8 个格子中活细胞数量少于 2 个,它会因为“孤独”而死。
  • 一个存活细胞,如果周围活细胞数量是 2 个或 3 个,它继续存活到下一代。
  • 一个存活细胞,如果周围活细胞数量超过 3 个,它会因为“拥挤”而死。
  • 一个死亡细胞,如果周围恰好有 3 个活细胞,它会“繁殖”变成存活状态。

网格边界外的区域,我们统一视为死亡细胞。这样处理后,即使网格边缘的细胞也能正常计算周围邻居数。

生命游戏常常会出现几种经典图案:

  • 静物(still life):无论演化多少代都不变。
  • 振荡器(oscillator):在一个周期后回到原来的状态。
  • 滑翔机(glider):图案会沿着对角线方向不断移动。

这些图案非常适合作为 vim 宏演示的初始数据。

1.3 为什么把 vim 宏和生命游戏放在一起

很多人会觉得,vim 只是一个文本编辑器,怎么能做生命游戏这种偏图形化的东西?

关键在于两点:

第一,生命游戏的演化本质上是对二维字符矩阵做逐代变换。vim 缓冲区天然就是一组文本行,可以把它当成一张字符画布。

第二,vimmacro 的一次录制、多次回放,正好对应生命游戏“一代又一代推进”的过程。我把“演化一代”所需的操作录成一个宏,然后连续回放 100 次,就能看到细胞图案在终端里自己动起来。

不需要图形界面,不需要额外的游戏引擎,只要一个终端、一个 vim,就能在服务器上演示一个生命游戏动画。这也是很多 Linux 老手喜欢做的一种“文本编辑器艺术”。

2. 环境准备与版本说明

2.1 环境与版本

本文示例主要基于 Vim 8.x,理论上 Vim 7.4 之后的大部分版本都能运行,前提是编译时包含了 vimscript 支持。多数 Linux 发行版默认安装的vim都满足条件。

可以用以下命令确认版本:

vim --version

如果系统只装了极简版vim-minimal,或者只有传统vi,某些 vimscript 功能可能不可用。本文后面的“vi 兼容方案”会单独说明。

版本需要根据你的实际环境调整,建议直接使用发行版自带的 vim,重点演示配置和操作思路。示例环境可以参考:

  • 操作系统:Linux 发行版均可
  • 编辑器:Vim 8.x 或更高版本
  • Shell:bash 或 zsh
  • 不需要额外的第三方库

2.2 让 vim 配置更适合演示

为了让生命游戏在终端里显示得更清楚,建议在~/.vimrc中加入以下几行配置:

set number set nowrap set lazyredraw set t_ut=

说明一下:

  • set number:显示行号,方便观察滑翔机移动。
  • set nowrap:关闭自动换行,避免网格被拆成多行导致错位。
  • set lazyredraw:宏执行过程中不频繁重绘屏幕,减少闪烁。
  • set t_ut=:避免清屏时背景色异常,在部分终端下有奇效。

如果你不想改全局 vimrc,也可以把这几行放进项目目录下的_vimrc.vimrc。演示完再删掉即可。

2.3 示例文件与网格数据约定

生命游戏的网格需要遵守固定约定,否则脚本无法正确处理:

  • 每行长度必须一致,不能有空格,也不能有空白行。
  • .表示死细胞,#表示活细胞。
  • 文件内容最好只有网格本身,不要混入注释或其他文本。

这看起来像是在操作一个“人为约束的文本文件”,但正是这种约束,让 vim 的文本处理能力可以直接套用到元胞自动机上。

3. vim 宏编程核心知识点

3.1 寄存器:宏的存储基础

在 vim 中,寄存器(register)是存放文本、按键和命令的容器。宏录制时,按键内容会被暂存到指定寄存器里。

常用字母寄存器有az共 26 个。录制宏时用小写字母,比如qa表示录制到寄存器a;如果再用大写QA,表示向寄存器a追加内容而不是覆盖。

查看寄存器内容:

:reg a

这个命令会显示寄存器a中保存的按键序列。调试宏的时候非常方便。

注意:寄存器是 vim 的特性,传统 vi 并没有这套机制,至少没有这么灵活的命名寄存器。

3.2 宏的录制与回放

宏的录制格式很简单:

  • qa:开始录制到寄存器 a
  • 执行你需要的操作
  • q:结束录制
  • @a:回放寄存器 a 中的宏
  • 50@a:连续回放 50 次
  • @@:重复上一次执行过的宏

来看一个最简单示例。打开文件:

hello world hello world hello world

光标放在第一行行首,执行:

qa 0dw j q

意思是:录制宏 a,把光标移到行首,删除第一个单词,然后移到下一行,结束录制。执行@a,第二行的hello被删掉;再按@a,第三行也被处理。

宏录制时要注意一个关键点:vim 记录的是按键操作序列,不是“当前这一行的状态”。所以宏里尽量使用相对移动、查找、文本对象,而不要使用绝对行号,否则回放到后面会跑偏。

3.3 宏里调用 ex 命令

宏不仅可以记录普通模式按键,还能记录:开头的 ex 命令。

例如,录一个宏,把当前文件的foo全部替换成bar

qa :%s/foo/bar/g q

录制过程中,: %s/foo/bar/g会被当成一次完整的命令保存。回放时,vim 会重新执行这个替换命令。

这个特性非常重要,因为生命游戏的演化计算,不能只靠普通模式的字符移动来完成,还需要在宏里调用 vimscript 函数或 ex 命令。我们后面会看到这种组合的威力。

3.4 宏的嵌套与递归

宏里可以调用另一个宏。比如:

qb @a q

这样宏 b 会执行宏 a。相当于把多个宏组合成更复杂的自动化流程。

宏也可以递归调用自己,也就是在宏 a 里调用@a。递归宏能实现类似循环的效果,但风险也大:如果缺少终止条件,会进入无限循环。更稳妥的做法是前面加次数上限,例如:

100@a

或者在宏里面用寄存器计数,当计数达到阈值时不再调用自己。

在生命游戏演示中,我们不需要递归宏,因为回放次数可以通过100@a这种重复次数来控制。

4. 实战:用 vim 宏驱动生命游戏演化

下面进入核心环节。我们要实现的效果是:打开一个life.txt文件,里面是一块字符网格,运行一个宏后,网格自动按照生命游戏规则演化一代,并且屏幕短暂停顿,接着我们连续回放宏,形成动画。

4.1 准备初始网格

先创建一个文件,命名为life.txt

................. ....###.......... ....#............ ....#..#......... .................

这是一个 5 行 17 列的小网格,里面包含了一个滑翔机图案的初始状态。滑翔机会在网格中向右下方移动,适合观察演化效果。

创建文件后,在 vim 中打开:

vim life.txt

确认每行等长,行尾不要有多余空格。后面我们会用 vimscript 函数处理整个文件内容,不规范的空白字符会影响邻居判断。

4.2 编写一次演化函数 LifeStep()

虽然在标题里写的是“vim 宏编程”,但一个单纯的按键宏很难独立完成 8 方向邻居计数和状态判断。工程上更稳妥的做法是:用 vimscript 函数处理计算逻辑,用宏负责驱动重复执行

把下面的函数放进~/.vimrc,或者在 vim 命令行模式下粘贴执行都可以。我建议放进 vimrc,因为宏回放时随时需要调用它。

" 生命游戏演算函数 " 使用范围调用方式,例如 :%call LifeStep() function! LifeStep() range let l:lines = getline(a:firstline, a:lastline) let l:newlines = [] for l:i in range(0, len(l:lines) - 1) let l:row = l:lines[l:i] let l:newrow = '' for l:j in range(0, len(l:row) - 1) let l:cnt = 0 " 统计周围 8 个方向的活细胞数量 for l:di in [-1, 0, 1] for l:dj in [-1, 0, 1] if l:di == 0 && l:dj == 0 continue endif let l:ni = l:i + l:di let l:nj = l:j + l:dj " 边界外的格子视为死细胞 if l:ni < 0 || l:ni >= len(l:lines) continue endif if l:nj < 0 || l:nj >= len(l:row) continue endif if l:lines[l:ni][l:nj] == '#' let l:cnt = l:cnt + 1 endif endfor endfor " 根据生命游戏规则决定下一代状态 let l:is_alive = (l:lines[l:i][l:j] == '#') if l:is_alive if l:cnt == 2 || l:cnt == 3 let l:newrow = l:newrow . '#' else let l:newrow = l:newrow . '.' endif else if l:cnt == 3 let l:newrow = l:newrow . '#' else let l:newrow = l:newrow . '.' endif endif endfor call add(l:newlines, l:newrow) endfor call setline(a:firstline, l:newlines) endfunction

函数的核心流程:

  1. 通过getline(a:firstline, a:lastline)取出范围内的所有行。
  2. 遍历每一行、每一个字符。
  3. 对每个字符统计周围 8 个格子中#的数量。
  4. 根据当前状态和活邻居数量,应用生命游戏规则,生成新的一行。
  5. 通过setline(a:firstline, l:newlines)一次性写回新网格。

这个函数使用了range属性,所以可以用范围调用。最简单的调用方式是:

:%call LifeStep()

也可以先选中一部分区域,再执行:

:'<,'>call LifeStep()

这样只处理选中区域,适合在一个大文件里单独观察某个局部图案。

4.3 录制“演化一代”宏

现在,我们在 vim 中把“演化一代并稍作停顿”录制成宏。

推荐这样录制:

qa :%call LifeStep() :echo 'Generation: ' . g:life_gen :redraw :sleep 200m q

不过这里有个问题,g:life_gen需要先初始化。如果你不想维护计数器,也可以直接删掉:echo那一行。为了演示更完整,建议初始化并保留计数。

先初始化计数器:

let g:life_gen = 0

然后把LifeStep()函数改一版,内部自动递增计数器:

function! LifeStep() range let g:life_gen = g:life_gen + 1 " 其余代码保持不变 endfunction

实现方式可以这样改,在函数开头加一行即可。完整代码不需要重复贴,你只需要在刚才的函数第一行后面加入:

let g:life_gen = g:life_gen + 1

然后重新执行source ~/.vimrc或重新粘贴函数。

录制宏的按键序列如下:

qa :%call LifeStep() :echo 'Generation: ' . g:life_gen :redraw :sleep 200m q

注意:上面每一行后面的回车都要按下去,vim 才会记录完整的 ex 命令。

各步骤的作用:

  • :%call LifeStep():对整个文件执行演化函数。
  • :echo 'Generation: ' . g:life_gen:在底部显示当前代数。
  • :redraw:强制重绘屏幕,让新网格显示出来。
  • :sleep 200m:等待 200 毫秒,让动画不要太快。

录完之后,可以按一次@a测试。如果看到网格发生了变化,底部显示Generation: 1,说明宏录制成功。

4.4 连续回放宏:100 代动画

测试成功后,把光标移到网格顶部,确保观察起始位置一致:

gg

然后连续回放宏:

100@a

此时 vim 会像放电影一样,每隔 200 毫秒演化一代,底部显示代数,网格内容不断更新。

如果中间想停下来,按Ctrl+C即可中断宏回放。

以滑翔机初始状态为例,你会看到#组成的图案会逐渐向右下方移动,并在边界处慢慢消失,因为边界外的细胞被当成死细胞处理。

4.5 效果验证与手动步进

宏回放停止后,有时你想确认当前到底演化到了哪一代,可以直接查看g:life_gen

:echo g:life_gen

如果刚才执行了100@a,这里通常会显示一个接近 100 的数字,除非中途被中断。

你还可以用单步命令验证某一步是否正确:

:%call LifeStep()

每执行一次,网格就往前走一代,相当于手动步进调试验证。

4.6 传统 vi 兼容方案说明

必须说明:传统 vi 没有 vim 的寄存器宏录制机制,qa@a这种操作在老式 vi 里是不支持的。如果你想在很老的环境里也实现“生命游戏逐代演化”,可以用 ex 命令配合外部过滤器,例如 awk 脚本。

思路是:把当前缓冲区内容通过管道交给 awk 程序,awk 计算下一代网格后,再把结果写回缓冲区。

在 vi 中执行:

:1,$!awk -f life.awk

其中life.awk是独立的 awk 脚本,内容可以参考下面这个版本:

{ rows[n++] = $0 len = length($0) } END { for (i = 0; i < n; i++) { out = "" for (j = 1; j <= length(rows[i]); j++) { alive = (substr(rows[i], j, 1) == "#") cnt = 0 for (di = -1; di <= 1; di++) for (dj = -1; dj <= 1; dj++) { if (di == 0 && dj == 0) continue ni = i + di nj = j + dj if (ni < 0 || ni >= n) continue if (nj < 1 || nj > length(rows[ni])) continue if (substr(rows[ni], nj, 1) == "#") cnt++ } if (alive) out = out ((cnt == 2 || cnt == 3) ? "#" : ".") else out = out (cnt == 3 ? "#" : ".") } print out } }

保存为life.awk,执行一次:1,$!awk -f life.awk,可以看到网格演化一代。再按一次就会再演化一代。这个方案不依赖 vim 宏,但符合传统 vi 也能用 ex 过滤器的思路。

不过,awk 脚本对网格格式要求同样严格:文件不能有空行,每行长度要一致,否则输出可能错位。这里只是提供一个兼容老 vi 的可行思路,日常开发中还是建议直接用完整版的 vim。

5. 常见问题与排查思路

实操过程中可能会遇到下面这些情况,整理成了一张排查表,方便你快速定位。

问题现象常见原因解决思路
执行@a时报错E492: Not an editor command: call LifeStep()LifeStep函数没有定义,或函数定义被覆盖检查函数是否已写入 vimrc 并在当前 vim 会话中重新 source
宏只执行一次就停止宏中使用了绝对行号或依赖可视区域的按键序列修改宏内容,改用相对移动和全局范围调用
网格越来越乱,行长度不齐初始数据包含空格、空行或不等长行重新规范life.txt,确保每行等长且无空白字符
动画闪烁严重,看不清画面没有设置lazyredraw在 vimrc 中加入set lazyredraw
没有动画效果,只是瞬间执行完宏里缺少:sleep:redraw重新录制宏,补上:sleep 200m:redraw
细胞代数不是预期增长g:life_gen没有初始化或函数未递增在 vimrc 中初始化let g:life_gen = 0,在函数中递增
在 vi 里输入q没反应传统 vi 没有 vim 的宏录制寄存器使用 ex 命令行过滤器方案,配合 awk 脚本演化
:'<,'>call LifeStep()时只处理了当前选中的部分这是范围调用的正常行为如果想处理整个文件,改用:%call LifeStep()

这里要特别提醒一点:宏录制时如果按错了键,尽量不要手动删除,因为删除动作也会被录进宏。最稳妥的方式是重新录制一遍,也就是再执行一次qaq,用新内容覆盖旧宏。

6. 最佳实践与工程建议

6.1 函数与宏分层

虽然题目叫“vim 宏编程实现”,但实际工程中不要把所有逻辑都塞进宏里。宏擅长的是“重复按键操作”,而生命游戏这种需要 8 方向邻居计算的逻辑,更适合写在 vimscript 函数中。

推荐的划分方式是:

  • vimscript 函数:负责网格读取、计算、写回。
  • 宏:负责调用函数、显示代数、等待、重绘画面。
  • 手动命令:负责初始化全局变量、设置网格文件。

这种分层方式可读性更强,也方便后续扩展,比如改规则、增加随机初始状态等。

6.2 宏的可重放性

宏最重要的特性就是可重放。为了让它不管在什么位置都能正常工作,录制时应遵循以下原则:

  • 不要在宏里使用绝对行号,比如5G
  • 尽量使用相对移动,比如jkw0
  • 如果宏要对整个文件操作,就用%范围,而不是依赖当前光标位置。
  • 宏结束时尽量让光标回到一个稳定位置,比如gg,方便下一次回放。

6.3 性能与网格规模

vim 本质上不是为大规模图像计算设计的。当一个网格扩大到几百行、几百列时,vimscript 逐字符遍历的速度会明显下降。

如果只是演示生命游戏,建议:

  • 网格控制在 50 行以内。
  • 每行宽度控制在 100 字符以内。
  • 不要开太多其他插件,避免回放宏时干扰。

如果确实需要大网格,更合理的方式是让外部程序处理计算,vim 只负责显示。这也是运维场景里常用的“编辑器 + 外部命令”协作模式。

6.4 安全与文件保护

虽然LifeStep()函数并不会直接损坏系统,但你要记住:它会对当前缓冲区内容进行批量改写。

因此:

  • 第一次实验时,请在一个独立的测试文件或复制副本上操作。
  • 不要把life.txt和真实代码文件混在同一个缓冲区里。
  • 如果改写了重要文件,及时u撤销,或提前cp一份备份。

在生产环境中使用任何批量编辑、批量替换、外部命令过滤时,都要先确认文件内容可恢复。

7. 总结与后续学习方向

通过这次实操,你应该已经掌握了几件事:vim 宏录制与回放的基本用法、@寄存器执行机制、vimscript 函数与宏的组合方式,以及如何用纯文本网格实现生命游戏的演化。这也证明了 vim 不只是一种编辑器,它本身就是一个可以被“编程”的文本处理环境。

如果还想继续深入,可以尝试下面几个方向:

  • 在生命游戏函数中加入更多参数,例如支持随机初始化。
  • LifeStep()扩展成通用的二维数组处理函数,应用到其他元胞自动机。
  • 学习 vim 的:help recording:help Q_a:help function-range等帮助文档,全面理解宏与函数的底层机制。
  • 用类似的思路做一个字符版迷宫生成或简单排序可视化。

当然,vim 宏并不是所有自动化的银弹,但它很适合处理“重复按键 + 少量计算”的场景。在服务器上不方便装图形界面的时候,一段宏加上几十行 vimscript,就能把一个生命游戏动画搬到终端里。这才是 Linux 文本操作最让人上瘾的地方。

如果这篇文章对你有帮助,可以收藏备用,也欢迎自己在终端里把滑翔机跑起来试试看。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 7:12:50

FunASR说话人分离完全指南:三步自动标记“谁说了什么“

FunASR说话人分离完全指南&#xff1a;三步自动标记"谁说了什么" 【免费下载链接】FunASR Open-source speech recognition toolkit for training, inference, streaming ASR, VAD, punctuation, speaker diarization pipelines, and OpenAI-compatible/MCP serving.…

作者头像 李华
网站建设 2026/9/7 7:12:39

Emblem工具实测:长文档自动生成PPT与溯源功能全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 7:10:33

猫抓插件教程:浏览器视频下载与网页资源嗅探完整指南

猫抓插件教程&#xff1a;浏览器视频下载与网页资源嗅探完整指南 【免费下载链接】cat-catch 猫抓 浏览器资源嗅探扩展 / cat-catch Browser Resource Sniffing Extension 项目地址: https://gitcode.com/GitHub_Trending/ca/cat-catch 猫抓&#xff08;Cat-Catch&#…

作者头像 李华
网站建设 2026/9/7 7:10:05

免费离线语音转录:Buzz——本地语音识别完整指南

免费离线语音转录&#xff1a;Buzz——本地语音识别完整指南 【免费下载链接】buzz Buzz transcribes and translates audio offline on your personal computer. Powered by OpenAIs Whisper. 项目地址: https://gitcode.com/GitHub_Trending/buz/buzz 访谈录音传上去&…

作者头像 李华