-- 簡書作者 謝恩銘 轉載請註明出處
第二部分第九課: 實戰"懸掛小人"遊戲
第二部分的理論知識基本講完了,上一課我們經歷了很有意思的C語言探索之旅 | 第二部分第八課:動態分配。
這一課我們來實戰一下,要實現的遊戲叫“懸掛小人”。
這個“小人”,不是“君子和小人”的小人。是little man(小小的人)的意思。
小編你有必要這麼強調嗎?... 簡直無聊嘛。
好了,話休絮煩...
俗語說得好:“實踐是必要的!”
對於大家來說這又尤爲重要,因爲我們剛剛結束了一輪C語言的高級技術的“猛烈進攻”,需要好好複習一下,消化消化。
不論你多厲害,在編程領域,不實踐是永遠不行的。儘管你可能讀懂了之前的所有課程,但是如果不配合一定的實踐,是不能深刻理解的。
以前我大學裏入門編程以前看C語言的書,覺得看懂了,但是一上手要寫程序,就像擠牙膏一樣費勁。
這次的實戰練習,我們一起來實現一個小遊戲:“懸掛小人”,或叫 “上吊遊戲”。英語叫 HangMan,是挺著名的一個休閒益智遊戲。
雖說是遊戲,但是比較可惜的是還不能有圖形界面 (不過課程後面會說怎麼實現在控制檯繪製小人,其實也可以實現簡陋的“圖形化”): 因爲C語言本身不具備繪製UI的能力,需要引入第三方的庫。
而我們真正開始圖形編程,要到第三部分:【用基於C語言的SDL庫開發2D遊戲】
不過我們第二部分的課程已經接近尾聲了,不要急,馬上我們就可以開始用C語言來做真正的圖形界面遊戲了。
懸掛小人遊戲是一個經典的字母遊戲,在規定步數內一個字母一個字母地猜單詞,直到猜出整個單詞。
所以我們的遊戲暫時還是以控制檯的形式(黑框框)與大家見面,當然如果你會圖形編程,也可以把這個遊戲擴展成圖形界面的。
相信不少讀者應該見過這個遊戲的圖形界面版本,就是每猜錯一個字母畫一筆,直到用完規定次數,小人被“吊死”,就真的變成“屌絲”了。
這個實戰的目的是讓我們可以複習之前學過的所有C語言知識:指針,字符串,文件讀寫,結構體,數組,… 等,都是好傢伙!
題目規定
既然是出題目的實戰,那麼就需要委屈大家按照我們的題目要求來編寫這個遊戲啦。
好,就來公佈我們的題目要求:
-
遊戲每一輪有7次(次數可以設置,不一定要7次)猜測的機會,用完則此輪失敗。
-
每輪會從字典中隨機抽取一個單詞供玩家猜,初始時單詞是以若干個星號(*)的方式來表示。說明所有字母都還隱藏着。
-
字典的所有單詞儲存在一個文本文件中(在Windows下通常是txt文件,在unix/linux下一般可以是任意後綴名的文件)。
-
每猜錯一個字母就扣掉一次機會,猜對一個字母不扣除機會數,猜對的字母會顯示在屏幕上的單詞中,替換掉星號。
一個回合的運作機制
假設要猜的單詞是OSCAR
假設我們給程序輸入一個字母B(猜的第一個字母),程序會驗證字母是否在這個單詞裏。
有兩種情況:
-
所猜的字母在單詞中,此時程序會顯示這個單詞,不是全部顯示,而是顯示猜到的那些字母,其他的還未猜到的字母用*表示。
-
所猜的字母不在單詞中(目前的情況,因爲字母B不在單詞OSCAR中),此時程序會告訴玩家“你猜錯了”,剩餘的機會數會被扣除一個。如果剩餘機會數變爲0,遊戲結束。
在圖形化的“懸掛小人”(Hangman)遊戲中,每猜一次會有一個小人被畫出來。我們的遊戲,雖然還不能真正實現圖形化,但是如果優化一下,也可以在控制檯實現類似這樣的效果:
假設玩家輸入一個C,因爲C在單詞OSCAR中,那麼程序不會扣除玩家的剩餘機會數,而且會顯示已猜到的字母,如下:
單詞:**C**
如果玩家繼續輸入,這回輸入的是O,那麼程序會顯示如下:
單詞:O*C**
多個相同字母的情況
有一些單詞中,同一個字母會出現多次。比如在APPLE(蘋果)中,P這個字母就出現了2次;在ELEGANCE(優雅)中,E這個字母出現了3次。
“Hangman”(懸掛小人)遊戲對此的規則很簡單:
只要猜出一個字母,其他重複的字母會同時顯示。
假如要猜的單詞是ELEGANCE,用戶輸入了一個E,那麼會如下顯示:
單詞:E*E****E
一個回合的例子
歡迎來到懸掛小人遊戲!
您還剩7次機會
神祕單詞是什麼呢?*****
輸入一個字母:E
您還剩6次機會
神祕單詞是什麼呢?*****
輸入一個字母:S
您還剩6次機會
神祕單詞是什麼呢?*S***
輸入一個字母:R
您還剩6次機會
神祕單詞是什麼呢?*S**R
輸入一個字母:
遊戲就會這樣進行下去,直到玩家在7個機會用完前猜到單詞,或者用完7個機會還沒猜到單詞,遊戲結束。
例如:
您還剩2次機會
神祕單詞是什麼呢?OS*AR
輸入一個字母:C
勝利了!神祕單詞是:OSCAR
在控制檯輸入一個字母
在控制檯中讓程序讀入一個字母,看起來簡單,但其實暗藏玄機。
不信我們來試一下:
要輸入一個字母,一般大家會認爲是這樣做:
scanf("%c", &myLetter);
確實是不錯的,因爲 %c 標明瞭等待用戶輸入一個字符。輸入的字符會儲存在myLetter這個變量(類型是char)中。
如果我們只寫一個scanf,那是沒問題的,但是假如有好幾個scanf,會怎麼樣呢?我們來測試一下:
int main(int argc, char* argv[])
{
char myLetter = 0;
scanf("%c", &myLetter);
printf("%c", myLetter);
scanf("%c", &myLetter);
printf("%c", myLetter);
return 0;
}
照我們的設想,上述程序應該會請求用戶輸入一個字符,再打印出來: 進行兩次。
測試一下,實際情況是怎麼樣的呢?你輸入了一個字符,沒錯,然後呢...
程序爲你打印出來了你輸入的那個字符,假如你輸入的是a,那麼程序輸出
a
然後程序就退出了,沒有下文了,爲什麼不提示我輸入第二個字符了呢?就好像它忽略了第二個scanf一樣。
到底發生了什麼呢?
事實上,當你在控制檯(console)裏面輸入時,你輸入的內容都被記錄到內存的某處,當然也包括按下Enter鍵(回車鍵)時產生的輸入:
\n
因此,你先輸入了一個字符(例如a),然後你按了一下回車鍵:
字符a就被第一個scanf取走了,第二個scanf則把你的回車鍵(\n)取走了。
爲了避免這個問題,我們寫一個函數readCharacter()來處理:
char readCharacter()
{
char character = 0;
character = getchar(); // 讀取輸入的第一個字母
character = toupper(character); // 把這個字母轉成大寫
// 讀取其他的字符,直到 \n (爲了忽略它們)
while(getchar() != '\n')
;
return character; // 返回讀到的第一個字母
}
可以看到,以上程序中,我們使用了getchar函數,這個函數是在標準庫的stdio.h中,用於讀取一個用戶輸入的字符,效果相當於
scanf("%c", &letter);
然後,我們又用到了一個在本課程中還沒學習過的函數: toupper。
根據字面意思to+upper是英語“轉換爲大寫”的意思,所以這個函數就是用於把一個字母轉成大寫字母。
看到了吧,如果函數名起得好,幾乎就不需要註釋,看名字就知道大致是幹什麼的(論編程命名的重要性)。
藉着toupper這個函數,玩家就可以輸入小寫字母或者大寫字母了,因爲在“懸掛小人”遊戲中,我們顯示的單詞中的字母都是大寫的。
toupper這個函數定義在ctype.h這個標準庫的頭文件中,所以需要
#include <ctype.h>
繼續看我們的函數,可以看到其中最關鍵的地方是:
while(getchar() != '\n')
;
這一小段代碼使得我們可以清除第一個輸入的字母外的其他字符,直到遇見 \n (回車符)。
函數返回的就是第一個輸入的字母,這樣可以保證不再受回車符的影響了。
我們用了一個while循環,而循環體部分只有一個分號(;),很簡潔吧。
也許你會問,之前的課程中while循環的循環體不是由大括號圍起來的麼,怎麼這裏只有一個分號呢?
事實上,這個分號就相當於
{
}
就是空循環體,什麼都不做,所以其實以上的代碼相當於:
while(getchar() != '\n')
{
}
但是分號比大括號寫起來更簡單麼,不要忘了程序員是懂得如何偷懶的一羣人!
此while循環一直執行,直到用戶輸入回車符,其他的字符都被從內存中清除了,我們稱其爲 “清空緩衝區”。
因此:
爲了在我們的程序中每次讀取用戶輸入的一個字母,我們不要使用
scanf("%c", &myLetter);
而須要藉助我們寫的函數:
myLetter = readCharacter();
於是,我們的測試程序變成這樣:
#include <stdio.h>
#include <ctype.h>
char readCharacter()
{
char character = 0;
character = getchar(); // 讀取一個字母
character = toupper(character); // 把這個字母轉成大寫
// 讀取其他的字符,直到 \n (爲了忽略它)
while(getchar() != '\n')
;
return character; // 返回讀到的第一個字母
}
int main(int argc, char* argv[])
{
char myLetter = 0;
myLetter = readCharacter();
printf("%c\n", myLetter);
myLetter = readCharacter();
printf("%c\n", myLetter);
return 0;
}
運行,輸出類似如下(假如用戶輸入o,回車;輸入k,回車)
o
O
k
K
字典 / 詞庫
因爲我們的遊戲是一步步寫成的,所以一開始,肯定先寫簡單的,再逐步完善遊戲
因此,猜測的單詞一開始我們只用一個,
所以,我們一開始會這麼寫:
char secretWord[] = "BOTTLE";
你會說:“這樣不是很無聊嘛,猜測的單詞總是這一個”
是的,但之後我們肯定會擴展,一開始這樣做是爲了不把問題複雜化,一次做一件事情,慢慢來麼
之後如果猜測一個單詞的代碼可以運行了,我們再用一個文件來儲存所有可能的單詞,這個文件可以起名爲dictionary(英語 “字典”的意思)
那什麼是字典或詞庫呢?
在我們的遊戲裏,就是一個文件,文件中的每一行存放了一個單詞,之後我們的程序會隨機從此文件中抽取一個單詞來作爲每一輪的猜測單詞
詞庫是類似這樣的:
YOU
MOTHER
LOVE
PANDA
BOTTLE
FUNNY
HONEY
LIKE
JAZZ
MUSIC
BREAD
APPLE
WATER
PEOPLE
對於這個文件裏有多少單詞,因爲我們的詞庫是可擴展的(之後肯定可以添加新的單詞),所以其實只要統計回車符('\n')的數目就可以,因爲是每行一個單詞
好了,遊戲的基本點我們介紹到這裏,其實有了前面所有課程的基礎,你已經有能力來完成這個看似有點複雜的遊戲了,不過要組織得好還是不那麼容易的,你可以用多個函數來實現不同的功能
加油,堅持不懈就是勝利,期待你的成果!
優化建議
因爲我們的項目是在Linux下用gcc來編譯的,如果你是在Windows下用CodeBlocks等IDE來編譯的,那麼請將字典文件dictionary改成dictionary.txt
因爲Windows的文件儲存形式和Linux(或Unix)有些不一樣。
改進遊戲
-
目前來說,我們只讓玩家玩一輪,如果能加一個循環,使得遊戲每次詢問玩家是否要再玩一次,那“真真是極好的”
-
目前還是單機模式,可以創建一個二人模式,就是一個玩家輸入一個單詞,第二個玩家來猜。
-
爲什麼不用printf函數來打印(繪製)一個懸掛小人呢?在每次我們猜錯的時候,就把它畫出來,每錯一個,多畫一筆,這樣可以增加樂趣,可以用如下的代碼:
if (猜錯1個字母)
{
printf(" _____\n");
printf(" | |\n");
printf(" | O\n");
printf(" |\n");
printf(" |\n");
printf(" |\n");
printf(" |\n");
printf("__|__\n");
}
else if (猜錯2個字母)
{
printf(" _____\n");
printf(" | |\n");
printf(" | O\n");
printf(" | |\n");
printf(" |\n");
printf(" |\n");
printf(" |\n");
printf("__|__\n");
}
else if (猜錯3個字母)
{
printf(" _____\n");
printf(" | |\n");
printf(" | O\n");
printf(" | \\|\n");
printf(" |\n");
printf(" |\n");
printf(" |\n");
printf("__|__\n");
}
else if (猜錯4個字母)
{
printf(" _____\n");
printf(" | |\n");
printf(" | O\n");
printf(" | \\|/\n");
printf(" |\n");
printf(" |\n");
printf(" |\n");
printf("__|__\n");
}
else if (猜錯5個字母)
{
printf(" _____\n");
printf(" | |\n");
printf(" | O\n");
printf(" | \\|/\n");
printf(" | |\n");
printf(" |\n");
printf(" |\n");
printf("__|__\n");
}
else if (猜錯6個字母)
{
printf(" _____\n");
printf(" | |\n");
printf(" | O\n");
printf(" | \\|/\n");
printf(" | |\n");
printf(" | /\n");
printf(" |\n");
printf("__|__\n");
}
else if (猜錯7個字母)
{
printf(" _____\n");
printf(" | |\n");
printf(" | O\n");
printf(" | \\|/\n");
printf(" | |\n");
printf(" | / \\\n");
printf(" |\n");
printf("__|__\n");
}
上面代碼中的空格也許不同平臺的顯示不一樣,我這裏是5個空格。可能需要大家自行調整。
如果7次機會全部用完,則小人掛掉,遊戲結束。
請大家花點時間,好好理解這個遊戲,並且儘可能地改進它。如果你可以不看我們的答案,而自己完成遊戲和改進,那麼你會收穫很多的!
第二部分第十課預告:
今天的課就到這裏,一起加油吧。
下一次我們就會公佈懸掛小人遊戲的解題思路和答案咯。