我用Bash編寫了一個掃雷游戲

我在編程教學(xué)方面不是專家井氢,但當(dāng)我想更好掌握某一樣?xùn)|西時意系,會試著找出讓自己樂在其中的方法腋逆。比方說荣回,當(dāng)我想在?shell?編程方面更進(jìn)一步時吃环,我決定用 Bash 編寫一個掃雷游戲來加以練習(xí)氯窍。

我在編程教學(xué)方面不是專家漏益,但當(dāng)我想更好掌握某一樣?xùn)|西時墩邀,會試著找出讓自己樂在其中的方法穆桂。比方說宫盔,當(dāng)我想在 shell 編程方面更進(jìn)一步時,我決定用 Bash 編寫一個掃雷游戲來加以練習(xí)享完。

如果你是一個有經(jīng)驗(yàn)的 Bash 程序員灼芭,希望在提高技巧的同時樂在其中,那么請跟著我編寫一個你的運(yùn)行在終端中的掃雷游戲般又。完整代碼可以在這個 GitHub 存儲庫中找到彼绷。

做好準(zhǔn)備

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

顯示雷區(qū)

創(chuàng)建游戲邏輯

創(chuàng)建判斷單元格是否可選的邏輯

記錄可用和已查明(已排雷)單元格的個數(shù)

創(chuàng)建游戲結(jié)束邏輯

顯示雷區(qū)

在掃雷中,游戲界面是一個由 2D 數(shù)組(列和行)組成的不透明小方格寄悯。每一格下都有可能藏有地雷萤衰。玩家的任務(wù)就是找到那些不含雷的方格,并且在這一過程中猜旬,不能點(diǎn)到地雷脆栋。這個 Bash 版本的掃雷使用 10x10 的矩陣,實(shí)際邏輯則由一個簡單的 Bash 數(shù)組來完成洒擦。

首先椿争,我先生成了一些隨機(jī)數(shù)字。這將是地雷在雷區(qū)里的位置秘遏∏鹧Γ控制地雷的數(shù)量,在開始編寫代碼之前邦危,這么做會容易一些。實(shí)現(xiàn)這一功能的邏輯可以更好舍扰,但我這么做倦蚪,是為了讓游戲?qū)崿F(xiàn)保持簡潔,并有改進(jìn)空間边苹。(我編寫這個游戲純屬娛樂陵且,但如果你能將它修改的更好,我也是很樂意的个束。)

下面這些變量在整個過程中是不變的慕购,聲明它們是為了隨機(jī)生成數(shù)字。就像下面的a-g?的變量茬底,它們會被用來計(jì)算可排除的地雷的值:

# 變量

score=0 # 會用來存放游戲分?jǐn)?shù)

# 下面這些變量沪悲,用來隨機(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 # 聲明一個 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ù),我們先將標(biāo)題顯示出來:兩個空行糠悯、列頭帮坚,和一行 -妻往,以示意往下是游戲界面:

printf '\n\n'

printf '%s' " ? ? a ? b ? c ? d ? e ? f ? g ? h ? i ? j"

printf '\n ? %s\n' "-----------------------------------------"

然后,我初始化一個計(jì)數(shù)器變量叶沛,叫?r蒲讯,它會用來記錄已顯示多少橫行。注意灰署,稍后在游戲代碼中判帮,我們會用同一個變量?r,作為我們的數(shù)組索引溉箕。 在?Bash for 循環(huán)中晦墙,用?seq命令從 0 增加到 9。我用數(shù)字(d%)占位肴茄,來顯示行號($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]?的行號顯示出來抗楔,我們會用這兩個范圍,來確定用戶排雷的確切位置拦坠。

接著连躏,在每行中,插入列贞滨,所以是時候?qū)懸粋€新的 for 循環(huán)了入热。這一循環(huán)管理著每一列,也就是說晓铆,實(shí)際上是生成游戲界面的每一格勺良。我添加了一些輔助函數(shù),你能在源碼中看到它的完整實(shí)現(xiàn)骄噪。 對每一格來說尚困,我們需要一些讓它看起來像地雷的東西,所以我們先用一個點(diǎn)(.)來初始化空格腰池。為了實(shí)現(xiàn)這一想法尾组,我們用的是一個叫 is_null_field 的自定義函數(shù)。 同時示弓,我們需要一個存儲每一格具體值的數(shù)組讳侨,這兒會用到之前已定義的全局?jǐn)?shù)組 room , 并用 變量 r作為索引。隨著 r 的增加奏属,遍歷所有單元格跨跨,并隨機(jī)部署地雷。

