Skip to content

Games programming from the ground up with C: Building a menu

You can see an index of all the posts in this series: go to index.

If you want to review where I’ve got up to, you can download the code from the end of the previous post here: building the user interface 2

In the previous post, I built my core windows management system. In this post, as promised, I’ll build the menu for my Hexapawn game. Before I do that, I should take care of possible error conditions. Up to now, I’ve blindly assumed that every attempt to create a window or panel will work correctly and that the user’s terminal window will be able to show my game at its full size of 24 rows by 80 characters.

If a window or panel cannot be created, it will return a null pointer rather than a pointer to the relevant window or panel. In either of these cases, continued execution of the game will not be viable, so I should exit with an error message. In my c-games-hexapawn folder, I created a new file called hexerror.c and gave it the following content:

//
// hexerror.c
// Hexapawn
//
// Error handling routines for Hexapawn
//

#include "hexapawn.h"

void exit_with_error(char * error_string) {
    if (stdscr != NULL) {
        endwin();
    }

    printf("Hexapawn has closed because of an error:\n%s\n", error_string);
    exit(1);
} 

Next I created a hexerror.h file:

//
// hexerror.h
// Hexapawn
//
// Error handling routines for Hexapawn
//
#ifndef __Hexapawn_hexerror__
#define __Hexapawn_hexerror__

void exit_with_error(char * error_string);

#endif /* defined(__Hexapawn__hexerror__) */

Finally I added this line just above the include for the ncurses library in hexapawn.h:

#include <stdlib.h>

and this line to the very end of hexapawn.h:

#include "hexerror.h"

As you can see, I have defined a simple function exit_with_error, which I can call in the event of a fatal error. The function accepts a single string with an error message to be displayed.

Note that on line 11 I check to see if stdscr is set to null. This variable will exist because it is defined by the ncurses library. However, it will be non-null if ncurses is currently initialised. In this case, I want to immediately end ncurses with the endwin function. There are two reasons for doing this. Firstly because I want to leave the user’s terminal window in a stable state, and secondly, because I want the user to see the error message that I am going to display to standard output on line 16.

Because I’ve added this new file, I need to update my makefile as follows:

gcc hexpawn.c hexwindows.c hexerror.c -l ncurses -l panel -o ../hexapawn

To test this function, I added the following temporary line at the very start of the main function in hexwindows.c:

exit_with_error("Testing the exit_with_error function");

When I built and ran the Hexpawn program, it immediately exited with an error message:

Output from the Hexapawn program showing how the exit_with_error function works.
Demonstrating how the exit_with_error function works

Now I edited hexapawn.c and moved the line that calls exit_with_error to just before the closing brace of the while loop (line 44).

When I now built and ran the program, I initially saw the menu window, but as soon as I pressed any key other than x, the program closed and showed the same error message. Having satisfied myself that exit_with_error worked as I expected it to, I removed that test call from hexapawn.c and updated the create_basic_window function in hexwindows.c as follows:

// Create a basic window with an associated panel and a border
void create_basic_window(WINDOW ** window, PANEL ** panel, int height, int width, int y, int x) {
    // Create the window
    *window = newwin(height, width, y, x);

    if (*window == NULL) {
        exit_with_error(STR_WINDOW_ERROR);
    }

    keypad(*window, TRUE);
    
    // Add the window to a panel
    *panel = new_panel(*window);

    if (*panel == NULL) {
        exit_with_error(STR_PANEL_ERROR);
    }

    // Display a border around the window
    box(*window, 0, 0);
}

I now need to add the string constants I’ve used to hexstrings.h ( I added these in the appropriate place to maintain the alphabetic order in this file):

#define STR_PANEL_ERROR "A panel required by Hexapawn could not be created"
#define STR_WINDOW_ERROR "A window required by Hexapawn could not be created"

At this point, it wa a good idea to rebuild and re-run the program to ensure I was not getting any errors.

The next potential error condition I need to check for is the user attempting to run the program in a terminal window that is too small. I can extend the initialise_curses function in hexwindows.c to check for this:

