猜词游戏:Hangman
介绍
Hangman是一个简单的猜单词游戏,要猜的词以几个短横线连接表示,用以提示玩家这个单词有多少个字母。如果玩家猜中其中一个字母,则系统会将单词中该字母出现的所有位置补充上;如果猜错,系统便会画出一个绞刑图的一笔。当玩家猜中整个词(玩家获胜),或系统画完了整个绞刑图(系统获胜),则游戏结束。
如图所示,系统生成了一个由7个字符组成的单词,并在 "So far, the word is:" 的提示语句后以短横线提示。

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

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

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

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

实现
Hangman的实现细节上比描述的更加细腻一些,比如系统会显示玩家已经使用过的字符;重复输入已使用过的字符,系统会大度的忽略掉而不会画出下一笔。程序启动后首先设置随机数种子,以便于rand()函数产生一个随机数,实现从单词库中随机抽取单词的功能。
srand((unsigned int)time(NULL));
在使用rand函数产生随机数前,需要系统提供的生成伪随机数序列的种子,rand根据这个种子的值产生一系列随机数。如果系统提供的种子没有变化,每次调用rand函数生成的伪随机数序列都是一样的。srand(unsigned seed) 通过参数seed改变系统提供的种子值,从而可以使得每次调用rand函数生成的伪随机数序列不同,以实现真正意义上的"随机"。通常可以利用系统时间来改变系统的种子值,即 srand(time(NULL)),继而为rand函数提供不同的种子值,进而产生不同的随机数序列
接下来使用#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);