通过构建扫雷游戏来磨练高级 Bash 技能

经典游戏的怀旧情怀可以成为掌握编程的绝佳来源。 通过扫雷游戏深入了解 Bash。
210 位读者喜欢这篇文章。
bash logo on green background

Opensource.com

我不是编程教学方面的专家,但当我想更好地掌握某些东西时,我会尝试寻找一种有趣的方式。 例如,当我想更好地掌握 shell 脚本时,我决定通过用 Bash 编写一个扫雷游戏的版本来练习。

如果您是一位经验丰富的 Bash 程序员,并且想在玩乐的同时磨练您的技能,请跟随我来编写您自己的终端扫雷游戏版本。 完整的源代码可以在这个 GitHub 存储库中找到。

准备工作

在开始编写任何代码之前,我概述了创建游戏所需的要素

  1. 打印雷区
  2. 创建游戏逻辑
  3. 创建逻辑来确定可用的雷区
  4. 统计可用和已发现(提取)的地雷
  5. 创建结束游戏逻辑

在扫雷游戏中,游戏世界是一个由隐藏单元格组成的二维数组(列和行)。 每个单元格可能包含或不包含爆炸性地雷。 玩家的目标是显示不包含地雷的单元格,并且永远不要显示地雷。 该游戏的 Bash 版本使用一个 10x10 矩阵,使用简单的 bash 数组实现。

首先,我分配一些随机变量。 这些是地雷可能放置在棋盘上的位置。 通过限制位置的数量,很容易在此基础上进行构建。 逻辑可能会更好,但我希望游戏看起来简单且有点不成熟。 (我编写这个是为了好玩,但我很乐意欢迎您提出改进意见。)

下面的变量是一些默认变量,声明为随机调用字段放置,例如变量 a-g,我们将使用它们来计算我们可提取的地雷

# variables
score=0 # will be used to store the score of the game
# variables below will be used to randomly get the extract-able cells/fields from our mine.
a="1 10 -10 -1"
b="-1 0 1"
c="0 1"
d="-1 0 1 -2 -3"
e="1 2 20 21 10 0 -10 -20 -23 -2 -1"
f="1 2 3 35 30 20 22 10 0 -10 -20 -25 -30 -35 -3 -2 -1"
g="1 4 6 9 10 15 20 25 30 -30 -24 -11 -10 -9 -8 -7"
#
# declarations
declare -a room  # declare an array room, it will represent each cell/field of our mine.

接下来,我打印我的棋盘,其中包含列 (0-9) 和行 (a-j),形成一个 10x10 矩阵,作为游戏的地雷区。 (M[10][10] 是一个包含 100 个值的数组,索引为 0-99。) 如果您想了解更多关于 Bash 数组的信息,请阅读 你不知道的 Bash:Bash 数组简介

让我们称它为一个函数,plough, 我们首先打印标题:两行空白行,列标题和一行来概述游戏区域的顶部

printf '\n\n'
printf '%s' "     a   b   c   d   e   f   g   h   i   j"
printf '\n   %s\n' "-----------------------------------------"

接下来,我建立一个计数器变量,称为 r,以跟踪已填充了多少行。 请注意,我们将在以后的游戏代码中使用相同的计数器变量“r”作为我们的数组索引。 在 Bash for 循环中,使用 seq 命令从 0 递增到 9,我打印一个数字 (d%) 来表示行号($row,由 seq 定义)

r=0 # our counter
for row in $(seq 0 9); do
  printf '%d  ' "$row" # print the row numbers from 0-9

在我们从这里继续前进之前,让我们检查一下我们到目前为止所做的事情。 我们首先水平打印序列 [a-j] ,然后我们打印范围 [0-9] 中的行号,我们将使用这两个范围作为我们的用户输入坐标来定位要提取的地雷。 

接下来, 在每一行中,都有一个列相交,所以现在是打开一个新的 for 循环的时候了。 这一个管理每一列,因此它本质上生成游戏区域中的每个单元格。 我添加了一些帮助函数,您可以在源代码中看到完整的定义。 对于每个单元格, 我们需要一些东西来使该字段看起来像一个地雷,因此我们使用一个名为 is_null_field 的自定义函数,使用点 (.) 初始化空单元格。 此外,我们需要一个数组变量来存储每个单元格的值,我们将使用预定义的全局 数组变量 room 以及索引 变量 r。 随着 r 的递增,我们迭代单元格,沿途放置地雷。

  for col in $(seq 0 9); do
    ((r+=1))  # increment the counter as we move forward in column sequence
    is_null_field $r  # assume a function which will check, if the field is empty, if so, initialize it with a dot(.)
    printf '%s \e[33m%s\e[0m ' "|" "${room[$r]}" # finally print the separator, note that, the first value of ${room[$r]} will be '.', as it is just initialized.
  #close col loop
  done

