Write a guessing game in ncurses

0

With ncurses, we can control where and how text gets displayed on the terminal. If you explore the ncurses library functions by reading the manual pages (man 3 ncurses) you’ll find there are a ton of different ways to display text, including bold text, colors, blinking text, windows, borders, graphic characters, and other features to make your application stand out.

If you’d like to explore a more advanced program that demonstrates a few of these interesting features, here’s a simple “guess the number” game, updated to use ncurses. The program picks a random number in a range, then asks the user to make repeated guesses until they find the secret number. As the user makes their guess, the program lets them know if the guess was too low or too high.

Good programmers take a problem and break it down into “steps” that can be implemented separately. With this idea, let’s start by creating a few “building blocks” for the program, as separate functions:

Pick a random value

This program limits the possible numbers from 1 to 8. I used the getrandom() kernel system call to generate random bits, masked with the number 7 to pick a random number from 0 (binary 0000) to 7 (binary 0111), plus 1 so the final value is in the range 1 to 8.

int rand8(void)
{
    int num;

    getrandom(&num, sizeof(int), GRND_NONBLOCK);

    /* 7 is binary 0111, so this returns a number from 0 to 7,
     * add 1 to get a number from 1 to 8 */
    return (num & 7) + 1;
}

Get the guess

Keeping the values to a limited range of single-digit numbers makes it easier to use getch() to read a single number from the user. This uses a shortcut by using the ASCII value of the key that was read from the keyboard. Assuming the standard ASCII ordering, we can subtract the character value and the character 0 to get a number that’s between 1 and 8:

int get_guess(void)
{
    int ch;

    do {
        ch = getch();
    } while ((ch < '1') || (ch > '8'));

    /* turn it into a number by using ascii value */
    return (ch - '0');
}

Center the title

By using ncurses, we can add some visual interest. Let’s add a function to display the title at the top of the screen. This is basically a “wrapper” function that uses strlen() to get the string length, then uses the COLS global variable to center the line:

void title(const char *s)
{
    mvaddstr(0, (COLS / 2) - (strlen(s) / 2), s);
}

Guess the number

With these functions, we can construct the main part of our number-guessing game. First, the program sets up the terminal for ncurses, then picks a random number from 1 to 8. After displaying a number scale, the program then enters a loop to ask the user for their guess.

As the user makes their guess, the program provides visual feedback. If the guess is too low, the program prints a left square bracket under the number on the screen. If the guess is too high, the game prints a right square bracket. This helps the user to narrow their choice until they guess the correct number:

#include <curses.h>                    /* curses */
#include <string.h>                    /* strlen */
#include <sys/random.h>                /* getrandom */

int rand8(void)
{
    int num;

    getrandom(&num, sizeof(int), GRND_NONBLOCK);

    /* 7 is binary 0111, so this returns a number from 0 to 7,
     * add 1 to get a number from 1 to 8 */
    return (num & 7) + 1;
}

int get_guess(void)
{
    int ch;

    do {
        ch = getch();
    } while ((ch < '1') || (ch > '8'));

    /* turn it into a number by using ascii value */
    return (ch - '0');
}

void title(const char *s)
{
    mvaddstr(0, (COLS / 2) - (strlen(s) / 2), s);
}

int main()
{
    int secret, guess;

    secret = rand8();

    initscr();
    cbreak();
    noecho();

    clear();
    title("Guess a number from 1 to 8");
    mvaddstr(5, 0, "Make your guess:");
    mvaddstr(10, 1, "12345678");

    move(15, 0);                       /* we'll print messages here */
    refresh();

    do {
        guess = get_guess();

        if (guess > secret) {
            mvaddch(11, guess, ']');
            mvaddstr(15, 0, "too high");
        }
        else if (guess < secret) {
            mvaddch(11, guess, '[');
            mvaddstr(15, 0, "too low ");
        }
        refresh();
    } while (guess != secret);

    mvaddch(11, guess, '*');
    mvaddstr(15, 0, "that's right!");
    mvaddstr(16, 0, "press any key to quit..");
    refresh();

    getch();

    endwin();
    return 0;
}

Let’s play!

Copy this program and compile it for yourself to try it out. Don’t forget that you need to tell GCC to link with the ncurses library:

$ gcc -o guess guess.c -lncurses

In this sample, the secret number was 2. I started by guessing 1 (too low), then 8 (too high), then 4 (too high). My next guess (2) was just right. You can see at each step, the program printed a “bracket” to indicate if my guess was too low or too high, and an asterisk when I guessed the secret number:

screenshot: guess the number

This program hard-codes a few values to keep things simple and easier to understand. For example, program always prints the title on the first line (line 0), the prompt on line 5, the list of numbers on line 10, and the “brackets” on line 11. Feedback (“too low” or “too high”) are printed on line 15. You might also notice that “too low” has a space at the end, so the string is the same length as “too high” and so will always overwrite the h at the end of “too high.”

To learn more about ncurses, read the online manual pages in section 3:

$ man 3 ncurses

This article is adapted from Write a guessing game in ncurses on Linux by Jim Hall, and is published with the author’s permission.

Leave a Reply