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

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

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

内容简介


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

1. 前言


通过上一课 C语言探索之旅 | 第二部分第九课: 实战"悬挂小人"游戏 以后,相信你们都或多或少都写了本身的“悬挂小人”的游戏代码吧。bash

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

"Yes, you are terminated."dom

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


若是你开始阅读这里,说明:编辑器

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

无论你是哪一种状况,我都会介绍一下如何来完成这个游戏。函数

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

事实上,我本身花了比想象中更多的时间来完成这游戏。测试

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

可是,我仍是坚信你们是有能力独自完成这个小游戏的(若是你认真学习了以前的 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 老师,终生学习者。 热爱生活,喜欢游泳,略懂烹饪。 人生格言:「向着标杆直跑」

相关文章
相关标签/搜索