Skip to content

Games programming from the ground up with C: Creating a data structure for the game

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 a menu

In the previous post, I left myself with the challenge of creating some code to draw a 3×3 grid in the game window on which the game will be played. Here’s how I approached this task.

Up to now I’ve been using the default colours of the terminal window, but I’m going to start introducing some other colours now. That means I need to update the initialisation code in hexwindows.c:

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

    // Check that terminal window is of a sufficient size to play
    if (LINES < WIN_MAIN_HEIGHT || COLS < WIN_MAIN_WIDTH) {
        exit_with_error(STR_TERMINAL_TOO_SMALL);
    }

    // Check that terminal window supports colour
    if (has_colors() == FALSE) {
        exit_with_error(STR_NO_COLOUR);
    }

    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
    start_color();  // Initialise colour mode
    init_pair(BLACK_GREEN_PAIR, COLOR_BLACK, COLOR_GREEN);
    init_pair(WHITE_GREEN_PAIR, COLOR_WHITE, COLOR_GREEN);
    refresh();      // Draw stdscr (workaround for windows bug)
}

There’s a new check to make sure the terminal supports colour by using the has_colors function. Before colour can be used the start_color function must be run. Pairs of foreground and background colours can be set up using the init_pair function. This expects a number for the pair followed by the foreground can background colours. COLOR_BLACK, COLOR_WHITE and COLOR_GREEN are standard colour values defined by ncurses. Note that BLACK_GREEN_PAIR and WHITE_GREEN_PAIR are numbers defined in an enum in hexapawn.h.

 typedef enum _hex_colour_pairs_t {
    BLACK_GREEN_PAIR = 1;
    WHITE_GREEN_PAIR;
} hex_colour_pairs_t;

Note that these are set to start at 1 because 0 is not a valid colour pair number. Also note that from this point forward I’m defining all enums and structs using typedefs so that I only have to use the type name without prefixing it with enum or struct. There is also a new entry to add to hexstrings.h:

#define STR_NO_COLOUR "Your terminal does not support colour, which is needed for Hexapawn"

In game.c I’ve declared two pointers with file scope that will point to the definitions of the text sprites for a blank space and a pawn:

static char_run_t * empty_sprite = NULL;
static char_run_t * pawn_sprite = NULL;

These are defined in the play_game function, which also now calls a draw_board function. This is one of four new functions:

/**
 * @brief Sets up and plays a game in the requested 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, ' '}
    };

    draw_board();
    show_window(WIN_GAME);
    while(wgetch(get_hexwindow(WIN_GAME)->w_ptr) != 'x');
}

/**
 * @brief Draws the game board
 */
void draw_board() {
    WINDOW * win = get_hexwindow(WIN_GAME)->w_ptr;
    wattron(win, COLOR_PAIR(WHITE_GREEN_PAIR));

    // Draw horizontal dividers and cross pieces
    for (int i = 0; i <= GRID_SIZE_Y; i++) {
        switch(i) {
            case 0:
                draw_horiz_board_divider(BOARD_Y, ACS_ULCORNER, ACS_TTEE, ACS_URCORNER);
                break;
            case GRID_SIZE_Y:
                draw_horiz_board_divider(BOARD_Y + BOARD_CELL_HEIGHT * i + i, ACS_LLCORNER, ACS_BTEE, ACS_LRCORNER);
                break;
            default:
                draw_horiz_board_divider(BOARD_Y + BOARD_CELL_HEIGHT * i + i, ACS_LTEE, ACS_PLUS, ACS_RTEE);
                break;
        }
    }

    // Draw vertical dividers
    for (int i = 0; i <= GRID_SIZE_Y; i++) {
        draw_vert_board_divider(BOARD_X + i * BOARD_CELL_WIDTH + i);
    }

    // Add numbers to squares on board
    for (int i = 0; i < GRID_SIZE_Y * GRID_SIZE_X; i++) {
        position_t pos = get_position(i);
        int y_pos = BOARD_Y + 1 + pos.y * BOARD_CELL_HEIGHT + pos.y;
        int x_pos = BOARD_X + 1 + pos.x * BOARD_CELL_WIDTH + pos.x;
        mvwaddch(win, y_pos, x_pos, '1' + i);
    }

    wattroff(win, COLOR_PAIR(WHITE_GREEN_PAIR));

    // Draw a blank piece in each position on the board
    for (int i = 0; i <= GRID_SIZEY * GRID_SIZE_X; i++) {
        draw_piece(i, PIECE_EMPTY);
    }
}

