1

在 Linux bash 中,前面文章介绍过显示彩色俄罗斯方块的 shell 脚本。

下面继续介绍如何通过 k、j、h、l 键来上下左右移动单个方块的 shell 脚本。

脚本执行效果

先贴出该shell脚本的具体执行截图如下:

从左上角移动到了右边的截图

实际执行时,可以在边框内部,上下左右移动 Z 字形的方块。

脚本代码

假设有一个 moveblock.sh 脚本,具体的代码内容如下所示。

在这个代码中,几乎每一行代码都提供了详细的注释,方便阅读。

这篇文章的后面也会对一些关键点进行说明,有助理解。

#!/bin/bash
# 实现一个可以上下左右移动的方块,移动范围限定在指定边框内.

# 下面几个常量指定长方形边框的上下左右边界
# 指定边框左边的列数
FRAME_LEFT=3
# 指定边框右边的列数
FRAME_RIGHT=26
# 指定边框上边的行数
FRAME_TOP=2
# 指定边框下边的行数
FRAME_BOTTOM=18

# 下面的 BLOCK 数组对应一个 Z 字形方块,具体形状为:
# [][]
#   [][]
# 这里使用行数、列数坐标点的方式来表示每一个小方块的位置.
# 第一个小方块的起始行数、列数都是 0,作为整个方块的原点.
# 第二个小方块和第一个小方块在同一行,行数也是0.每一个小方块
#   显示两个字符,所以第二个小方块的起始列数是 2.
# 第三个小方块在第一个小方块的下一行,行数是 1. 它的列数是 2.
# 第四个小方块和第三个小方块在同一行,行数是 1. 它的列数是 4.
# 使用这些行列数加上方块的起始行列数,就能定位出每个小方块要
# 显示在哪一行、哪一列.之后可以使用ANSI转义码设置光标的位置.
BLOCK=(0 0 0 2 1 2 1 4)
# 土字形方块.第一个小方块的行数是 0,列数是 2.其他小方块类似.
# 可以放开下面注释来查看土字形方块的移动效果.
## BLOCK=(0 2 1 0 1 2 1 4)

# 这个值加上 BLOCK 数组里面的小方块行数,
# 会指定每一个小方块要显示在哪一行.
# 其初始值是边框上边行数的下一行.
blockLine=$((FRAME_TOP + 1))
# blockColumn 指定整个方块显示的起始列.
# 这个值加上 BLOCK 数组里面的小方块列数,
# 会指定每一个小方块要显示在哪一列.
# 其初始值是边框左边列数的下一列.
blockColumn=$((FRAME_LEFT + 1))

# 定义下面两个常量来检查方块最右边和最下边的边界.
# Z 字形方块有两行. 边框下边的行数减去 2,就是方块
# 起始最大的行数. 大于这个行数就会超过边框范围.
BLOCK_MAX_BASE_LINE=$((FRAME_BOTTOM - 2))
# Z 字形方块的长度是 6 个字符. 边框右边的列数减去 6,
# 就是方块起始最大的列数.大于这个列数就会超过边框范围.
BLOCK_MAX_BASE_COLUMN=$((FRAME_RIGHT - 6))

# 显示一个长方形边框,作为方块移动的边界范围
function showFrame()
{
    # 设置边框字符的显示属性: 高亮反白显示,绿色文本,绿色背景
    printf "\e[1;7;32;42m"

    local i
    # 下面使用 "\e[line;columnH" ANSI 转义码移动
    # 光标到指定的行和列,然后显示对应的边框边界字符.
    # 行数递增,列数不变,竖向显示边框的左右边界
    for ((i = FRAME_TOP; i <= FRAME_BOTTOM; ++i)); do
        printf "\e[${i};${FRAME_LEFT}H|"
        printf "\e[${i};${FRAME_RIGHT}H|"
    done

    # 列数递增,行数不变,横向显示边框的上下边界
    for ((i = FRAME_LEFT + 1; i < FRAME_RIGHT; ++i)); do
        printf "\e[${FRAME_TOP};${i}H="
        printf "\e[${FRAME_BOTTOM};${i}H="
    done

    # 显示边框之后,重置终端的字符属性为原来的状态
    printf "\e[0m"
}

