C that man? Hang him!

C that man? Hang him!

A Simple hangman implementation to show beginners some core concepts of C-17

ยท

12 min read

A Simple Hangman Game

Overview

When I first started, making of HANGMAN GAME was the first challenge I put on to me. It was fun and taught me many things I wanted to know and better ace it. Hope it does the same.

We will simply create a game that ressembles good-old Hangman's core-concept. This is a rewrite of my own blog at Learn-C

Attribution

Following implementation is adopted from saroj22322's-hangman you can find in the GitHub and few techniques from my first hangman implementation!

Cover Photo by M Ashraful Alam.

Building Blocks

Let's build the game step-by-step. Create a skeleton C file like this. Later we will discuss what each one is, but for now headthrough

/*
    to get rid of warning during compilation on Windows platform.
    Bound-check functions are not supported in GNU C, thus for
    portability this is defined! Feel free to ignore this
*/
#define _CRT_SECURE_NO_WARNINGS 1

/* this defines no of mistakes/health of the gamer! */
#define HEALTH 6

/* entry point for our game */
int main(void)
{
    /*
        from stdlib.h, tells OS exit state is OK.
        You can also return 0 to indicate OK state,
        but this is much schemantically pretty :)

        Any non-zero value will indicate OS exit state is failure
    */
    return EXIT_SUCCESS;
}

Output to Stdout

First off, let's display a welcome banner to the gamer ๐ŸŽฎ. C already provide us some convienient. We just need to use include them in our source text and use them.

Let's include stdio.h which will provide neccessary functions from the libc.

Add line #include <stdio.h> after first include-statement. This is will include printf definitions and some other into our source text during preprocessing time.

