作者 謝恩銘,公眾號「程序員聯盟」(微信號:coderhub)。
轉載請注明出處。
原文:http://www.lxweimin.com/p/b239b1774f4b
《C語言探索之旅》全系列
內容簡介
- 前言
- 解方(1. 游戲的代碼)
- 解方(2. 詞庫的代碼)
- 第二部分第十一課預告
1. 前言
經過上一課 C語言探索之旅 | 第二部分第九課: 實戰"懸掛小人"游戲 之后,相信大家都或多或少都寫了自己的“懸掛小人”的游戲代碼吧。
這一課我們就來"終結"這個游戲吧 (聽著怎么有點嚇人...)。
"Yes, you are terminated."
2. 解方(1. 游戲的代碼)
如果你開始閱讀這里,說明:
- 或者你寫完了游戲,想來看看我們怎么寫。
- 或者你沒完成這個游戲,想來看看怎么寫。
不管你是哪種情況,我都會介紹一下如何來完成這個游戲。
“說不說在我,聽不聽在您”~
事實上,我自己花了比想象中更多的時間來完成這游戲。
人生總是這樣的,“理想豐滿,現實骨感;看似美滿,人艱不拆”。
但是,我還是堅信大家是有能力獨自完成這個小游戲的(如果你認真學習了之前的 C語言課程),可以去查閱網上資料,花點時間(幾十分鐘,幾小時,幾天?),這并不是一次競賽,所以不用著急。
我更希望您花了不少時間,最終實現了這個游戲; 比之您只花 5 分鐘,然后就來看答案要好很多。
千萬不要覺得我是一蹴而就寫成這個游戲的,這個游戲雖小,但也還沒簡單到可以在腦中構思好一切,然后“下筆如有神”: 我也是一步步寫出來的。
我們將會分 2 步來介紹我們的解方:
首先我們會演示如何一步步寫游戲的主體部分,一開始我們會只有一個猜測的單詞,而且是固定的;我選了 BOTTLE(表示“瓶子”),因為我們要測試對于單詞中有大于等于兩個相同字母的情況是否處理正確了(BOTTLE 中有 2 個 T)。
然后我們會演示如何加入詞庫的處理程序,以便每一輪游戲可以從詞庫中隨機抽取一個單詞。
牢記:重要的不是結果,而是我們思考的方式和過程。
分析 main 函數
大家都知道,我們的 C語言程序都是由 main 函數作為入口的。
我們也不要忘了引入一些標準庫的頭文件:stdio.h,stdlib.h,ctype.h(為了 toupper 函數)。
因此,我們的程序一開始會是這樣的:
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
int main(int argc, char* argv[])
{
return 0;
}
是不是很簡單啊,慢慢來么。
我們的 main 函數將控制游戲的大部分運作,并且調用我們將要寫的不少函數。
我們來聲明一些必要的變量吧。這些變量也不是一次就能全部想到的,都是寫一點,想到一些。“羅馬不是一日建成的, 小人也不是一日能懸掛完的”。
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
int main(int argc, char* argv[])
{
char letter = 0; // 存儲用戶輸入的字母
char secretWord[] = "BOTTLE"; // 要猜測的單詞
int letterFound[6] = {0}; // 布爾值的數組。數組的每一個元素對應猜測單詞的一個字母。0 = 還沒猜到此字母, 1 = 已猜到字母
int leftTimes = 7; // 剩余猜測次數(0 = 失敗)
int i = 0; // 為了遍歷數組,需要一個下標
return 0;
}
上述的變量中,起到關鍵作用的就是 letterFound 這個 int 型數組了。這個數組用于表示猜測的單詞中哪些字母已經猜到,哪些還沒猜到。
一開始,我們實現得簡單些:我們的單詞 BOTTLE 有 6 個字母,因此我們的數組就固定是 6 個元素的數組。
如果元素為 0,表示對應的那個字母還沒猜到;如果為 1,則表示已猜到。隨著游戲的進行,這個數組的元素值會被修改。
例如,如果當下我們玩游戲直到:
B*TT*E
那么,letterFound 這個數組的值應該是這樣:
101101
之后我們要測試游戲的一輪是否已經勝利也就比較簡單了:只需要測試 letterFound 數組的所有元素是否都等于 1。
我們就來寫判斷一輪是否勝利的函數吧,取名為 win(表示 “勝利”)好了。
int win(int letterFound[])
{
int i = 0;
int win = 1; // 1 為勝利,0 為失敗
for (i = 0 ; i < 6 ; i++)
{
if (letterFound[i] == 0)
win = 0;
}
return win;
}
可以看到,我們的 win 函數的參數是一個 int 型數組,我們在 main 函數中調用 win 函數時,會將我們的 letterFound 數組傳給它。
這個函數很簡單:遍歷數組,只要還有一個元素為 0,那游戲還沒勝利;如果所有元素都為 1,則游戲勝利。
為了與此函數搭配,我們還需要寫一個函數,起名叫 researchLetter,這個函數將有兩個功能:
返回一個布爾值(在 C語言里用 int 型表示),用于表示所猜的字母是否存在于單詞中。
更新 letterFound 數組的元素,如果所猜的字母在單詞中,那么就把對應的元素值修改為 1。
int researchLetter(char letter, char secretWord[], int letterFound[])
{
int i = 0;
int correctLetter = 0; // 0 表示字母不在單詞里,1 表示字母在單詞里
// 遍歷單詞數組 secretWord,以判斷所猜字母是否在單詞中
for (i = 0 ; secretWord[i] != '\0' ; i++)
{
if (letter == secretWord[i]) // 如果字母在單詞中
{
correctLetter = 1; // 表示猜對了一個字母
letterFound[i] = 1; // 對于所有等于所猜字母的數組位置,都使其數值變為 1
}
}
return correctLetter;
}
researchLetter 這個函數的好處還在于:不會在找到第一個存在的字母后就停止,而會繼續查找,所以對于像 BOTTLE 這樣有兩個字母相同的單詞就可以一次揭示兩個 T 了。
好,寫完這兩個函數(放在 main 函數后面),我們繼續寫我們的 main 函數。我們添加一句歡迎詞:
printf("歡迎來到懸掛小人游戲!\n");
然后添加一個主循環,是一個 while 循環:
while (leftTimes > 0 && !win(letterFound))
{
}
每輪游戲在 leftTimes(剩余猜測機會)大于 0 并且還沒勝利的情況下,是不會停止的。
- 如果剩余次數為 0,則本輪游戲失敗。
- 如果勝利,那本輪就贏了。
在這兩種情況下,都要停止游戲。
我們在 while 循環里添加如下代碼:
printf("\n\n您還剩 %d 次機會", leftTimes);
printf("\n神秘單詞是什么呢 ? ");
/* 我們顯示猜測的單詞,將還沒猜到的字母用*表示例如 : *O**LE */
for (i = 0 ; i < 6 ; i++)
{
if (letterFound[i]) // 如果第 i+1 個字母已經猜到
printf("%c", secretWord[i]); // 打印出來
else
printf("*"); // 還沒猜到,打印一個星號 *
}
上面的代碼用于:
- 打印剩余機會數。
- 打印單詞(其中還沒猜到的字母用星號
*
表示)。
接下來,我們寫請求用戶輸入一個字母的代碼:
printf("\n輸入一個字母 : ");
letter = readCharacter();
還記得我們之前寫的函數 readCharacter 嗎?它用于讀取用戶的第一個輸入的字母,讀到回車符結束,而且它會把該字母轉成大寫。
// 如果用戶輸入的字母不存在于單詞中
if (!researchLetter(letter, secretWord, letterFound))
{
leftTimes--; // 將剩余猜測機會數減 1
}
以上代碼調用 researchLetter 函數在單詞中查找用戶輸入的字母,如果沒找到,則剩余猜測機會數扣除一次。
如果字母存在于單詞中,則 researchLetter 函數還會更新 letterFound 數組(每個元素對應了神秘單詞的每一個字母的猜測情況),將其中對應的 0(還沒猜到)改為 1(已經猜到)。
這樣,win 函數在判斷的時候,如果 letterFound 數組的每一個元素都為 1,則返回 1,表示本輪勝利,猜到單詞的全部字母了。
暫時,while 循環體的內容就到這里了,然后我們還要寫跳出 while 循環之后的代碼(或者勝利或者失敗):
if (win(letterFound))
printf("\n\n勝利了! 神秘單詞是 : %s\n", secretWord);
else
printf("\n\n失敗了! 神秘單詞是 : %s\n", secretWord);
游戲主體部分的代碼就到這里了,給出我們到目前為止的完整程序:
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
int win(int letterFound[]);
int researchLetter(char letter, char secretWord[], int letterFound[]);
char readCharacter();
int main(int argc, char* argv[])
{
char letter = 0; // 存儲用戶輸入的字母
char secretWord[] = "BOTTLE"; // 要猜測的單詞
int letterFound[6] = {0}; // 布爾值的數組。數組的每一個元素對應猜測單詞的一個字母。0 = 還沒猜到此字母,1 = 已猜到字母
int leftTimes = 7; // 剩余猜測次數(0 = 失敗)
int i = 0; // 為了遍歷數組,需要一個下標
printf("歡迎來到懸掛小人游戲!\n");
while (leftTimes > 0 && !win(letterFound))
{
printf("\n\n您還剩 %d 次機會", leftTimes);
printf("\n神秘單詞是什么呢 ? ");
/* 我們顯示猜測的單詞,將還沒猜到的字母用 * 表示例如 : *O**LE */
for (i = 0 ; i < 6 ; i++)
{
if (letterFound[i]) // 如果第 i+1 個字母已經猜到
printf("%c", secretWord[i]); // 打印出來
else
printf("*"); // 還沒猜到,打印一個*
}
printf("\n輸入一個字母 : ");
letter = readCharacter();
// 如果用戶輸入的字母不存在于單詞中
if (!researchLetter(letter, secretWord, letterFound))
{
leftTimes--; // 將剩余猜測機會數減 1
}
}
if (win(letterFound))
printf("\n\n勝利了! 神秘單詞是 : %s\n", secretWord);
else
printf("\n\n失敗了! 神秘單詞是 : %s\n", secretWord);
return 0;
}
int win(int letterFound[])
{
int i = 0;
int win = 1; // 1 為勝利,0 為失敗
for (i = 0 ; i < 6 ; i++)
{
if (letterFound[i] == 0)
win = 0;
}
return win;
}
int researchLetter(char letter, char secretWord[], int letterFound[])
{
int i = 0;
int correctLetter = 0; // 0 表示字母不在單詞里,1 表示字母在單詞里
// 遍歷單詞數組 secretWord,以判斷所猜字母是否在單詞中
for (i = 0 ; secretWord[i] != '\0' ; i++)
{
if (letter == secretWord[i]) // 如果字母在單詞中
{
correctLetter = 1; // 表示猜對了一個字母
letterFound[i] = 1; // 對于所有等于所猜字母的數組位置,都將其數值變為1
}
}
return correctLetter;
}
char readCharacter()
{
char character = 0;
character = getchar(); // 讀取一個字母
character = toupper(character); // 把這個字母轉成大寫
// 讀取其他的字符,直到 \n(為了忽略它)
while (getchar() != '\n')
;
return character; // 返回讀到的第一個字母
}
這一部分的程序,你可以將其存放在一個 .c
文件中,例如叫 hangman.c。
然后用 gcc 編譯(如果是在 IDE 里面,例如 CodeBlocks,那直接點擊編譯運行):
gcc hangman.c -o hangman
運行:
./hangman
接下來我們要開始第二部分:詞庫的代碼。
根據這部分的代碼,我們還會接著修改和添加 main 函數的內容。
好吧,稍作休息,繼續前進!
3. 解方(2. 詞庫的代碼)
我們已經編寫了游戲主體部分的基本代碼,但是我們的游戲目前還不能做到每輪隨機抽取一個單詞。
因此,接下來我們就帶大家編寫處理詞庫的代碼。
首先,我們需要創建一個文件,用于存放所有的單詞。
在 Linux / Unix / macOS 操作系統下,我們都可以直接創建一個不帶后綴名的文件。在 Windows 下可以創建 .txt 結尾的文本文件。
我寫這個游戲是在 Linux 系統下,所以直接用 Vim 或 Emacs 或其他編輯器創建一個文件, 位于我們源文件的相同目錄下:dictionary。
在里面寫入以下單詞(每行一個,用回車符隔開):
YOU
MOTHER
LOVE
PANDA
BOTTLE
FUNNY
HONEY
LIKE
JAZZ
MUSIC
BREAD
APPLE
WATER
PEOPLE
DOG
CAT
GLASS
SKY
GOD
ZERO
當然了,我這里只是舉個例子,你可以創建屬于自己的詞庫。
新建兩個文件
處理這個文件的代碼將會不少(至少,我是這么預感的),因此,我們新建一個 .c 源文件,可以命名為 dictionary.c。
順便,我們也創建 dictionary.h 這個頭文件,其中存放 dictionary.c 中的函數的原型,這樣我們在 main 函數里就可以通過
#include "dictionary.h"
來引入這些函數的定義了。
在 dictionary.c 中,首先我們引入一些頭文件:
#include <stdio.h>
#include <stdlib.h>
#include <time.h> // 我們需要這里面的隨機數函數,還記得我們的第一個小游戲“或多或少”嗎?
#include <string.h> // 我們需要 strlen 這個計算字符串長度的函數
#include "dictionary.h"
chooseWord 函數
這個函數用于從文件 dictionary 中隨機選取一個單詞,此函數只有一個參數: 指向內存中可以寫入單詞的地址的指針,這個指針實參將由 main 函數提供。
函數返回值是 int 變量:1 表示一切順利;0 表示出現錯誤。
此函數的開頭是這樣:
int chooseWord(char *wordChosen)
{
FILE* dictionary = NULL; // 指向我們的文件 dictionary(詞庫)的文件指針
int wordNum = 0; // 詞庫中單詞總數
int chosenWordNum = 0; // 選中的單詞編號
int i = 0; // 下標
int characterRead = 0; // 讀入的字符
}
聲明了一些變量,我們接著寫:
dictionary = fopen("dictionary", "r"); // 以只讀模式打開詞庫(dictionary 文件)
if (dictionary == NULL) // 如果打開文件不成功
{
printf("\n無法裝載詞庫\n");
return 0; // 返回 0 表示出錯
}
這段代碼不難吧,就是嘗試打開詞庫(dictionary 文件),并檢測 dictionary 文件指針是否為 NULL。
如果為 NULL,表示打開失敗。如果打開文件失敗,則程序中止,因為沒有進行下去的必要了。
// 統計詞庫中的單詞總數,也就是統計回車符 `\n` 的數目
do
{
characterRead = fgetc(dictionary);
if (characterRead == '\n')
wordNum++;
} while (characterRead != EOF);
上面這段代碼中,我們借助 fgetc 函數遍歷整個文件(一個字符一個字符讀取)。
我們統計讀到的回車符(\n
)的數目,每讀到一個 \n
,我們對 wordNum(單詞總數)的值加 1。
我們通過以上代碼,就可以知道詞庫中的單詞總數了,就是 wordNum 的值。
然后,我們需要一個函數,根據 wordNum 的值計算一個偽隨機數出來,作為隨機選取的單詞編號,我們就來寫一個函數,命名為:randomNum。
randomNum 函數
此函數里的代碼我們之前編寫第一個 C語言小游戲: “或多或少” 時已經用過了,就是簡單的偽隨機數生成。
作用:用于返回一個介于 0 ~ (單詞總數 - 1) 之間的隨機數。
int randomNum(int maxNum)
{
srand(time(NULL));
return (rand() % maxNum);
}
寫好了 randomNum 函數,我們立即來使用它:
chosenWordNum = randomNum(wordNum); // 隨機選取一個單詞(編號)
接著,我們需要重新回到文件開始處來進行讀取,為了回到文件開始處,可以調用函數 rewind。
// 我們重新從文件開始處讀取(rewind 函數),直到遇到選中的那個單詞
rewind(dictionary);
while (chosenWordNum > 0)
{
characterRead = fgetc(dictionary);
if (characterRead == '\n')
chosenWordNum--;
}
/* 文件指針已經指向正確位置,我們就用fgets來讀取那一行(也就是那個選中的單詞)*/
fgets(wordChosen, 100, dictionary);
// 放置 \0 字符用于表示字符串結束
wordChosen[strlen(wordChosen) - 1] = '\0';
fclose(dictionary);
return 1; // 一切順利,返回1
dictionary.h 文件
其中包含我們的 dictionary.c 中的函數原型,內容如下:
#ifndef DICTIONARY_H
#define DICTIONARY_H
int chooseWord(char *wordChosen);
int randomNum(int maxNum);
#endif
完整的 dictionary.c 文件
/*
懸掛小人游戲
dictionary.c
------------
這里定義了兩個函數:
1. chooseWord 用于每輪從 dictionary 文件中隨機抽取一個單詞
2. randomNum 用于返回一個介于 0 ~ (單詞總數 - 1) 之間的隨機數
*/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
#include "dictionary.h"
int chooseWord(char *wordChosen)
{
FILE* dictionary = NULL; // 指向我們的文件 dictionary 的文件指針
int wordNum = 0; // 單詞總數
int chosenWordNum = 0; // 選中的單詞編號
int i = 0; // 下標
int characterRead = 0; // 讀入的字符
dictionary = fopen("dictionary", "r"); // 以只讀模式打開詞庫(dictionary 文件)
if (dictionary == NULL) // 如果打開文件不成功
{
printf("\n無法裝載詞庫\n");
return 0; // 返回 0 表示出錯
}
// 統計詞庫中的單詞總數,也就是統計回車符 \n 的數目
do
{
characterRead = fgetc(dictionary);
if (characterRead == '\n')
wordNum++;
} while (characterRead != EOF);
chosenWordNum = randomNum(wordNum); // 隨機選取一個單詞(編號)
// 我們重新從文件開始處讀取(rewind 函數),直到遇到選中的那個單詞
rewind(dictionary);
while (chosenWordNum > 0)
{
characterRead = fgetc(dictionary);
if (characterRead == '\n')
chosenWordNum--;
}
/* 文件指針已經指向正確位置,我們就用fgets來讀取那一行(也就是那個選中的單詞)*/
fgets(wordChosen, 100, dictionary);
// 放置 \0 字符用于表示字符串結束
wordChosen[strlen(wordChosen) - 1] = '\0';
fclose(dictionary);
return 1; // 一切順利,返回1
}
int randomNum(int maxNum)
{
srand(time(NULL));
return (rand() % maxNum);
}
修改 hangman.c 文件
現在,既然我們的處理詞庫的函數已經寫完了,也就是在 dictionary.c 中,那么我們需要相應地修改我們的 hangman.c 文件中的 main 函數和其他幾個子函數:
有了之前所有課程的知識,靠著注釋,應該不難看懂。
完整的 hangman.c 文件
/*
懸掛小人游戲
main.c
------------
游戲的主體代碼
*/
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <string.h>
#include "dictionary.h"
int win(int letterFound[], long wordSize);
int researchLetter(char letter, char secretWord[], int letterFound[]);
char readCharacter();
int main(int argc, char* argv[])
{
char letter = 0; // 存儲用戶輸入的字母
char secretWord[100] = {0}; // 要猜測的單詞
int *letterFound = NULL; // 布爾值的數組. 數組的每一個元素對應猜測單詞的一個字母。0 = 還沒猜到此字母, 1 = 已猜到字母
int leftTimes = 7; // 剩余猜測次數 (0 = 失敗)
int i = 0; // 下標
long wordSize = 0; // 單詞的長度(字母數目)
printf("歡迎來到懸掛小人游戲!\n");
// 從詞庫(文件 dictionary)中隨機選取一個單詞
if (!chooseWord(secretWord))
exit(0); // 退出游戲
// 獲取單詞的長度
wordSize = strlen(secretWord);
letterFound = malloc(wordSize * sizeof(int)); // 動態分配數組的大小,因為我們一開始不知道單詞長度
if (letterFound == NULL)
exit(0);
// 初始化布爾值數組,都置為 0,表示還沒有字母被猜到
for (i = 0 ; i < wordSize ; i++)
letterFound[i] = 0;
// 主while循環,如果還有猜測機會并且還沒勝利,繼續
while (leftTimes > 0 && !win(letterFound, wordSize))
{
printf("\n\n您還剩 %d 次機會", leftTimes);
printf("\n神秘單詞是什么呢 ? ");
/* 我們顯示猜測的單詞,將還沒猜到的字母用*表示
例如 : *O**LE */
for (i = 0 ; i < wordSize ; i++)
{
if (letterFound[i]) // 如果第 i+1 個字母已經猜到
printf("%c", secretWord[i]); // 打印出來
else
printf("*"); // 還沒猜到,打印一個*
}
printf("\n輸入一個字母 : ");
letter = readCharacter();
// 如果用戶輸入的字母不存在于單詞中
if (!researchLetter(letter, secretWord, letterFound))
{
leftTimes--; // 將剩余猜測機會數減 1
}
}
if (win(letterFound, wordSize))
printf("\n\n勝利了! 神秘單詞是 : %s\n", secretWord);
else
printf("\n\n失敗了! 神秘單詞是 : %s\n", secretWord);
return 0;
}
// 判斷是否勝利
int win(int letterFound[], long wordSize)
{
int i = 0;
int win = 1; // 1 為勝利,0 為失敗
for (i = 0 ; i < wordSize ; i++)
{
if (letterFound[i] == 0)
win = 0;
}
return win;
}
// 在所要猜的單詞中查找用戶輸入的字母
int researchLetter(char letter, char secretWord[], int letterFound[])
{
int i = 0;
int correctLetter = 0; // 0 表示字母不在單詞里,1 表示字母在單詞里
// 遍歷單詞數組 secretWord,以判斷所猜字母是否在單詞中
for (i = 0 ; secretWord[i] != '\0' ; i++)
{
if (letter == secretWord[i]) // 如果字母在單詞中
{
correctLetter = 1; // 表示猜對了一個字母
letterFound[i] = 1; // 對于所有等于所猜字母的數組位置,都將其數值變為1
}
}
return correctLetter;
}
char readCharacter()
{
char character = 0;
character = getchar(); // 讀取一個字母
character = toupper(character); // 把這個字母轉成大寫
// 讀取其他的字符,直到 \n (為了忽略它)
while (getchar() != '\n')
;
return character; // 返回讀到的第一個字母
}
好了,這個小游戲已經寫完了,用 gcc 編譯并運行看看吧!
gcc dictionary.c hangman.c -o hangman
然后:
./hangman
4. 第二部分第十一課預告
今天的課就到這里,一起加油吧!
我是 謝恩銘,公眾號「程序員聯盟」(微信號:coderhub)運營者,慕課網精英講師 Oscar 老師,終生學習者。
熱愛生活,喜歡游泳,略懂烹飪。
人生格言:「向著標桿直跑」