Bash技巧:可以上下左右移动俄罗斯方块的Shell脚本

码农天地 -
Bash技巧:可以上下左右移动俄罗斯方块的Shell脚本

在 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 语句就能显示指定形状的方块。

如何清除原先的方块

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

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

特别申明:本文内容来源网络,版权归原作者所有,如有侵权请立即与我们联系(cy198701067573@163.com),我们将及时处理。

Tags 标签

加个好友,技术交流

1628738909466805.jpg