Writing a fun turn-based game
When I need a distraction, I make a little program to amuse myself. Today, I wrote a simple “code-breaker” game.
This game is similar to the Bulls and Cows game, or the Mastermind board game. An early version of Unix included a version of Bulls and Cows, where you try to guess a series of four random digits, nonrepeating. After each guess, the computer tells you how many digits are correct, or correct but in the wrong position. For example, if the secret code was 4930 and you guessed 1234, the computer would respond with “1 bull and 1 cow: the bull is 3 and the cow is 4” because the 3 is correct and in the right place and the 4 appears in the secret code but not at that position.
I wanted to play a similar game, but I made the hints easier to follow. The user has just five attempts to guess a secret code of five nonrepeating digits. For each guess, the program prints my code, and for each digit indicates ! if that digit is correct and in the right place, ? if the digit appears in the secret but not at that position, and x if the number doesn’t appear in the secret at all. It turns out that five guesses is challenging but not unreasonable.
Here’s how you can write your own version of this “code-breaker” game.
Making a secret code
The core part of the game involves reading input from the user and comparing it to a secret value. To generate a random string of nonrepeating digits, I first started with the numbers 0 to 9, and “shuffled” them to make a random order.
One way to get random numbers is with the getrandom system call, which queries the Linux kernel to fill random bits into a variable. This is a much better method than the C standard library function rand, which only generates pseudorandom values. Here’s a test program to shuffle a string of digits into a random order:
#include <stdio.h>
#include <sys/random.h>
void shuffle(char *list, int len)
{
char c;
unsigned int n;
for (int i = 0; i < len; i++) {
getrandom(&n, sizeof(unsigned int), GRND_NONBLOCK);
n %= len;
c = list[n];
list[n] = list[i];
list[i] = c;
}
}
int main()
{
char secret[] = "1234567890";
puts(secret);
shuffle(secret, 10);
puts(secret);
return 0;
}
In the shuffle function, I’ve stored the random bits in an unsigned variable so that the random bits will always be positive. Then I use that value in a modulo (%) operation to get a number between 0 and len-1.
Save this test program as shuffle.c and compile it:
$ gcc -o shuffle shuffle.c
When I run the program, I can see it print out the original list of digits, then the shuffled list of digits:
1234567890
5413702869
With this method, I effectively have created a random order of digits. If I use only the first five digits, I have a secret code of five nonrepeating numbers, which I can use in my game.
Read guesses
Reading input from the user is a tricky business. If my program prompts the user to enter some text, my program needs to have enough memory to store the full line. This is where a lot of programmers might create the conditions for a buffer “overflow,” where the user enters more data than the program can save into a variable.
For example, my “code-breaker” game only uses five digits, you might assume that I only need to make room for five characters of input. But what if the user enters “123456” as their guess? That’s longer than the five characters that are reserved for input.
Instead, I can use the getline function to read input. This is a flexible method to read data from the user, because it automatically reserves more memory to store all of the text. To use getline, start by defining a pointer to a string variable, and another variable that stores the size. You can allocate some memory to start with, but if you set the pointer to NULL and the size to zero then getline will do the rest to automatically allocate memory as it goes.
Let’s demonstrate with a sample program that just reads a string from the user and prints it back to the screen. Note that getline returns the number of characters that it read, or -1 if it reached the end of the input (such as the end of a file).
#include <stdio.h>
#include <stdlib.h>
int main()
{
char *s = NULL;
size_t len = 0;
if (getline(&s, &len, stdin) == -1) {
puts("(end)");
}
else {
fputs(s, stdout);
}
free(s);
return 0;
}
This program uses the variable s for the string, which has its size stored in len. Note that s starts with a NULL value, and len with zero. That means getline will allocate some default amount of memory to start. You should release the memory with the free function before you end the program, which I’ve done here.
If I save my sample program as input.c and compile it, then run it, I can type in some sample text and the program will print it back to me.
Hello world
Hello world
Comparing a guess
One other component we need for the “code-breaking” game is a method to compare the user’s guess with the secret value. To do this, I like to first print the user’s guess on a line by itself, then print another line to provide the hints.
To show the user’s guess, I could just print the full string. But the game only uses a five digit code; if the user entered a longer string, I don’t want to compare the extra characters in their guess. Similarly, if the user’s guess was too short, I don’t need to compare the secret values with text was wasn’t entered. That means when I display the guess, I only want to limit it to the first five characters, and keep track of how long the user’s guess was.
int compare(char *guess, char *secret, int ncompar)
{
int len = ncompar;
int correct = 0;
/* show guess */
putchar('\t');
for (int i = 0; i < ncompar; i++) {
if ((len == ncompar) && (strnchr("\0\n", 2, guess[i]) >= 0)) {
len = i;
}
if (i < len) {
putchar(guess[i]);
}
else {
putchar(' '); /* too short, pad with ' ' */
}
}
putchar('\n');
The function starts by assigning the len variable to the same value as the ncompar parameter, which is the number of characters to compare from the user’s guess and the secret code. The important stuff is in the for loop. When the program encounters a null character (\0) or a new line character (\n) it resets the len variable to the current position in the string. When the loop is finished, the len variable will be the length of the user’s input, or the length of the secret code, whichever is greater. That means I can use len in another loop to compare just the digits from the user’s guess:
/* show hints */
putchar('\t');
for (int i = 0; i < len; i++) {
if (guess[i] == secret[i]) {
putchar('!'); /* correct */
correct++;
}
else if (strnchr(secret, len, guess[i]) >= 0) {
putchar('?'); /* correct, but not correct position */
}
else {
putchar('x'); /* not there */
}
}
putchar('\n');
return correct;
}
For each digit, the function prints ! if that digit is correct and in the right place, ? if the digit appears in the secret but not at that position, and x if the number doesn’t appear in the secret at all.
To make this function easier to write, I also created a separate “helper” function called strnchr that indicates if a character exists in a longer string. This is similar to the C standard library function strchr but it only looks at the first n characters. Also, my strnchr function returns an integer value, where strchr returns a pointer into the string. The function also returns -1 if the character cannot be found in the string.
int strnchr(char *s, int len, char c)
{
int pos;
for (pos = 0; pos < len; pos++) {
if (c == s[pos]) {
return pos;
}
}
return -1;
}
Putting it all together
With these components, I can now write a full version of the game. After generating a secret code, my program reads guesses from the user, then compares each guess to the secret code. For each guess, the program provides hints for correct and incorrect digits from the secret code. If the user can guess the secret code within five guesses, the program prints a “That’s right” message. Otherwise, the program prints different messages depending on how close the user was to the secret value.
Here’s the complete program, with extra comments:
#include <stdio.h>
#include <stdlib.h>
#include <sys/random.h>
void shuffle(char *list, int len)
{
char c;
unsigned int n;
/* shuffle the items in the list. this isn't a very secure shuffle,
* but it's only for a simple game so it's okay. */
for (int i = 0; i < len; i++) {
getrandom(&n, sizeof(unsigned int), GRND_NONBLOCK);
n %= len;
c = list[n];
list[n] = list[i];
list[i] = c;
}
}
int strnchr(char *s, int len, char c)
{
int pos;
/* find the first occurrence of c in s, for first len chars,
* and return the position in the string. */
for (pos = 0; pos < len; pos++) {
if (c == s[pos]) {
return pos;
}
}
/* not found, return error */
return -1;
}
int compare(char *guess, char *secret, int ncompar)
{
int len = ncompar;
int correct = 0;
/* show guess */
putchar('\t');
for (int i = 0; i < ncompar; i++) {
if ((len == ncompar) && (strnchr("\0\n", 2, guess[i]) >= 0)) {
len = i;
}
if (i < len) {
putchar(guess[i]);
}
else {
putchar(' '); /* too short, pad with ' ' */
}
}
putchar('\n');
/* show hints */
putchar('\t');
for (int i = 0; i < len; i++) {
if (guess[i] == secret[i]) {
putchar('!'); /* correct */
correct++;
}
else if (strnchr(secret, len, guess[i]) >= 0) {
putchar('?'); /* correct, but not correct position */
}
else {
putchar('x'); /* not there */
}
}
putchar('\n');
return correct;
}
int main()
{
char secret[] = "1234567890";
char *guess;
size_t size;
int turns = 5, found;
/* print instructions */
puts("You have five turns to guess a 5-digit number, non-repeating.");
puts("After each guess, I'll tell you what you got right and wrong,");
puts("so you can make your next guess.");
/* let the user guess the secret number */
shuffle(secret, 10);
do {
printf("%d:", turns); /* prompt */
if (getline(&guess, &size, stdin) == -1) {
turns = 0;
}
else {
found = compare(guess, secret, 5);
}
} while ((--turns > 0) && (found < 5));
/* give feedback */
if (found == 5) {
puts("That's right!");
}
else if (found >= 3) {
puts("You almost had it");
}
else if (found >= 1) {
puts("You were getting there");
}
else {
puts("Way off");
}
free(guess);
return 0;
}
Save this as codebrk.c and compile it. The game is quite fun to play. It turns out that five guesses is challenging but reasonable. I usually start my guess with 12345:
$ ./codebrk
You have five turns to guess a 5-digit number, non-repeating.
After each guess, I'll tell you what you got right and wrong,
so you can make your next guess.
5:12345
12345
xxx??
After my first guess, I know that the secret has the digits 4 and 5, but not in the last two positions. The numbers 1, 2, and 3 do not appear at all in the secret code. So I might update my guess to put 4 and 5 at the start, and guess a few other digits:
4:45678
45678
???!x
With this guess, I know that 7 is in the correct position. and that 4, 5, and 6 appear in the secret but not in those positions. The number 8 isn’t in the secret code at all. This is enough information to make a new guess:
3:64579
64579
???!x
It doesn’t feel like I’m getting closer, but I am. I know that 9 doesn’t appear in the secret, and the 4, 5, and 6 are still in the wrong positions. I can make another guess:
2:06475
06475
?!!!?
And now I have the information I need to solve the puzzle. I know that 6, 4, and 7 are in the right positions, but 0 and 5 are not. There’s only one possible code that matches this criteria:
1:56470
56470
!!!!!
That's right!
Writing a simple game like this is a fun way to explore programming. Feel free to modify the program to make it your own, such as giving the user more guesses, or making a longer secret code.