# 显示或者清除方块.方块的具体形状由 BLOCK 数组指定.
# 传入的第一个参数为 1,会显示方块.
# 传入的第一个参数为 0,会清除方块.
function drawBlock()
{
    local i
    # square 变量保存要显示的小方块内容.
    # 如果内容为 "[]",会显示具体的方块.
    # 如果内容为 "  ",也就是两个空格,会清除方块
    local square
    # line 变量指定某个小方块显示在哪一行
    local line
    # column 变量指定某个小方块显示在哪一列
    local column

    # 所给的第一个参数值为 1,表示要显示具体的方块
    # 所给的第一个参数值为 0,表示要清除当前的方块
    # 方块显示的位置由 blockLine 和 blockColumn 指定
    if [ $1 -eq 1 ]; then
        square="[]"
        # 显示方块时,把方块的背景色设成红色
        printf "\e[41m"
    else
        square="  "
        # 清除方块时,背景色要显示为原先的颜色
        printf "\e[0m"
    fi

    for ((i = 0; i < 8; i += 2)); do
        # 使用 blockLine 和 BLOCK 数组指定的小方块行数
        # 来获取每一个小方块要显示在哪一行.
        line=$((blockLine + ${BLOCK[i]}))
        # 使用 blockLine 和 BLOCK 数组指定的小方块列数
        # 来获取每一个小方块要显示在哪一列.
        column=$((blockColumn + ${BLOCK[i + 1]}))
        # 使用 "\e[line;columnH" 转义码移动光标到指定的
        # 行和列,然后开始显示对应的小方块.
        printf "\e[${line};${column}H${square}"
    done
}

# 重置终端的显示状态为原先的状态
function resetDisplay()
{
    # 把光标显示到边框底部的下一行,
    # 以便终端提示符显示在边框之后,避免错乱
    printf "\e[$((FRAME_BOTTOM + 1));0H"
    # 显示光标
    printf "\e[?25h"
    # 重置终端的字符属性为原来的状态
    printf "\e[0m"
}

# 初始化显示状态.例如显示边框,隐藏光标,等等
function initDisplay()
{
    # 由于方块会显示在指定的行和列,
    # 为了避免已有内容的干扰,先清屏.
    clear
    # 隐藏光标
    printf "\e[?25l"
    # 显示提示字符串
    echo "Usage: k/j/h/l 键: 上/下/左/右移动方块. q 键: 退出"
    # 显示边框
    showFrame
}

initDisplay

# bash 的 : 命令什么都不做,永远返回 true,
# 用在 while 命令中形成死循环.
while :; do
    # 基于 blockLine 和 blockColumn 的值显示方块
    drawBlock 1
    # 获取用户的按键, h/l/j/k 会 左/右/下/上 移动方块
    read -s -n 1 char
    # 获取用户按键,要移动方块.移动之前,先清除原先方块
    drawBlock 0
    case "$char" in
        # h 键要向左移,移动的距离间隔是一个小方块的宽度,
        # 由于每个小方块占据两列,blockColumn 值要减去 2.
        "h") ((blockColumn -= 2)) ;;
        # l 键要向右移.类似的, blockColumn 值加上 2
        "l") ((blockColumn += 2)) ;;
        # j 键要下移一行, blockLine 值加上 1
        "j") ((++blockLine)) ;;
        # k 键要上移一行, blockLine 值减去 1
        "k") ((--blockLine)) ;;
        # q 键退出
        "q") break ;;
    esac
    # 检查方块是否移动到边框边界,避免移到边框外面
    if [ $blockColumn -gt $BLOCK_MAX_BASE_COLUMN ]; then
        # 当下次要显示的列数大于方块最大的起始列数时,
        # 将列数设置为最大的起始列数.
        blockColumn=$BLOCK_MAX_BASE_COLUMN
    elif [ $blockColumn -le $FRAME_LEFT ]; then
        # 当下次要显示的列数小于或等于边框左边的列数时,
        # 将列数设置成边框左边的列数加 1.
        blockColumn=$((FRAME_LEFT + 1))
    elif [ $blockLine -le $FRAME_TOP ]; then
        # 当下次要显示的行数小于或等于边框上边的行数时,
        # 将行数设置成边框上边的行数加 1.
        blockLine=$((FRAME_TOP + 1))
    elif [ $blockLine -gt $BLOCK_MAX_BASE_LINE ]; then
        # 当下次要显示的行数大于方块最大的起始行数时,
        # 将行数设置成最大的起始行数.
        blockLine=$BLOCK_MAX_BASE_LINE
    fi
