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: creating a data structure
In the previous post, I got the game to the point where I had a functional main menu and I could display a board ready to start a game. For the player to be able to play the game though, I need a way of getting user input. As this game uses a text-based interface, rather than a graphical user interface, I will do that with a simple form.
Before I do that though, I want to prepare by doing two things. Firstly, in my previous work, I lumped the code for managing windows and the code for managing menus together in a single file. Now that I’m also about to add code for managing forms, it’s all going to be a little bit unwieldy as a single file. So the first thing I’ll do is split the menu management code out into its own file.
Secondly, I’ve discovered that debugging an ncurses app is trickier than debugging a standard terminal app. The main reason for this is that ncurses takes over the terminal, which means I can’t simply use a printf statement to write out useful debug info to the terminal, as it won’t be seen until the app ends. My research has also indicated that ncurses doesn’t play nicely with debuggers like GDB because ownership of keystrokes becomes an issue.
So I’m going to pre-empt all these issues by building in a simple debugging mode. First I added a new window to display debug information and then a new file to implement this:
/** * @file debug.c * @brief Manages displaying messages in the debug window * @author Laurence Scotford */ #include "hexapawn.h" static bool debug_mode = false; static char err_str[DEBUG_STR_LENGTH]; void set_debug_mode(bool mode) { debug_mode = mode; } bool get_debug_mode() { return debug_mode; } void debug_message(char * message, ...) { if (debug_mode) { va_list args; va_start (args, message); vsprintf(err_str, message, args); va_end(args); hexwindow_t * debug_win = get_hexwindow(WIN_DEBUG); wclear(debug_win->w_ptr); wprintw(debug_win->w_ptr, err_str); wrefresh(debug_win->w_ptr); } }
As you can see in line 9 of debug.c, the debugger has a simple boolean value, debug_mode that is used to turn debug mode on and off. The getter and setter from lines 12 – 18 enable access to this by external functions. Note, it would be simpler to just expose the debug_mode variable, but having a getter and setter gives me the opportunity to make this functionality a bit more sophisticated in future, if need be, without breaking the interface.
The debug_message function (lines 20 – 32) writes the debug message to the new window. An interesting feature of this function is the passing of variable arguments to a variadic function. A variadic function is one that accepts a variable number of arguments. An example is sprintf, which writes a formatted string to memory. This accepts a formatted string as its initial argument and then any number of further arguments to match the specifiers in the formatted string.
I want to be able to show formatted strings in the debug window, so I can embed variable values, etc., so I’ve also made my debug_message a variadic function, by using the … operator as the second parameter. This indicates to the compiler that a variable number of unnamed arguments will follow the initial argument. And here we hit a small hitch, I can’t simply pass on my arguments to sprintf.
Fortunately, there is a special version of sprintf called vsprintf which will accept variadic arguments from an enclosing function. To pass these, I need to use the va_list object and the va_start and va_end macros as shown in lines 23 – 25.
You can see the new debug window in the screenshot below with its initial message:
Notice the window is purposefully bright green, so there’s no danger of me accidentally shipping production code with debug switched on!
Now sending a debug message to the window is a simple as calling the debug_message function. For example, if I wanted to show the value of an int called score and a string called outcome, I could use:
debug_message("The final score is: %d and the outcome is: %s", score, outcome);
The debug facility is quite primitive. One issue in particular is that it only shows the most current message. If I find I need more sophisticated functionality, for example, the ability to pause when a message is shown or the ability to show a scrolling history of messages, then I will have to add this in the future as the need arises.
With those conveniences in place I can now turn my attention to getting input from the user. Similar to the menus library, ncurses also comes with an optional forms library. As I did with the menus, I created a new file with some generalised form handling functions:
/** * @file hexforms.c * @brief Functions for managing the forms in Hexapawn * @author Laurence Scotford */ #include "hexapawn.h" static apr_pool_t * hex_forms_pool; static hexform_t hexforms[] = { {FORM_PLAYER_MOVE, NULL, WIN_MOVE_FORM, NULL, MOVE_FORM_Y, MOVE_FORM_X, MOVE_FORM_HEIGHT, MOVE_FORM_WIDTH, MOVE_FORM_FIELDS, NULL, (form_field_t[MOVE_FORM_FIELDS]){ { MOVE_FORM_HEIGHT, MOVE_FORM_FIELD_WIDTH, MOVE_FORM_FIELD_Y, MOVE_FORM_FROM_FIELD_X, 0, 0, STR_MOVE_FORM_FROM, MOVE_FORM_FIELD_Y, MOVE_FORM_FROM_LABEL_X }, { MOVE_FORM_HEIGHT, MOVE_FORM_FIELD_WIDTH, MOVE_FORM_FIELD_Y, MOVE_FORM_TO_FIELD_X, 0, 0, STR_MOVE_FORM_TO, MOVE_FORM_FIELD_Y, MOVE_FORM_TO_LABEL_X } } } }; /** * @brief Initialise forms */ void initialise_forms() { // Create a data pool for hexforms if (apr_pool_create(&hex_forms_pool, NULL) != APR_SUCCESS) { exit_with_error(STR_DATA_ERROR); } for (int i = 0; i < ARRAY_SIZE(hexforms); i++) { // Create the form fields hexforms[i].fields = apr_palloc(hex_forms_pool, sizeof(FIELD *) * hexforms[i].num_fields + 1); if (hexforms[i].fields == NULL) { exit_with_error(STR_FORM_ERROR); } int rows, cols; WINDOW * window = get_hexwindow(hexforms[i].window)->w_ptr; for (int j = 0; j < hexforms[i].num_fields; j++) { form_field_t field_desc = hexforms[i].field_list[j]; hexforms[i].fields[j] = new_field(field_desc.height, field_desc.width, field_desc.y, field_desc.x, field_desc.vis_rows, field_desc.buffers); if (hexforms[i].fields[j] == NULL) { exit_with_error(STR_FORM_ERROR); } set_field_opts(hexforms[i].fields[j], O_VISIBLE | O_PUBLIC | O_EDIT | O_ACTIVE | O_STATIC | O_BLANK); set_field_back(hexforms[i].fields[j], A_UNDERLINE); } hexforms[i].fields[hexforms[i].num_fields] = NULL; // Create the form hexforms[i].form = new_form(hexforms[i].fields); if (hexforms[i].form == NULL) { exit_with_error(STR_FORM_ERROR); } // Set the form's window set_form_win(hexforms[i].form, window); scale_form(hexforms[i].form, &rows, &cols); hexforms[i].subwindow = derwin(window, rows, cols, 0, 0); if (hexforms[i].subwindow == NULL) { exit_with_error(STR_FORM_ERROR); } set_form_sub(hexforms[i].form, hexforms[i].subwindow); int result = post_form(hexforms[i].form); if (result != E_OK) { exit_with_error(STR_FORM_ERROR); } // Draw the labels on the parent form for (int j = 0; j < hexforms[i].num_fields; j++) { form_field_t field_desc = hexforms[i].field_list[j]; mvwprintw(window, field_desc.label_y, field_desc.label_x, field_desc.label); } pos_form_cursor(hexforms[i].form); wrefresh(window); } } /** * @brief Return a pointer to the struct that matches the form uid or NULL if no match is found * @param f_id The unique ID of the required form * @returns A pointer to the structure describing the form or NULL if a match cannot be found */ hexform_t * get_hexform(form_id_t f_id) { for (int i = 0; i < ARRAY_SIZE(hexforms); i++) { if (hexforms[i].uid == f_id) { return &hexforms[i]; } } return NULL; } /** * @brief Safely destroy the game forms */ void destroy_forms() { for (int i = 0; i < ARRAY_SIZE(hexforms); i++) { unpost_form(hexforms[i].form); free_form(hexforms[i].form); for (int j = 0; j < hexforms[i].num_fields; j++) { free_field(hexforms[i].fields[j]); } } apr_pool_destroy(hex_forms_pool); } /** * @brief Handle navigation for the given form * @param f_id The unique ID of the form to be navigated * @param c The numeric representation of the character passed through from the navigation function * @returns A nav_ouput struct indicating that navigation should continue */ nav_output_t form_navigation(form_id_t f_id, int c) { hexform_t *form = get_hexform(f_id); FORM *f_ptr = form->form; form_driver(f_ptr, REQ_OVL_MODE); switch(c) { case '\t': form_driver(f_ptr, REQ_NEXT_FIELD); break; case KEY_BTAB: form_driver(f_ptr, REQ_PREV_FIELD); default: form_driver(f_ptr, c); } show_window(form->window); return (nav_output_t){true, -1}; } /** * @brief Return a pointer to the current value for the given field in given form * @param f_id The unique ID of the form to be queried * @param field_num The zero-indexed number of the field to query * @returns A pointer to the field buffer */ char * get_form_field_pointer(form_id_t f_id, int field_num) { hexform_t *form = get_hexform(f_id); form_driver(form->form, REQ_VALIDATION); // Ensure current value is written to buffer return field_buffer(form->fields[field_num], 0); } /** * @brief Resets all the fields in a form * @param f_id The unique ID of the form to be reset * @param alert True if an audio alert is to accompany the reset */ void reset_form(form_id_t f_id, bool alert) { if (alert) { beep(); } hexform_t *form = get_hexform(f_id); form_driver(form->form, REQ_FIRST_FIELD); for (int i = 0; i < ARRAY_SIZE(form->fields) - 1; i++) { form_driver(form->form, REQ_CLR_FIELD); form_driver(form->form, REQ_NEXT_FIELD); } }
The initialise code (lines 23 to 86) sets up all the forms in the game. At present there is only one, defined in lines 11 to 18, which I’ll use to get the player’s next move in the game window. There will be further forms added as I develop the other screens in the game.
The form and form_field data structures are defined in hexapawn.h as foillows:
/** * @brief Describes a form field */ typedef struct _form_field_t { int height; ///< The height of the field in characters int width; ///< The width of the field in characters int y; ///< The y position of the top-left corner of the field int x; ///< The x position of the top-left corner of the field int vis_rows; ///< The number of visible rows (or 0 if all rows visible) int buffers; ///< The number of additional buffers const char * label; ///< The label to appear with the field int label_y; ///< The y position of the label int label_x; ///< The x position of the label } form_field_t; /** * @brief Describes a form */ typedef struct _hexform_t { form_id_t uid; ///< The unique ID for the form FORM * form; ///< A pointer to the form data window_id_t window; ///< The uid of the window to which the form belongs WINDOW * subwindow; ///< A pointer to the subwindow in which the fields of the form will be drawn int form_y; ///< The y position of the top left corner of the form int form_x; ///< The x position of the top left corner of the form int form_height; ///< The height of the form int form_width; ///< The width of the form int num_fields; ///< The number of fields in the form FIELD ** fields; ///< A pointer to an array of fields form_field_t * field_list; ///< A pointer to an array of field descriptors } hexform_t;
Now that I have both menus and forms potentially co-existing on the same screen, I can no longer allow the menu code to exclusively own the navigation. So I added a further file, hexinput.c to handle the initial input and then dispatch events to the relevant code:
/** * @file hexinput.c * @brief Functions for managing user input in Hexapawn * @author Laurence Scotford */ #include "hexapawn.h" /** * @brief Handle navigation within a screen for a given menu and/or a given item * @param m_id The unique ID of the menu to be navigated (MENU_NONE if none) * @param f_id The unique ID of the form to be navigated (FORM_NONE if none) * @returns An output code when navigation ends */ int navigation_dispatch(menu_id_t m_id, form_id_t f_id) { hexmenu_t *m_ptr = NULL; // Store pointer to menu hexform_t *f_ptr = NULL; // Store pointer to form hexwindow_t *ws_ptr = NULL; // Store pointer to common window nav_output_t nav_output; // Stores the output from each nav operation int c; // Used to collect input characters // Get pointers to the relevant menu and / or form if (m_id != MENU_NONE) { m_ptr = get_hexmenu(m_id); } if (f_id != FORM_NONE) { f_ptr = get_hexform(f_id); } // Check the combination of items is valid if (!(m_ptr || f_ptr)) { exit_with_error(STR_INVALID_NAV_DISPATCH); } // If there is both a form and a menu, get lowest common window if (m_ptr && f_ptr) { window_id_t commonWindow = get_lowest_common_window(m_ptr->window, f_ptr->window); // Get the pointer to this window, if it exists if (commonWindow != WIN_NONE) { ws_ptr = get_hexwindow(commonWindow); } else { exit_with_error(STR_INVALID_NAV_DISPATCH); } } else { // Otherwise set the window pointer to the relevant window for the single item ws_ptr = get_hexwindow(m_ptr ? m_ptr->window : f_ptr->window); } nav_output = (nav_output_t){ true, 0 }; // Continue dispatching navigation events until the end of nav is signalled while (nav_output.in_nav) { c = wgetch(ws_ptr->w_ptr); switch(c) { case KEY_DOWN: case KEY_UP: case KEY_LEFT: case KEY_RIGHT: case KEY_HOME: case KEY_END: case '\n': // dispatch to menu if one is set if (m_ptr) { nav_output = menu_navigation(m_id, c); } break; default: // dispatch to form if one is set if (f_ptr) { nav_output = form_navigation(f_id, c); } } } return nav_output.output; }
The navigation_dispatch function expects to receive either a menu or a form reference or both. If either a menu or form is not present on the window calling the function, it can send either MENU_NONE or FORM_NONE respectively. These enum values are defined in hexapawn.h.
Note in lines 37 – 49, that because menus and forms use subwindows, if both are present, the navigation_dispatch function looks for the lowest common window and takes its input from there. To facilitate this, I added a new function to hexwindows.c:
/** * @brief Get lowest common window * @param win_id_1 The unique ID of the first window * @param win_id_2 The unique ID of the second window * @returns The ID of the lowest common window or null if none was found */ window_id_t get_lowest_common_window(window_id_t win_id_1, window_id_t win_id_2) { window_id_t match = WIN_NONE; // Will be set to lowest matching window if found bool match_failed = false; // Will be set to true if no match found // Create temporary stores for current windows while walking the tree window_id_t current_win_1 = win_id_1; window_id_t current_win_2 = win_id_2; // Walk the tree to try and find a match do { // If the current IDs match, we're done if (current_win_1 == current_win_2) { match = current_win_1; } else { // Otherwise get the parent of the current window 2 window_id_t next_win_2 = get_hexwindow(current_win_2)->parent_id; if (next_win_2 != WIN_NONE) { // If there is a parent, this is what we'll compare next current_win_2 = next_win_2; } else { // If there isn't, reset window 2 and get the parent of current window 1 current_win_2 = win_id_2; window_id_t next_win_1 = get_hexwindow(current_win_1)->parent_id; if (next_win_1 != WIN_NONE) { // If there is a parent, this is what we'll compare next current_win_1 = next_win_1; } else { // If there isn't, we've explored the whole tree without finding a match match_failed = true; } } } } while (match == WIN_NONE && !match_failed); // Return a match if we found one, or WIN_NONE if we didn't return match; }
This simply walks up the windows hierarchy until it finds a parent window that is common to both windows and returns that.
Because I’ve established that arrow keys and the HOME, END and ENTER keys are used to navigate menus and TAB and BACKTAB keys are used to navigate forms, it is simply a case, in lines 54 – 77 of hexinput.c, of using a switch statement to dispatch to the correct function based on the key pressed. Note that any keys not used by menu are dispatched to the form handler. This is because the form handler also needs to accept typed alphanumeric input as well as navigation keys.
The revised handler for menus and the new handler for forms need to send an output value but also to indicate if navigation in the current window should continue. To facilitate this, I defined a new structure in hexapawn.h to enable both values to be returned.
/** * @brief Describes the output from a navigation action */ typedef struct _nav_output_t { bool in_nav; ///< True if navigation should continue int output; ///< The value to be passed back to the caller } nav_output_t;
Additionally, as the menus now potentially have to play nicely with a form, it was necessary to update the menu structure in hexapawn.h to include a form reference:
/** * @brief Describes a game menu */ typedef struct _hexmenu_t { menu_id_t uid; ///< The unique ID for the menu window_id_t window; ///< The uid of the window to which the menu belongs WINDOW * subwindow; ///< A pointer to the subwindow in which the menu will be drawn MENU * menu; ///< A pointer to the menu data ITEM ** menu_items; ///< A pointer to an array of menu items form_id_t form; ///< The ID of a form associated with this menu int menu_y; ///< The y position of the menu in the subwindow int menu_x; ///< The x position of the menu in the subwindow int menu_height; ///< The height of the menu int menu_width; ///< The width of the menu int num_items; ///< The number of items in the menu menu_orientation_t orientation; ///< Whether the menu displays horizontally or vertically char ** item_list; ///< A pointer to an array of strings describing the menu items } hexmenu_t;
The revised handler for menus now looks like this:
/** * @brief Handle navigation for the given menu and return the index of the selected item * @param m_id The unique ID of the menu to be navigated * @param c The numeric representation of the character passed through from the navigation function * @returns A nav_ouput struct indicating if navigation should continue and the * position of the selected item (0 = top item in list) if it should not */ nav_output_t menu_navigation(menu_id_t m_id, int c) { hexmenu_t * m_ptr = get_hexmenu(m_id); nav_output_t nav_output = {true, -1}; switch(c) { case KEY_DOWN: if (m_ptr->orientation == MENU_VERTICAL) { menu_driver(m_ptr->menu, REQ_DOWN_ITEM); } break; case KEY_UP: if (m_ptr->orientation == MENU_VERTICAL) { menu_driver(m_ptr->menu, REQ_UP_ITEM); } break; case KEY_LEFT: if (m_ptr->orientation == MENU_HORIZONTAL) { menu_driver(m_ptr->menu, REQ_LEFT_ITEM); } break; case KEY_RIGHT: if(m_ptr->orientation == MENU_HORIZONTAL) { menu_driver(m_ptr->menu, REQ_RIGHT_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; case '\n': for (int i = 0; i < m_ptr->num_items; i++) { if (current_item(m_ptr->menu) == m_ptr->menu_items[i]) { nav_output = (nav_output_t){false, i}; break; } } break; } if (m_ptr->form != FORM_NONE) { pos_form_cursor(get_hexform(m_ptr->form)->form); } show_window(m_ptr->window); return nav_output; }
The main difference is that the handler no longer has a loop (this is now in the outer nav handler), it simply processes a single character and then returns the output code inside the nav_output struct. Note in line 138 that the in_nav element of nav_output defaults to true so that navigation continues. It is only set to false on line 170 if the enter key is pressed to select an item.
The other change here, is on lines 177 – 179. If the menu has an associated form, I need to reset the cursor back to the form after a change to the menu.
The handler for forms is simpler and can be seen on lines 119-143 of hexforms.c above. It simply processes TAB and BACKTAB characters for navigating the form and passes any other character on to the form driver for input to the current form field.
The complete form for entering a player move looks like this:
The modifications to game.c to make use of the form are:
/** * @brief Sets up and plays a game in the requested mode * @param mode An enum (GAME_MODE_NEW or GAME_MODE_RESUME) to indicate the game mode */ void play_game(game_mode_t mode) { empty_sprite = (char_run_t[EMPTY_SPRITE_DATA_LENGTH]){ {50, ' '} }; pawn_sprite = (char_run_t[PAWN_SPRITE_DATA_LENGTH]){ {4, ' '}, {1, '/'}, {1, '\\'}, {8, ' '}, {1, '\\'}, {1, '/'}, {8, ' '}, {2, ACS_VLINE}, {7, ' '}, {1, '/'}, {2, ' '}, {1, '\\'}, {5, ' '}, {1, ACS_LLCORNER}, {4, ACS_HLINE}, {1, ACS_LRCORNER}, {2, ' '} }; char *temp_str = calloc(30, sizeof(char)); //Temporary fix to stop resume game breaking mode = GAME_MODE_NEW; // If starting a new game, create a new game in the linked list if (mode == GAME_MODE_NEW) { current_game = create_new_game(); } else { // TO DO: Resume a game in progress } draw_board(); show_window(WIN_GAME); show_window(WIN_MOVE_FORM); int choice; curs_set(1); // Switch on the cursor display_prompt(STR_ENTER_MOVE); reset_form(FORM_PLAYER_MOVE, false); do { choice = navigation_dispatch(MENU_PLAY, FORM_PLAYER_MOVE); if (choice == PLAY_MOVE) { char * from_ptr = get_form_field_pointer(FORM_PLAYER_MOVE, 0); char * to_ptr = get_form_field_pointer(FORM_PLAYER_MOVE, 1); int from = get_cell_number(from_ptr); int to = get_cell_number(to_ptr); if (from < 1 || to < 1 || from > 9 || to > 9) { display_prompt(STR_ENTER_VALID_MOVE); reset_form(FORM_PLAYER_MOVE, true); } else { sprintf(temp_str, "You're moving from %i to %i!", from, to); display_prompt(temp_str); reset_form(FORM_PLAYER_MOVE, false); } } } while (choice != PLAY_EXIT); curs_set(0); // Switch off the cursor free(temp_str); } /** * @brief Gets a cell number from the given field buffer * @param buffer A pointer to buffer for a form field * @returns An integer number that was entered or -1 if a non-integer was entered */ int get_cell_number(char * buffer) { errno = 0; long val = strtol(buffer, NULL, 10); if (errno == EINVAL) { return -1; } return (int)val; } /** * @brief Displays a prompt * @param prompt A pointer to the prompt string to be displayed */ void display_prompt(char * prompt) { write_to_rect(WIN_GAME, prompt, GAME_PROMPT_Y, GAME_PROMPT_X, GAME_PROMPT_HEIGHT, GAME_PROMPT_WIDTH); }
Note the new function get_cell_number that reads a cell number from a form input buffer. For now the play_game function simply displays the numbers that were entered in the form.
But this now gives me everything I need to begin implementing the code to play a full game, and I’ll work on this in the next post.
Be First to Comment