Hangman is a word-guessing game. In Hangman, a word is chosen. You are shown a sequence of blanks, one for each character, so you know how long the word is. You must guess the word by guessing which letters are in it.
Every time you guess a letter correctly, all copies of that letter are revealed. If you guess all the letters, or if you guess the word, you win.
Every time you guess a letter or the word incorrectly, you get a strike. When you get five strikes, the game is over and you lose.
Here is a sample game of the player guessing the word “apple” successfully:
$ ./hangman
Welcome to hangman! Your word has 5 letters:
_ _ _ _ _ Guess a letter or type the whole word: s
Strike 1!
_ _ _ _ _ Guess a letter or type the whole word: e
_ _ _ _ e Guess a letter or type the whole word: a
a _ _ _ e Guess a letter or type the whole word: p
a p p _ e Guess a letter or type the whole word: appie
Strike 2!
a p p _ e Guess a letter or type the whole word: apple
You got it! The word was 'apple'.
Notice when they guessed 'p', it showed all the p’s in the word.
And here they are failing to guess the word “apple”:
$ ./hangman
Welcome to hangman! Your word has 5 letters:
_ _ _ _ _ Guess a letter or type the whole word: s
Strike 1!
_ _ _ _ _ Guess a letter or type the whole word: after
Strike 2!
_ _ _ _ _ Guess a letter or type the whole word: x
Strike 3!
_ _ _ _ _ Guess a letter or type the whole word: q
Strike 4!
_ _ _ _ _ Guess a letter or type the whole word: s
Strike 5!
Sorry, you lost! The word was 'apple'.
Notice they guessed the letter 's' twice. You don’t have to remember “wrong” letters. They can just look at their previous guesses 😜
Last, to help with testing and grading, your program should accept one command-line argument as the word to guess:
$ ./hangman scrumbus
Welcome to hangman! Your word has 8 letters:
_ _ _ _ _ _ _ _ Guess a letter or type the whole word: s
s _ _ _ _ _ _ s Guess a letter or type the whole word: u
s _ _ u _ _ u s Guess a letter or type the whole word: scrumbus
You got it! The word was 'scrumbus'.
That way, you can run it with a known word and see if it behaves the way you expect.
The Dictionary ¶
I am providing you with a text file containing a dictionary of words to use. Here it is. Right-click and save the link. You’ll have to upload it to your AFS space with your SFTP client.
If you open this file, you will see a number on the first line, and then a bunch of words.
The first line says how many words there are in the dictionary. Then, each line after that is one word.
So, to read the dictionary:
Read the first line with fgets and use atoi (from stdlib.h) or sscanf (from stdio.h) to parse the number out of it.
Create an array of strings with that length.You can use the syntax char words[length][20] to make the array.
This will limit each word to 19 characters, but none of the words are nearly that long.
Read each subsequent line of the file into the array.You can use words[i] and sizeof(words[i]) the way you’re used to.
Tips: ¶
Remember to check if fopen returned NULL!If so, you can return from main or use exit from stdlib.h to exit the program.
Feel free to use any example code I have given you.Things like get_line, streq_nocase etc will be helpful.
You’ll probably want to make a version of get_line to read from an arbitrary FILE* instead of only stdin.
Test your code before you make the game.Print out the dictionary after reading it into the array.
If you’re getting blank lines between words, you probably didn’t chop the \n off the lines when you read them.
The game ¶
Once you’re sure you’re reading the dictionary in properly, you can move onto the game.
It works like this:
Pick a random word from the dictionary (see below).Alternatively, if argc 1, use argv[1] as the word instead.
Tell the player how many letters there are in the word.
In a loop:Show the current state of the word (see the interactions above to see how to print it out).
Ask the user for a letter or word.
If they didn’t type anything, go back to the previous step and ask them again.
If they typed a single letter,If it’s in the word at least once, mark all instances of that letter as “revealed”.If all the letters are revealed, they win, and the program is over.
If it’s not in the word, they get a strike. Show how many strikes they have.
If they typed a word (more than 1 letter),If it’s correct (equal to the word you picked), they win, and the program is over.
If it’s not correct, they get a strike. Show how many strikes they have.
If they have 5 strikes, they lose, and the program is over.Show them the correct word.
Notes:
If they guess a letter they have guessed before, don’t handle it specially.If they guess a correct letter twice, nothing bad will happen.
If they guess an incorrect letter twice, that’s the user’s fault 😜
Try to split things into functions.
Do not add extra prompts/questions, and do not be too picky about user input.Accept lower- AND upper-case letters for their guesses, using streq_nocase from lab2.
User experience is an important part of program design!And it will make it faster and easier to grade your projects ;)
Random numbers ¶
To generate random numbers:
#include <stdlib.h and #include <time.h
ONCE at the beginning of the program, “seed” the random number generator like so:
srand((unsigned int)time(NULL));
This method will generate slightly biased results and should not be used to generate a range of random numbers in general. For something this simple, it’s fine.
When you need an exclusive range of random numbers such as [0, 2) do this:
rand() % (high_value - low_value) + low_value
Write a function to do this. random_range(1, 10) makes more sense and is more readable than rand() % 9 + 1.
wavedit.c
Now you’ll write a program to read and do simple operations on a common sound file format called WAV.
The way we record sound is by recording how the air pressure changes over time. In digital sound, we take thousands of measurements (“samples”) each second, and store each measurement as a number.
Here’s a diagram of a simple sound wave, represented by the red line; and the digital version of it, represented by the individual blue dots. Each dot is one sample, and each sample can be one of 16 different values.
Thanks, Wikipedia. Thikipedia.
The WAV file format ¶
If you’re curious, chunk-based file formats are very common and pretty simple and flexible. Python even has a library for them. Cause, like, of course it does.
WAV files use a binary file format called RIFF, which is a “chunk-based” file format. However, the file format is simple enough that we can kind of treat it as a fixed format.
At the very beginning of the file, this structure appears (offsets and sizes are measured in bytes). What these fields should contain is explained a little further down.
Offset
Size
Name
Description
0
4
riff_id
chunk ID
4
4
file_size
size of rest of file, in bytes
8
4
wave_id
WAV file ID
12
4
fmt_id
chunk ID
16
4
fmt_size
size of the format info, in bytes
20
2
data_format
how the data is encoded
22
2
number_of_channels
how many channels (mono, stereo, surround etc)
24
4
samples_per_second
how many times per second the pressure was measured
28
4
bytes_per_second
how many bytes per second of sound
32
2
block_alignment
how many bytes per sample of all channels
34
2
bits_per_sample
how many bits each pressure measurement is
36
4
data_id
chunk ID
40
4
data_size
size of the sound data, in bytes
You must make a struct to represent this file header. (You could name it WAVHeader or something.)
When converting this table to a struct, keep these things in mind when deciding what types to use:
The four _id members are char arrays.Look at the size to see how long they should be.
All the other members are unsigned integers.The size will decide which type they should be.
Look back at your exploration from lab 2 to see which types are which size.
This whole struct should be 44 bytes.You can print sizeof(WAVHeader) or whatever you’ve named it to check.
If you like, there is a header file called stdint.h which contains shorter, more precise type names for integers.uint8_t, uint16_t, uint32_t, and uint64_t are unsigned 8-, 16-, 32-, and 64-bit integer types.
You use them like any other type, e.g. uint8_t x; makes an unsigned 8-bit integer variable x.
Your program ¶
OK, enough talk, let’s program!
You will make a utility that can:
Check if a file really is a WAV file
If it is, read and display useful information about it
Change its speed
Reverse the sound
Your program will work as a command-line program taking command-line arguments – NOT asking for input with fgets!
You will be able to run it four different ways:
./wavedit
./wavedit somefile.wav
./wavedit somefile.wav -rate 22050
./wavedit somefile.wav -reverse
No arguments: ./wavedit ¶
When run like this, your program should show a help message telling how to use it.
Try running cat --help on thoth to see what this kind of help or “usage” information usually looks like. Then make yours look kind of similar. :)
One argument: ./wavedit FILENAME ¶
When run like this, the first argument (argv[1]) will be the name of a file.
This should:
Check that the file exists (by using fopen)Which mode should you use to read a binary file?
Check that it really is a WAV file (see below)
Display the format information and some other information. (see below)
Don’t forget to close the file.
Make a function for each of these steps. Then it’ll be easy to reuse them for the other modes.
Checking that a file really is a WAV file ¶
Remember from class that a file extension is just part of its name, and doesn’t mean that the file really is what it says it is. So, you must check the data in the file to ensure that it’s valid.
If any of these checks fail, display a message saying it’s an invalid WAV file and exit. Here’s how to check:
Make a variable of your WAVHeader struct.
Use a single fread call to read the struct from the file.
Ensure that these fields have these values:riff_id should contain the characters "RIFF"
wave_id should contain the characters "WAVE"
fmt_id should contain the characters "fmt " (NOTE: there is a space in there!)
data_id should contain the characters "data"
fmt_size should be 16
data_format should be 1
number_of_channels should be 1 or 2
samples_per_second should be greater than 0 and less than or equal to 192000
bits_per_sample should be 8 or 16
bytes_per_second should be samples_per_second * (bits_per_sample)/8 * number_of_channels
block_alignment should be (bits_per_sample)/8 * number_of_channels
To check the four _id fields, you CANNOT use strcmp/streq, because these are not C-style strings. They have no 0 terminator. Instead, use strncmp. Look it up!
Displaying the information ¶
You should display the following information:
The number of bits per sample
The sample rate (samples per second), as Hz
Whether it is mono (1 channel) or stereo (2 channels)
The length of the data in samples (data_size / block_alignment)
The length of the data in seconds (length in samples, divided by samples_per_second)You’re gonna get a fraction here, so you must cast one of the values to a float!
Display this value with 3 digits after the decimal point.
As an example, here is what my program outputs on the sine440_16.wav test file:
This is a 16-bit 44100Hz mono sound.
It is 99225 samples (2.250 seconds) long.
Setting the sound’s sample rate: ./wavedit somefile.wav -rate 22050 ¶
This will change the file, by setting its samples_per_second field to the given number. It should:
Parse the argument after the -rate argument, and ensure that it is greater than 0 and less than or equal to 192000.Give an error and exit if not.
Open the file and read the headerYou’ll have to open it for reading AND writing
Check that it really is a WAV file, like before
Set the samples_per_second in the struct to the rate given on the command line.
Set the bytes_per_second in the struct to a new value calculated as:samples_per_second * (bits_per_sample)/8 * number_of_channels
Seek back to the beginning of the file
fwrite the struct back out to the file
Don’t forget to close the file!
Did I do it right? ¶
If you set the rate too high, your computer might refuse to play it. If so, try to stay at 48000 Hz and below.
Now, if you read the file’s properties again, everything should be the same, except the rate should have changed. If it says that the header is invalid, maybe you forgot to update the bytes_per_second value in the header.
Also, you can download the file with your SFTP client, and play it, and it should sound faster or slower :)
Warning: iTunes likes to do stupid shit and will make copies of your files when you open them, and you’ll think your program isn’t working, when it is. This happened to me. Either delete the files from iTunes after listening, or use a different media player (VLC?).
Reversing a sound: ./wavedit somefile.wav -reverse ¶
This will change the file, by reading all the samples, reversing them, and then writing them all back out again. It should work on 8- or 16-bit, mono or stereo sounds.
Here’s what it should do
Open the file and read the headerYou’ll have to open it for reading AND writing
Check that it really is a WAV file, like before
Calculate the data length in samples, like before
Allocate an array of the right type and that length (see below)
Use a single fread call to read the whole array from the fileAfter you read the header, the file is already in the right place to read the data
Reverse the array’s valuesThat is, swap the first and last values; then the second and second-to-last; etc.
Seek back to the beginning of the file plus sizeof(WAVHeader)
Use a single fwrite call to write the whole array back to the file
Don’t forget to close the file!
How samples are stored ¶
For mono (1-channel) sounds, the samples are simply an array. If bits_per_sample is 8, it’s an array of 8-bit integers. If it’s 16, it’s an array of 16-bit integers.
For stereo (2-channel) sounds, each sample is stored as a pair of 8- or 16-bit integers. The first number in the pair is the left channel’s sample, and the second number is the right channel’s.
Since your reversing function doesn’t have to treat the stereo channels individually, you can treat the data array as follows:
If bits_per_sample is 8 and number_of_channels is 1:treat it as an array of 8-bit integers.
If bits_per_sample is 16 and number_of_channels is 1; OR
If bits_per_sample is 8 and number_of_channels is 2:treat it as an array of 16-bit integers.
If bits_per_sample is 16 and number_of_channels is 2:treat it as an array of 32-bit integers.
Since C doesn’t have generics or function overloading, you’ll have to repeat this code three times, but with different types. Sorry ¯\_(ツ)_/¯
You can allocate the arrays like in hangman: sometype samples[sample_length]; where sometype is one of the types listed above.
When you fread, you can actually use sizeof() on that array, and it will give you the correct size of the array in bytes. At runtime. I know. I kind of lied. ;)
Did I reverse it correctly? ¶
The best way to check is to use your ears. Use the stereo_ and speech_ test files given below. After you run your program, download it in your SFTP client and listen to it.
Test files ¶
Here are some test WAV files to use, but your program will be graded with others. You can copy them to your directory like so:
cp ~jfb42/public/cs449/*.wav .
Don’t forget the space and period in the above command.
If you mess up or corrupt the files somehow, don’t worry, just run this command again to get clean copies of them.
The files should have the following properties by default:
sine440_16.wav: 16-bit, 44100Hz, mono, 99225 samples, 2.250 seconds
sine440_8.wav: 8-bit, 44100Hz, mono, 99225 samples, 2.250 seconds
speech_16.wav: 16-bit, 44100Hz, mono, 103936 samples, 2.357 seconds
speech_8.wav: 8-bit, 44100Hz, mono, 103936 samples, 2.357 seconds
stereo_16.wav: 16-bit, 44100Hz, stereo, 112128 samples, 2.543 seconds
stereo_8.wav: 8-bit, 44100Hz, stereo, 112128 samples, 2.543 seconds
woosh_16.wav: 16-bit, 44100Hz, stereo, 220500 samples, 5.000 seconds
woosh_8.wav: 8-bit, 44100Hz, stereo, 220500 samples, 5.000 seconds