张银峰的编程课堂

猜词游戏:Hangman

介绍

Hangman是一个简单的猜单词游戏,要猜的词以几个短横线连接表示,用以提示玩家这个单词有多少个字母。如果玩家猜中其中一个字母,则系统会将单词中该字母出现的所有位置补充上;如果猜错,系统便会画出一个绞刑图的一笔。当玩家猜中整个词(玩家获胜),或系统画完了整个绞刑图(系统获胜),则游戏结束。

如图所示,系统生成了一个由7个字符组成的单词,并在 "So far, the word is:" 的提示语句后以短横线提示。

glimix.com

当玩家输入字符'E',则单词中含有E的位置都被填充,

glimix.com

当玩家输入字符'B',由于单词中没有此字符,因此系统会画出绞刑图的下一笔(与上图比较多了一个圈)。

glimix.com

当玩家猜对所有字符时,玩家获得胜利,系统回显预设的单词,游戏结束。这里系统产生的单词是'Palette',玩家在整个游戏过程中尝试输入的字符称为已使用过的字符,打印在 "You've used the following letters:" 下面。

glimix.com

当系统画完了整个绞刑图,即玩在在指定步数内没有猜对单词,则系统胜利。

glimix.com

实现

Hangman的实现细节上比描述的更加细腻一些,比如系统会显示玩家已经使用过的字符;重复输入已使用过的字符,系统会大度的忽略掉而不会画出下一笔。程序启动后首先设置随机数种子,以便于rand()函数产生一个随机数,实现从单词库中随机抽取单词的功能。

srand((unsigned int)time(NULL));

在使用rand函数产生随机数前,需要系统提供的生成伪随机数序列的种子,rand根据这个种子的值产生一系列随机数。如果系统提供的种子没有变化,每次调用rand函数生成的伪随机数序列都是一样的。srand(unsigned seed) 通过参数seed改变系统提供的种子值,从而可以使得每次调用rand函数生成的伪随机数序列不同,以实现真正意义上的"随机"。通常可以利用系统时间来改变系统的种子值,即 srand(time(NULL)),继而为rand函数提供不同的种子值,进而产生不同的随机数序列

ZYF

接下来使用#include指令,将Hangman的字符画从一个文本文件中读取进来。

const char *hangma[] =
{
    #include "hangman.txt"
};

一张Hangman的字符画实际上就是一个多行字符串

"     ------       \n"
"     |    |       \n"
"     |    O       \n"
"     |  /-+-/     \n"
"     |    |       \n"
"     |    |       \n"
"     |   | |      \n"
"     |   | |      \n"
"     |            \n"
"    ----------    \n"

整套Hangman字符画共有8张,因此使用一个常量字符串数组来存储。这里的技巧在于,#include指令会将文件的内容插入到当前位置,借此完成了数组的初始化;再次声明,一条预处理指令要占据一个单独的代码行。然后我们根据Hangman字符画的张数,计算出最大的尝试次数。

int max_wrong = sizeof(hangma) / sizeof(hangma[0]) - 1;

接下来我们构建了一个单词库,作为示例,我们仅仅使用数组存储了几个预设值。

const char *words[] = {"RUSH", "HUNTER", "HUMAN", "DELICIOUS", "PALETTE"};

然后,我们根据单词总数,使用rand()函数随机产生一个索引,并取得对应的单词。

int num_of_word = sizeof(words) / sizeof(words[0]);
const char *word = words[rand() % num_of_word];

接下来,程序准备了游戏过程中要使用的变量。 其中so_far表示了猜测的进度,一开始时,因为没有进行博弈,所以它会输出几个短横线,依赖于猜测的正确性,它会在相应位置显示单词对应字符。

char used[MAX_WORDLEN] = {0};       // 记录已使用的字符
char so_far[MAX_WORDLEN] = {0};     // 当前已经匹配的字符
int  used_count = 0;                // used计数
int  wrong = 0;                     // 错误次数

for (int len = strlen(word), i = 0; i < len; i++)
    so_far[i] = '-';

现在,程序进入正式的猜词过程,因为需要多次接收玩家的输入,所以这个过程包含在一个循环内。首先打印出当前状态的字符画,然后输出玩家已经尝试过的字符,再输出单词的提示。

// 猜测循环
while (wrong < max_wrong && _stricmp(so_far, word) != 0)
{
    printf(hangma[wrong]);
    printf("\nYou've used the following letters:\n");
    print_used(used, used_count);
    printf("\nSo far, the word is:\n%s\n", so_far);

现在,开始读取玩家的输入,存储到guess变量中。scanf函数返回正确读取的格式控制符指定的数据个数,我们的目标是读取1个字符,当玩家输入不为字符时,scanf读取失败,while循环继续接收输入;当读取到1个字符时,scanf返回1结束循环。%*c用以表示该输入项读入后不赋予相应的变量,即跳过该输入值,在这里相当于吃掉了回车符。否则下次外部循环执行到这里,由于回车符刚才被读入,scanf直接返回,这里guess里面的值就是10,发生了用户没有有效输入却产生了输入值的错误。

    // 读取猜测字符
    char guess;
    printf("\n\nEnter your guess: ");
    while (scanf_s("%c%*c", &guess, 1) != 1);
    guess = toupper(guess);

接下来判断这个字符是否已经使用过,方法是使用strchr函数查找字符是否在已输入字符串中,如果存在,打印提示并让玩家再次输入。

    // 如果这个字符已经使用过则继续读取直到一个未使用字符
    char *pos = strchr(used, guess);
    while (pos)
    {
        printf("You've already guessed the letter %c\n", guess);
        printf("\nEnter your guess: ");
        while (scanf_s("%c%*c", &guess, 1) != 1);
        guess = toupper(guess);
        pos = strchr(used, guess);
    }

现在,开始执行游戏的逻辑部分。我们首先将当前字符记录在已使用字符集中;然后根据字符是否在单词中进行分支处理,如果在,则用字符填充单词相应字符在提示词的相应位置;否则输出提示信息,然后执行到循环头,判断游戏是否已经结束,没有,则继续猜迷。

    // 记录当前字符
    used[used_count++] = guess;

    // 所选单词中存在当前字符
    pos = strchr(word, guess);
    if (pos)
    {
        // 单词对应的位显示出来
        printf("\nYes! %c is in the word!\n", guess);
        for (int len = strlen(word), i = 0; i < len; i++)
        {
            if (word[i] == guess)
                so_far[i] = guess;
        }
    }
    else
    {
        printf("\nSorry, %c isn't in the word.\n", guess);
        wrong++;
    }
}

否则,依据获胜方,打印消息,退出游戏。

if (wrong == max_wrong)
{
    printf(hangma[wrong]);
    printf("\nYou've been hanged!\n");
}
else
{
    printf("\nYou guessed it!");
}

printf("\nThe word was %s\n", word);

ZYF