// Initialise ncurses
{
    initscr();    // Start ncurses

    if (LINES < WIN_MAIN_HEIGHT || COLS < WIN_MAIN_WIDTH) {
        exit_with_error(STR_TERMINAL_TOO_SMALL);
    }

    cbreak();      // Disable line buffer
    noecho();    // User input will not be echoed to screen
    keypad(stdscr, TRUE);    // Enable special keyboard characters
    curs_set(0);    // Switch off the cursor
    refresh();     // Draw standard screen (workaround for windows bug)
}

I also need to add another macro to hexstrings.h for this error. I could add something like this:

#define STR_TERMINAL_TOO_SMALL "Your terminal window is too small to run Hexapawn\nPlease expand your terminal window to be at least 24 lines high by 80 characters wide and try again"

The problem with this is that if I ever decide to change the size of Hexapawn’s main windows, I’ll have to remember to not only change the WIN_MAIN_HEIGHT and WIN_MAIN_WIDTH macros, I’ll also have to remember to update the values in this string. Wouldn’t it be easier if I could just change those values in one place?

Well I can, by using a handy feature of the pre-processor called stringizing. To use this I’ll need to add a couple of macros of a different type to hexstrings.h. I added these just before the first of the string definitions:

#define xstr(arg) str(arg)
#define str(arg) #arg

Now I added the following modified macro to define the error string:

#define STR_TERMINAL_TOO_SMALL "Your terminal window is too small to run Hexapawn\nPlease expand your terminal window to be at least" xstr(WIN_MAIN_HEIGHT) " lines high by " xstr(WIN_MAIN_WIDTH) " characters wide and try again"

These macros are different because they take an argument, represented by arg in parentheses. Whenever arg is encountered in the following macro, the argument provided is substituted at that point. The second macro, on line 12, is the key one here. When a macro parameter is used with a leading # the pre-processor converts it to a string literal. You might be wondering about the purpose of the first macro on line 11, because all this seems to do is to pass its argument through to the second macro. The reason for having these nested function-like macros like this has to do with how the pre-processor handles macro expansion. Essentially, the macro given as an argument to xstr will be expanded first before it is passed as an argument to str, which then turns it into a string literal.

The net result is that I can use xstr in our error message macro on line 23 to insert the WIN_MAIN_HEIGHT and WIN_MAIN_WIDTH macros into the error message. The pre-processor will take each part of the provided string and concatenate them into a single string constant. Now, if I make changes to either the width or the height, they will still be shown correctly in this error message.

When I rebuilt and re-ran the program it ran as before. Then I exited the program and made my terminal window smaller than 24 lines by 80 characters, before running again. This time, it immediately exited with the following error message:

The output of Hexapawn, showing the error message when the terminal window is too small.
The Error message shown when the terminal window is too small to run Hexapawn

Now that I’ve taken care of those potential errors, I can get on with building my game menu.

I’m going to take the same approach to building menus as I do with building windows, i.e. I’ll have a structure to keep track of each menu I need. I’ll start by adding that structure to hexapawn.h, along with a few other things I’ll need.

First I’ll need to include the menu library. I added this below the existing includes at the top of the file:

#include <menu.h>

I added the following lines in the correct alphabetic placement within my existing macros:

#define MAIN_MENU_HEIGHT 10
#define MAIN_MENU_ITEMS 10
#define MAIN_MENU_WIDTH 26
#define MAIN_MENU_X 3
#define MAIN_MENU_Y 5

I added a new enum type for our menu IDs, just below the two existing enum types:

 enum menu_id {
    MAIN_MENU
}; 

Finally, I added the definition for the hexmenu struct just below the existing definition for the hexwindow struct:

struct hexmenu {
    enum menu_id uid;
    enum window_id window;
    WINDOW * subwindow;
    MENU * menu;
    ITEM ** menu_items;
    int menu_y;
    int menu_x;
    int menu_height;
    int menu_width;
    int num_items;
    char * item_list[10];
};

I also need to add a few items to my hexstrings.h file. I added these just below the definition for STR_MAIN_TITLE (Note that STR_MENU_MARK has a space after the greater than symbol):

#define STR_MENU_ERROR "A menu required by Hexapawn could not be created"
#define STR_MENU_EXIT "Exit"
#define STR_MENU_MARK "> "
#define STR_MENU_NEW "New game"
#define STR_MENU_RESET "Reset AI and history"
#define STR_MENU_RESUME "Resume game" 

