阅读 92

C语言探索之旅 | 第二部分第十课: 实战"悬挂小人"游戏答案

作者 谢恩铭,公众号「程序员联盟」(微信号:coderhub)。 转载请注明出处。 原文:www.jianshu.com/p/b239b1774…

《C语言探索之旅》全系列

内容简介


  1. 前言
  2. 解方(1. 游戏的代码)
  3. 解方(2. 词库的代码)
  4. 第二部分第十一课预告

1. 前言


经过上一课 C语言探索之旅 | 第二部分第九课: 实战"悬挂小人"游戏 之后,相信大家都或多或少都写了自己的“悬挂小人”的游戏代码吧。

这一课我们就来"终结"这个游戏吧 (听着怎么有点吓人...)。

"Yes, you are terminated."

2. 解方(1. 游戏的代码)


如果你开始阅读这里,说明:

  • 或者你写完了游戏,想来看看我们怎么写。
  • 或者你没完成这个游戏,想来看看怎么写。

不管你是哪种情况,我都会介绍一下如何来完成这个游戏。

“说不说在我,听不听在您”~

事实上,我自己花了比想象中更多的时间来完成这游戏。

人生总是这样的,“理想丰满,现实骨感;看似美满,人艰不拆”。

但是,我还是坚信大家是有能力独自完成这个小游戏的(如果你认真学习了之前的 C语言课程),可以去查阅网上资料,花点时间(几十分钟,几小时,几天?),这并不是一次竞赛,所以不用着急。

我更希望您花了不少时间,最终实现了这个游戏; 比之您只花 5 分钟,然后就来看答案要好很多。

千万不要觉得我是一蹴而就写成这个游戏的,这个游戏虽小,但也还没简单到可以在脑中构思好一切,然后“下笔如有神”: 我也是一步步写出来的。

我们将会分 2 步来介绍我们的解方:

  1. 首先我们会演示如何一步步写游戏的主体部分,一开始我们会只有一个猜测的单词,而且是固定的;我选了 BOTTLE(表示“瓶子”),因为我们要测试对于单词中有大于等于两个相同字母的情况是否处理正确了(BOTTLE 中有 2 个 T)。

  2. 然后我们会演示如何加入词库的处理程序,以便每一轮游戏可以从词库中随机抽取一个单词。

牢记:重要的不是结果,而是我们思考的方式和过程。

分析 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,这个函数将有两个功能:

  1. 返回一个布尔值(在 C语言里用 int 型表示),用于表示所猜的字母是否存在于单词中。

  2. 更新 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. 第二部分第十一课预告


今天的课就到这里,一起加油吧!

下一课:C语言探索之旅 | 第二部分第十一课:练习题和习作


我是 谢恩铭,公众号「程序员联盟」(微信号:coderhub)运营者,慕课网精英讲师 Oscar 老师,终生学习者。 热爱生活,喜欢游泳,略懂烹饪。 人生格言:「向着标杆直跑」