在 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
语句就能显示指定形状的方块。
如何清除原先的方块
当移动方块后,会在新的位置重新显示方块,并清除之前显示的方块。
清除原先方块的方法非常简单:重置终端字符属性后,在原先方块的位置重新输出空格,就会显示为空,起到清除原先内容的效果。