阅读 499

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

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

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

内容简介


  1. 前言
  2. 题目规定
  3. 优化建议
  4. 第二部分第十课预告

1. 前言


第二部分的理论知识基本讲完了。上一课我们经历了很有意思的 C语言探索之旅 | 第二部分第八课:动态分配

这一课我们来实战一下,要实现的游戏叫“悬挂小人”。

这个“小人”,不是“君子和小人”的小人。是 little man(小小的人)的意思。

读者:“你有必要这么强调吗?简直无聊嘛...”

好的,话休絮烦...

俗语说得好:“实践是必要的!”

对于大家来说这又尤为重要,因为我们刚刚结束了一轮 C语言的高级技术的“猛烈进攻”,需要好好复习一下,消化消化。

不论你多厉害,在编程领域,不实践是永远不行的。尽管你可能读懂了之前的所有课程,但是如果不配合一定的实践,是不能深刻理解的。

以前我大学里入门编程以前看 C语言的书,觉得看懂了,但是一上手要写程序,就像挤牙膏一样费劲。

这次的实战练习,我们一起来实现一个小游戏:“悬挂小人”,或叫 “上吊游戏”。英语叫 HangMan,是挺著名的一个休闲益智游戏。

虽说是游戏,但是比较可惜的是还不能有图形界面 (不过课程后面会说怎么实现在控制台绘制小人,其实也可以实现简陋的“图形化”): 因为 C语言本身不具备绘制 GUI(Graphical User Interface 的缩写,表示“图形用户接口”)的能力,需要引入第三方的库。

悬挂小人游戏是一个经典的字母游戏,在规定步数内一个字母一个字母地猜单词,直到猜出整个单词。

所以我们的游戏暂时还是以控制台的形式(黑框框)与大家见面,当然如果你会图形编程,也可以把这个游戏扩展成图形界面的。

相信不少读者应该见过这个游戏的图形界面版本,就是每猜错一个字母画一笔,直到用完规定次数,小人被“吊死”。

这个实战的目的是让我们可以复习之前学过的所有 C语言知识:指针,字符串,文件读写,结构体,数组,等等,都是好家伙!

2. 题目规定


既然是出题目的实战,那么就需要委屈大家按照我的题目要求来编写这个游戏啦。

好,就来公布我们的题目要求:

  • 游戏每一轮有 7 次(次数可以设置,不一定是 7 次)猜测的机会,用完则此轮失败。

  • 每轮会从字典中随机抽取一个单词供玩家猜,初始时单词是以若干个星号(*)的方式来表示。说明所有字母都还隐藏着。

  • 字典的所有单词储存在一个文本文件中(在 Windows 下通常是 txt 文件,在 Unix/Linux/macOS 下一般可以是任意后缀名的文件)。

  • 每猜错一个字母就扣掉一次机会,猜对一个字母不扣除机会数。猜对的字母会显示在屏幕上的单词中,替换掉星号。

一个回合的运作机制


假设要猜的单词是 OSCAR。

假设我们给程序输入一个字母 B(猜的第一个字母),程序会验证字母是否在这个单词里。

有两种情况:

  • 所猜的字母在单词中,此时程序会显示这个单词,不是全部显示,而是显示猜到的那些字母,其他的还未猜到的字母用 * 表示。

  • 所猜的字母不在单词中(目前的情况,因为字母 B 不在单词 OSCAR 中),此时程序会告诉玩家“你猜错了”,剩余的机会数会被扣除一个。如果剩余机会数变为 0,游戏结束。

在图形化的“悬挂小人”(Hangman)游戏中,每猜一次会有一个小人被画出来。我们的游戏,虽然还不能真正实现图形化,但是如果优化一下,也可以在控制台实现类似这样的效果:

假设玩家输入一个 C,因为 C 在单词 OSCAR 中,那么程序不会扣除玩家的剩余机会数,而且会显示已猜到的字母,如下:

单词:**C**
复制代码

如果玩家继续输入,这回输入的是 O,那么程序会显示如下:

单词:O*C**
复制代码

多个相同字母的情况


有一些单词中,同一个字母会出现多次。比如在 APPLE(表示“苹果”)中,P 这个字母就出现了 2 次;在 ELEGANCE(表示“优雅”)中,E 这个字母出现了 3 次。

Hangman 游戏对此的规则很简单:只要猜出一个字母,其他重复的字母会同时显示。

假如要猜的单词是 ELEGANCE,用户输入了一个 E,那么会如下显示:

单词:E*E****E
复制代码

一个回合的例子


欢迎来到悬挂小人游戏!

您还剩 7 次机会
神秘单词是什么呢?*****
输入一个字母:E

您还剩 6 次机会
神秘单词是什么呢?*****
输入一个字母:S

您还剩 6 次机会
神秘单词是什么呢?*S***
输入一个字母:R