/**
 * @brief Draws a horizontal divider
 * @param y The y position to draw the divider at within the game window
 * @param left_edge The character to use at the join on the left edge of the divider
 * @param middle The character to use at joins in the middle of the divider
 * @param right_edge The character to use at the join on the right edge of the divider
 */
void draw_horiz_board_divider(int y, chtype left_edge, chtype middle, chtype right_edge) {
    WINDOW * win = get_hexwindow(WIN_GAME)->w_ptr;

    // Draw left edge join at starting position
    mvwaddch(win, y, BOARD_X, left_edge);

    // Draw a horizontal line for each cell, ending with an appropriate join character
    for (int i = 0; i < GRID_SIZE_X; i++) {
        mvwhline(win, y, BOARD_X + i * (BOARD_CELL_WIDTH) + i + 1, ACS_HLINE, BOARD_CELL_WIDTH);

        chtype join_char = (i < GRID_SIZE_X -1 ? middle : right_edge);
        mvwaddch(win, y, BOARD_X + BOARD_CELL_WIDTH + i * (BOARD_CELL_WIDTH) + i + 1, join_char);
    }
}

/**
 * @brief Draws a vertical divider
 * @param x The x position to draw the divider at within the game window
 */
void draw_vert_board_divider(int x) {
    WINDOW * win = get_hexwindow(WIN_GAME)->w_ptr;

    // Draw a vertical line for each cell, leaving gaps for the joins (drawn by draw_horizontal_board_divider)
    for (int i = 0; i < GRID_SIZE_Y; i++) {
        mvwvline(win, BOARD_Y + 1 + BOARD_CELL_HEIGHT * i + i, x, ACS_VLINE, BOARD_CELL_HEIGHT);
    }
}

/**
 * @brief Draws a single piece on the board
 * @param location A number representing the board cell in which to draw the piece (0-9)
 * @param piece_type The type of piece to draw
 */
void draw_piece(int location, enum piece piece_type) {
    char_run_t * piece_sprite;
    piece_sprite = (piece_type == PIECE_EMPTY ? empty_sprite : pawn_sprite);

    WINDOW * win = get_hexwindow(WIN_GAME)->w_ptr;

    wattron(win, COLOR_PAIR(piece_type == PIECE_BLACK ? BLACK_GREEN_PAIR : WHITE_GREEN_PAIR));

    // Calculate position of top left character in sprite
    position_t pos = get_position(location);
    int y_pos = BOARD_Y + 1 + pos.y * BOARD_CELL_HEIGHT + pos.y;
    int x_pos = BOARD_X + 1 + pos.x * BOARD_CELL_WIDTH + pos.x;

    int char_run_index = -1;    // Current poisition in sprite definition
    int char_run_count = 0;     // Number of instances of current character left to display
    chtype current_char;        // The character currently being written

    for (int i = 0; i < BOARD_CELL_HEIGHT; i++) {
        // Move cursor to start of current line (note 1st character of 1st line is skipped over to avoid overwriting cell numbers)
        wmove(win, y_pos + i, i == 0 ? x_pos + 1 : x_pos);

        for (int j = 0; j < BOARD_CELL_WIDTH; j++) {
            // If all the instances of the current character have been drawn (or first character is needed) get the next character to draw
            if (char_run_count == 0) {
                char_run_index++;
                char_run_count = piece_sprite[char_run_index].count;
                current_char = piece_sprite[char_run_index].character;
            }

            // Draw the character (note 1st character of 1st line is skipped over to avoid overwriting cell numbers)
            if (i > 0 || j > 0) {
                waddch(win, current_char);
            }
            char_run_count--;
        }
    }

    wattroff(win, COLOR_PAIR(piece_type == PIECE_BLACK ? BLACK_GREEN_PAIR : WHITE_GREEN_PAIR));
}

