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

本文主要是介绍C语言探索之旅 | 第二部分第九课: 实战悬挂小人游戏,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

 

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

《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 老师,终生学习者。
热爱生活,喜欢游泳,略懂烹饪。
人生格言:「向着标杆直跑」

这篇关于C语言探索之旅 | 第二部分第九课: 实战悬挂小人游戏的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/499381

相关文章

Spring Boot + MyBatis Plus 高效开发实战从入门到进阶优化(推荐)

《SpringBoot+MyBatisPlus高效开发实战从入门到进阶优化(推荐)》本文将详细介绍SpringBoot+MyBatisPlus的完整开发流程,并深入剖析分页查询、批量操作、动... 目录Spring Boot + MyBATis Plus 高效开发实战:从入门到进阶优化1. MyBatis

MyBatis 动态 SQL 优化之标签的实战与技巧(常见用法)

《MyBatis动态SQL优化之标签的实战与技巧(常见用法)》本文通过详细的示例和实际应用场景,介绍了如何有效利用这些标签来优化MyBatis配置,提升开发效率,确保SQL的高效执行和安全性,感... 目录动态SQL详解一、动态SQL的核心概念1.1 什么是动态SQL?1.2 动态SQL的优点1.3 动态S

Pandas使用SQLite3实战

《Pandas使用SQLite3实战》本文主要介绍了Pandas使用SQLite3实战,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学... 目录1 环境准备2 从 SQLite3VlfrWQzgt 读取数据到 DataFrame基础用法:读

Mysql删除几亿条数据表中的部分数据的方法实现

《Mysql删除几亿条数据表中的部分数据的方法实现》在MySQL中删除一个大表中的数据时,需要特别注意操作的性能和对系统的影响,本文主要介绍了Mysql删除几亿条数据表中的部分数据的方法实现,具有一定... 目录1、需求2、方案1. 使用 DELETE 语句分批删除2. 使用 INPLACE ALTER T

C语言中的数据类型强制转换

《C语言中的数据类型强制转换》:本文主要介绍C语言中的数据类型强制转换方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录C语言数据类型强制转换自动转换强制转换类型总结C语言数据类型强制转换强制类型转换:是通过类型转换运算来实现的,主要的数据类型转换分为自动转换

利用Go语言开发文件操作工具轻松处理所有文件

《利用Go语言开发文件操作工具轻松处理所有文件》在后端开发中,文件操作是一个非常常见但又容易出错的场景,本文小编要向大家介绍一个强大的Go语言文件操作工具库,它能帮你轻松处理各种文件操作场景... 目录为什么需要这个工具?核心功能详解1. 文件/目录存javascript在性检查2. 批量创建目录3. 文件

C语言实现两个变量值交换的三种方式

《C语言实现两个变量值交换的三种方式》两个变量值的交换是编程中最常见的问题之一,以下将介绍三种变量的交换方式,其中第一种方式是最常用也是最实用的,后两种方式一般只在特殊限制下使用,需要的朋友可以参考下... 目录1.使用临时变量(推荐)2.相加和相减的方式(值较大时可能丢失数据)3.按位异或运算1.使用临时

使用C语言实现交换整数的奇数位和偶数位

《使用C语言实现交换整数的奇数位和偶数位》在C语言中,要交换一个整数的二进制位中的奇数位和偶数位,重点需要理解位操作,当我们谈论二进制位的奇数位和偶数位时,我们是指从右到左数的位置,本文给大家介绍了使... 目录一、问题描述二、解决思路三、函数实现四、宏实现五、总结一、问题描述使用C语言代码实现:将一个整

Python实战之屏幕录制功能的实现

《Python实战之屏幕录制功能的实现》屏幕录制,即屏幕捕获,是指将计算机屏幕上的活动记录下来,生成视频文件,本文主要为大家介绍了如何使用Python实现这一功能,希望对大家有所帮助... 目录屏幕录制原理图像捕获音频捕获编码压缩输出保存完整的屏幕录制工具高级功能实时预览增加水印多平台支持屏幕录制原理屏幕

最新Spring Security实战教程之Spring Security安全框架指南

《最新SpringSecurity实战教程之SpringSecurity安全框架指南》SpringSecurity是Spring生态系统中的核心组件,提供认证、授权和防护机制,以保护应用免受各种安... 目录前言什么是Spring Security?同类框架对比Spring Security典型应用场景传统