您好,登錄后才能下訂單哦!
我在編程教學方面不是專家,但當我想更好掌握某一樣東西時,會試著找出讓自己樂在其中的方法。比方說,當我想在?shell?編程方面更進一步時,我決定用 Bash 編寫一個掃雷游戲來加以練習。 |
我在編程教學方面不是專家,但當我想更好掌握某一樣東西時,會試著找出讓自己樂在其中的方法。比方說,當我想在 shell 編程方面更進一步時,我決定用 Bash 編寫一個掃雷游戲來加以練習。
如果你是一個有經(jīng)驗的 Bash 程序員,希望在提高技巧的同時樂在其中,那么請跟著我編寫一個你的運行在終端中的掃雷游戲。完整代碼可以在這個 GitHub 存儲庫中找到。
做好準備
在我編寫任何代碼之前,我列出了該游戲所必須的幾個部分:
顯示雷區(qū)
創(chuàng)建游戲邏輯
創(chuàng)建判斷單元格是否可選的邏輯
記錄可用和已查明(已排雷)單元格的個數(shù)
創(chuàng)建游戲結(jié)束邏輯
顯示雷區(qū)
在掃雷中,游戲界面是一個由 2D 數(shù)組(列和行)組成的不透明小方格。每一格下都有可能藏有地雷。玩家的任務就是找到那些不含雷的方格,并且在這一過程中,不能點到地雷。這個 Bash 版本的掃雷使用 10x10 的矩陣,實際邏輯則由一個簡單的 Bash 數(shù)組來完成。
首先,我先生成了一些隨機數(shù)字。這將是地雷在雷區(qū)里的位置。控制地雷的數(shù)量,在開始編寫代碼之前,這么做會容易一些。實現(xiàn)這一功能的邏輯可以更好,但我這么做,是為了讓游戲?qū)崿F(xiàn)保持簡潔,并有改進空間。(我編寫這個游戲純屬娛樂,但如果你能將它修改的更好,我也是很樂意的。)
下面這些變量在整個過程中是不變的,聲明它們是為了隨機生成數(shù)字。就像下面的?a
?-?g
?的變量,它們會被用來計算可排除的地雷的值:
#?變量 score=0?#?會用來存放游戲分數(shù) #?下面這些變量,用來隨機生成可排除地雷的實際值 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?數(shù)組,它用來表示雷區(qū)的每一格。
接下來,我會用列(0-9)和行(a-j)顯示出游戲界面,并且使用一個 10x10 矩陣作為雷區(qū)。(M[10][10]?是一個索引從 0-99,有 100 個值的數(shù)組。) 如想了解更多關(guān)于 Bash 數(shù)組的內(nèi)容,請閱讀這本書那些關(guān)于 Bash 你所不了解的事: Bash 數(shù)組簡介。
創(chuàng)建一個叫 plough 的函數(shù),我們先將標題顯示出來:兩個空行、列頭,和一行 -,以示意往下是游戲界面:
printf?'\n\n' printf?'%s'?"?????a???b???c???d???e???f???g???h???i???j" printf?'\n???%s\n'?"-----------------------------------------"
然后,我初始化一個計數(shù)器變量,叫?r,它會用來記錄已顯示多少橫行。注意,稍后在游戲代碼中,我們會用同一個變量?r,作為我們的數(shù)組索引。 在?Bash for 循環(huán)中,用?seq?命令從 0 增加到 9。我用數(shù)字(d%)占位,來顯示行號($row,由?seq?定義):
r=0?#?計數(shù)器 for?row?in?$(seq?0?9);?do printf?'%d?'?"$row"?#?顯示?行數(shù)?0-9
在我們接著往下做之前,讓我們看看到現(xiàn)在都做了什么。我們先橫著顯示?[a-j]?然后再將?[0-9]?的行號顯示出來,我們會用這兩個范圍,來確定用戶排雷的確切位置。
接著,在每行中,插入列,所以是時候?qū)懸粋€新的 for 循環(huán)了。這一循環(huán)管理著每一列,也就是說,實際上是生成游戲界面的每一格。我添加了一些輔助函數(shù),你能在源碼中看到它的完整實現(xiàn)。 對每一格來說,我們需要一些讓它看起來像地雷的東西,所以我們先用一個點(.)來初始化空格。為了實現(xiàn)這一想法,我們用的是一個叫 is_null_field 的自定義函數(shù)。 同時,我們需要一個存儲每一格具體值的數(shù)組,這兒會用到之前已定義的全局數(shù)組 room , 并用 變量 r作為索引。隨著 r 的增加,遍歷所有單元格,并隨機部署地雷。
??for?col?in?$(seq?0?9);?do ((r+=1))?#?循環(huán)完一列行數(shù)加一 is_null_field?$r?#?假設(shè)這里有個函數(shù),它會檢查單元格是否為空,為真,則此單元格初始值為點(.) printf?'%s?\e[33m%s\e[0m?'?"|"?"${room[$r]}"?#?最后顯示分隔符,注意,${room[$r]}?的第一個值為?'.',等于其初始值。 #結(jié)束?col?循環(huán) done
最后,為了保持游戲界面整齊好看,我會在每行用一個豎線作為結(jié)尾,并在最后結(jié)束行循環(huán):
printf?'%s\n'?"|"?#?顯示出行分隔符 printf?'?%s\n'?"-----------------------------------------" #?結(jié)束行循環(huán) 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 的具體功能是什么。讓我們來看看,它到底能做些什么。在最開始,我們需要游戲有一個固定的狀態(tài)。你可以隨便選擇個初始值,可以是一個數(shù)字或者任意字符。我最后決定,所有單元格的初始值為一個點(.),因為我覺得,這樣會讓游戲界面更好看。下面就是這一函數(shù)的完整代碼:
is_null_field() { local?e=$1?#?在數(shù)組?room?中,我們已經(jīng)用過循環(huán)變量?'r'?了,這次我們用?'e' if?[[?-z?"${room[$e]}"?]];then room[$r]="."?#這里用點(.)來初始化每一個單元格 fi }
現(xiàn)在,我已經(jīng)初始化了所有的格子,現(xiàn)在只要用一個很簡單的函數(shù)就能得出當前游戲中還有多少單元格可以操作:
get_free_fields() { free_fields=0?#?初始化變量? for?n?in?$(seq?1?${#room[@]});?do if?[[?"${room[$n]}"?=?"."?]];?then?#?檢查當前單元格是否等于初始值(.),結(jié)果為真,則記為空余格子。? ((free_fields+=1)) ????fi ??done }
這是顯示出來的游戲界面,[a-j]?為列,[0-9]?為行。
Minefield
創(chuàng)建玩家邏輯
玩家操作背后的邏輯在于,先從 stdin 中讀取數(shù)據(jù)作為坐標,然后再找出對應位置實際包含的值。這里用到了 Bash 的參數(shù)擴展,來設(shè)法得到行列數(shù)。然后將代表列數(shù)的字母傳給分支語句,從而得到其對應的列數(shù)。為了更好地理解這一過程,可以看看下面這段代碼中,變量 o 所對應的值。 舉個例子,玩家輸入了 c3,這時 Bash 將其分成兩個字符:c 和 3。為了簡單起見,我跳過了如何處理無效輸入的部分。
colm=${opt:0:1}?#?得到第一個字符,一個字母 ro=${opt:1:1}?#?得到第二個字符,一個整數(shù) case?$colm?in a?)?o=1;;?#?最后,通過字母得到對應列數(shù)。 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
下面的代碼會計算用戶所選單元格實際對應的數(shù)字,然后將結(jié)果儲存在變量中。
這里也用到了很多的?shuf?命令,shuf?是一個專門用來生成隨機序列的?Linux?命令。-i?選項后面需要提供需要打亂的數(shù)或者范圍,-n?選項則規(guī)定輸出結(jié)果最多需要返回幾個值。Bash 中,可以在兩個圓括號內(nèi)進行數(shù)學計算,這里我們會多次用到。
還是沿用之前的例子,玩家輸入了 c3。 接著,它被轉(zhuǎn)化成了 ro=3 和 o=3。 之后,通過上面的分支語句代碼, 將 c 轉(zhuǎn)化為對應的整數(shù),帶進公式,以得到最終結(jié)果 i 的值。
i=$(((ro*10)+o))?#?遵循運算規(guī)則,算出最終值 is_free_field?$i?$(shuf?-i?0-5?-n?1)?#?調(diào)用自定義函數(shù),判斷其指向空/可選擇單元格。
仔細觀察這個計算過程,看看最終結(jié)果 i 是如何計算出來的:
i=$(((ro*10)+o)) i=$(((3*10)+3))=$((30+3))=33
最后結(jié)果是 33。在我們的游戲界面顯示出來,玩家輸入坐標指向了第 33 個單元格,也就是在第 3 行(從 0 開始,否則這里變成 4),第 3 列。
創(chuàng)建判斷單元格是否可選的邏輯
為了找到地雷,在將坐標轉(zhuǎn)化,并找到實際位置之后,程序會檢查這一單元格是否可選。如不可選,程序會顯示一條警告信息,并要求玩家重新輸入坐標。
在這段代碼中,單元格是否可選,是由數(shù)組里對應的值是否為點(.)決定的。如果可選,則重置單元格對應的值,并更新分數(shù)。反之,因為其對應值不為點,則設(shè)置變量?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 }
如輸入坐標有效,且對應位置為地雷,如下圖所示。玩家輸入?h7,游戲界面會出現(xiàn)一些隨機生成的值。在發(fā)現(xiàn)地雷后,這些值會被加入用戶得分。
Extracting mines
還記得我們開頭定義的變量,a?-?g?嗎,我會用它們來確定隨機生成地雷的具體值。所以,根據(jù)玩家輸入坐標,程序會根據(jù)(m)中隨機生成的數(shù),來生成周圍其他單元格的值(如上圖所示)。之后將所有值和初始輸入坐標相加,最后結(jié)果放在?i(計算結(jié)果如上)中。
請注意下面代碼中的 X,它是我們唯一的游戲結(jié)束標志。我們將它添加到隨機列表中。在 shuf 命令的魔力下,X 可以在任意情況下出現(xiàn),但如果你足夠幸運的話,也可能一直不會出現(xiàn)。
m=$(shuf?-e?a?b?c?d?e?f?g?X?-n?1)?#?將?X?添加到隨機列表中,當?m=X,游戲結(jié)束 if?[[?"$m"?!=?"X"?]];?then?#?X?將會是我們爆炸地雷(游戲結(jié)束)的觸發(fā)標志 for?limit?in?${!m};?do?#?!m?代表?m?變量的值 field=$(shuf?-i?0-5?-n?1)?#?然后再次獲得一個隨機數(shù)字 index=$((i+limit))?#?將?m?中的每一個值和?index?加起來,直到列表結(jié)尾 is_free_field?$index?$field ????done
我想要游戲界面中,所有隨機顯示出來的單元格,都靠近玩家選擇的單元格。
Extracting mines
記錄已選擇和可用單元格的個數(shù)
這個程序需要記錄游戲界面中哪些單元格是可選擇的。否則,程序會一直讓用戶輸入數(shù)據(jù),即使所有單元格都被選中過。為了實現(xiàn)這一功能,我創(chuàng)建了一個叫?free_fields?的變量,初始值為?0。用一個?for?循環(huán),記錄下游戲界面中可選擇單元格的數(shù)量。 如果單元格所對應的值為點(.),則?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?'\n\n\t%s:?%s?%d\n\n'?"You?Win"?"you?scored"?"$score" ??????exit?0 fi
創(chuàng)建游戲結(jié)束邏輯
對于游戲結(jié)束這種情況,我們這里使用了一些很巧妙的技巧,將結(jié)果在屏幕中央顯示出來。我把這部分留給讀者朋友們自己去探索。
if?[[?"$m"?=?"X"?]];?then g=0?#?為了在參數(shù)擴展中使用它 room[$i]=X?#?覆蓋此位置原有的值,并將其賦值為X for?j?in?{42..49};?do?#?在游戲界面中央, out="gameover" k=${out:$g:1}?#?在每一格中顯示一個字母 room[$j]=${k^^} ??????((g+=1)) ????done fi
最后,我們顯示出玩家最關(guān)心的兩行。
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
文章到這里就結(jié)束了,朋友們!如果你想了解更多,具體可以查看我的?GitHub 存儲庫,那兒有這個掃雷游戲的源代碼,并且你還能找到更多用 Bash 編寫的游戲。 我希望,這篇文章能激起你學習 Bash 的興趣,并樂在其中。
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。