Now I’m ready to add the code to create the menus to hexwindows.c. At the top of the file, underneath the array of window definitions, I added a definition for my main menu:

static struct hexmenu hexmenus[] = {
    // Main menu
    {MAIN_MENU, WIN_MAIN_MENU, NULL, NULL, NULL, MAIN_MENU_Y, MAIN_MENU_X, MAIN_MENU_HEIGHT, MAIN_MENU_WIDTH, MAIN_MENU_ITEMS,
        {STR_INSTRUCTIONS_SUBTITLE,
        STR_MENU_NEW,
        STR_MENU_RESUME,
        STR_AI_SUBTITLE,
        STR_HIST_SUBTITLE,
        STR_SAVE_SUBTITLE,
        STR_LOAD_SUBTITLE,
        STR_MENU_RESET,
        STR_MENU_EXIT,
        (char *) NULL}}
};

There are a couple of things to note about the array of menu item names. Firstly, for some of them I can simply reuse the subtitle of the relevant window, but for others I had to create new entries in hexstrings.h. Secondly, the list of menu items must end with a null pointer.

Now, at the very end of the file, I added the initialise_menus function:

// Create the menus
void initialise_menus() {
     for (int i = 0; i < ARRAY_SIZE(hexmenus); i++) {

         // Create the menu items
         hexmenus[i].menu_items = (ITEM **) calloc(hexmenus[i].num_items, sizeof(ITEM *));

         if (hexmenus[i].menu_items == NULL) {
             exit_with_error(STR_MENU_ERROR);
         }

         for (int j = 0; j < hexmenus[i].num_items; j++) {
             hexmenus[i].menu_items[j] = new_item(hexmenus[i].item_list[j], hexmenus[i].item_list[j]);

             if (j < hexmenus[i].num_items - 1 && hexmenus[i].menu_items[j] == null) {
                 exit_with_error(STR_MENU_ERROR);
             }
         }

         // Create the menu
         hexmenus[i].menu = new_menu((ITEM **)hexmenus[i].menu_items);

         if (hexmenus[i].menu == NULL) {
             exit_with_error(STR_MENU_ERROR);
         }

         // Add menu to the relevant window
         struct hexwindow * menu_window = get_hexwindow(hexmenus[i].window); 
         set_menu_win(hexmenus[i].menu, menu_window->w_ptr);
         hexmenus[i].subwindow = derwin(menu_window->w_ptr, hexmenus[i].menu_height, hexmenus[i].menu_width, hexmenus[i].menu_y, hexmenus[i].menu_x);

         if (hexmenus[i].subwindow == NULL) {
             exit_with_error(STR_MENU_ERROR);
         }

         set_menu_sub(hexmenus[i].menu, hexmenus[i].subwindow);
         set_menu_mark(hexmenus[i].menu, STR_MENU_MARK);

         // Post the menu         
         post_menu(hexmenus[i].menu);
     }
}

This function loops through all our menus and creates them. At the moment I have only the one, but now I can easily add further menus in future if I need them.

The first thing I need to do is set up the menu items. You’ll notice on line 133 that I’m using a new function, calloc. Up to now, whenever I’ve needed to store data, I’ve created a new variable and that variable has been stored in the stack for the current scope. The compiler then manages the access to that variable and the lifetime of that variable. Sometimes though, I’ll want to take matters into my own hands. You might recall the analogy I used in an earlier post of C’s variable management being somewhat like a cloakroom attendant – I hand over one or more items, and get a ticket (variable name) for each one. The attendant will then manage the security of my items so that only I can access them and will remove them from the cloakroom (by handing them back to me) when I leave.

Imagine instead I go to a venue without a cloakroom, a private party let’s say. In this instance, I might ask my host where I can leave my coat and they will direct me to a free clothes hook in the hall or in a closet. But from this point on it’s down to me to manage my access to that item and to ensure I remove the item when I leave the party. C allows me to do a similar thing using something called the heap. This is the memory that is currently free but can be allocated for my program’s use. Think of it like the rows of clothes hooks in my host’s closets. I ask my host, C, for a chunk of memory. Just like my party host finds one or more free pegs for my use, C does the same with the free memory. It then gives me a pointer to that memory and leaves me to it. It’s now up to me to manage my access to that memory and ensure I free it up when I’m done (in the same way as I free up the clothes hook by removing my items as I leave the party).