Then after { of main add line printf("Welcome to The Hangman Game!\n"). This line tells our CPU to display the message we provided to stdout of current runtime.

At this point, your source file should look something like this...

#include <stdlib.h>

/* provide some functions for IO ops */
#include <stdio.h>

int main(void)
{
    /* output welcome banner to gamer */
    printf("Welcome to The Hangman Game!\n");

    return EXIT_SUCCESS;
}

Input from stdin

Next, let's grab our gamer's name and print a hello-message back to the gamer.

First define a buffer (simply an array) char gamer[25] to hold the name. We will limit the name of the gamer to 25 characters (hope this is enoughโœŒ). Then using scanf provided by libc, we will read stdin into out buffer. At this point your source file would look like this...

#include <stdlib.h>
#include <stdio.h>

int main(void)
{
    printf("Welcome to The Hangman Game!\n");

    /* get name of the gamer */

    printf("Please input your name, within 25 characters without any spaces: ");

    /*
        this array will act as a buffer for the gamer's name
        when OS flushes stdin to our program. The length is specified to
        mitigate an attack vector called `buffer overflow`. You can give
        any length but limit exists as par platforms.
     */
    char gamer[25];

    /*
        this line actually does the job we wanted. this will save any content
        of stdin to the given buffers.

        params:-
            "%24s": is a control string/format string
            gamer: buffer we specified

     */
    scanf("%24s", gamer);

    /* outputs what we got to stdout */
    printf("Hello, %s!\n", gamer);

    return EXIT_SUCCESS;
}

Buffer Overflow/Overrun If we don't add 24 in between % and s in the first argument, then any extra characters may be read and may lead to Buffer Overflow/Overrun. This is a old-but-gold vulnerability you may find in amateur programs. So, we specify 24 to just read 24 characters and leave the rest.

Why 24? ๐Ÿค” Find out why 24 not 25 characters to be read! For head start, search how c-string works.

Bound-checked functions Microsoft wrote some extensions to ISO C std, and submitted a standardization request to C Committe, it is published as TR 24731 but GNU C won't implement it ๐Ÿฅถ. So, these safety functions are available in Windows environments and not that much portable ๐Ÿ™

Comparing two characters

Another thing we would want to do in our game is compare two character and verify gamer guessed correctly. Let's just compare two characters.

char c = 'c';
char a = 'a';

if (c == a)
{
    printf("%c is equal to %c", c, a);
}
else
{
    printf("%c is not equal to %c", c, a);
}

This would print they are not equal. Indeed, they are not!

Underthehood ๐Ÿคจ

Under the hood, char is just an unsigned int representing each english alphabets. The way C knows what value represents what alphabet is by looking up in the ASCII TABLE and tell the OS, in this context, these unsigned ints represents alphabets, not the arabic numbers.

So, by comparing the unsigned int value, C runtime knows, whether c equals to or not equals to a!

Memory allocation in heap

Prerequisites:

  1. Pointers
  2. Heap & Stack

Normally when you declare a variable/constant in C, you allocate a piece of memory large enough to hold the data is allocated in the stack/heap.

e.g. int i;

i, here, is initialized to a garbage data but in the stack living. Allocation in stack is ideal and actually recommended for small amounts of data, as retireval from the stack is much faster than from the heap.

But allocation in heap is recommended for large amount of data (e.g., strings, arrays with huge elements etc.) as OS allocates more heap space than stack size for a user-land mode.

In C, heap allocation is done via malloc or calloc. Since heap is not cleaned after function epilogue/prologue(assembly jargon!), we need to free the memory piece we requested when it is appropriate. Failure to do so, is known as Memory Leak and eventually our program will endeavor big chunks of memory as it requests more memory!

Allocation

char* str = malloc(size_needed);

malloc will return a pointer to the first memory block allocated by the OS with width as indicated by the type defined and bounds to the size_needed.

Deallocation

free(str)

:::danger Scope should be considered when using identifiers. :::

Stitching together

Alright, enough of building-blocks. Let's get this done with ๐Ÿ˜Ž!

Here is our game plan!

1. Include necessary libraries we *think* we need
2. Define some utility functions
3. Allocate and set necessary memory space in both *heap* and *stack*
4. Print welcome banner
5. Tell gamer about their game status (mistakes, gibbet, to-be-guessed)
6. Ask for their guess
7. Check whether the guess was right
8. Update the game status as necessary
9.  Implement the game loop

๐Ÿฅฑ quite a handful of tasks, let's go.

1. Include necessary libraries we think we need

Make sure you include these libraries at the very top of the source text file. You can omit the comments, they are there for your clarification!

/* this will provide us some variables we need */
#include <stdlib.h>

/* this library will give us scanf and printf and some other */
#include <stdio.h>

/* for string manipulations */
#include <string.h>

/* to have true/false aliases etc. */
#include <stdbool.h>

/* for strchr to check for presence of a character in a str */
#include <mbstring.h>

2. Define some utility functions

We will use these statements in few places exactly as same, so, I decided to extract them away to emboss DRY.

print_str function takes a pointer to a string and an int indicating number of characters from the beginning of the string we need to print to the stdout.

print_gibbet function takes str hangman and mistakes so far and prints the gibbet given the state of the game. It utilizes switch statement to appoint body parts to str hangman and stimulates hanging animation(๐Ÿ™„).

/* this will print given string to the given length limit */
void print_str(char *str, int len)
{
    for (size_t i = 0; i < len; ++i)
    {
        printf("%c", str[i]);
    }
}

/* this will print the gibbet and its current state */
void print_gibbet(char *hangman, int mistakes)
{
    switch (mistakes)
    {
    case 6:
        hangman[6] = '\\';
        break;
    case 5:
        hangman[5] = '/';
        break;
    case 4:
        hangman[4] = '\\';
        break;
    case 3:
        hangman[3] = '|';
        break;
    case 2:
        hangman[2] = '/';
        break;
    case 1:
        hangman[1] = '}', hangman[0] = '{';
        break;
    default:
        break;
    }

    printf("\t ____\n"
           "\t|    |\n"
           "\t|   %c %c\n"
           "\t|   %c%c%c\n"
           "\t|   %c %c\n"
           "\t|\n"
           "\t|\n"
           "\t----------\n",
           hangman[0], hangman[1],
           hangman[2], hangman[3], hangman[4],
           hangman[5], hangman[6]);
}

3. Allocate necessary memory space in both heap and stack

These statements go inside our main function!

/* this heap-location will be our hangman */
    char *hangman = malloc(HEALTH + 1);
    memset(hangman, ' ', HEALTH + 1);

    /* choose a random target */
    char *target = "birnadin";

    int len = strlen(target);

    /* gamer correctly guessed indicator */
    char *guessed = malloc(len);
    memset(guessed, '_', len);

    /* buffer to hold missed characters */
    char missed[HEALTH];

    char guess;       /* current gamer guess */
    bool found;       /* trigger for misseds update */
    int mistakes = 0; /* no of mistakes */
    char *win;        /* trigger for win status */

4. Print welcome banner

This is dead simple, just add printf("Welcome to Hangman Game!\n");. Feel free to customize the message!

5. Tell gamer about their game status (mistakes, gibbet, to-be-guessed)

These will print the game status of the gamer at the time of execution!

/* print mistakes and guessed indicators so far in the game */
printf("Mistakes: ");
if (mistakes)
{
    print_str(missed, mistakes);
}
else
{
    printf("None yet!");
}
print_str(guessed, len);

/* print gibbet */
print_gibbet(hangman, mistakes);

6. Ask for their guess

printf("\nguess: ");
do
{
    scanf("%c", &guess);
} while (getchar() != '\n'); /* scan stdin till newline character */

7. Check whether the guess was right

/*
    meat of the program!
    this will iterate via target chosen and checks whether gamer
    guess was correct or not. If correct updates the guesses-indicator
    and updates found trigger.
*/
for (size_t i = 0; i < len; ++i)
{
    if (target[i] == guess)
    {
        found = true;
        guessed[i] = guess;
    }
}

8. Update the game status as necessary

/*
    if guess was wrong, then updates mistakes count and missed buffer
    for the gamer to have a reference what were wrong!
    */
if (!found)
{
    missed[mistakes] = guess;
    mistakes++;
}

win = strchr(guessed, '_'); /* CHECKS FOR WIN STATUS */

9. Implement the game loop

do {
    /* 
        the game logic we developed from step 4. to step 8. 
    */
} while (mistakes < HEALTH && win != NULL);

Finally, with few adjustments and few miscellaneous enhancements

/*
    to get rid of warning during compilation on Windows platform.
    Bound-check functions are not supported in GNU C, thus for
    portability this is defined! Feel free to ignore this
*/
#define _CRT_SECURE_NO_WARNINGS 1

/* this will provide us some variables we need */
#include <stdlib.h>

/* this library will give us scanf and printf and some other */
#include <stdio.h>

/* for string manipulations */
#include <string.h>

/* to have true/false aliases etc. */
#include <stdbool.h>

/* to check to _ in guessed to determine win status */
#include <mbstring.h>

/* this defines no of mistakes/health of the gamer! */
#define HEALTH 6

/*
    begin::utilis
    some function definitions for modularity and DRY principle
*/
void print_str(char *str, int len);
void print_gibbet(char *hangman, int mistakes);
/* end::utils */

int main(void)
{
    /* this heap-location will be our hangman */
    char *hangman = malloc(HEALTH + 1);
    memset(hangman, ' ', HEALTH + 1);

    /* choose a random target */
    char *target = "birnadin";

    int len = strlen(target);

    /* gamer correctly guessed indicator */
    char *guessed = malloc(len);
    memset(guessed, '_', len);

    /* buffer to hold missed characters */
    char missed[HEALTH];

    char guess;       /* current gamer guess */
    bool found;       /* trigger for missed update */
    int mistakes = 0; /* no of mistakes */
    char *win;        /* trigger for win status */

    /* flush stdin as soon as interrupt ends */
    setvbuf(stdin, NULL, _IONBF, 0);

    do
    {

/* this will clear the console using appropriate system commands */
#if defined(linux) || defined(__unix__)
        system("clear");
#elif defined(_WIN32) || defined(_WIN64)
        system("cls");
#endif

        printf("Welcome to Hangman Game!\n");

        found = false;

        /* print mistakes and guessed indicators so far in the game */
        printf("Mistakes: ");
        if (mistakes)
        {
            print_str(missed, mistakes);
        }
        else
        {
            printf("None yet!");
        }
        printf("\n");
        print_str(guessed, len);

        printf("\n");

        /* print gibbet */
        print_gibbet(hangman, mistakes);

        printf("\nguess: ");
        do
        {
            scanf("%c", &guess);
        } while (getchar() != '\n'); /* scan stdin till newline character */

        /*
            meat of the program!
            this will iterate via target chosen and checks whether gamer
            guess was correct or not. If correct updates the guesses-indicator
            and updates found trigger.
         */
        for (size_t i = 0; i < len; ++i)
        {
            if (target[i] == guess)
            {
                found = true;
                guessed[i] = guess;
            }
        }

        /*
            if guess was wrong, then updates mistakes count and missed buffer
            for the gamer to have a reference what were wrong!
         */
        if (!found)
        {
            missed[mistakes] = guess;
            mistakes++;
        }

        win = strchr(guessed, '_'); /* CHECKS FOR WIN STATUS */

    } while (mistakes < HEALTH && win != NULL);

    if (win == NULL)
    {
        printf("\nCongrats, You Won!");
    }
    else
    {
        printf("\nOops! try again, better luck next time!");
    }

    /* free the memory allocated on the heap before exit */
    free(hangman);
    free(target);
    free(guessed);

    /* let OS know operation was successful */
    return EXIT_SUCCESS;
}

/* this will print given string to the given length limit */
void print_str(char *str, int len)
{
    for (size_t i = 0; i < len; ++i)
    {
        printf("%c", str[i]);
    }
}

/* this will print the gibbet and its current state */
void print_gibbet(char *hangman, int mistakes)
{
    switch (mistakes)
    {
    case 6:
        hangman[6] = '\\';
        break;
    case 5:
        hangman[5] = '/';
        break;
    case 4:
        hangman[4] = '\\';
        break;
    case 3:
        hangman[3] = '|';
        break;
    case 2:
        hangman[2] = '/';
        break;
    case 1:
        hangman[1] = '}', hangman[0] = '{';
        break;
    default:
        break;
    }

    printf("\t ____\n"
           "\t|    |\n"
           "\t|   %c %c\n"
           "\t|   %c%c%c\n"
           "\t|   %c %c\n"
           "\t|\n"
           "\t|\n"
           "\t----------\n",
           hangman[0], hangman[1],
           hangman[2], hangman[3], hangman[4],
           hangman[5], hangman[6]);
}

Compilation and Gameplay

It is recommended to compile with -std=c17 and source text is platform independent. Example command to compile with clang-14 in windows is as follows:

clang -std=c17 -o hman.exe .\hangman.c

That's it, enjoy the game.

Homework

We hardcoded the guess target. But try to get the guess from array of names randomly at the runtime.

Hints

1. Use `rand` from `stdlib.h`
2. Get names from a local text-file

That's it, it's been a long run, see with another assignment! Till then, this is me the BE signing off ๐Ÿ‘‹. ๐Ÿ’– from Jaffna!

ย