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



内容简介


1、课程大纲

2、第二部分第九课: 实战"悬挂小人"游戏

3、第二部分第十课预告: 安全的文本输入


课程大纲


我们的课程分为四大部分,每一个部分结束后都会有练习题,并会公布答案。还会带大家用C语言编写三个游戏。


C语言编程基础知识


  • 什么是编程?

  • 工欲善其事,必先利其器

  • 你的第一个程序

  • 变量的世界

  • 运算那点事

  • 条件表达式

  • 循环语句

  • 实战:第一个C语言小游戏

  • 函数

  • 练习题

  • 习作:完善第一个C语言小游戏


C语言高级技术


  • 模块化编程

  • 进击的指针,C语言王牌

  • 数组

  • 字符串

  • 预处理

  • 创建你自己的变量类型

  • 文件读写

  • 动态分配

  • 实战:“悬挂小人”游戏

  • 安全的文本输入

  • 练习题

  • 习作:用自己的语言解释指针


用基于C语言的SDL库开发2D游戏


  • 安装SDL

  • 创建窗口和画布

  • 显示图像

  • 事件处理

  • 实战:“超级玛丽推箱子”游戏

  • 掌握时间的使用

  • 用SDL_ttf编辑文字

  • 用FMOD控制声音

  • 实战:可视化的声音谱线

  • 练习题


数据结构


  • 链表

  • 堆,栈和队列

  • 哈希表

  • 练习题


第二部分第九课:实战"悬挂小人"游戏


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

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

小编你有必要这么强调吗?... 简直无聊嘛。


好了,话休絮烦...


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

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


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


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


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


而我们真正开始图形编程,要到第三部分:【用基于C语言的SDL库开发2D游戏】


不过我们第二部分的课程已经接近尾声了,不要急,马上我们就可以开始用C语言来做真正的图形界面游戏了。


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


所以我们的游戏暂时还是以控制台的形式(黑框框)与大家见面,当然如果你会图形编程,这个游戏也可以自己扩展成图形界面的。相信不少读者应该见过这个游戏的图形界面版本,就是每猜错一个字母画一笔,直到用完规定次数,小人被“吊死”,就真的变成“屌丝”了。


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


题目规定


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


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


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

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

  3. 字典的所有单词储存在一个文本文件中(在Windows下通常是txt文件,在unix/linux下一般可以是任意后缀名的文件)

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




一个回合的运作机制

假设要猜的单词是OSCAR


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


有两种情况:


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

  2. 所猜的字母不在单词中(目前的情况,因为字母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>


但是我在Fedora 21(一种Linux发行版)中测试,不加引入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')的数目就可以,因为是每行一个单词


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


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



点击左下角“阅读原文”查看解题思路和答案。但是我们希望你先自己做,实在做不出来才看答案,不然就没什么收获了。



优化建议



因为我们的项目是在Linux下用gcc来编译的,如果你是在Windows下用CodeBlocks等IDE来编译的,那么请将字典文件dictionary改成dictionary.txt 


因为Windows的文件储存形式和Linux(或Unix)有些不一样。


改进游戏


  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");

}


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

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


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


完成或优化的代码可以打包成zip或rar文件发送给小编,欢迎讨论和交流,联系邮箱在下面。

也可以上传到我们的百度云盘或QQ群文件里,有问题可以在群里讨论。



第二部分第十课预告:


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

下一次我们学习:  安全的文本输入

将是第二部分的最后一课,不难。

结束之后我们就可以开始用C语言来编写图形化的游戏了!

因为我们第二部分的练习题和习作已经在“悬挂小人”这一课之前发出来了。


程序员联盟社区

目前有一个微信群和一个QQ群(微信群130人以上,QQ群420人以上),凡是对编程感兴趣的朋友都可以加,大家可以交流,学习,互动,讨论编写的程序的源代码,编程问答等。


手机微信里的二维码图片如何“扫描”呢?
小窍门:
在微信里长按图片,选择“识别图中二维码”,就可以了。


微信群(程序员联盟),加群请私信我(微信群人数超过100之后,不能通过扫描二维码加入了,只能私信我,谢谢)


QQ群(程序员联盟),群号是 413981577

QQ群文件里有很多编程书籍PDF和其他资料。扫描下面二维码加QQ群:




我们还建立了一个公共的百度云盘,2TB容量,已有很多优秀编程资源,大家也可以上传。链接加群之后会发送。


《程序员联盟》的微社区,方便大家提问和互动。可以关注一下。

微社区地址和二维码如下:

http://m.wsq.qq.com/264152148


谢谢!



程序员联盟 微信公众号 *您若觉得本文不错,点击画面右上角《···》按钮“分享到朋友圈或“发送给朋友

*新朋友请关注「程序员联盟」微信搜公众号  ProgrammerLeague

小编微信号 frogoscar

小编QQ号:  379641629

小编邮箱:    [email protected]

程序员联盟QQ群:413981577

程序员联盟微信群:先加我微信


有朋友反映看手机端的文章太累,其实是可以用浏览器网页来看的:

方法1. 点击画面右上角的《···》按钮,然后选择“复制链接”,再把链接黏贴到你的浏览器里面或用邮件发送给自己,就可以在电脑的浏览器里打开了



方法2. 头条网www.toutiao.com,搜索我的自媒体“程序员联盟”,内有所有文章,也可以直接进这个链接:http://www.toutiao.com/m3750422747/


方法3. 我的51CTO博客,CSDN博客和博客园链接(所有文章都在上面)
http://4526621.blog.51cto.com

http://blog.csdn.net/frogoscar

www.cnblogs.com/frogoscar


如何查看所有文章

点击“查看公众号”,再点击“查看历史消息





程序员联盟”公众号专为程序员,App设计师,各位喜爱编程和热爱分享的小伙伴们推送各样编程相关知识,优秀软件推荐,业界动态等。



点击下方 阅读原文 查看 “悬挂小人”解题思路和答案
↓↓↓

阅读原文

你可能感兴趣的:(游戏,程序员,C语言,Hangman,悬挂小人)