打字功能的实现
在完成所有准备工作之后,我们终于可以开始实现打字功能了。让我们来看一下下面的代码:
function main() {
declare -i gamestarttime=0
declare -i gamedonetime=0
declare -i starttime
declare -i deadtime
declare -i curtime
declare -i donetime
declare -i numright=0
declare -i numtotal=0
declare -i accuracy=0
## 将相应的字符存储到数组中,$1 表示用户选择的字符类型
putarray $1
## 初始化游戏开始时间
gamestarttime=$(date +%s)
while [ 1 ]; do
echo -e "\033[2;2H 请在屏幕上的字母消失之前输入它!"
echo -e "\033[3;2H 游戏时间: "
curtime=$(date +%s)
gamedonetime=$curtime-$gamestarttime
echo -e "\033[31;40m\033[3;15H$gamedonetime s\033[37;40m"
echo -e "\033[3;60H 总数: \033[31;26m$numtotal\033[37;40m"
echo -e "\033[3;30H 准确率: \033[31;40m$accuracy %\033[37;40m"
echo -ne "\033[22;2H 你的输入: "
clear_all_area
## 循环10次,检查一列字符是否超时或被击中
for ((line = 20; line <= 60; line = line + 10)); do
## 检查该列字符是否被击中
if [ "${ifchar[$line]}" == "" ] || [ "${donetime[$line]}" -gt "$time" ]; then
## 清除该列的显示
clear_line $line
## 生成一个随机字符
if [ "$1" == "word" ]; then
read -u 4 word
if [ "$word" == "" ]; then ## 文件读取结束
exec 4< $file
fi
putchar[$line]=$word
else
get_random_char $1
putchar[$line]=$random_char
fi
numtotal=$numtotal+1
## 设置标志为1
ifchar[$line]=1
## 重置定时器
starttime[$line]=$(date +%s)
curtime[$line]=${starttime[$line]}
donetime[$line]=$time
## 将列位置重置为0
column[$line]=0
if [ "$1" == "word" ]; then
move 0 $line ${putchar[$line]}
fi
else
## 如果未超时或未被击中,则更新定时器和当前位置
curtime[$line]=$(date +%s)
donetime[$line]=${curtime[$line]}-${starttime[$line]}
move ${donetime[$line]} $line ${putchar[$line]}
fi
done
if [ "$1"!= "word" ]; then
echo -ne "\033[22;14H" ## 清除输入行字符
## 检查用户输入的字符,并作为一个一秒的定时器
if read -n 1 -t 0.5 tmp; then
## 输入成功,循环检查输入是否与某一列匹配
for ((line = 20; line <= 60; line = line + 10)); do
if [ "$tmp" == "${putchar[$line]}" ]; then
## 清除该列的显示
clear_line $line
## 如果匹配,清除标志
ifchar[$line]=""
echo -e "\007\033[32;40m\033[4;62H 正确!\033[37;40m"
numright=$numright+1
## 退出循环
break
else
## 否则,一直显示错误直到超时
echo -e "\033[31;40m\033[4;62H错误,请重试!\033[37;40m"
fi
done
fi
else
echo -ne "\033[22;14H" ## 清除输入行字符
## 检查用户输入的字符,并作为一个定时器
if read tmp; then
## 输入成功,循环检查输入是否与某一列匹配
for ((line = 20; line <= 60; line = line + 10)); do
if [ "$tmp" == "${putchar[$line]}" ]; then
## 清除该列的显示
clear_line $line
## 如果匹配,清除标志
ifchar[$line]=""
echo -e "\007\033[32;40m\033[4;62H 正确!\033[37;40m"
numright=$numright+1
## 退出循环
break
else
## 否则,一直显示错误直到超时
echo -e "\033[31;40m\033[4;62H错误,请重试!\033[37;40m"
fi
done
fi
fi
trap " doexit " 2 ## 捕获特殊信号
## 计算准确率
accuracy=$numright*100/$numtotal
done
}
现在让我们分别解释一下输入单个字符和单词的方法,因为它们略有不同。
对于输入单个字符,我们希望同时出现五个字符,它们将以设定的时间间隔持续下落,直到到达一定高度(由开始时选择的超时期限决定)时消失。如果在消失之前未被击中(用户未输入相应的正确字符),则该列将出现一个新字符。因此,首先我们循环初始化一个字符序列并将其存储在数组中。数组的索引表示终端的列号,这样做的优点是可以独立管理每列字符,无论它是否超时或被击中,以及应该放置在哪里。缺点是我们将创建一个相对较大的数组,但不会大到无法处理。对于 shell 来说这没关系,因为它不会根据数组索引的大小来分配内存。第一个 for
循环负责此操作,并且它还在每个主循环结束时检查某一列是否为空,然后在该列中出现一个新字符。
接下来是一个很长的 if...else
语句。你还记得调用 main
函数时传递的参数吗?最外层用于区分是输入单个字符还是单词。让我们先看输入单个字符的情况,其中包含关键的一行:
if read -n 1 -t 0.5 tmp
read
命令的 -n
参数指定要读取的字符长度。这里指定长度为 1
,这意味着用户输入一个字符后输入立即结束,无需按下回车键。-t
参数指定输入超时时间。如果用户在超时期间未输入或输入未完成,读取将立即结束。因为 read
命令在读取用户输入时对于同一个 shell 是同步操作,字符下落的实现依赖于此超时(间接实现下落延迟)。这里设置为 0.5s
,因为大多数用户可以在 0.5s
内完成一个字符的输入。读取用户输入后,它使用一个 for
循环比较与每列对应的每个字符数组,如果有匹配项,则调用 clear_line
函数清除当前列中的字符,并将标志变量 ifchar[$line]
设置为 0
,表示已清除。
输入单词的流程与输入字符基本相同,只是因为我们无法估计用户输入一个长度不确定的单词所需的时间,所以我们不能为 read
命令设置输入超时时间。没有这个超时,输入单词的最终实现可能不会像输入字符时那样自动下落,而是在我们输入一个单词并按下回车键后,单词才会下落一行。当然,你可以考虑使用其他方法来实现与输入字符相同或更好的效果。
此外,为每列获取新单词的处理略有不同,因为我们是从文件中读取,而不是生成随机单词。你还记得我们之前创建的文件描述符 4
吗?它指向一个单词文件。我们在这里使用了这个文件描述符:
read -u 4 word
if [ "$word" == "" ]; then ## 文件读取结束
exec 4< $file
fi
putchar[$line]=$word
这里我们仍然使用 read
命令,并使用 -u
参数指定从哪个文件描述符逐行读取文件到变量中。后续的空检查语句是为了在文件到达末尾时重新创建指向该文件的文件描述符,以便 read
可以再次从文件开头读取。
你是否觉得有点复杂?别担心,还没完呢。接下来,注意 main
函数的倒数第二行:
trap " doexit " 2 ## 捕获特殊信号
trap
命令用于在 shell 中捕获特殊信号,例如 Ctrl+C
、Ctrl+D
、Ctrl+Z
、ESC
等。原本这些特殊信号由 shell 本身处理。因为我们希望游戏在退出时更优雅地退出,所以我们可以拦截第二个特殊信号,即 Ctrl+C
,来实现一些定制处理。例如,在这里,捕获到第二个信号后,它会调用下面的 doexit
函数来优雅地退出程序:
function doexit() {
draw_border
echo -e "\033[10;30H此游戏将退出....."
echo -e "\033[0m"
sleep 2
clear
exit 1
}