I also updated game.h with the four new function declarations. Note that now that the code is growing larger I’m adding Doxygen compatible comments to each function.

Notice that the new draw_board function is called before I show the window. This ensures it can be seen as soon as the window appears.

The draw_board function consists of four loops: one to draw the horizontal dividers, one to draw the vertical dividers, one to add numbers to the top-left corner of each cell and one to draw a blank piece in each cell. Note that the cross pieces (i.e. the characters that are drawn where the horizontal and vertical lines meet) are taken care of by draw_horiz_board_divider, which is why this function is a little more complex and wants to receive both the y position where the line is to be drawn and the characters to use for the left join, middle joins and right join, as these will change depending on whether the line is at the top, middle or bottom of the grid. I’ve used a switch statement in draw_board to call draw_horiz_board_divider with different arguments depending on that position.

Because draw_horiz_board_divider has already done all the hard work of drawing the joins, draw_vert_board_divider can be simpler and just wants the x position of the line.

The final loop draws a blank piece in each position on the board. This calls the draw_piece function. This uses the piece definitions, which are encoded as a sequence of run-lengths and characters when the play_game function first starts. The empty space is nice and easy because it’s just 50 spaces, but the pawn is more complex as it’s a combination of spaces, characters from the standard character set and the special drawing characters I introduced earlier when I was building the windows for the game. Note that this routine suppresses the drawing of the very first character of the sprite so it does not overwrite the number in each board position.

The draw_piece function and the loop to draw the numbers make use of a function to convert a single digit position to y and x values, which I’ll put into a new file called hexutils.c (and a corresponding hexutils.h with the function declaration). I discuss this further later in this post, but here’s the file:

/**
 * @file hexutils.c
 * @brief Utility functions for Hexapawn
 * @author Laurence Scotford
 */

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

/**
 * @brief Converts a single digit board position to a y and x position on the board
 * @param index The single digit position on the board
 * @returns A position_t structure holding the y and x position
 */
position_t get_position(int index) {
    position_t pos;
    pos.y = index / GRID_SIZE_X;
    pos.x = index % GRID_SIZE_X;
    return pos;
}

The other thing to note about this code is that it follows my previous pattern of using macros instead of hard coded values. This makes it easy to change the size and position of the grid, or even the number of cells in the grid. I added these macros to hexapawn.h in addition to including hexutils.h:

#define BOARD_CELL_HEIGHT 5
#define BOARD_CELL_WIDTH 10
#define BOARD_X 2
#define BOARD_Y 3
#define BORDER_WIDTH 1
#define EMPTY_SPRITE_DATA_LENGTH 1
#define GRID_SIZE_X 3
#define GRID_SIZE_Y 3
#define PAWN_SPRITE_DATA_LENGTH 17

/**
 * @brief The possible piece types that can occupy a board position
 */
typedef enum _piece_t {
    PIECE_EMPTY,
    PIECE_WHITE,
    PIECE_BLACK
} piece_t;

/**
 * @brief Describes a character and run length for use in a character sprite definition
 */
typedef struct _char_run_t {
    int count;          ///< The number of times the character is to be repreated
    chtype character;   ///< The character to display
} char_run_t;

/**
 * @brief Describes a position on the game board
 */
typedef struct _position_t {
    int y;  ///< The y position or row (0 = top row)
    int x;  ///< The x position or column (0 = left column)
} position_t;