您还剩 6 次机会
神秘单词是什么呢?*S**R
输入一个字母:
复制代码

游戏就会这样进行下去,直到玩家在 7 个机会用完前猜到单词,或者用完 7 个机会还没猜到单词,游戏结束。

例如:


您还剩 2 次机会
神秘单词是什么呢?OS*AR
输入一个字母:C

胜利了!神秘单词是:OSCAR
复制代码

在控制台输入一个字母


在控制台中让程序读入一个字母,看起来简单,但其实暗藏玄机。不信我们来试一下。

要输入一个字母,一般大家会认为是这样做:

scanf("%c", &myLetter);
复制代码

确实是不错的,因为 %c 标明了等待用户输入一个字符。输入的字符会储存在 myLetter 这个变量(类型是 char)中。

如果我们只写一个 scanf,那是没问题的。但是假如有好几个 scanf,会怎么样呢?我们来测试一下:

int main(int argc, char* argv[])
{
    char myLetter = 0;

    scanf("%c", &myLetter);
    printf("%c", myLetter);

    scanf("%c", &myLetter);
    printf("%c", myLetter);

    return 0;
}
复制代码

照我们的设想,上述程序应该会请求用户输入一个字符,再打印出来: 进行两次。

测试一下,实际情况是怎么样的呢?你输入了一个字符,没错,然后呢...

程序为你打印出来了你输入的那个字符,假如你输入的是 a,那么程序输出

a
复制代码

然后程序就退出了,没有下文了。为什么不提示我输入第二个字符了呢?就好像它忽略了第二个 scanf 一样。到底发生了什么呢?

事实上,当你在控制台(console)里面输入时,你输入的内容都被记录到内存的某处,当然也包括按下 Enter 键(回车键)时产生的输入:

\n
复制代码

因此,你先输入了一个字符(例如 a),然后你按了一下回车键:

字符 a 就被第一个 scanf 取走了,第二个 scanf 则把你的回车键(\n)取走了。

为了避免这个问题,我们写一个函数 readCharacter() 来处理:

char readCharacter()
{
    char character = 0;
    character = getchar();  // 读取输入的第一个字母

    character = toupper(character); // 把这个字母转成大写

    // 读取其他的字符,直到 \n (为了忽略它们)
    while (getchar() != '\n')
        ;

    return character;  // 返回读到的第一个字母
}
复制代码

可以看到,以上程序中,我们使用了 getchar 函数,这个函数是在标准库的 stdio.h 中,用于读取一个用户输入的字符,效果相当于

scanf("%c", &letter);
复制代码

然后,我们又用到了一个在本课程中还没学习过的函数:toupper。

根据字面意思 to + upper 是英语“转换为大写”的意思,所以这个函数就是用于把一个字母转成大写字母。

看到了吧,如果函数名起得好,几乎就不需要注释,看名字就知道大致是干什么的(论编程命名的重要性)。

借着 toupper 这个函数,玩家就可以输入小写字母或者大写字母了,因为在“悬挂小人”游戏中,我们显示的单词中的字母都是大写的。

toupper 这个函数定义在 ctype.h 这个标准库的头文件中,所以需要

#include <ctype.h>
复制代码

继续看我们的函数,可以看到其中最关键的地方是:

while (getchar() != '\n')
    ;
复制代码

这一小段代码使得我们可以清除第一个输入的字母外的其他字符,直到遇见 \n(回车符)。

函数返回的就是第一个输入的字母,这样可以保证不再受回车符的影响了。

我们用了一个 while 循环,而循环体部分只有一个分号(;),很简洁吧。

也许你会问,之前的课程中 while 循环的循环体不是由大括号围起来的么,怎么这里只有一个分号呢?

事实上,这个分号就相当于

{
}
复制代码

就是空循环体,什么都不做,所以其实以上的代码相当于:

while (getchar() != '\n')
{
}
复制代码

但是分号比大括号写起来更简单么,不要忘了程序员是懂得如何偷懒的一群人!

此 while 循环一直执行,直到用户输入回车符,其他的字符都被从内存中清除了,我们称其为 “清空缓冲区”。

因此:

为了在我们的程序中每次读取用户输入的一个字母,我们不要使用

scanf("%c", &myLetter);
复制代码

而须要借助我们写的函数:

myLetter = readCharacter();
复制代码

于是,我们的测试程序变成这样:

#include <stdio.h>
#include <ctype.h>

char readCharacter()
{
    char character = 0;

    character = getchar();  // 读取一个字母

    character = toupper(character);  // 把这个字母转成大写

    // 读取其他的字符,直到 \n (为了忽略它)
    while (getchar() != '\n')
        ;

    return character;  // 返回读到的第一个字母
}

int main(int argc, char* argv[])
{
    char myLetter = 0;

    myLetter = readCharacter();

    printf("%c\n", myLetter);

    myLetter = readCharacter();

    printf("%c\n", myLetter);

    return 0;
}
复制代码

