返回首页

Awk实现Linux终端模拟器下黑客帝国代码雨效果

Awk实现Linux终端模拟器下黑客帝国代码雨效果

代码雨

在网上看过 Python,JavaScript,Windows 脚本等写的代码雨,感觉挺有意思。Linux 下终端字符操作工具众多,其中 awk 算是一众工具中的老大哥了。最近也是记录了一下学习 awk 的历程。这次代码雨就使用 awk 来实现了。

最终实现效果

总体思路

因为在 Linux 下的 bash 等 shell 中执行,所以结合了小部分 shell(此处特指 bash 脚本,不同 shell 的使用不同,比如 zsh,fish 等语法不尽相同)脚本实现。首先使用 shell 脚本获取终端的最大行和一个不超过最大列数的随机列数以及一个随机数来定位字符,再通过管道将数据传入 awk,awk 对所有列的每一行打印。如此循环,代码雨就实现了。这是总体逻辑,具体还有些细节具体实现再说。

具体实现

shell 脚本部分

首先需要获取当前终端的最大行和随机列数以及随机字符的下标,在一般的 shell 中可以使用下面的语句。

echo $(tput lines) $(($RANDOM % $(tput cols))) $(($RANDOM % 72))

$(tput lines) 获得当前终端的行数,$RANDOM 的结果是一个 0~215-1 也就是 32767 之间的一个随机整数。

最后一列对随机数取余 72 的结果是 1~71,这其实是产生随机字符的下标。下面是可能出现的字符集合。因为有 71 个,而且字符串字符下标从 1 开始。

ok,在产生了相关的数字之后,还需要一个循环来实现不断生成这些数字。也就是下面的循环。

while :;
do
  echo $(tput lines) $(( $RANDOM % $(tput cols) )) $(( $RANDOM % 72 ));
  sleep 0.05;
done

使用 : shell 内置命令(该命令始终返回 true,让我想到了进程炸弹)作为循环条件始终不退出。并且使用sleep在产生数字之后每次休息 0.05 秒,这个之后看情况可以根据实际效果进行修改。

下面是运行效果

shell 脚本的部分就结束了。下面开始 awk 的部分。

awk 语言部分

首先定义雨点的长度,为了让雨点长度随机,就使用 rand 函数并且使用 rl 数组来存储每一列的雨长度,在一次雨点落地之后,重新计算新的雨点的随机长度。为了最开始有一个数值作为初始雨点长度,就直接使用第一个数据(最大行数)的五分之三作为长度值啦。

RAINLENGTH = int($1 * 3 / 5);

注意这里的分号不是必须的,awk 使用换行和分号来间隔语句或者表达式,为了更好区分我就使用了分号。

定义会出现的字符串

letters="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#$%^&*()";

对于一个随机列如果数组中不存在则初始化该列的雨点长度为 RAINLENGTH。

 if (rl[$2] == 0)
   rl[$2] = RAINLENGTH;

获取对应位置的字符。这里$3 是之前随机产生的字符位置。

letter=substr(letters, $3, 1);

初始计算需要打印空白符的位置。

l[$2] = ( a[$2] - rl[$2] >= 0 ? a[$2] - rl[$2] : a[$2] );

a 数组中存放的是当前列已经走过的行数,此处判断已经走过的行属是否已经大于该列雨点定义的长度。如果大于则计算需要抹除字符的位置,如果没有则还是使用原来的行数。

之后对 a 数组中的每一个列进行遍历,也就是横向打印。(这个其实在实现效果中也能看出来,每一列的雨的流下速度是相同的)。

for (x in a) {
    if (a[x] == rl[x] && isr[x] == 0) {
      isr[x]=1
    }
    if (isr[x]) {
      printf "\033[%s;%sH\033[2;30m%s",l[x], x," ";
    }
}

判断该列是否已经达到了雨点的最大长度,如果已经被标记(也就是 isr 数组中对应下标存放了 1),则在之前计算的位置中擦除靠近上端需要擦除的字符(实际上就是打印一个空格)。