最后,我通过 用一条线封闭每一行的底部来保持棋盘的良好定义,然后关闭行循环

printf '%s\n' "|"   # print the line end separator
printf '   %s\n' "-----------------------------------------"
# close row for loop
done
printf '\n\n'

完整的 plough 函数如下所示: 

plough()
{
  r=0
  printf '\n\n'
  printf '%s' "     a   b   c   d   e   f   g   h   i   j"
  printf '\n   %s\n' "-----------------------------------------"
  for row in $(seq 0 9); do
    printf '%d  ' "$row" 
    for col in $(seq 0 9); do
       ((r+=1))
       is_null_field $r
       printf '%s \e[33m%s\e[0m ' "|" "${room[$r]}"
    done
    printf '%s\n' "|" 
    printf '   %s\n' "-----------------------------------------"
  done
  printf '\n\n'
}

我花了一些时间来决定是否需要 is_null_field,所以让我们仔细看看它的作用。 我们从游戏开始就需要一个可靠的状态。 这个选择是任意的——它可以是一个数字或任何字符。 我决定假设所有内容都声明为一个点 (.),因为我相信它可以使游戏板看起来很漂亮。 这是它的样子

is_null_field()
{
  local e=$1 # we used index 'r' for array room already, let's call it 'e'
    if [[ -z "${room[$e]}" ]];then
      room[$r]="."  # this is where we put the dot(.) to initialize the cell/minefield
    fi
}

现在,我 初始化了我们地雷中的所有单元格,我通过声明并稍后调用下面显示的简单函数来获得所有可用地雷的计数

get_free_fields()
{
  free_fields=0    # initialize the variable
  for n in $(seq 1 ${#room[@]}); do
    if [[ "${room[$n]}" = "." ]]; then  # check if the cells has initial value dot(.), then count it as a free field.
      ((free_fields+=1))
    fi
  done
}

这是打印的雷区,其中 [a-j] 是列,[0-9] 是行。

Minefield

创建驱动玩家的逻辑

玩家逻辑从 stdin 中读取一个选项,作为地雷的坐标,并提取雷区上的确切字段。 它使用 Bash 的 参数扩展来提取列和行输入,然后将列输入到开关,该开关指向棋盘上等效的整数表示法,要理解这一点,请参见分配给下面 switch case 语句中变量“o'”的值。 例如,玩家可能会输入 c3,Bash 会将其拆分为两个字符:c 3。 为了简单起见,我跳过了如何处理无效条目的过程。

  colm=${opt:0:1}  # get the first char, the alphabet
  ro=${opt:1:1}    # get the second char, the digit
  case $colm in
    a ) o=1;;      # finally, convert the alphabet to its equivalent integer notation.
    b ) o=2;;
    c ) o=3;;
    d ) o=4;;
    e ) o=5;;
    f ) o=6;;
    g ) o=7;;
    h ) o=8;;
    i ) o=9;;
    j ) o=10;;
  esac

然后,它计算确切的索引,并将输入坐标的索引分配给该字段。

这里也大量使用了 shuf 命令,shuf 是一个 Linux 实用程序,旨在提供信息的随机 排列,其中 -i 选项表示要打乱的索引或可能的范围 ,而 -n 表示给出的最大数字或输出。 双括号允许在 Bash 中进行数学计算,我们将在这里大量使用它们。

让我们假设我们之前的示例 通过 stdin 收到了 c3 。 然后,来自上述 switch case 语句的 ro=3o=3c 转换为其等效整数,将其放入我们的公式 以计算最终索引“i'。

  i=$(((ro*10)+o))   # Follow BODMAS rule, to calculate final index. 
  is_free_field $i $(shuf -i 0-5 -n 1)   # call a custom function that checks if the final index value points to a an empty/free cell/field.

遍历这个数学运算,以了解最终索引“i”是如何计算的

i=$(((ro*10)+o))
i=$(((3*10)+3))=$((30+3))=33

最终索引值为 33。在上面打印的棋盘上,最终索引指向第 33 个单元格,该单元格应为第 3(从 0 开始,否则为第 4) 行 和第 3 (C) 列。

创建逻辑来确定可用的雷区

要提取地雷,在对坐标进行解码并找到索引之后,程序会检查该字段是否可用。 如果不可用,程序将显示一条警告,并且玩家选择另一个坐标。