When I build and run the program, and select either New game or Resume game from the menu, I see that the game window now has a blank grid. Note that I’ve placed the grid on the left side of the screen, because I will eventually use the right side for instructions, a game log and controls. It also has a green background so that I can show the pawns with a black or white outline.

The game window in Hexapawn showing the blank game grid.
The game window in Hexapawn showing the blank game grid

By changing the piece in the loop that draws the blank pieces, I can also use this to check that the pawns are drawing correctly:

The game grid filled with black pawns.
The grid filled with black pawns

Ok that was a fun little diversion to exercise more of the power of ncurses. But before I work further on the interface, I need to turn my attention to the underlying data structure for the game. When I’m planning the data structure for a game, I find it’s useful to start by listing all the things I need. Here’s a list I came up with for Hexapawn of things that I’ll need to have data representations for:

  • the board
  • the pieces on the board
  • a single move
  • which player is due to move next
  • the logged moves for a single game, including a date and time stamp for the start and end of the game, and the current state of the game: whether it’s in progress or has been won, which player won and how it was won, e.g. resignation, piece reached end of board, all pieces taken or no more legal moves
  • a chronological history of all the games played
  • the board configurations for the AI player to use
  • possible AI moves for each configuration including whether the move is enabled or disabled and, for disabled moves, a link to the game that caused that move to be disabled
  • a way of persisting all this information in a file.

Let me think about how I might implement each of these, starting with the board and the pieces. It’s a 3×3 grid, and each cell within that grid can either be empty or occupied by a white pawn or a black pawn. One possibility is to use a 2-dimensional array. I declare this in a similar way to a 1-dimensional array. If, for example, I decided to use an int to represent the content of each cell, I could do this:

// Assuming that 0 = empty, 1 = white pawn and 2 = black pawn
int board[3][3];

// Place a white pawn in the top-left corner
board[0][0] = 1;

// Place a black pawn in the bottom-right corner
board[2][2] = 2;

// Empty the middle cell
board[1][1] = 0;

Seems like a sensible solution right? However, there is a catch. You’ll notice that one of the other things I need to represent on my list is a move. For each move I’ll need to store the starting cell and the ending cell, and, with this scheme, for each of these items I’ll need to store an x and y position. So, I could imagine a structure like this:

struct move {
    int start_x;
    int start_y;
    int end_x;
    int end_y;
};

Because I’ll be logging each game, I’ll be storing a lot of moves. If I was to store the board as a 1-dimensional array instead, it’s actually a fairly simple matter to convert a two-dimensional reference to an index into a 1 dimensional array, if I wanted to keep using a 2D coordinate system:

// Assuming that 0 = empty, 1 = white pawn and 2 = black pawn
int board[9];

// Place a white pawn in the top-left corner
update_board(0, 0, 1);

// Place a black pawn in the bottom-right corner
update_board(2, 2, 2);

// Empty the middle cell
update_board(1, 1, 0)

void update_board(int x, int y, int piece) {
    board[y * 3 + x] = piece;
} 

This has added a little bit of overhead to my code. However, it does now mean that I only need half the amount of space to store each move (which might not seem significant for just one move, but quickly adds up when I’m storing hundreds or thousands of moves):

struct move {
    int start;
    int end;
};

And if I want to convert a move’s start or end position back to a 2D system, I would simply reverse the process above:

struct position {
    int x_pos;
    int y_pos;
};

int index = 7; // Bottom, middle cell of board
struct position bot_middle = get_position(index);
/* bot_middle will be:
    x: 1
    y: 2
*/

struct position get_position(int index) {
    struct position pos;
    pos.y = index / 3;
    pos.x = index % 3;
    return pos;
}

Note that this takes advantage of a feature of integer division whereby the fractional part of the result is lost. If I was to divide 7 by 3 with a pocket calculator, I’d get 2.333… but when I use integer division, the result is just 2. This gives me the row number (for the third row). Taking the remainder of the division, which I get using the modulo operator (%) gives me the column number (1 for the middle row).

