做者 謝恩銘,公衆號「程序員聯盟」(微信號:coderhub)。 轉載請註明出處。 原文:www.jianshu.com/p/b239b1774…程序員
《C語言探索之旅》全系列數組
通過上一課 C語言探索之旅 | 第二部分第九課: 實戰"懸掛小人"遊戲 以後,相信你們都或多或少都寫了本身的「懸掛小人」的遊戲代碼吧。bash
這一課咱們就來"終結"這個遊戲吧 (聽着怎麼有點嚇人...)。微信
"Yes, you are terminated."dom
若是你開始閱讀這裏,說明:編輯器
無論你是哪一種狀況,我都會介紹一下如何來完成這個遊戲。函數
「說不說在我,聽不聽在您」~學習
事實上,我本身花了比想象中更多的時間來完成這遊戲。測試
人生老是這樣的,「理想豐滿,現實骨感;看似美滿,人艱不拆」。ui
可是,我仍是堅信你們是有能力獨自完成這個小遊戲的(若是你認真學習了以前的 C語言課程),能夠去查閱網上資料,花點時間(幾十分鐘,幾小時,幾天?),這並非一次競賽,因此不用着急。
我更但願您花了很多時間,最終實現了這個遊戲; 比之您只花 5 分鐘,而後就來看答案要好不少。
千萬不要以爲我是一蹴而就寫成這個遊戲的,這個遊戲雖小,但也還沒簡單到能夠在腦中構思好一切,而後「下筆若有神」: 我也是一步步寫出來的。
咱們將會分 2 步來介紹咱們的解方:
首先咱們會演示如何一步步寫遊戲的主體部分,一開始咱們會只有一個猜想的單詞,並且是固定的;我選了 BOTTLE(表示「瓶子」),由於咱們要測試對於單詞中有大於等於兩個相同字母的狀況是否處理正確了(BOTTLE 中有 2 個 T)。
而後咱們會演示如何加入詞庫的處理程序,以便每一輪遊戲能夠從詞庫中隨機抽取一個單詞。
牢記:重要的不是結果,而是咱們思考的方式和過程。
你們都知道,咱們的 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 而且還沒勝利的狀況下,是不會中止的。
在這兩種狀況下,都要中止遊戲。
咱們在 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 函數的內容。
好吧,稍做休息,繼續前進!
咱們已經編寫了遊戲主體部分的基本代碼,可是咱們的遊戲目前還不能作到每輪隨機抽取一個單詞。
所以,接下來咱們就帶你們編寫處理詞庫的代碼。
首先,咱們須要建立一個文件,用於存放全部的單詞。
在 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"
複製代碼
這個函數用於從文件 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。
此函數裏的代碼咱們以前編寫第一個 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.c 中的函數原型,內容以下:
#ifndef DICTIONARY_H
#define DICTIONARY_H
int chooseWord(char *wordChosen);
int randomNum(int maxNum);
#endif
複製代碼
/*
懸掛小人遊戲
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);
}
複製代碼
如今,既然咱們的處理詞庫的函數已經寫完了,也就是在 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
複製代碼
今天的課就到這裏,一塊兒加油吧!
我是 謝恩銘,公衆號「程序員聯盟」(微信號:coderhub)運營者,慕課網精英講師 Oscar 老師,終生學習者。 熱愛生活,喜歡游泳,略懂烹飪。 人生格言:「向着標杆直跑」