Linux中國

通過編寫掃雷遊戲提高你的 Bash 技巧

我在編程教學方面不是專家,但當我想更好掌握某一樣東西時,會試著找出讓自己樂在其中的方法。比方說,當我想在 shell 編程方面更進一步時,我決定用 Bash 編寫一個掃雷遊戲來加以練習。

如果你是一個有經驗的 Bash 程序員,希望在提高技巧的同時樂在其中,那麼請跟著我編寫一個你的運行在終端中的掃雷遊戲。完整代碼可以在這個 GitHub 存儲庫中找到。

做好準備

在我編寫任何代碼之前,我列出了該遊戲所必須的幾個部分:

  1. 顯示雷區
  2. 創建遊戲邏輯
  3. 創建判斷單元格是否可選的邏輯
  4. 記錄可用和已查明(已排雷)單元格的個數
  5. 創建遊戲結束邏輯

顯示雷區

掃雷中,遊戲界面是一個由 2D 數組(列和行)組成的不透明小方格。每一格下都有可能藏有地雷。玩家的任務就是找到那些不含雷的方格,並且在這一過程中,不能點到地雷。這個 Bash 版本的掃雷使用 10x10 的矩陣,實際邏輯則由一個簡單的 Bash 數組來完成。

首先,我先生成了一些隨機數字。這將是地雷在雷區里的位置。控制地雷的數量,在開始編寫代碼之前,這麼做會容易一些。實現這一功能的邏輯可以更好,但我這麼做,是為了讓遊戲實現保持簡潔,並有改進空間。(我編寫這個遊戲純屬娛樂,但如果你能將它修改的更好,我也是很樂意的。)

下面這些變數在整個過程中是不變的,聲明它們是為了隨機生成數字。就像下面的 a - g 的變數,它們會被用來計算可排除的地雷的值:

# 變數
score=0 # 會用來存放遊戲分數
# 下面這些變數,用來隨機生成可排除地雷的實際值
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"
#
# 聲明
declare -a room  # 聲明一個 room 數組,它用來表示雷區的每一格。

接下來,我會用列(0-9)和行(a-j)顯示出遊戲界面,並且使用一個 10x10 矩陣作為雷區。(M[10][10] 是一個索引從 0-99,有 100 個值的數組。) 如想了解更多關於 Bash 數組的內容,請閱讀這本書那些關於 Bash 你所不了解的事: Bash 數組簡介

創建一個叫 plough 的函數,我們先將標題顯示出來:兩個空行、列頭,和一行 -,以示意往下是遊戲界面:

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

然後,我初始化一個計數器變數,叫 r,它會用來記錄已顯示多少橫行。注意,稍後在遊戲代碼中,我們會用同一個變數 r,作為我們的數組索引。 在 Bash for 循環中,用 seq 命令從 0 增加到 9。我用數字(d%)佔位,來顯示行號($row,由 seq 定義):

r=0 # 計數器
for row in $(seq 0 9); do
  printf '%d  ' "$row" # 顯示 行數 0-9 

在我們接著往下做之前,讓我們看看到現在都做了什麼。我們先橫著顯示 [a-j] 然後再將 [0-9] 的行號顯示出來,我們會用這兩個範圍,來確定用戶排雷的確切位置。

接著,在每行中,插入列,所以是時候寫一個新的 for 循環了。這一循環管理著每一列,也就是說,實際上是生成遊戲界面的每一格。我添加了一些輔助函數,你能在源碼中看到它的完整實現。 對每一格來說,我們需要一些讓它看起來像地雷的東西,所以我們先用一個點(.)來初始化空格。為了實現這一想法,我們用的是一個叫 is_null_field 的自定義函數。 同時,我們需要一個存儲每一格具體值的數組,這兒會用到之前已定義的全局數組 room , 並用 變數 r作為索引。隨著 r 的增加,遍歷所有單元格,並隨機部署地雷。

  for col in $(seq 0 9); do
    ((r+=1))  # 循環完一列行數加一
    is_null_field $r  #  假設這裡有個函數,它會檢查單元格是否為空,為真,則此單元格初始值為點(.)
    printf '%s e[33m%se[0m ' "|" "${room[$r]}" #  最後顯示分隔符,注意,${room[$r]} 的第一個值為 '.',等於其初始值。
  #結束 col 循環
  done

最後,為了保持遊戲界面整齊好看,我會在每行用一個豎線作為結尾,並在最後結束行循環:

printf '%sn' "|"   # 顯示出行分隔符
printf '   %sn' "-----------------------------------------"
# 結束行循環
done
printf 'nn'

完整的 plough 代碼如下:

plough()
{
  r=0
  printf 'nn'
  printf '%s' "     a   b   c   d   e   f   g   h   i   j"
  printf 'n   %sn' "-----------------------------------------"
  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%se[0m ' "|" "${room[$r]}"
    done
    printf '%sn' "|"
    printf '   %sn' "-----------------------------------------"
  done
  printf 'nn'
}

我花了點時間來思考,is_null_field 的具體功能是什麼。讓我們來看看,它到底能做些什麼。在最開始,我們需要遊戲有一個固定的狀態。你可以隨便選擇個初始值,可以是一個數字或者任意字元。我最後決定,所有單元格的初始值為一個點(.),因為我覺得,這樣會讓遊戲界面更好看。下面就是這一函數的完整代碼:

is_null_field()
{
  local e=$1 # 在數組 room 中,我們已經用過循環變數 'r' 了,這次我們用 'e'
    if [[ -z "${room[$e]}" ]];then
      room[$r]="."  #這裡用點(.)來初始化每一個單元格
    fi
}

現在,我已經初始化了所有的格子,現在只要用一個很簡單的函數就能得出當前遊戲中還有多少單元格可以操作:

get_free_fields()
{
  free_fields=0    # 初始化變數 
  for n in $(seq 1 ${#room[@]}); do
    if [[ "${room[$n]}" = "." ]]; then  # 檢查當前單元格是否等於初始值(.),結果為真,則記為空餘格子。 
      ((free_fields+=1))
    fi
  done
}

這是顯示出來的遊戲界面,[a-j] 為列,[0-9] 為行。

![Minefield](/data/attachment/album/201910/07/115159fb5z9b3qn11v111q.png "Minefield")

創建玩家邏輯

玩家操作背後的邏輯在於,先從 stdin 中讀取數據作為坐標,然後再找出對應位置實際包含的值。這裡用到了 Bash 的參數擴展,來設法得到行列數。然後將代表列數的字母傳給分支語句,從而得到其對應的列數。為了更好地理解這一過程,可以看看下面這段代碼中,變數 o 所對應的值。 舉個例子,玩家輸入了 c3,這時 Bash 將其分成兩個字元:c3。為了簡單起見,我跳過了如何處理無效輸入的部分。

  colm=${opt:0:1}  # 得到第一個字元,一個字母
  ro=${opt:1:1}    # 得到第二個字元,一個整數
  case $colm in
    a ) o=1;;      # 最後,通過字母得到對應列數。
    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 中,可以在兩個圓括弧內進行數學計算,這裡我們會多次用到。

還是沿用之前的例子,玩家輸入了 c3。 接著,它被轉化成了 ro=3o=3。 之後,通過上面的分支語句代碼, 將 c 轉化為對應的整數,帶進公式,以得到最終結果 i 的值。

  i=$(((ro*10)+o))   # 遵循運算規則,算出最終值
  is_free_field $i $(shuf -i 0-5 -n 1)   #  調用自定義函數,判斷其指向空/可選擇單元格。

仔細觀察這個計算過程,看看最終結果 i 是如何計算出來的:

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

最後結果是 33。在我們的遊戲界面顯示出來,玩家輸入坐標指向了第 33 個單元格,也就是在第 3 行(從 0 開始,否則這裡變成 4),第 3 列。

創建判斷單元格是否可選的邏輯

為了找到地雷,在將坐標轉化,並找到實際位置之後,程序會檢查這一單元格是否可選。如不可選,程序會顯示一條警告信息,並要求玩家重新輸入坐標。

在這段代碼中,單元格是否可選,是由數組裡對應的值是否為點(.)決定的。如果可選,則重置單元格對應的值,並更新分數。反之,因為其對應值不為點,則設置變數 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](/data/attachment/album/201910/07/115200yun3rw2r9b8fjw69.png "Extracting mines")

如輸入坐標有效,且對應位置為地雷,如下圖所示。玩家輸入 h6,遊戲界面會出現一些隨機生成的值。在發現地雷後,這些值會被加入用戶得分。

![Extracting mines](/data/attachment/album/201910/07/115203umy7fft9vmfrvfto.png "Extracting mines")

還記得我們開頭定義的變數,a - g 嗎,我會用它們來確定隨機生成地雷的具體值。所以,根據玩家輸入坐標,程序會根據(m)中隨機生成的數,來生成周圍其他單元格的值(如上圖所示)。之後將所有值和初始輸入坐標相加,最後結果放在 i(計算結果如上)中。

請注意下面代碼中的 X,它是我們唯一的遊戲結束標誌。我們將它添加到隨機列表中。在 shuf 命令的魔力下,X 可以在任意情況下出現,但如果你足夠幸運的話,也可能一直不會出現。

m=$(shuf -e a b c d e f g X -n 1)   # 將 X 添加到隨機列表中,當 m=X,遊戲結束
  if [[ "$m" != "X" ]]; then        # X 將會是我們爆炸地雷(遊戲結束)的觸發標誌
    for limit in ${!m}; do          # !m 代表 m 變數的值
      field=$(shuf -i 0-5 -n 1)     # 然後再次獲得一個隨機數字
      index=$((i+limit))            # 將 m 中的每一個值和 index 加起來,直到列表結尾
      is_free_field $index $field
    done

我想要遊戲界面中,所有隨機顯示出來的單元格,都靠近玩家選擇的單元格。

![Extracting mines](/data/attachment/album/201910/07/115204ri4htjh4554yg4gd.png "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   # 這意味著你已選擇過所有格子
      printf 'nnt%s: %s %dnn' "You Win" "you scored" "$score"
      exit 0
fi

創建遊戲結束邏輯

對於遊戲結束這種情況,我們這裡使用了一些很巧妙的技巧,將結果在屏幕中央顯示出來。我把這部分留給讀者朋友們自己去探索。

if [[ "$m" = "X" ]]; then
    g=0                      # 為了在參數擴展中使用它
    room[$i]=X               # 覆蓋此位置原有的值,並將其賦值為X
    for j in {42..49}; do    # 在遊戲界面中央,
      out="gameover"
      k=${out:$g:1}          # 在每一格中顯示一個字母
      room[$j]=${k^^}
      ((g+=1))
    done
fi

最後,我們顯示出玩家最關心的兩行。

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

![Minecraft Gameover](/data/attachment/album/201910/07/115207h9eshwh1sil1wsho.png "Minecraft Gameover")

文章到這裡就結束了,朋友們!如果你想了解更多,具體可以查看我的 GitHub 存儲庫,那兒有這個掃雷遊戲的源代碼,並且你還能找到更多用 Bash 編寫的遊戲。 我希望,這篇文章能激起你學習 Bash 的興趣,並樂在其中。

via: https://opensource.com/article/19/9/advanced-bash-building-minesweeper

作者:Abhishek Tamrakar 選題:lujun9972 譯者:wenwensnow 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的電子郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國