So that takes care of the board, the pieces and the moves. Next on the list is which player is due to play next. Well this can just be an int representing the piece for that player. Simple.

Next, the log of moves for a single game. Well this could just be an array of move structs. The order of the structs in the array would mirror the order of moves in the game. It’s a nice idea, but there’s a fundamental problem with it. As you have seen from my dealings with arrays to date, C wants me to be very precise about how much space I’m allocating for arrays. However, if you think about how much space I will need to store the moves for a single game, it’s going to be variable, because each game is going to have a different number of moves. One solution to this is to calculate the maximum number of moves I could possibly make and set the size of each element to match this. While that would work, it does mean, for most games, I am wasting a lot of space.

I have exactly the same problem with the chronological history of games played. I can’t know in advance how many games a user of the program will play. Fortunately there is a solution that allows me to keep a chronological record of games played without having to waste memory and disk space. That structure is something known as a linked list. A conceptualisation of a linked list is shown in the diagram below.

A diagram showing the potential structure of a linked list to store the Hexapawn game history.
Conceptualisation of the linked list for the Hexapawn game history

The fundamental idea behind a linked list is that each item is a node that, in addition to its data, contains a pointer to the next node in the list. The list shown shown here is a type called a doubly linked list in which each node also contains a link to the previous node. The difference between the two is that a standard link list can only be navigated in a forward direction, while a doubly-linked list can be navigated forward or backward.

The node at the end of the list will have a null pointer for its Next pointer, and correspondingly, the node at the beginning of the list will have null pointer for its Previous pointer.

Note that the list of moves within each game could also be a linked list.

The chief advantages of a linked list over an array are:

  • each node occupies its own space in memory and each node’s data can be of a different size
  • the nodes do not need to occupy a contiguous memory block, or be stored in the same order that they appear in the list
  • It’s possible to add or remove nodes from any point in a linked list, while adding or removing array elements is only possible through a costly operation in which the original array is copied.

The chief disadvantages of a linked list compared to an array are:

  • creating a node has more overhead because the node must be allocated memory space before being added to the list
  • the memory for each node must be freed individually when it is no longer required
  • it’s easy to break a linked list, e.g. if you destroy a node without updating the back and previous links of the node to either side
  • Nodes can only be accessed by walking the list (sequential access) rather than going directly to the required element in an array (random access).

I also need to store the start and end date and time for each game. Well there is a C library that can store a timestamp, which is the number of elapsed seconds since a fixed date and time. One small problem with this, is that the implementation, for example which fixed date and time is used, is system dependent. I’ll be introducing a solution to this shortly, but for now, know that I can store these values as a timestamp.

I need to store who won the game – again that can be an int representing the piece.

I need to store the method of winning. The simplest solution to this is to use an enum with values for each of the possible win types.

The next items on my list are the the board configurations and the legal moves for each configuration used by the AI. Each board configuration can be stored using the board and pieces scheme I’ve already established and the list of legal moves can use the move struct. I need to know whether a move is enabled or disabled, and if it is disabled, I need to know which game resulted in it being disabled. I can combine these two bits of information by having a pointer to the game record in which the move was disabled and simply setting this to NULL if the move is currently enabled.

If you think about these items, you’ll realise I have a similar problem in that the number of moves available for each configuration will vary. So it makes sense to also use a linked list for this information. This is especially the case as the AI will work by walking through the list searching for a configuration.

The final item on the list concerns how I will make all this information persistent by storing it in a file. This is a topic I will look at in depth in a future post. For now, I’ll focus on building the core data structures.

