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

经典游戏的怀旧感可以成为掌握编程的绝佳来源。深入 Bash 和扫雷。
210 位读者喜欢这个。
bash logo on green background

Opensource.com

我不是编程教学专家,但是当我想在某方面做得更好时,我会尝试找到一种有趣的方式。例如,当我想提高 shell 脚本编写技能时,我决定通过在 Bash 中编写一个 扫雷 游戏版本来练习。

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

准备工作

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

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

在扫雷游戏中,游戏世界是一个隐藏单元格的 2D 数组(列和行)。每个单元格可能包含也可能不包含爆炸性地雷。玩家的目标是显示不包含地雷的单元格,并且永远不要显示地雷。游戏的 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 。然后,ro=3o=3 从上面的 switch case 语句中将 c 转换为其等效的整数,将其放入我们的公式中以计算最终索引 '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

创建游戏结束的逻辑

对于游戏结束的情况,我们使用一些 巧妙的逻辑 打印到终端的中间,我将其留给读者探索它是如何工作的。

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 别名可以带来天壤之别。

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

2 条评论

很棒的文章。感谢分享!

© . All rights reserved.