There are two functions I can use to do this, malloc and calloc. The difference between them is that calloc will initialise all the bytes of allocated memory to zero and malloc won’t. The calloc function expects to receive two arguments. The first is the maximum number of items I intend to store and the second is the size of each of those items (incidentally, malloc takes only a single argument, which is the total size of the required memory block in bytes). In this case I am storing a series of pointers to pointers to instances of ITEM, which is the menu library’s structure for menu items.

The calloc function returns either a pointer to the start of the allocated memory, or a NULL pointer if the memory could not be allocated. In the latter case I end the program with an error as it can’t proceed if the menu cannot be created.

Note that the pointer returned by calloc is a void pointer. This means it does not have an associated type. To make it easier for me to use this pointer, I cast it to the required type, ITEM ** in this case. The benefit of this becomes apparent in the next section of code where I create each of the menu items in a loop. Because I have specified a type for the pointer to the menu items, the compiler is able to calculate the size for each item and determine where individual items start. This is a technique called pointer arithmetic. If I wanted to refer to the fourth item, for example, I could use hexmenus[i].menu_items + 3. Instead of adding the integer 3 to the address held in the pointer, this will add 3 * the size of the pointer type. I can use the +, -, ++, and -- operators to perform pointer arithmetic.

The compiler also has another handy trick, which is that, instead of using hexmenus[i].menu_items + 3, I can use array notation. So I’d refer to the fourth item as hexmenus[i].menu_items[3]. You can see I use array indexing on line 140 to point to individual items within the allocated memory block.

Once I have constructed my menu items, I create the menu on line 148.

Since I want the menu to appear on a specific window rather than on stdscr, I now need to associate the menu with that window, which I do with the set_menu_win function on line 156. This function expects to receive a pointer to a menu followed by a pointer to a window.

Optionally I can place the menu within a subwindow inside its parent window. This is useful when the menu is just one part of the data you will write to a larger window. Subwindows in ncurses are simply a way to reference a portion of an existing window. I create a subwindow using the derwin function on line 157. This function expects to receive a pointer to a window, followed by the height, width, y and x coordinate of the subwindow. One thing to note about subwindows is that they don’t have their own memory – when I read or write to a subwindow, I am simply reading from or writing to a portion of the memory allocated to the parent window.

I associate the subwindow with the menu using the set_menu_sub function on 163. This function expects a pointer to the menu, followed by a pointer to the subwindow.

I can specify a custom mark for the currently selected item in my menu. I do this with the set_menu_mark function on line 164. This expects to receive a pointer to the menu, followed by a string to define the mark. I defined the mark in hextrings.h as a > symbol followed by a space.

Finally I need to apply my menu to the window, using the post_menu function on line 167. This simply needs to be passed a pointer to the menu. Note that the menu won’t appear until I refresh the window. There’s no need to do that here, as it happens whenever I use my show_window function to show one of the game windows.

Earlier I mentioned that part of the bargain for having memory allocated to my program directly is that the program takes care of tidying up after itself. To return to my previous analogy, imagine you’re the party host and, every time you threw a party one or two guests forgot to take their coats or hats when they left. Over time, the clothes hooks in your closet would fill up with unclaimed items and soon there would be no room for new items. A similar thing can happen in a C program if I don’t release the memory I’ve been allocated. That memory can’t be reused for other processes, even though I am not using it any more. This type of fault is known as a memory leak.

To make sure I’m not guilty of this, I’ll add another function to tidy up my menus once I’ve finished with them. I’ve added this function to the bottom of hexwindows.c:

void destroy_menus() {                                                                                                      
    for (int i = 0; i < ARRAY_SIZE(hexmenus); i++) {                                                                            
        unpost_menu(hexmenus[i].menu);                                                                                          
        free_menu(hexmenus[i].menu);                                                                                                                                                                                                                    

        for (int j = 0; j < hexmenus[i].num_items; j++) {                                                                           
            free_item(hexmenus[i].menu_items[j]);                                                                                
        }
    }
} 

This loops through each menu, first removing the link to the window with the unpost_menu function, then freeing the memory allocated to the menu and each of the menu items, with the free_menu and free_item functions respectively.