? for col in $(seq 0 9); do

((r+=1)) # 循環(huán)完一列行數(shù)加一

is_null_field $r # 假設(shè)這里有個函數(shù),它會檢查單元格是否為空勇婴,為真忱嘹,則此單元格初始值為點(diǎn)(.)

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'

}

我花了點(diǎn)時間來思考添诉,is_null_field 的具體功能是什么屁桑。讓我們來看看,它到底能做些什么栏赴。在最開始蘑斧,我們需要游戲有一個固定的狀態(tài)。你可以隨便選擇個初始值须眷,可以是一個數(shù)字或者任意字符竖瘾。我最后決定,所有單元格的初始值為一個點(diǎn)(.)花颗,因?yàn)槲矣X得准浴,這樣會讓游戲界面更好看。下面就是這一函數(shù)的完整代碼:

is_null_field()

{

local e=$1 # 在數(shù)組 room 中捎稚,我們已經(jīng)用過循環(huán)變量 'r' 了,這次我們用 'e'

if [[ -z "${room[$e]}" ]];then

room[$r]="." #這里用點(diǎn)(.)來初始化每一個單元格

fi

}

現(xiàn)在求橄,我已經(jīng)初始化了所有的格子今野,現(xià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

}

這是顯示出來的游戲界面,[a-j]?為列涵亏,[0-9]?為行宰睡。

Minefield

創(chuàng)建玩家邏輯

玩家操作背后的邏輯在于,先從 stdin 中讀取數(shù)據(jù)作為坐標(biāo)气筋,然后再找出對應(yīng)位置實(shí)際包含的值拆内。這里用到了 Bash 的參數(shù)擴(kuò)展,來設(shè)法得到行列數(shù)宠默。然后將代表列數(shù)的字母傳給分支語句麸恍,從而得到其對應(yīng)的列數(shù)。為了更好地理解這一過程,可以看看下面這段代碼中抹沪,變量 o 所對應(yīng)的值刻肄。 舉個例子,玩家輸入了 c3融欧,這時 Bash 將其分成兩個字符:c 和 3敏弃。為了簡單起見,我跳過了如何處理無效輸入的部分噪馏。

colm=${opt:0:1} # 得到第一個字符麦到,一個字母

ro=${opt:1:1} # 得到第二個字符,一個整數(shù)

case $colm in

a ) o=1;; # 最后逝薪,通過字母得到對應(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

下面的代碼會計(jì)算用戶所選單元格實(shí)際對應(yīng)的數(shù)字,然后將結(jié)果儲存在變量中董济。

這里也用到了很多的?shuf命令步清,shuf?是一個專門用來生成隨機(jī)序列的Linux?命令。-i?選項(xiàng)后面需要提供需要打亂的數(shù)或者范圍虏肾,-n?選項(xiàng)則規(guī)定輸出結(jié)果最多需要返回幾個值廓啊。Bash 中,可以在兩個圓括號內(nèi)進(jìn)行數(shù)學(xué)計(jì)算封豪,這里我們會多次用到谴轮。

還是沿用之前的例子,玩家輸入了 c3吹埠。 接著第步,它被轉(zhuǎn)化成了 ro=3 和 o=3。 之后缘琅,通過上面的分支語句代碼粘都, 將 c 轉(zhuǎn)化為對應(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ì)觀察這個計(jì)算過程,看看最終結(jié)果 i 是如何計(jì)算出來的:

i=$(((ro*10)+o))

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

最后結(jié)果是 33雷酪。在我們的游戲界面顯示出來淑仆,玩家輸入坐標(biāo)指向了第 33 個單元格,也就是在第 3 行(從 0 開始太闺,否則這里變成 4)糯景,第 3 列。

創(chuàng)建判斷單元格是否可選的邏輯

為了找到地雷,在將坐標(biāo)轉(zhuǎn)化蟀淮,并找到實(shí)際位置之后最住,程序會檢查這一單元格是否可選。如不可選怠惶,程序會顯示一條警告信息涨缚,并要求玩家重新輸入坐標(biāo)。

在這段代碼中策治,單元格是否可選脓魏,是由數(shù)組里對應(yīng)的值是否為點(diǎn)(.)決定的。如果可選通惫,則重置單元格對應(yīng)的值茂翔,并更新分?jǐn)?shù)。反之履腋,因?yàn)槠鋵?yīng)值不為點(diǎn)珊燎,則設(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

}