为了在下落过程中有光标闪动的效果,可以每次输出两个字符(实际上每次循环就是给每一列推进两个字符)。一个用于显示绿色,再在下一行显示一个相同但是颜色为白色的字符。

o=a[x];
a[x]=a[x]+1;
printf "\033[%s;%sH\033[2;32m%s",o,x,letter;
printf "\033[%s;%sH\033[1;37m%s\033[0;0H",a[x],x,letter;
l[x] = ( a[x] - rl[x] >= 0 ? a[x] - rl[x] : a[x] );

首先记住当前列的当前行,然后向下推进一行,最后分别用绿色和白色打印两个字符。当然每次打印完要重新计算对应列的需要擦除的位置。

为了实现雨落地之后会掉入地面的效果,还需要增加: a[x] >= $1 + rl[x] 的条件,即判断当前列的行数是否已经大于等于最大行加雨点本身的长度了,如果达到了条件需要重置雨点的行位置以及长度和雨点标志。

也就是下面一段代码

if (a[x] >= $1 + rl[x]) {
  # a[x]=0;
  delete a[x];
  srand();
  # 随机下次雨点的长度
  rl[x] = int(rand() * $1 / 2);
  # 清空雨点标志
  isr[x]=0;
}

这里可以看到我把 a[x]=0 给注释掉了,使用了 delete a[x]。其实使用 a[x]=0 也是可以达到将行数重置的效果的。但是 a[x]这个变量就会以 0 的值存在 a 数组中,在下次遍历时,所有列遍历的顺序就会是相同的。呈现的效果就是每次迭代每一条雨的长度间隔是相同的。这个感觉不是很好,后来想了想就用了 delete,结果还不错的。

Extra

颜色上的变化使用了终端控制符,之前也用其,实现了文本高亮。具体可以看我之前的 vim 入门文章,里面使用 python 实现了终端下的 grep,其中就使用了一些颜色控制码,也给出了基本颜色对应的控制码表格。

既然字体颜色可以控制,那背景也是可以控制的。我们可以将 shell 与 awk 的部分合为一个 shell 脚本,在最开始设置背景颜色。

#!/bin/bash

# 此处设置背景为黑色
echo -e "\e[1;40m";
# 清屏
clear;
while :;
do
  echo $(tput lines) $(( $RANDOM % $(tput cols) )) $(( $RANDOM % 72 ));
  sleep 0.05;
done | awk '
{ RAINLENGTH = int($1 * 3 / 5)
  letters="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@#$%^&*()";
  if (rl[$2] == 0)
    rl[$2] = RAINLENGTH;
  # 获取对应位置的字符
  letter=substr(letters, $3, 1);
  # 初始计算需要打印空白符的位置
  l[$2] = ( a[$2] - rl[$2] >= 0 ? a[$2] - rl[$2] : a[$2] );
  for (x in a) {
    if (a[x] == rl[x] && isr[x] == 0) {
      isr[x]=1
    }
    if (isr[x]) {
      printf "\033[%s;%sH\033[2;30m%s",l[x], x," ";
    }
    o=a[x];
    a[x]=a[x]+1;
    printf "\033[%s;%sH\033[2;32m%s",o,x,letter;
    printf "\033[%s;%sH\033[1;37m%s\033[0;0H",a[x],x,letter;
    l[x] = ( a[x] - rl[x] >= 0 ? a[x] - rl[x] : a[x] );
    if (a[x] >= $1 + rl[x]) {
      # a[x]=0;
      delete a[x];
      srand();
      rl[x] = int(rand() * $1 / 2);
      isr[x]=0;
    }
  }
}'

添加了黑色背景的实现效果是下面这样的。

总结

拥有一个终端,你就拥有一个世界。加油啊。----来自一个 Linux 爱好者😄

评论 (...)

加载中...