As with my windows, I’ll need a way to find a specific menu, given the ID for that menu. I added this function to the bottom of hexwindows.c:

// Return a pointer to the struct that matches the menu uid or NULL if no match is found
struct hexmenu * get_hexmenu(enum menu_id m_id) {
    for (int i = 0; i < ARRAY_SIZE(hexmenus); i++) {
        if (hexmenus[i].uid == m_id) {
            return &hexmenu[i];
        }
    }

    return NULL;
}

There’s one more generic function I’ll need for menus, and that’s to handle the navigation within the menu. I added the following function to the bottom of hexwindows.c:

// Handle navigation for the given menu and return the index of the selected item
int menu_navigation(enum menu_id m_id) {
    struct hexmenu * m_ptr = get_hexmenu(m_id);
    struct hexwindow * ws_ptr = get_hexwindow(m_ptr->window);
    int c;

    while ((c = wgetch(ws_ptr->w_ptr)) != '\n') {
        switch(c) {
            case KEY_DOWN:
                menu_driver(m_ptr->menu, REQ_DOWN_ITEM);
               break;
            case KEY_UP:
                menu_driver(m_ptr->menu, REQ_UP_ITEM);
                break;
            case KEY_HOME:
                menu_driver(m_ptr->menu, REQ_FIRST_ITEM);
                break;
            case KEY_END:
                menu_driver(m_ptr->menu, REQ_LAST_ITEM);
                break;
        }

        show_window(m_ptr->window);
    }

    for (int i = 0; i < m_ptr->num_items; i++) {
        if (current_item(m_ptr->menu) == m_ptr->menu_items[i]) {
            return i;
        }
    }

    return -1;
}

This function expects to be given the uid of a menu. It then enters a loop between 199 and 216 to respond to navigation commands. The user can use the up and down arrow keys to move up and down the menu one selection at a time, or the HOME or END keys to move the first item or last item respectively. Note that selection of items within the menu is achieved with the menu_driver function. This function expects a pointer to a menu and then a command. Note that I call show_window each time there is a change, so that the window is updated.

The loop exits when the user presses the enter key to select an item. The function current_item returns a pointer to the currently selected menu item. I simply compare this in a loop to each of the menu_items to obtain an index number for the item, which is returned to the calling function.

Again, I must not forget to add function declarations to hexwindows.h for each of the four functions I’ve added.

Now I’ve created those generic menu functions, I’m ready to create the code to control the main menu window. I created a new file called menu.c with the following content:

//
// menu.c
// Hexapawn
//
// Manages the main menu for the Hexapawn game
//

#include "hexapawn.c"
#include "hexstrings.c"

void (* funcPtrs[])(void) = {
    &instructions_controller,
    &new_game_controller,
    &resume_game_controller,
    &ai_controller,
    &history_controller,
    &save_controller,
    &load_controller,
    &reset_controller
};

void main_menu_controller() {
    int running = 1;
    show_window(WIN_MAIN_MENU);

    while (running > 0) {
        int choice = menu_navigation(MENU_MAIN);

        if (choice != MENU_EXIT) {
            (*funcPtrs[choice])();
            show_window(WIN_MAIN_MENU);
        } else {
            running = 0;
        }
    }
}

void reset_controller() {
    // TO DO: Add reset controller code
}

I’ll define a controller for each of my windows. The pattern each controller follows will be similar. It will show its window and will then carry out any operations required for that window before returning to the calling function.

For my menu controller I enter a loop between lines 26 and 35. Note that this loop is controlled by an int called running, which I set to 1 on line 23. The loop will continue until running is set to 0.

The first thing that happens in the loop is that I hand over responsibility for menu navigation to the generic menu_navigation function I defined in hexwindows.c. As you saw, this function handles navigation between menu entries until the user hits enter to select an item. At that point it returns an int representing the position of the selected item in the menu.

