C語言探索之旅 | 第二部分第九課: 實戰"懸掛小人"遊戲

做者 謝恩銘,公衆號「程序員聯盟」(微信號:coderhub)。
轉載請註明出處。
原文: https://www.jianshu.com/p/6cb...

《C語言探索之旅》全系列程序員

內容簡介


  1. 前言
  2. 題目規定
  3. 優化建議
  4. 第二部分第十課預告

1. 前言


第二部分的理論知識基本講完了。上一課咱們經歷了頗有意思的 C語言探索之旅 | 第二部分第八課:動態分配編程

這一課咱們來實戰一下,要實現的遊戲叫「懸掛小人」。數組

這個「小人」,不是「君子和小人」的小人。是 little man(小小的人)的意思。

讀者:「你有必要這麼強調嗎?簡直無聊嘛...」微信

好的,話休絮煩...函數

俗語說得好:「實踐是必要的!」學習

對於你們來講這又尤其重要,由於咱們剛剛結束了一輪 C語言的高級技術的「猛烈進攻」,須要好好複習一下,消化消化。測試

不論你多厲害,在編程領域,不實踐是永遠不行的。儘管你可能讀懂了以前的全部課程,可是若是不配合必定的實踐,是不能深入理解的。優化

之前我大學裏入門編程之前看 C語言的書,以爲看懂了,可是一上手要寫程序,就像擠牙膏同樣費勁。spa

此次的實戰練習,咱們一塊兒來實現一個小遊戲:「懸掛小人」,或叫 「上吊遊戲」。英語叫 HangMan,是挺著名的一個休閒益智遊戲。3d

雖然說是遊戲,可是比較惋惜的是還不能有圖形界面 (不過課程後面會說怎麼實如今控制檯繪製小人,其實也能夠實現簡陋的「圖形化」): 由於 C語言自己不具有繪製 GUI(Graphical User Interface 的縮寫,表示「圖形用戶接口」)的能力,須要引入第三方的庫。

懸掛小人遊戲是一個經典的字母遊戲,在規定步數內一個字母一個字母地猜單詞,直到猜出整個單詞。

因此咱們的遊戲暫時仍是以控制檯的形式(黑框框)與你們見面,固然若是你會圖形編程,也能夠把這個遊戲擴展成圖形界面的。

相信很多讀者應該見過這個遊戲的圖形界面版本,就是每猜錯一個字母畫一筆,直到用完規定次數,小人被「吊死」。

這個實戰的目的是讓咱們能夠複習以前學過的全部 C語言知識:指針,字符串,文件讀寫,結構體,數組,等等,都是好傢伙!

2. 題目規定


既然是出題目的實戰,那麼就須要委屈你們按照個人題目要求來編寫這個遊戲啦。

好,就來公佈咱們的題目要求:

  • 遊戲每一輪有 7 次(次數能夠設置,不必定是 7 次)猜想的機會,用完則此輪失敗。
  • 每輪會從字典中隨機抽取一個單詞供玩家猜,初始時單詞是以若干個星號(*)的方式來表示。說明全部字母都還隱藏着。
  • 字典的全部單詞儲存在一個文本文件中(在 Windows 下一般是 txt 文件,在 Unix/Linux/macOS 下通常能夠是任意後綴名的文件)。
  • 每猜錯一個字母就扣掉一次機會,猜對一個字母不扣除機會數。猜對的字母會顯示在屏幕上的單詞中,替換掉星號。

一個回合的運做機制


假設要猜的單詞是 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)的數目就能夠,由於是每行一個單詞。

好了,遊戲的基本點咱們介紹到這裏,其實有了前面全部課程的基礎,你已經有能力來完成這個看似有點複雜的遊戲了,不過要組織得好仍是不那麼容易的,你能夠用多個函數來實現不一樣的功能。

加油,堅持不懈就是勝利,期待你的成果!

3. 優化建議


若是你是在 Windows 下用 CodeBlocks 等 IDE 來編譯的,那麼請將字典文件 dictionary 改爲 dictionary.txt。
由於 Windows 的文件儲存形式和 Linux/Unix/macOS 有些不同。

改進遊戲


  1. 目前來講,咱們只讓玩家玩一輪,若是能加一個循環,使得遊戲每次詢問玩家是否要再玩一次,那「真真是極好的」。
  2. 目前仍是單機模式,能夠建立一個二人模式,就是一個玩家輸入一個單詞,第二個玩家來猜。
  3. 爲何不用 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");
}

上面代碼中的空格也許不一樣平臺的顯示不同,可能須要你們自行調整。

若是 7 次機會所有用完,則小人掛掉,遊戲結束。

請你們花點時間,好好理解這個遊戲,而且儘量地改進它。若是你能夠不看咱們的答案,而本身完成遊戲和改進,那麼你會收穫不少的!

4. 第二部分第十課預告


今天的課就到這裏,一塊兒加油吧!

下一課咱們就會公佈懸掛小人遊戲的解題思路和答案咯。

下一課:C語言探索之旅 | 第二部分第十課: 實戰"懸掛小人"遊戲 答案


我是 謝恩銘,公衆號「程序員聯盟」(微信號:coderhub)運營者,慕課網精英講師 Oscar 老師,終生學習者。 熱愛生活,喜歡游泳,略懂烹飪。 人生格言:「向着標杆直跑」
相關文章
相關標籤/搜索