运行,输出类似如下(假如用户输入 o,回车;输入 k,回车):

o
O
k
K
复制代码

字典 / 词库


因为我们的游戏是一步步写成的,所以一开始,肯定先写简单的,再逐步完善游戏。

因此,猜测的单词一开始我们只用一个。所以,我们一开始会这么写:

char secretWord[] = "BOTTLE";
复制代码

你会说:“这样不是很无聊嘛,猜测的单词总是这一个”。

是的,但之后我们肯定会扩展。一开始这样做是为了不把问题复杂化,一次做一件事情,慢慢来么。

之后如果猜测一个单词的代码可以运行了,我们再用一个文件来储存所有可能的单词,这个文件可以起名为 dictionary(表示“字典”)。

那什么是字典或词库呢?

在我们的游戏里,就是一个文件,文件中的每一行存放了一个单词,之后我们的程序会随机从此文件中抽取一个单词来作为每一轮的猜测单词。

词库是类似这样的:

YOU
MOTHER
LOVE
PANDA
BOTTLE
FUNNY
HONEY
LIKE
JAZZ
MUSIC
BREAD
APPLE
WATER
PEOPLE
复制代码

至于这个文件里有多少单词,因为我们的词库是可扩展的(之后肯定可以添加新的单词),所以其实只要统计回车符(\n)的数目就可以,因为是每行一个单词。

好了,游戏的基本点我们介绍到这里,其实有了前面所有课程的基础,你已经有能力来完成这个看似有点复杂的游戏了,不过要组织得好还是不那么容易的,你可以用多个函数来实现不同的功能。

加油,坚持不懈就是胜利,期待你的成果!

3. 优化建议


如果你是在 Windows 下用 CodeBlocks 等 IDE 来编译的,那么请将字典文件 dictionary 改成 dictionary.txt。 因为 Windows 的文件储存形式和 Linux/Unix/macOS 有些不一样。

改进游戏


  1. 目前来说,我们只让玩家玩一轮,如果能加一个循环,使得游戏每次询问玩家是否要再玩一次,那“真真是极好的”。

  2. 目前还是单机模式,可以创建一个二人模式,就是一个玩家输入一个单词,第二个玩家来猜。

  3. 为什么不用 printf 函数来打印(绘制)一个悬挂小人呢?在每次我们猜错的时候,就把它画出来,每错一个,多画一笔,这样可以增加乐趣,可以用如下的代码:

if (猜错1个字母)
{
    printf(" _____\n");
    printf(" |  |\n");
    printf(" |  O\n");
    printf(" |\n");
    printf(" |\n");
    printf(" |\n");
    printf(" |\n");
    printf("_|__\n");
}
else if (猜错2个字母)
{
    printf(" _____\n");
    printf(" |  |\n");
    printf(" |  O\n");
    printf(" |  |\n");
    printf(" |\n");
    printf(" |\n");
    printf(" |\n");
    printf("_|__\n");
}
else if (猜错3个字母)
{
    printf(" _____\n");
    printf(" |  |\n");
    printf(" |  O\n");
    printf(" | \\|\n");
    printf(" |\n");
    printf(" |\n");
    printf(" |\n");
    printf("_|__\n");
}
else if (猜错4个字母)
{
    printf(" _____\n");
    printf(" |  |\n");
    printf(" |  O\n");
    printf(" | \\|/\n");
    printf(" |\n");
    printf(" |\n");
    printf(" |\n");
    printf("_|__\n");
}
else if (猜错5个字母)
{
    printf(" _____\n");
    printf(" |  |\n");
    printf(" |  O\n");
    printf(" | \\|/\n");
    printf(" |  |\n");
    printf(" |\n");
    printf(" |\n");
    printf("_|__\n");
}
else if (猜错6个字母)
{
    printf(" _____\n");
    printf(" |  |\n");
    printf(" |  O\n");
    printf(" | \\|/\n");
    printf(" |  |\n");
    printf(" | /\n");
    printf(" |\n");
    printf("_|__\n");
}
else if (猜错7个字母)
{
    printf(" _____\n");
    printf(" |  |\n");
    printf(" |  O\n");
    printf(" | \\|/\n");
    printf(" |  |\n");
    printf(" | / \\\n");
    printf(" |\n");
    printf("_|__\n");
}
复制代码

上面代码中的空格也许不同平台的显示不一样,可能需要大家自行调整。

如果 7 次机会全部用完,则小人挂掉,游戏结束。

请大家花点时间,好好理解这个游戏,并且尽可能地改进它。如果你可以不看我们的答案,而自己完成游戏和改进,那么你会收获很多的!

4. 第二部分第十课预告


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

下一课我们就会公布悬挂小人游戏的解题思路和答案咯。

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


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