done

resetDisplay
exit

代码关键点说明

如何移动光标到指定的行和列

一般来说,显示字符之后,光标就会往前移动。新显示的内容会在光标之后继续显示。

但是当需要向左移动方块时,方块要在当前光标之前显示。

难点就在于如何移动光标到左边的位置。

这里使用了 \e[line;columnH 这个 ANSI 转义码来设置光标到指定的行和列。

所给的 line 指定行数。所给的 column 指定列数。

这个转义码要求以大写的 H 结尾。

关于 ANSI 转义码的详细说明,可以参考前面的文章。

如何数字化表示方块形状

由于俄罗斯方块有多个不同形状的方块,如果硬编码显示每一个方块,需要定义多个小函数来负责显示。

当需要显示某个形状时,就调用对应的函数。这个调用关系很复杂,也不方便代码复用。

为了避免这个问题,需要数字化表示方块形状。

在代码中解析这些数字信息,就能显示出对应的形状。

在俄罗斯方块中,任意一个方块都是由四个小方块组成。
所以可以用四个行列坐标点来确定具体方块的样式。
以方块自身左上角为坐标原点。每一个行列坐标点指定一个小方块的行数和列数。

例如,在上面代码注释中,详细说明了 BLOCK=(0 0 0 2 1 2 1 4) 这个数组对应 Z 字形方块。

同时,也提供了另一个参考的 BLOCK=(0 2 1 0 1 2 1 4) 数组对应土字形方块。其具体形状如下:

  []
[][][]

使用类似的表示方法,BLOCK=(0 0 1 0 1 2 1 4) 数组对应 L 字形的方块。其具体形状如下:

[]
[][][]

BLOCK=(0 0 1 0 1 2 1 4) 这个数组中:

  • 第一个小方块的行列坐标点是 (0,0)
  • 第二个小方块的行列坐标点是 (1,0)。相比于第一个小方块,列数不变,行数加 1,所以第二个小方块在第一个小方块的下一行
  • 第三个小方块的行列坐标点是 (1,2)。相比于第二个小方块,行数不变,列数加 2,所以第三个小方块在第二小方块的右边一列。由于每一个小方块显示两个字符,所以列数要加 2
  • 第四个小方块的行列坐标对是 (1,4)。类似可知,它显示在第三个小方块的右边一列

参考上面说明,其他形状的方块可以用类似的数字化表示。

调用脚本里面的 drawBlock 1 语句就能显示指定形状的方块。

如何清除原先的方块

当移动方块后,会在新的位置重新显示方块,并清除之前显示的方块。

清除原先方块的方法非常简单:重置终端字符属性后,在原先方块的位置重新输出空格,就会显示为空,起到清除原先内容的效果。


霜鱼片
446 声望331 粉丝

解读权威文档,编写易懂文章。如有恰好解答您的疑问,多谢赞赏支持~