作者 謝恩銘,公眾號「程序員聯(lián)盟」(微信號:coderhub)昆婿。
轉(zhuǎn)載請注明出處。
原文:http://www.reibang.com/p/6cbf452666bd
《C語言探索之旅》全系列
內(nèi)容簡介
- 前言
- 題目規(guī)定
- 優(yōu)化建議
- 第二部分第十課預(yù)告
1. 前言
第二部分的理論知識基本講完了痕支。上一課我們經(jīng)歷了很有意思的 C語言探索之旅 | 第二部分第八課:動態(tài)分配 品擎。
這一課我們來實(shí)戰(zhàn)一下,要實(shí)現(xiàn)的游戲叫“懸掛小人”揍异。
這個(gè)“小人”全陨,不是“君子和小人”的小人。是 little man(小小的人)的意思衷掷。
讀者:“你有必要這么強(qiáng)調(diào)嗎辱姨?簡直無聊嘛...”
好的,話休絮煩...
俗語說得好:“實(shí)踐是必要的戚嗅!”
對于大家來說這又尤為重要雨涛,因?yàn)槲覀儎倓偨Y(jié)束了一輪 C語言的高級技術(shù)的“猛烈進(jìn)攻”,需要好好復(fù)習(xí)一下懦胞,消化消化替久。
不論你多厲害,在編程領(lǐng)域躏尉,不實(shí)踐是永遠(yuǎn)不行的蚯根。盡管你可能讀懂了之前的所有課程,但是如果不配合一定的實(shí)踐胀糜,是不能深刻理解的颅拦。
以前我大學(xué)里入門編程以前看 C語言的書,覺得看懂了教藻,但是一上手要寫程序距帅,就像擠牙膏一樣費(fèi)勁。
這次的實(shí)戰(zhàn)練習(xí)怖竭,我們一起來實(shí)現(xiàn)一個(gè)小游戲:“懸掛小人”锥债,或叫 “上吊游戲”陡蝇。英語叫 HangMan痊臭,是挺著名的一個(gè)休閑益智游戲哮肚。
雖說是游戲,但是比較可惜的是還不能有圖形界面 (不過課程后面會說怎么實(shí)現(xiàn)在控制臺繪制小人广匙,其實(shí)也可以實(shí)現(xiàn)簡陋的“圖形化”): 因?yàn)?C語言本身不具備繪制 GUI(Graphical User Interface 的縮寫允趟,表示“圖形用戶接口”)的能力,需要引入第三方的庫鸦致。
懸掛小人游戲是一個(gè)經(jīng)典的字母游戲潮剪,在規(guī)定步數(shù)內(nèi)一個(gè)字母一個(gè)字母地猜單詞,直到猜出整個(gè)單詞分唾。
所以我們的游戲暫時(shí)還是以控制臺的形式(黑框框)與大家見面抗碰,當(dāng)然如果你會圖形編程,也可以把這個(gè)游戲擴(kuò)展成圖形界面的绽乔。
相信不少讀者應(yīng)該見過這個(gè)游戲的圖形界面版本弧蝇,就是每猜錯(cuò)一個(gè)字母畫一筆,直到用完規(guī)定次數(shù)折砸,小人被“吊死”看疗。
這個(gè)實(shí)戰(zhàn)的目的是讓我們可以復(fù)習(xí)之前學(xué)過的所有 C語言知識:指針,字符串睦授,文件讀寫两芳,結(jié)構(gòu)體,數(shù)組去枷,等等怖辆,都是好家伙!
2. 題目規(guī)定
既然是出題目的實(shí)戰(zhàn)删顶,那么就需要委屈大家按照我的題目要求來編寫這個(gè)游戲啦疗隶。
好,就來公布我們的題目要求:
游戲每一輪有 7 次(次數(shù)可以設(shè)置翼闹,不一定是 7 次)猜測的機(jī)會斑鼻,用完則此輪失敗。
每輪會從字典中隨機(jī)抽取一個(gè)單詞供玩家猜猎荠,初始時(shí)單詞是以若干個(gè)星號(
*
)的方式來表示坚弱。說明所有字母都還隱藏著。字典的所有單詞儲存在一個(gè)文本文件中(在 Windows 下通常是 txt 文件关摇,在 Unix/Linux/macOS 下一般可以是任意后綴名的文件)荒叶。
每猜錯(cuò)一個(gè)字母就扣掉一次機(jī)會,猜對一個(gè)字母不扣除機(jī)會數(shù)输虱。猜對的字母會顯示在屏幕上的單詞中些楣,替換掉星號。
一個(gè)回合的運(yùn)作機(jī)制
假設(shè)要猜的單詞是 OSCAR。
假設(shè)我們給程序輸入一個(gè)字母 B(猜的第一個(gè)字母)愁茁,程序會驗(yàn)證字母是否在這個(gè)單詞里蚕钦。
有兩種情況:
所猜的字母在單詞中,此時(shí)程序會顯示這個(gè)單詞鹅很,不是全部顯示嘶居,而是顯示猜到的那些字母,其他的還未猜到的字母用
*
表示促煮。所猜的字母不在單詞中(目前的情況邮屁,因?yàn)樽帜?B 不在單詞 OSCAR 中),此時(shí)程序會告訴玩家“你猜錯(cuò)了”菠齿,剩余的機(jī)會數(shù)會被扣除一個(gè)佑吝。如果剩余機(jī)會數(shù)變?yōu)?0,游戲結(jié)束绳匀。
在圖形化的“懸掛小人”(Hangman)游戲中迹蛤,每猜一次會有一個(gè)小人被畫出來。我們的游戲襟士,雖然還不能真正實(shí)現(xiàn)圖形化盗飒,但是如果優(yōu)化一下,也可以在控制臺實(shí)現(xiàn)類似這樣的效果:
假設(shè)玩家輸入一個(gè) C陋桂,因?yàn)?C 在單詞 OSCAR 中逆趣,那么程序不會扣除玩家的剩余機(jī)會數(shù),而且會顯示已猜到的字母嗜历,如下:
單詞:**C**
如果玩家繼續(xù)輸入宣渗,這回輸入的是 O,那么程序會顯示如下:
單詞:O*C**
多個(gè)相同字母的情況
有一些單詞中梨州,同一個(gè)字母會出現(xiàn)多次痕囱。比如在 APPLE(表示“蘋果”)中,P 這個(gè)字母就出現(xiàn)了 2 次暴匠;在 ELEGANCE(表示“優(yōu)雅”)中鞍恢,E 這個(gè)字母出現(xiàn)了 3 次。
Hangman 游戲?qū)Υ说囊?guī)則很簡單:只要猜出一個(gè)字母每窖,其他重復(fù)的字母會同時(shí)顯示帮掉。
假如要猜的單詞是 ELEGANCE,用戶輸入了一個(gè) E窒典,那么會如下顯示:
單詞:E*E****E
一個(gè)回合的例子
歡迎來到懸掛小人游戲蟆炊!
您還剩 7 次機(jī)會
神秘單詞是什么呢?*****
輸入一個(gè)字母:E
您還剩 6 次機(jī)會
神秘單詞是什么呢瀑志?*****
輸入一個(gè)字母:S
您還剩 6 次機(jī)會
神秘單詞是什么呢涩搓?*S***
輸入一個(gè)字母:R
您還剩 6 次機(jī)會
神秘單詞是什么呢污秆?*S**R
輸入一個(gè)字母:
游戲就會這樣進(jìn)行下去,直到玩家在 7 個(gè)機(jī)會用完前猜到單詞昧甘,或者用完 7 個(gè)機(jī)會還沒猜到單詞良拼,游戲結(jié)束。
例如:
您還剩 2 次機(jī)會
神秘單詞是什么呢疾层?OS*AR
輸入一個(gè)字母:C
勝利了将饺!神秘單詞是:OSCAR
在控制臺輸入一個(gè)字母
在控制臺中讓程序讀入一個(gè)字母贡避,看起來簡單痛黎,但其實(shí)暗藏玄機(jī)。不信我們來試一下刮吧。
要輸入一個(gè)字母湖饱,一般大家會認(rèn)為是這樣做:
scanf("%c", &myLetter);
確實(shí)是不錯(cuò)的,因?yàn)?%c
標(biāo)明了等待用戶輸入一個(gè)字符杀捻。輸入的字符會儲存在 myLetter 這個(gè)變量(類型是 char)中井厌。
如果我們只寫一個(gè) scanf,那是沒問題的致讥。但是假如有好幾個(gè) scanf仅仆,會怎么樣呢?我們來測試一下:
int main(int argc, char* argv[])
{
char myLetter = 0;
scanf("%c", &myLetter);
printf("%c", myLetter);
scanf("%c", &myLetter);
printf("%c", myLetter);
return 0;
}
照我們的設(shè)想垢袱,上述程序應(yīng)該會請求用戶輸入一個(gè)字符墓拜,再打印出來: 進(jìn)行兩次。
測試一下请契,實(shí)際情況是怎么樣的呢咳榜?你輸入了一個(gè)字符,沒錯(cuò)爽锥,然后呢...
程序?yàn)槟愦蛴〕鰜砹四爿斎氲哪莻€(gè)字符涌韩,假如你輸入的是 a,那么程序輸出
a
然后程序就退出了氯夷,沒有下文了臣樱。為什么不提示我輸入第二個(gè)字符了呢?就好像它忽略了第二個(gè) scanf 一樣腮考。到底發(fā)生了什么呢擎淤?
事實(shí)上,當(dāng)你在控制臺(console)里面輸入時(shí)秸仙,你輸入的內(nèi)容都被記錄到內(nèi)存的某處嘴拢,當(dāng)然也包括按下 Enter 鍵(回車鍵)時(shí)產(chǎn)生的輸入:
\n
因此,你先輸入了一個(gè)字符(例如 a)寂纪,然后你按了一下回車鍵:
字符 a 就被第一個(gè) scanf 取走了席吴,第二個(gè) scanf 則把你的回車鍵(\n
)取走了赌结。
為了避免這個(gè)問題,我們寫一個(gè)函數(shù) readCharacter()
來處理:
char readCharacter()
{
char character = 0;
character = getchar(); // 讀取輸入的第一個(gè)字母
character = toupper(character); // 把這個(gè)字母轉(zhuǎn)成大寫
// 讀取其他的字符孝冒,直到 \n (為了忽略它們)
while (getchar() != '\n')
;
return character; // 返回讀到的第一個(gè)字母
}
可以看到柬姚,以上程序中,我們使用了 getchar 函數(shù)庄涡,這個(gè)函數(shù)是在標(biāo)準(zhǔn)庫的 stdio.h 中量承,用于讀取一個(gè)用戶輸入的字符,效果相當(dāng)于
scanf("%c", &letter);
然后穴店,我們又用到了一個(gè)在本課程中還沒學(xué)習(xí)過的函數(shù):toupper撕捍。
根據(jù)字面意思 to + upper 是英語“轉(zhuǎn)換為大寫”的意思,所以這個(gè)函數(shù)就是用于把一個(gè)字母轉(zhuǎn)成大寫字母泣洞。
看到了吧忧风,如果函數(shù)名起得好,幾乎就不需要注釋球凰,看名字就知道大致是干什么的(論編程命名的重要性)狮腿。
借著 toupper 這個(gè)函數(shù),玩家就可以輸入小寫字母或者大寫字母了呕诉,因?yàn)樵凇皯覓煨∪恕庇螒蛑性迪幔覀冿@示的單詞中的字母都是大寫的。
toupper 這個(gè)函數(shù)定義在 ctype.h 這個(gè)標(biāo)準(zhǔn)庫的頭文件中甩挫,所以需要
#include <ctype.h>
繼續(xù)看我們的函數(shù)贴硫,可以看到其中最關(guān)鍵的地方是:
while (getchar() != '\n')
;
這一小段代碼使得我們可以清除第一個(gè)輸入的字母外的其他字符,直到遇見 \n
(回車符)捶闸。
函數(shù)返回的就是第一個(gè)輸入的字母夜畴,這樣可以保證不再受回車符的影響了。
我們用了一個(gè) while 循環(huán)删壮,而循環(huán)體部分只有一個(gè)分號(;
)贪绘,很簡潔吧。
也許你會問央碟,之前的課程中 while 循環(huán)的循環(huán)體不是由大括號圍起來的么税灌,怎么這里只有一個(gè)分號呢?
事實(shí)上亿虽,這個(gè)分號就相當(dāng)于
{
}
就是空循環(huán)體菱涤,什么都不做,所以其實(shí)以上的代碼相當(dāng)于:
while (getchar() != '\n')
{
}
但是分號比大括號寫起來更簡單么洛勉,不要忘了程序員是懂得如何偷懶的一群人!
此 while 循環(huán)一直執(zhí)行粘秆,直到用戶輸入回車符,其他的字符都被從內(nèi)存中清除了收毫,我們稱其為 “清空緩沖區(qū)”攻走。
因此:
為了在我們的程序中每次讀取用戶輸入的一個(gè)字母殷勘,我們不要使用
scanf("%c", &myLetter);
而須要借助我們寫的函數(shù):
myLetter = readCharacter();
于是,我們的測試程序變成這樣:
#include <stdio.h>
#include <ctype.h>
char readCharacter()
{
char character = 0;
character = getchar(); // 讀取一個(gè)字母
character = toupper(character); // 把這個(gè)字母轉(zhuǎn)成大寫
// 讀取其他的字符昔搂,直到 \n (為了忽略它)
while (getchar() != '\n')
;
return character; // 返回讀到的第一個(gè)字母
}
int main(int argc, char* argv[])
{
char myLetter = 0;
myLetter = readCharacter();
printf("%c\n", myLetter);
myLetter = readCharacter();
printf("%c\n", myLetter);
return 0;
}
運(yùn)行玲销,輸出類似如下(假如用戶輸入 o,回車摘符;輸入 k贤斜,回車):
o
O
k
K
字典 / 詞庫
因?yàn)槲覀兊挠螒蚴且徊讲綄懗傻模砸婚_始逛裤,肯定先寫簡單的瘩绒,再逐步完善游戲。
因此别凹,猜測的單詞一開始我們只用一個(gè)草讶。所以洽糟,我們一開始會這么寫:
char secretWord[] = "BOTTLE";
你會說:“這樣不是很無聊嘛炉菲,猜測的單詞總是這一個(gè)”。
是的坤溃,但之后我們肯定會擴(kuò)展拍霜。一開始這樣做是為了不把問題復(fù)雜化,一次做一件事情薪介,慢慢來么祠饺。
之后如果猜測一個(gè)單詞的代碼可以運(yùn)行了,我們再用一個(gè)文件來儲存所有可能的單詞汁政,這個(gè)文件可以起名為 dictionary(表示“字典”)道偷。
那什么是字典或詞庫呢?
在我們的游戲里记劈,就是一個(gè)文件勺鸦,文件中的每一行存放了一個(gè)單詞,之后我們的程序會隨機(jī)從此文件中抽取一個(gè)單詞來作為每一輪的猜測單詞目木。
詞庫是類似這樣的:
YOU
MOTHER
LOVE
PANDA
BOTTLE
FUNNY
HONEY
LIKE
JAZZ
MUSIC
BREAD
APPLE
WATER
PEOPLE
至于這個(gè)文件里有多少單詞换途,因?yàn)槲覀兊脑~庫是可擴(kuò)展的(之后肯定可以添加新的單詞),所以其實(shí)只要統(tǒng)計(jì)回車符(\n
)的數(shù)目就可以刽射,因?yàn)槭敲啃幸粋€(gè)單詞军拟。
好了,游戲的基本點(diǎn)我們介紹到這里誓禁,其實(shí)有了前面所有課程的基礎(chǔ)懈息,你已經(jīng)有能力來完成這個(gè)看似有點(diǎn)復(fù)雜的游戲了,不過要組織得好還是不那么容易的摹恰,你可以用多個(gè)函數(shù)來實(shí)現(xiàn)不同的功能辫继。
加油阁最,堅(jiān)持不懈就是勝利,期待你的成果骇两!
3. 優(yōu)化建議
如果你是在 Windows 下用 CodeBlocks 等 IDE 來編譯的速种,那么請將字典文件 dictionary 改成 dictionary.txt。
因?yàn)?Windows 的文件儲存形式和 Linux/Unix/macOS 有些不一樣低千。
改進(jìn)游戲
目前來說配阵,我們只讓玩家玩一輪,如果能加一個(gè)循環(huán)示血,使得游戲每次詢問玩家是否要再玩一次棋傍,那“真真是極好的”。
目前還是單機(jī)模式难审,可以創(chuàng)建一個(gè)二人模式瘫拣,就是一個(gè)玩家輸入一個(gè)單詞,第二個(gè)玩家來猜告喊。
為什么不用 printf 函數(shù)來打郁镏簟(繪制)一個(gè)懸掛小人呢?在每次我們猜錯(cuò)的時(shí)候黔姜,就把它畫出來拢切,每錯(cuò)一個(gè),多畫一筆秆吵,這樣可以增加樂趣淮椰,可以用如下的代碼:
if (猜錯(cuò)1個(gè)字母)
{
printf(" _____\n");
printf(" | |\n");
printf(" | O\n");
printf(" |\n");
printf(" |\n");
printf(" |\n");
printf(" |\n");
printf("_|__\n");
}
else if (猜錯(cuò)2個(gè)字母)
{
printf(" _____\n");
printf(" | |\n");
printf(" | O\n");
printf(" | |\n");
printf(" |\n");
printf(" |\n");
printf(" |\n");
printf("_|__\n");
}
else if (猜錯(cuò)3個(gè)字母)
{
printf(" _____\n");
printf(" | |\n");
printf(" | O\n");
printf(" | \\|\n");
printf(" |\n");
printf(" |\n");
printf(" |\n");
printf("_|__\n");
}
else if (猜錯(cuò)4個(gè)字母)
{
printf(" _____\n");
printf(" | |\n");
printf(" | O\n");
printf(" | \\|/\n");
printf(" |\n");
printf(" |\n");
printf(" |\n");
printf("_|__\n");
}
else if (猜錯(cuò)5個(gè)字母)
{
printf(" _____\n");
printf(" | |\n");
printf(" | O\n");
printf(" | \\|/\n");
printf(" | |\n");
printf(" |\n");
printf(" |\n");
printf("_|__\n");
}
else if (猜錯(cuò)6個(gè)字母)
{
printf(" _____\n");
printf(" | |\n");
printf(" | O\n");
printf(" | \\|/\n");
printf(" | |\n");
printf(" | /\n");
printf(" |\n");
printf("_|__\n");
}
else if (猜錯(cuò)7個(gè)字母)
{
printf(" _____\n");
printf(" | |\n");
printf(" | O\n");
printf(" | \\|/\n");
printf(" | |\n");
printf(" | / \\\n");
printf(" |\n");
printf("_|__\n");
}
上面代碼中的空格也許不同平臺的顯示不一樣,可能需要大家自行調(diào)整纳寂。
如果 7 次機(jī)會全部用完主穗,則小人掛掉,游戲結(jié)束毙芜。
請大家花點(diǎn)時(shí)間忽媒,好好理解這個(gè)游戲,并且盡可能地改進(jìn)它爷肝。如果你可以不看我們的答案猾浦,而自己完成游戲和改進(jìn),那么你會收獲很多的灯抛!
4. 第二部分第十課預(yù)告
今天的課就到這里金赦,一起加油吧!
下一課我們就會公布懸掛小人游戲的解題思路和答案咯对嚼。
下一課:C語言探索之旅 | 第二部分第十課: 實(shí)戰(zhàn)"懸掛小人"游戲 答案
我是 謝恩銘夹抗,公眾號「程序員聯(lián)盟」(微信號:coderhub)運(yùn)營者,慕課網(wǎng)精英講師 Oscar 老師纵竖,終生學(xué)習(xí)者漠烧。
熱愛生活杏愤,喜歡游泳,略懂烹飪已脓。
人生格言:「向著標(biāo)桿直跑」