I get this int into a variable, choice, on line 27. I will execute a different controller for each function a user selects from the menu. One obvious way to do this is with a switch statement, but this is a bit unwieldy. Instead I am using a type of pointer called a function pointer. Up to now, whenever I’ve needed to execute a function, I’ve called that function using its name. But actually whenever the compiler sees a function name it eventually converts that name into the address of the function in memory. Function pointers are a way of short-cutting that process. The advantage of this is that, like other pointers, function pointers can be stored in variables. In this case, I store the function pointers in an array on lines 11 – 20. When I declare a function pointer, I need to provide the return type (void in this case, as my controllers won’t return a value) and the arguments in parentheses (again, void in this case as my controllers don’t accept arguments). Note that to generate the pointers, I use just the name of the function, preceded by the reference operator &, because I want the address of the function, not its value.

The controllers are placed in the array in an order matching the position of the relevant menu item. This way I can simply use the choice variable as an index into that array. Line 30 shows how I call a function using a function pointer. This will execute the relevant controller, which will show its window and carry out the required operations before returning.

After the selected controller has returned, I show the menu window again and continue the loop.

The reset_controller function is a slightly special case as this will be handled inside menu.c. Note that I’ve defined an empty function on lines 38 – 40 that will simply return immediately when executed. I’ll add the functionality to this later. This is a common technique in programming known as creating a stub function. A stub function acts as a placeholder for a function yet to be written (or occasionally as a substitute for an existing function during testing). A stub function generally either does nothing or returns a passive value that matches the function’s return type, e.g. a stub for a function that will eventually report a statistic might return an arbitrary value like 50. Stub functions are useful for allowing you to test the functionality of parts of your program before you have completed the entire program.

Another special case is the Exit menu item. This item doesn’t have a matching controller function, so I need to check for it on line 29. If this menu item is selected, I simply set the running variable to 0 on line 33, which causes the loop to end and the menu controller will then return to the calling function.

I’ll need to add a header file for this file, called menu.h:

//
// menu.h
// Hexapawn
//
// Manages the main menu for the Hexapawn game
//

#ifndef __Hexapawn__menu__
#define __Hexapawn__menu__

void main_menu_controller();
void reset_controller();

#endif /* defined(__Hexapawn__menu__) */

Finally, I need to add the MENU_EXIT macro to hexapawn.h (I added this just below the MAIN_MENU_Y macro):

#define MENU_EXIT 8

And I include the menu header file at the bottom of hexapawn.h:

#include "menu.h"

To make my menu controller fully functional I now need to create stubs for the other controllers. All these stubs will do is show the relevant window and then wait for the user to press x before returning to the menu controller. This is sufficient to allow me to test that the correct window is being selected. I created the following files.

instructions.c

//
// instructions.c
// Hexapawn
//
// Manages the instructions window for Hexapawn
//

#include "hexapawn.h"
#include "hexstrings.h"

void instructions_controller() {
    show_window(WIN_INSTRUCTIONS);
    while(wgetch(get_hexwindow(WIN_INSTRUCTIONS)->w_ptr) != 'x');
}

instructions.h

//
// instructions.h
// Hexapawn
//
// Manages the instructions window in Hexapawn
//

#ifndef __Hexapawn__instructions__
#define __Hexapawn__instructions__

void instructions_controller(void);

#endif /* defined(__Hexapawn__instructions__) */

game.c

//
// game.c
// Hexapawn
//
// Manages the game in Hexapawn
//

#include "hexapawn.h"
#include "hexstrings.h"

void new_game_controller() {
    play_game(GAME_MODE_NEW);
}

void resume_game_controller() {
    play_game(GAME_MODE_RESUME);
}

void play_game(enum game_mode mode) {
    show_window(WIN_GAME);
    while(wgetch(get_hexwindow(WIN_GAME)->w_ptr) != 'x');
}

game.h

//
// game.h
// Hexapawn
//
// Manages the game in Hexapawn
//

#ifndef __Hexapawn__game__
#define __Hexapawn__game__

void new_game_controller(void);
void resume_game_controller(void);
void play_game(enum game_mode);

#endif /* defined(__Hexapawn__game__) */

ai.c

//
// ai.c
// Hexapawn
//
// Manages the game artificial intelligence browser for Hexapawn
//

#include "hexapawn.h"
#include "hexstrings.h"

void ai_controller() {
    show_window(WIN_AI);
    while(wgetch(get_hexwindow(WIN_AI)->w_ptr) != 'x');
}

ai.h

//
// ai.h
// Hexapawn
//
// Manages the game artificial intelligence browser in Hexapawn
//