I mentioned earlier that one element I wish to use, timestamps, is hampered by differences between platforms. Unfortunately, as I start to use more advanced features, such as memory management and file management, I will find this is increasingly the case. The problem of cross-platform compatibility is one that has been faced by many developers, among them the team that created the popular Apache web server. They solved the issue by creating a cross platform application programming interface (API) called the Apache Portable Runtime (which I’ll refer to as APR from now on). Although originally written to support the Apache web server project, the Apache Software Foundation have made APR available as a stand-alone library for other applications to use.

I referred to the APR homepage for instructions on how to install it on my development platform.

I’ll keep the code to handle the game’s data structure in its own file. I created a new file called hexdata.c with the following content:

/**
 * @file hexdata.c
 * @brief Manages the underlying Hexapawn data structure
 * @author Laurence Scotford
 */

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

apr_pool_t * hex_data_pool;
games_list_t * games_list;

/**
 * @brief Initialise data structure
 */
void initialise_game_data() {
    // Attempt to initialise Apache Portable Runtime and create the memory pool for the game data
    if (apr_initialize() != APR_SUCCESS || apr_pool_create(&hex_data_pool, NULL) != APR_SUCCESS) {
        exit_with_error(STR_DATA_ERROR);
    }

    // Create a new linked list to hold the game history
    games_list = apr_palloc(hex_data_pool, sizeof(games_list_t));
    APR_RING_INIT(games_list, _hexgame_t, link);
}

/**
 * @brief Create the data structure for a new game
 * @returns A pointer to the game structure
 */
hexgame_t * create_new_game() {
    // The starting board configuration for a new game
    piece_t starting_board[9] = {
        PIECE_BLACK, PIECE_BLACK, PIECE_BLACK,
        PIECE_EMPTY, PIECE_EMPTY, PIECE_EMPTY,
        PIECE_WHITE, PIECE_WHITE, PIECE_WHITE
    };

    // Create a new game with the initial board configuration and an empty moves list
    hexgame_t * new_game = (hexgame_t *) apr_palloc(hex_data_pool, sizeof(hexgame_t));
    memcpy(new_game->board, starting_board, sizeof(starting_board));
    new_game->next_player = PIECE_WHITE;
    new_game->state = STATE_IN_PROGRESS;
    new_game->moves = apr_palloc(hex_data_pool, sizeof(moves_list_t));
    APR_RING_INIT(new_game->moves, _hexmove_t, link);
    new_game->start_time = apr_time_now();
    APR_RING_INSERT_TAIL(games_list, new_game, _hexgame_t, link);
    return new_game;
}

/**
 * @brief Removes the game data and terminates Apache Portable Runtime
 */
void close_game_data() {
    apr_pool_destroy(hex_data_pool);
    apr_terminate();
}

Before any APR functions or macros can be used, APR must be initialised with the apr_initialize function, which I do in the intialise_game_data function. Many APR functions return a status. I check against APR_SUCCESS to ensure the initialisation happened without error.

APR includes a set of memory management functions to replace the standard malloc and calloc functions. These functions use the concept of a memory pool, which is a way of grouping together several memory allocations. The chief advantage of this is that, when the pool is destroyed, it automatically frees any associated allocations, which would otherwise have to be freed individually. After the APR initialisation, the next thing I do is create a memory pool to hold the game data structures.

Note that the close_game_data function reverses these two operations to destroy the memory pool (and any associated allocations) and terminate APR.

Going back to initalise_game_data, the next thing I do is set up a linked list to hold a record of the games as they are played. Each item in the list will be the record for one game. This makes use of an APR structure called a ring. This is a cyclic doubly-linked list. This means, not only can the list be navigated forward and backward, the tail of the list also links back to the head and vice versa. You can see that APR has its own version of malloc, called apr_palloc. This works in similar way to malloc, except that the allocated memory is assigned to a memory pool.

The new_game function creates a new game record and appends it to the list. Note that one member of the record for a single game is another linked list that records the moves made in the game.

Here’s the corresponding header file: hexdata.h:

/**
 * @file hexdata.h
 * @brief Manages the underlying Hexapawn data structure
 * @author Laurence Scotford
 */