在此代码中,如果单元格包含点(.)字符,则该单元格可用。 假设它可用,则重置单元格中的值并更新分数。 如果单元格不可用,因为它不包含点,则设置一个变量 not_allowed。 为了简洁起见,我将警告语句的内容留给您查看游戏逻辑的源代码。

is_free_field()
{
  local f=$1
  local val=$2
  not_allowed=0
  if [[ "${room[$f]}" = "." ]]; then
    room[$f]=$val
    score=$((score+val))
  else
    not_allowed=1
  fi
}

Extracting mines

如果输入的坐标可用,则会发现地雷,如下所示。 当提供 h6 作为输入时,随机填充在我们雷区上的一些值,这些值在提取分钟后会添加到用户的分数中。 

Extracting mines

现在记住我们在开始时声明的变量 [a-g],我现在将使用它们在这里提取随机地雷,使用 Bash 间接将其值分配给变量 m。 因此,根据输入坐标,程序会选择一组随机的附加数字(m),以通过将它们添加到原始输入坐标(此处由 i 表示(上面计算))来计算要填充的附加字段。

请注意以下代码段中的字符 X,它是我们唯一的 GAME-OVER 触发器,我们将其添加到我们的 shuffle 列表中以随机显示,凭借 shuf 命令的强大功能,它可以出现在任何次数的机会之后,甚至可能不会为我们幸运的获胜用户显示。

m=$(shuf -e a b c d e f g X -n 1)   # add an extra char X to the shuffle, when m=X, its GAMEOVER
  if [[ "$m" != "X" ]]; then        # X will be our explosive mine(GAME-OVER) trigger
    for limit in ${!m}; do          # !m represents the value of value of m
      field=$(shuf -i 0-5 -n 1)     # again get a random number and
      index=$((i+limit))            # add values of m to our index and calculate a new index till m reaches its last element.
      is_free_field $index $field
    done

我希望所有显示的单元格都与玩家选择的单元格相邻。

Extracting mines

统计可用和提取的地雷

程序需要跟踪雷区中的可用单元格; 否则,即使所有单元格都已显示,它也会一直要求玩家输入。 为了实现这一点,我创建一个名为 free_fields 的变量,最初将其设置为 0。在由我们雷区中剩余的可用单元格/字段定义的 for 循环中。 如果一个单元格包含一个点(.),则 free_fields 的计数会递增。

get_free_fields()
{
  free_fields=0
  for n in $(seq 1 ${#room[@]}); do
    if [[ "${room[$n]}" = "." ]]; then
      ((free_fields+=1))
    fi
  done
}

等等,如果 free_fields=0 怎么办? 这意味着我们的用户 已 提取了所有地雷。 请随时查看 确切的代码以更好地理解。

if [[ $free_fields -eq 0 ]]; then   # well that means you extracted all the mines.
      printf '\n\n\t%s: %s %d\n\n' "You Win" "you scored" "$score"
      exit 0
fi

创建 Gameover 的逻辑

对于 Gameover 情况,我们使用一些 巧妙的逻辑打印到终端的中间,我将其留给读者去探索它的工作原理。

if [[ "$m" = "X" ]]; then
    g=0                      # to use it in parameter expansion
    room[$i]=X               # override the index and print X
    for j in {42..49}; do    # in the middle of the minefields,
      out="gameover"
      k=${out:$g:1}          # print one alphabet in each cell
      room[$j]=${k^^}
      ((g+=1)) 
    done
fi

 最后,我们可以 打印出最令人期待的两行。

if [[ "$m" = "X" ]]; then
      printf '\n\n\t%s: %s %d\n' "GAMEOVER" "you scored" "$score"
      printf '\n\n\t%s\n\n' "You were just $free_fields mines away."
      exit 0
fi

Minecraft Gameover

就是这样,各位! 如果您想了解更多信息,请从我的 GitHub 存储库访问此扫雷 游戏和其他 Bash 游戏的代码。 我希望它能给您一些灵感,让您学习更多 Bash 并在此过程中获得乐趣。 

接下来阅读什么

你离不开的 Bash 别名

厌倦了一遍又一遍地输入相同的长命令? 您是否觉得在命令行上工作效率低下? Bash 别名可以带来天翻地覆的变化。

(团队,Red Hat)
2019 年 7 月 31 日
标签
iamabhi
我担任 Lead DevOps,是一名程序员。 我是开源爱好者、博主、作家。 您会发现我主要在帮助人们学习。

2 条评论

很棒的文章。 感谢分享!

© . All rights reserved.