#ifndef __Hexapawn__ai__
#define __Hexapawn__ai__

void ai_controller(void);

#endif /* defined(__Hexapawn__ai__ */

history.c

//
// history.c
// Hexapawn
//
// Manages the history browser for Hexapawn
//

#include "hexapawn.h"
#include "hexstrings.h"

void history_controller() {
    show_window(WIN_HISTORY);
    while(wgetch(get_hexwindow(WIN_HISTORY)->w_ptr) != 'x');
}

history.h

//
// history.h
// Hexapawn
//
// Manages the history browser in Hexapawn
//

#ifndef __Hexapawn__history__
#define __Hexapawn__history__

void history_controller(void);
#endif /* defined(__Hexapawn__history__) */

save_load.c

//
// save_load.c
// Hexapawn
//
// Manages the interface for saving and loading of game state for Hexapawn
//

#include "hexapawn.h"
#include "hexstrings.h"

void save_controller() {
    show_window(WIN_SAVE);
    while(wgetch(get_hexwindow(WIN_SAVE)->w_ptr) != 'x');
}

void load_controller() {
    show_window(WIN_LOAD);
    while(wgetch(get_hexwindow(WIN_LOAD)->w_ptr) != 'x');
}

save_load.h

//
// save_load.h
// Hexapawn
//
// Manages the interface for saving and loading of game states in Hexapawn
//

#ifndef __Hexapawn__save_load__
#define __Hexapawn__save_load__

void save_controller(void);
void load_controller(void);

#endif /* defined(__Hexapawn__save_load__) */

With those files created, I now need to update hexapawn.h. First I added a definition for the game_mode enum used in game.c. I added this just below the existing enum definitions in hexapawn.h:

enum game_mode {
    GAME_MODE_NEW,
    GAME_MODE_RESUME
};

Now I include all the header files. I added these to the bottom of hexapawn.h below the existing includes:

#include "instructions.h"
#include "game.h"
#include "ai.h"
#include "history.h"
#include "save_load.h"

I’ll adjust hexapawn.c now to call the menu controller when the game is started:

//
// hexapawn.c
// Hexapawn
//

#include "hexapawn.h"
#include "hexstrings.h"

int main(void) {
    // Start ncurses
    initialise_curses();

    // Build the game windows
    initialise_windows();

    // Build the menus
    initialise_menus();

    // Start the main menu
    main_menu_controller();

    // End ncurses
    destroy_menus();
    endwin();

    return 0;
}

You can see that main is now very simple. It performs some initialisation tasks and calls the menu_controller which takes care of dispatching all the game operations to the relevant functions. Then, when the menu_controller returns following an Exit command, it does a bit of tidying up before closing. Generally, this is about as complex as I ever want main to get, unless my program is very short and simple.

The last thing to do, before I can test all this new code, is update the makefile:

# makefile for Hexapawn

.DEFAULT_GOAL := build

build:
    @echo "Building hexapawn..."
    gcc hexapawn.c hexwindows.c hexerror.c menu.c instructions.c game.c ai.c history.c save_load.c -l menu -l ncurses -l panel -o ../hexapawn

Note that as well as adding all the new files to be compiled, I’m also now linking to the menu library.

When I now build and run the program I see my menu window with the main menu:

The Hexapawn game running, showing the main menu.
The Hexapawn main menu

I can navigate in the menu using the up and down arrow keys to move up or down one item at a time, the HOME key to move to the first item or the END key to move to the last item. If I press Enter or Return when any of the first seven items is shown then the relevant window will appear. I press x to return to the menu. Currently Reset AI and history will do nothing. Selecting Exit will end the program.

I’ve added quite a bit of code to create this menu, but not anything like as much as if I had built this functionality from scratch, so I hope you can see the power of using third party libraries whenever possible. Now that I have a solid framework in place, in the next part of this series I’ll introduce another powerful library, the Apache Portable Runtime, which I’ll use to create the underlying data structures for our game as well as handling file operations in a platform neutral way.

In the meantime, I’ve set myself the exercise of using the ncurses functions I’ve seen so far to draw a 3 x 3 grid on the game window in which the game will be played.

Published inGamesProgramming

Be First to Comment

Leave a Reply

Your email address will not be published. Required fields are marked *