如輸入坐標(biāo)有效延旧,且對應(yīng)位置為地雷谋国,如下圖所示。玩家輸入?h6迁沫,游戲界面會出現(xiàn)一些隨機(jī)生成的值芦瘾。在發(fā)現(xiàn)地雷后,這些值會被加入用戶得分集畅。

Extracting mines

還記得我們開頭定義的變量旅急,a?-?g?嗎,我會用它們來確定隨機(jī)生成地雷的具體值牡整。所以,根據(jù)玩家輸入坐標(biāo)溺拱,程序會根據(jù)(m)中隨機(jī)生成的數(shù)逃贝,來生成周圍其他單元格的值(如上圖所示)。之后將所有值和初始輸入坐標(biāo)相加迫摔,最后結(jié)果放在?i(計(jì)算結(jié)果如上)中沐扳。

請注意下面代碼中的 X,它是我們唯一的游戲結(jié)束標(biāo)志句占。我們將它添加到隨機(jī)列表中沪摄。在 shuf 命令的魔力下,X 可以在任意情況下出現(xiàn),但如果你足夠幸運(yùn)的話杨拐,也可能一直不會出現(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 將會是我們爆炸地雷(游戲結(jié)束)的觸發(fā)標(biāo)志

for limit in ${!m}; do # !m 代表 m 變量的值

field=$(shuf -i 0-5 -n 1) # 然后再次獲得一個隨機(jī)數(shù)字

index=$((i+limit)) # 將 m 中的每一個值和 index 加起來帆阳,直到列表結(jié)尾

is_free_field $index $field

? ? done

我想要游戲界面中,所有隨機(jī)顯示出來的單元格屋吨,都靠近玩家選擇的單元格蜒谤。

Extracting mines

記錄已選擇和可用單元格的個數(shù)

這個程序需要記錄游戲界面中哪些單元格是可選擇的。否則至扰,程序會一直讓用戶輸入數(shù)據(jù)鳍徽,即使所有單元格都被選中過。為了實(shí)現(xiàn)這一功能敢课,我創(chuàng)建了一個叫?free_fields?的變量阶祭,初始值為?0。用一個?for?循環(huán)翎猛,記錄下游戲界面中可選擇單元格的數(shù)量胖翰。 如果單元格所對應(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 呢? 這意味著疫稿,玩家已選擇過所有單元格培他。如果想更好理解這一部分,可以看看這里的源代碼遗座。

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ù)擴(kuò)展中使用它

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 編寫的游戲号胚。 我希望,這篇文章能激起你學(xué)習(xí) Bash 的興趣浸遗,并樂在其中猫胁。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市乙帮,隨后出現(xiàn)的幾起案子杜漠,更是在濱河造成了極大的恐慌,老刑警劉巖察净,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件驾茴,死亡現(xiàn)場離奇詭異,居然都是意外死亡氢卡,警方通過查閱死者的電腦和手機(jī)锈至,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來译秦,“玉大人峡捡,你說我怎么就攤上這事≈玻” “怎么了们拙?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長阁吝。 經(jīng)常有香客問我砚婆,道長,這世上最難降的妖魔是什么突勇? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任装盯,我火速辦了婚禮,結(jié)果婚禮上甲馋,老公的妹妹穿的比我還像新娘埂奈。我一直安慰自己,他們只是感情好定躏,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布账磺。 她就那樣靜靜地躺著,像睡著了一般痊远。 火紅的嫁衣襯著肌膚如雪绑谣。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天拗引,我揣著相機(jī)與錄音,去河邊找鬼幌衣。 笑死矾削,一個胖子當(dāng)著我的面吹牛壤玫,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播哼凯,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼欲间,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了断部?” 一聲冷哼從身側(cè)響起猎贴,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蝴光,沒想到半個月后她渴,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蔑祟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年趁耗,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片疆虚。...
    茶點(diǎn)故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡苛败,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出径簿,到底是詐尸還是另有隱情罢屈,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布篇亭,位于F島的核電站缠捌,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏暗赶。R本人自食惡果不足惜鄙币,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蹂随。 院中可真熱鬧十嘿,春花似錦、人聲如沸岳锁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽激率。三九已至咳燕,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間乒躺,已是汗流浹背招盲。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留嘉冒,地道東北人曹货。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓咆繁,卻偏偏與公主長得像,于是被迫代替她去往敵國和親顶籽。 傳聞我的和親對象是個殘疾皇子玩般,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內(nèi)容