#ifndef __Hexapawn__hexdata__
#define __Hexapawn__hexdata__

void initialise_game_data(void);
hexgame_t * create_new_game(void);
void close_game_data(void);

#endif /* defined(__Hexapawn__hexdata__) */

I’ve created several new data types in hexapawn.h to support these functions:

/**
 * @brief Possible states for a game
 */
typedef enum _game_state_t {
    STATE_IN_PROGRESS,
    STATE_ENDED_RESIGNED,
    STATE_ENDED_FAR_SIDE,
    STATE_ENDED_BLOCKED,
    STATE_ENDED_TAKEN
} game_state_t;

// Model structures

/**
 * @brief Describes a single move in a game - used in an APR ring type (cyclic linked list)
 */
typedef struct _hexmove_t {
    APR_RING_ENTRY(_hexmove_t) link;    ///< Holds the links to the neighbouring elements in the linked list
    int from;                           ///< The location the piece moved from
    int to;                             ///< The location the piece moved to
} hexmove_t;

/**
 * @brief APR linked list that holds the move history for a game
 */
typedef struct _moves_list_t moves_list_t;
APR_RING_HEAD(_moves_list_t, _hexmove_t);

/**
 * @brief Describes an entire game of Hexapawn - used in an APR Ring type (cyclic linked list)
 */
typedef struct _hexgame_t {
    APR_RING_ENTRY(_hexgame_t) link;    ///< Holds the links to the neighbouring elements in the linked list
    piece_t board[9];                   ///< An array holding the current configuration of the board
    apr_time_t start_time;              ///< A time stamp recording when the game started
    apr_time_t end_time;                ///< A time stamp recording when the gaem ended
    piece_t next_player;                ///< The player due to play next
    game_state_t state;                 ///< The current state of the game
    moves_list_t * moves;               ///< A pointer to a cyclic linked list with the move history for the game
} hexgame_t;

/**
 * @brief An APR ring type (cyclic linked list) holding descriptions of games of hexapawn
 */
typedef struct _games_list_t games_list_t;
APR_RING_HEAD(_games_list_t, _hexgame_t);

I’ve also added a new string macro, so this need to be added to hexstrings.h:

#define STR_DATA_ERROR "The data required by Hexapawn could not be created"

I’ll expand hexapawn.c to correctly initialise and destroy the game data when the program starts and ends respectively:

/**
 * @file hexapawn.c
 * @brief A C version of Martin Gardner's Hexapawn game and matchbox AI
 * @author Laurence Scotford
 */

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

int main(void) {
    // Setup data structure
    initialise_game_data();

    // 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();

    // Close game data
    close_game_data();

    return 0;
}

Next up is to modify game.c to use the new_game function and to display the board in its initial state ready for the game to begin. Just beneath the declarations for the character sprites, I’ve added a new declaration for a variable that will point to the structure for the current game:

static hexgame_t * current_game;

In the play_game function, I’ve added some code just above the call to draw_board that will start a new game:

    //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
    }

Note that, until the code for resuming a game has been added, for now I ensure that the mode is always GAME_MODE_NEW. This stops the game from breaking if Resume Game is selected from the menu.

Finally I modify the final loop in the draw_board function, so that, instead of drawing all blank pieces, it draws the pieces in their positions in the board configuration in the current game:

    // Draw the correct piece in each position on the board
    for (int i = 0; i < GRID_SIZE_Y * GRID_SIZE_X; i++) {
        draw_piece(i, current_game->board[i]);
    }

After recompiling, if I run the game and select either New game or Resume game from the menu, I see the board in its starting configuration:

The board showing the configuration at the start of a game.
The board showing the configuration at the start of a game

Now that the underlying data structures are in place and I have the capability to display the board, in the next post I’ll turn my attention to enabling the player to enter a move, validating the move, recording it and updating the board.

Published inGamesProgramming

Be First to Comment

Leave a Reply

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