新聞中心
我在編程教學(xué)方面不是專(zhuān)家,但當(dāng)我想更好掌握某一樣?xùn)|西時(shí),會(huì)試著找出讓自己樂(lè)在其中的方法。比方說(shuō),當(dāng)我想在 shell 編程方面更進(jìn)一步時(shí),我決定用 Bash 編寫(xiě)一個(gè)掃雷游戲來(lái)加以練習(xí)。

作為一家“創(chuàng)意+整合+營(yíng)銷(xiāo)”的成都網(wǎng)站建設(shè)機(jī)構(gòu),我們?cè)跇I(yè)內(nèi)良好的客戶(hù)口碑。創(chuàng)新互聯(lián)提供從前期的網(wǎng)站品牌分析策劃、網(wǎng)站設(shè)計(jì)、成都做網(wǎng)站、網(wǎng)站設(shè)計(jì)、創(chuàng)意表現(xiàn)、網(wǎng)頁(yè)制作、系統(tǒng)開(kāi)發(fā)以及后續(xù)網(wǎng)站營(yíng)銷(xiāo)運(yùn)營(yíng)等一系列服務(wù),幫助企業(yè)打造創(chuàng)新的互聯(lián)網(wǎng)品牌經(jīng)營(yíng)模式與有效的網(wǎng)絡(luò)營(yíng)銷(xiāo)方法,創(chuàng)造更大的價(jià)值。
我在編程教學(xué)方面不是專(zhuān)家,但當(dāng)我想更好掌握某一樣?xùn)|西時(shí),會(huì)試著找出讓自己樂(lè)在其中的方法。比方說(shuō),當(dāng)我想在 shell 編程方面更進(jìn)一步時(shí),我決定用 Bash 編寫(xiě)一個(gè)掃雷游戲來(lái)加以練習(xí)。
如果你是一個(gè)有經(jīng)驗(yàn)的 Bash 程序員,希望在提高技巧的同時(shí)樂(lè)在其中,那么請(qǐng)跟著我編寫(xiě)一個(gè)你的運(yùn)行在終端中的掃雷游戲。完整代碼可以在這個(gè) GitHub 存儲(chǔ)庫(kù)中找到。
做好準(zhǔn)備
在我編寫(xiě)任何代碼之前,我列出了該游戲所必須的幾個(gè)部分:
-
顯示雷區(qū)
-
創(chuàng)建游戲邏輯
-
創(chuàng)建判斷單元格是否可選的邏輯
-
記錄可用和已查明(已排雷)單元格的個(gè)數(shù)
-
創(chuàng)建游戲結(jié)束邏輯
顯示雷區(qū)
在掃雷中,游戲界面是一個(gè)由 2D 數(shù)組(列和行)組成的不透明小方格。每一格下都有可能藏有地雷。玩家的任務(wù)就是找到那些不含雷的方格,并且在這一過(guò)程中,不能點(diǎn)到地雷。這個(gè) Bash 版本的掃雷使用 10×10 的矩陣,實(shí)際邏輯則由一個(gè)簡(jiǎn)單的 Bash 數(shù)組來(lái)完成。
首先,我先生成了一些隨機(jī)數(shù)字。這將是地雷在雷區(qū)里的位置??刂频乩椎臄?shù)量,在開(kāi)始編寫(xiě)代碼之前,這么做會(huì)容易一些。實(shí)現(xiàn)這一功能的邏輯可以更好,但我這么做,是為了讓游戲?qū)崿F(xiàn)保持簡(jiǎn)潔,并有改進(jìn)空間。(我編寫(xiě)這個(gè)游戲純屬娛樂(lè),但如果你能將它修改的更好,我也是很樂(lè)意的。)
下面這些變量在整個(gè)過(guò)程中是不變的,聲明它們是為了隨機(jī)生成數(shù)字。就像下面的 a – g 的變量,它們會(huì)被用來(lái)計(jì)算可排除的地雷的值:
# 變量
score=0 # 會(huì)用來(lái)存放游戲分?jǐn)?shù)
# 下面這些變量,用來(lái)隨機(jī)生成可排除地雷的實(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 # 聲明一個(gè) room 數(shù)組,它用來(lái)表示雷區(qū)的每一格。
接下來(lái),我會(huì)用列(0-9)和行(a-j)顯示出游戲界面,并且使用一個(gè) 10×10 矩陣作為雷區(qū)。(M10 是一個(gè)索引從 0-99,有 100 個(gè)值的數(shù)組。) 如想了解更多關(guān)于 Bash 數(shù)組的內(nèi)容,請(qǐng)閱讀這本書(shū)那些關(guān)于 Bash 你所不了解的事: Bash 數(shù)組簡(jiǎn)介。
創(chuàng)建一個(gè)叫 plough 的函數(shù),我們先將標(biāo)題顯示出來(lái):兩個(gè)空行、列頭,和一行 -,以示意往下是游戲界面:
printf '\n\n'
printf '%s' " a b c d e f g h i j"
printf '\n %s\n' "-----------------------------------------"
然后,我初始化一個(gè)計(jì)數(shù)器變量,叫 r,它會(huì)用來(lái)記錄已顯示多少橫行。注意,稍后在游戲代碼中,我們會(huì)用同一個(gè)變量 r,作為我們的數(shù)組索引。 在 Bash for 循環(huán)中,用 seq 命令從 0 增加到 9。我用數(shù)字(d%)占位,來(lái)顯示行號(hào)($row,由 seq 定義):
r=0 # 計(jì)數(shù)器
for row in $(seq 0 9); do
printf '%d ' "$row" # 顯示 行數(shù) 0-9
在我們接著往下做之前,讓我們看看到現(xiàn)在都做了什么。我們先橫著顯示 [a-j] 然后再將 [0-9] 的行號(hào)顯示出來(lái),我們會(huì)用這兩個(gè)范圍,來(lái)確定用戶(hù)排雷的確切位置。
接著,在每行中,插入列,所以是時(shí)候?qū)懸粋€(gè)新的 for 循環(huán)了。這一循環(huán)管理著每一列,也就是說(shuō),實(shí)際上是生成游戲界面的每一格。我添加了一些輔助函數(shù),你能在源碼中看到它的完整實(shí)現(xiàn)。 對(duì)每一格來(lái)說(shuō),我們需要一些讓它看起來(lái)像地雷的東西,所以我們先用一個(gè)點(diǎn)(.)來(lái)初始化空格。為了實(shí)現(xiàn)這一想法,我們用的是一個(gè)叫 is_null_field 的自定義函數(shù)。 同時(shí),我們需要一個(gè)存儲(chǔ)每一格具體值的數(shù)組,這兒會(huì)用到之前已定義的全局?jǐn)?shù)組 room , 并用 變量 r作為索引。隨著 r 的增加,遍歷所有單元格,并隨機(jī)部署地雷。
for col in $(seq 0 9); do
((r+=1)) # 循環(huán)完一列行數(shù)加一
is_null_field $r # 假設(shè)這里有個(gè)函數(shù),它會(huì)檢查單元格是否為空,為真,則此單元格初始值為點(diǎn)(.)
printf '%s \e[33m%s\e[0m ' "|" "${room[$r]}" # 最后顯示分隔符,注意,${room[$r]} 的第一個(gè)值為 '.',等于其初始值。
#結(jié)束 col 循環(huán)
done
最后,為了保持游戲界面整齊好看,我會(huì)在每行用一個(gè)豎線作為結(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'
}
我花了點(diǎn)時(shí)間來(lái)思考,is_null_field 的具體功能是什么。讓我們來(lái)看看,它到底能做些什么。在最開(kāi)始,我們需要游戲有一個(gè)固定的狀態(tài)。你可以隨便選擇個(gè)初始值,可以是一個(gè)數(shù)字或者任意字符。我最后決定,所有單元格的初始值為一個(gè)點(diǎn)(.),因?yàn)槲矣X(jué)得,這樣會(huì)讓游戲界面更好看。下面就是這一函數(shù)的完整代碼:
is_null_field()
{
local e=$1 # 在數(shù)組 room 中,我們已經(jīng)用過(guò)循環(huán)變量 'r' 了,這次我們用 'e'
if [[ -z "${room[$e]}" ]];then
room[$r]="." #這里用點(diǎn)(.)來(lái)初始化每一個(gè)單元格
fi
}
現(xiàn)在,我已經(jīng)初始化了所有的格子,現(xiàn)在只要用一個(gè)很簡(jiǎn)單的函數(shù)就能得出當(dāng)前游戲中還有多少單元格可以操作:
get_free_fields()
{
free_fields=0 # 初始化變量
for n in $(seq 1 ${#room[@]}); do
if [[ "${room[$n]}" = "." ]]; then # 檢查當(dāng)前單元格是否等于初始值(.),結(jié)果為真,則記為空余格子。
((free_fields+=1))
fi
done
}
這是顯示出來(lái)的游戲界面,[a-j] 為列,[0-9] 為行。
Minefield
創(chuàng)建玩家邏輯
玩家操作背后的邏輯在于,先從 stdin 中讀取數(shù)據(jù)作為坐標(biāo),然后再找出對(duì)應(yīng)位置實(shí)際包含的值。這里用到了 Bash 的參數(shù)擴(kuò)展,來(lái)設(shè)法得到行列數(shù)。然后將代表列數(shù)的字母?jìng)鹘o分支語(yǔ)句,從而得到其對(duì)應(yīng)的列數(shù)。為了更好地理解這一過(guò)程,可以看看下面這段代碼中,變量 o 所對(duì)應(yīng)的值。 舉個(gè)例子,玩家輸入了 c3,這時(shí) Bash 將其分成兩個(gè)字符:c 和 3。為了簡(jiǎn)單起見(jiàn),我跳過(guò)了如何處理無(wú)效輸入的部分。
colm=${opt:0:1} # 得到第一個(gè)字符,一個(gè)字母
ro=${opt:1:1} # 得到第二個(gè)字符,一個(gè)整數(shù)
case $colm in
a ) o=1;; # 最后,通過(guò)字母得到對(duì)應(yīng)列數(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
下面的代碼會(huì)計(jì)算用戶(hù)所選單元格實(shí)際對(duì)應(yīng)的數(shù)字,然后將結(jié)果儲(chǔ)存在變量中。
這里也用到了很多的 shuf 命令,shuf 是一個(gè)專(zhuān)門(mén)用來(lái)生成隨機(jī)序列的 Linux 命令。-i 選項(xiàng)后面需要提供需要打亂的數(shù)或者范圍,-n 選項(xiàng)則規(guī)定輸出結(jié)果最多需要返回幾個(gè)值。Bash 中,可以在兩個(gè)圓括號(hào)內(nèi)進(jìn)行數(shù)學(xué)計(jì)算,這里我們會(huì)多次用到。
還是沿用之前的例子,玩家輸入了 c3。 接著,它被轉(zhuǎn)化成了 ro=3 和 o=3。 之后,通過(guò)上面的分支語(yǔ)句代碼, 將 c 轉(zhuǎn)化為對(duì)應(yīng)的整數(shù),帶進(jìn)公式,以得到最終結(jié)果 i 的值。
i=$(((ro*10)+o)) # 遵循運(yùn)算規(guī)則,算出最終值
is_free_field $i $(shuf -i 0-5 -n 1) # 調(diào)用自定義函數(shù),判斷其指向空/可選擇單元格。
仔細(xì)觀察這個(gè)計(jì)算過(guò)程,看看最終結(jié)果 i 是如何計(jì)算出來(lái)的:
i=$(((ro*10)+o))
i=$(((3*10)+3))=$((30+3))=33
最后結(jié)果是 33。在我們的游戲界面顯示出來(lái),玩家輸入坐標(biāo)指向了第 33 個(gè)單元格,也就是在第 3 行(從 0 開(kāi)始,否則這里變成 4),第 3 列。
創(chuàng)建判斷單元格是否可選的邏輯
為了找到地雷,在將坐標(biāo)轉(zhuǎn)化,并找到實(shí)際位置之后,程序會(huì)檢查這一單元格是否可選。如不可選,程序會(huì)顯示一條警告信息,并要求玩家重新輸入坐標(biāo)。
在這段代碼中,單元格是否可選,是由數(shù)組里對(duì)應(yīng)的值是否為點(diǎn)(.)決定的。如果可選,則重置單元格對(duì)應(yīng)的值,并更新分?jǐn)?shù)。反之,因?yàn)槠鋵?duì)應(yīng)值不為點(diǎn),則設(shè)置變量 not_allowed。為簡(jiǎn)單起見(jiàn),游戲中警告消息這部分源碼,我會(huì)留給讀者們自己去探索。
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
}
如輸入坐標(biāo)有效,且對(duì)應(yīng)位置為地雷,如下圖所示。玩家輸入 h6,游戲界面會(huì)出現(xiàn)一些隨機(jī)生成的值。在發(fā)現(xiàn)地雷后,這些值會(huì)被加入用戶(hù)得分。
Extracting mines
還記得我們開(kāi)頭定義的變量,a – g 嗎,我會(huì)用它們來(lái)確定隨機(jī)生成地雷的具體值。所以,根據(jù)玩家輸入坐標(biāo),程序會(huì)根據(jù)(m)中隨機(jī)生成的數(shù),來(lái)生成周?chē)渌麊卧竦闹担ㄈ缟蠄D所示)。之后將所有值和初始輸入坐標(biāo)相加,最后結(jié)果放在 i(計(jì)算結(jié)果如上)中。
請(qǐng)注意下面代碼中的 X,它是我們唯一的游戲結(jié)束標(biāo)志。我們將它添加到隨機(jī)列表中。在 shuf 命令的魔力下,X 可以在任意情況下出現(xiàn),但如果你足夠幸運(yùn)的話,也可能一直不會(huì)出現(xiàn)。
m=$(shuf -e a b c d e f g X -n 1) # 將 X 添加到隨機(jī)列表中,當(dāng) m=X,游戲結(jié)束
if [[ "$m" != "X" ]]; then # X 將會(huì)是我們爆炸地雷(游戲結(jié)束)的觸發(fā)標(biāo)志
for limit in ${!m}; do # !m 代表 m 變量的值
field=$(shuf -i 0-5 -n 1) # 然后再次獲得一個(gè)隨機(jī)數(shù)字
index=$((i+limit)) # 將 m 中的每一個(gè)值和 index 加起來(lái),直到列表結(jié)尾
is_free_field $index $field
done
我想要游戲界面中,所有隨機(jī)顯示出來(lái)的單元格,都靠近玩家選擇的單元格。
Extracting mines
記錄已選擇和可用單元格的個(gè)數(shù)
這個(gè)程序需要記錄游戲界面中哪些單元格是可選擇的。否則,程序會(huì)一直讓用戶(hù)輸入數(shù)據(jù),即使所有單元格都被選中過(guò)。為了實(shí)現(xiàn)這一功能,我創(chuàng)建了一個(gè)叫 free_fields 的變量,初始值為 0。用一個(gè) for 循環(huán),記錄下游戲界面中可選擇單元格的數(shù)量。 如果單元格所對(duì)應(yīng)的值為點(diǎn)(.),則 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 呢? 這意味著,玩家已選擇過(guò)所有單元格。如果想更好理解這一部分,可以看看這里的源代碼。
if [[ $free_fields -eq 0 ]]; then # 這意味著你已選擇過(guò)所有格子
printf '\n\n\t%s: %s %d\n\n' "You Win" "you scored" "$score"
exit 0
fi
創(chuàng)建游戲結(jié)束邏輯
對(duì)于游戲結(jié)束這種情況,我們這里使用了一些很巧妙的技巧,將結(jié)果在屏幕中央顯示出來(lái)。我把這部分留給讀者朋友們自己去探索。
if [[ "$m" = "X" ]]; then
g=0 # 為了在參數(shù)擴(kuò)展中使用它
room[$i]=X # 覆蓋此位置原有的值,并將其賦值為X
for j in {42..49}; do # 在游戲界面中央,
out="gameover"
k=${out:$g:1} # 在每一格中顯示一個(gè)字母
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 存儲(chǔ)庫(kù),那兒有這個(gè)掃雷游戲的源代碼,并且你還能找到更多用 Bash 編寫(xiě)的游戲。 我希望,這篇文章能激起你學(xué)習(xí) Bash 的興趣,并樂(lè)在其中。
新聞名稱(chēng):使用Bash編寫(xiě)掃雷游戲
網(wǎng)頁(yè)URL:http://www.dlmjj.cn/article/cdehoip.html


咨詢(xún)
建站咨詢(xún)
