GTK C Tic-Tac-Toe Game Tutorial

GTK C tic-tac-toe game, or noughts and crosses game, using Glade user interface designer. Build this game using GTK 3, Glade and the C programming language to learn more about GTK programming. This tutorial shows how to place an image (GtkImage) in a button (GtkButton). It also breaks a GTK C program into parts, using two C files and a header file.

The image below shows the tic-tac-toe game created in this part of the GTK 3 programming tutorial and is written using C and Glade.

GTK C Tic-Tac-Toe Game Built in this Tutorial
GTK C Tic-Tac-Toe Game Built in this Tutorial

Part 21 of GTK 3 Programming with C and Glade Tutorial

See the full GTK3 tutorial index

Overview of the Game Structure and Design

This section gives a brief overview of the structure of this game project. First it shows what the project consists of in terms of files and folders. It then gives an overview of the design of the game.

GTK C Tic-Tac-Toe Game Project Files and Folders Structure

Main Folder

The game is created from the template files from part four of this tutorial series. As can be seen in the image that follows (scroll down for the image), the project is contained in a folder called tic-tac-toe.

Glade File

A glade folder contains the Glade file for the project and is part of the template files. It is modified to make the graphical user interface for the game.

Resource Files

A resource folder called res is added to the project and is not part of the original template. This folder contains resources for the game in the form of PNG image files. The image files were created using GIMP.

Source Code

The src folder contains the source code for the project. In this folder main.c is from the template. Two files are added to the src folder, namely game_logic.c and game_logic.h that contain the logic for the game.

Make File

The make file, called makefile, is from the template. It must be modified to change the name of the project. In addition to the name change, it must be modified to compile and link the extra game logic C source code file.

Gtk C Tic-Tac-Toe Game Project Files and Folders Structure
Gtk C Tic-Tac-Toe Game Project Files and Folders Structure

Game Design Overview

This game has been made to be as simple as possible, but still be a usable working game.

Principle of Operation

Clicking one of the blocks on the game board puts either a O or X symbol in the block, depending on which player’s turn it is. This is done by inserting an image into the block.

If a player gets three symbols in a row, the game is won. With three O symbols in a row, the O player wins. With three X symbols in a row, the X player wins.

To win the game, the three symbols in a row can be either horizontally placed, vertically placed, or diagonally placed.

Glade GUI Design

From a Glade GUI point of view, the game consists of a main window containing the following parts.

Status Label

There is a status text label at the top of the window. This label gives the players instructions on how to play the game. When a game is completed, it displays the result of the game, which will be either O wins or X wins, or the game is a tie, otherwise know as a draw.

Game Board

The game board consists of 9 blocks in a 3 by 3 matrix. Each block is a GtkButton with a GtkImage placed in it. The buttons get their 3 by 3 arrangement because they are placed in a GtkGrid container.

New Game Button

A New Game button is found at the bottom of the application window. It is for resetting the game so that a new game can be started.

C Code Design

C code for the GTK C tic-tac-toe game is written to be as simple as possible. The code was not designed to be optimized to be the most efficient code. It was also designed to avoid using if-else constructs that have a huge number of expressions for working out the game result.

Game Board

The game board consists of 9 blocks in a 3 by 3 arrangement. These blocks are represented by a 9 element array.

Game Logic C Code

Game logic used to detect if the game is won or a tie is separated into the game_logic.c file. The rest of the game code is found in the main.c file and consists of two handler functions. One handler function is called whenever a block is clicked on the game board. The other handler function is for the new game button.

GTK C Tic-Tac-Toe Game Tutorial Parts

To build the tic-tac-toe game, follow the tutorial steps below.

1. Start a New GTK Glade C Project

Start a new project from the template files, modify the make file as described below. Create the res folder and save the game images to it.

1.1 Copy the Template Files

Copy the template files from part 4 of this tutorial series and rename the copied folder to tic-tac-toe.

1.2 Modify the Make File

Because the project is split into two C source code files, the make file must be modified to compile and link the extra source file.

Open the make file called makefile and modify it as follows.

  • Rename the the target in the make file to tic-tac-toe
  • Add the lines in the make file to compile game_logic.c as can be seen in the makefile listing that follows
  • Modify the make file to link the game_logic.o object file to the project as can be seen in the makefile listing below

makefile

# change application name here (executable output name)
TARGET=tic-tac-toe

# compiler
CC=gcc
# debug
DEBUG=-g
# optimisation
OPT=-O0
# warnings
WARN=-Wall

PTHREAD=-pthread

CCFLAGS=$(DEBUG) $(OPT) $(WARN) $(PTHREAD) -pipe

GTKLIB=`pkg-config --cflags --libs gtk+-3.0`

# linker
LD=gcc
LDFLAGS=$(PTHREAD) $(GTKLIB) -export-dynamic

OBJS=   game_logic.o \
		main.o

all: $(OBJS)
	$(LD) -o $(TARGET) $(OBJS) $(LDFLAGS)
	
game_logic.o: src/game_logic.c src/game_logic.h
	$(CC) -c $(CCFLAGS) src/game_logic.c $(GTKLIB) -o game_logic.o
    
main.o: src/main.c
	$(CC) -c $(CCFLAGS) src/main.c $(GTKLIB) -o main.o
    
clean:
	rm -f *.o $(TARGET)

1.3 Create a Resource Folder

Create a new folder in the main tic-tac-toe folder called res.

1.4 Save Images to the Resource Folder

Right-click each image below and save it to the res folder created in the previous step. Be sure to right-click the blank image on the white area above the caption and shaded area.

2. Create the Application Window in Glade

These step assume that you have been following this GTK C Glade tutorial series and know how to use Glade to place various widgets. Therefore not every detail of how to place each widget will be included. For more details on how to place every widget, see the video that is embedded near the top of this article.

2.1 Open the Glade File and Modify the Main Window

Open the glade file window_main.glade from the glade folder of the project in the Glade user interface designer application.

  • Change the title of the window to Tic Tac Toe under the General tab.
  • Uncheck the Resizable checkbox.
  • Under the Common tab, give the window a Border width of 10 pixels.

2.2 Place a GtkBox Container

Place a GtkBox container in the main window. Leave the defaults, but give it a Spacing of 10 pixels.

2.3 Place a GtkLabel at the Top

Place a GtkLabel in the top open slot of the GtkBox placed in the previous step.

  • Give the label an ID of lbl_status
  • Give the label a Label of Make a Move by Clicking a Block
  • Click the Edit Attributes button and make the label bold and blue

2.4 Place a GtkGrid in the Middle

Place a GtkGrid in the middle slot of the GtBox. Leave the defaults so that the grid container is 3 columns by 3 rows.

2.5 Place a GtkButton at the Bottom

Place a GtkButton in the bottom slot of the GtkBox.

  • Give the button an ID of btn_new_game
  • Give the button a Label of New Game
  • Add a handler function for the clicked signal called on_btn_new_game_clicked

2.6 Place a GtkButton in the GtkGrid

  • Place a GtkButton in the top left open block of the GtkGrid
  • Give the button an ID of btn_0
  • Delete the default Label text which is button
  • Under the Common tab, give the Widget name a value of 0
  • Give the button a handler called on_btn_clicked for the clicked signal

2.7 Place a GtkImage in the GtkButton

With the button still selected from the previous step, click General tab.

Click the pencil icon at the right of the Image box near the bottom of the right pane in Glade

In the dialog box that pops up, click the New button

A new GtkImage widget appears in the left pane of Glade, click it in the left pane it to select it

Change the ID of the GtkImage to img_0

Still under the General tab, click the File name radio button under the Image heading

Click the folder icon at the right of the box next to File name

In the dialog box that pops up, navigate to the res folder and select the blank.png image

2.8 Duplicate the GtkButton with GtkImage

  • Right-click the the GtkButton btn_0 in the left pane of Glade and select copy from the menu that pops up
  • Now right-click in each open box of the GtkGrid in order from left to right, top to bottom and click paste from the pop-up menu to paste a copy of the button and image

The ID of each button and its image are duplicated and incremented with each paste. The only things that are not incremented or copied are the widget name under the Common tab and the signal handler function name under the Signals tab.

  • For each pasted button, change the Widget name under the Common tab so that the buttons have go from 0 to 9 in order from left to right and top to bottom
  • Give each button the same handler name of on_btn_clicked for the clicked signal under the Signals tab

3. Write the C Code

The C code for this GTK C tic-tac-toe game tutorial is found in three files in the src folder of the project. These files are main.c, game_logic.h and game_logic.c Create these files as described below.

Modify the main.c template file found in the src folder to add the following code.

main.c

#include <gtk/gtk.h>
#include "game_logic.h"

// Structure for pointer access to widgets, but also keeping game variables here
typedef struct {
    GtkWidget *w_img[9];    	// Images in buttons hold blank, O or X
    GtkWidget *w_lbl_status;	// Status label
    gboolean turn;          	// Keeps track of turns - TRUE = O, FALSE = X
    gchar game_board[9];    	// Tracks game moves on board, maps board left to right, top to bottom
    game_state gm_state;    	// Game state: busy, O won, X won or tie
} app_widgets;


int main(int argc, char *argv[])
{
    GtkBuilder      *builder; 
    GtkWidget       *window;
    app_widgets     *widgets = g_slice_new(app_widgets);
    gchar           str_img[] = "img_0";    // For accessing images from Glade file

    widgets->turn = TRUE;       			// O turn (FALSE is X turn)
    widgets->gm_state = GM_BUSY;			// Game state is initially busy

    gtk_init(&argc, &argv);

    builder = gtk_builder_new_from_file("glade/window_main.glade");
    window = GTK_WIDGET(gtk_builder_get_object(builder, "window_main"));
    
    // Get a pointer to each image
    for (gint i = 0; i < 9; i++) {
        str_img[4] = i + '0';
        widgets->w_img[i] = GTK_WIDGET(gtk_builder_get_object(builder, str_img));
        // Reset game board
        widgets->game_board[i] = 0;
    }
    // Get a pointer to the status label
    widgets->w_lbl_status = GTK_WIDGET(gtk_builder_get_object(builder, "lbl_status"));
    
    gtk_builder_connect_signals(builder, widgets);
    g_object_unref(builder);

    gtk_widget_show(window);                
    gtk_main();
    g_slice_free(app_widgets, widgets);

    return 0;
}

// Button clicked handler function shared by all buttons in grid
void on_btn_clicked(GtkButton *button, app_widgets *app_wdgts)
{
    gint btn_num;       			// Number of button that was clicked
    gint winning_blocks[3] = {0};	// Numbers of blocks if 3 in row of same type
    
    if (app_wdgts->gm_state != GM_BUSY) {
        // Don't do anything if game is won or a tie
        return;
    }
    
    // Find the button that was clicked, by reading the wiget name
    // The name is a string "0" to "8", convert ASCII to int by subtracting '0'
    btn_num = gtk_widget_get_name(GTK_WIDGET(button))[0] - '0';
    
    // Only service buttons that have not been clicked, i.e. contain 0 in game_board
    if (app_wdgts->game_board[btn_num] == 0) {
        // Set image in clicked button to O or X
        gtk_image_set_from_file(GTK_IMAGE(app_wdgts->w_img[btn_num]), app_wdgts->turn ? "res/o.png" : "res/x.png");
        // Track game move
        app_wdgts->game_board[btn_num] = app_wdgts->turn ? 'o' : 'x';
        
        // Find out if the game is won, a tie or still busy
        app_wdgts->gm_state = get_game_state(app_wdgts->game_board, winning_blocks);
        
        // O won the game, so mark the winning blocks with the O image with green background
        if (app_wdgts->gm_state == GM_O_WON) {
            for (int i = 0; i < 3; i++) {
                gtk_image_set_from_file(GTK_IMAGE(app_wdgts->w_img[winning_blocks[i]]), "res/o_win.png");
            }
            gtk_label_set_text(GTK_LABEL(app_wdgts->w_lbl_status), "O Won!");
        }
        // X won the game, so mark the winning blocks with the X image with green background
        else if (app_wdgts->gm_state == GM_X_WON) {
            for (int i = 0; i < 3; i++) {
                gtk_image_set_from_file(GTK_IMAGE(app_wdgts->w_img[winning_blocks[i]]), "res/x_win.png");
            }
            gtk_label_set_text(GTK_LABEL(app_wdgts->w_lbl_status), "X Won!");
        }
        // The game was a tie
        else if (app_wdgts->gm_state == GM_TIE) {
			gtk_label_set_text(GTK_LABEL(app_wdgts->w_lbl_status), "The Game was a Tie!");
		}
        
        // Flag next players turn
        app_wdgts->turn = !app_wdgts->turn;
    }
}

// New Game button - reset the game
void on_btn_new_game_clicked(GtkButton *button,  app_widgets *app_wdgts)
{
	// Reset the game board
	for (int i = 0; i < 9; i++) {
		app_wdgts->game_board[i] = 0;
	}
	// Clear the button images by loading blank image
	for (int i = 0; i < 9; i++) {
		gtk_image_set_from_file(GTK_IMAGE(app_wdgts->w_img[i]), "res/blank.png");
	}
	// Reset the status message to the default text
	gtk_label_set_text(GTK_LABEL(app_wdgts->w_lbl_status), "Make a Move by Clicking a Block");
	
	// Game starts in the busy state
    app_wdgts->gm_state = GM_BUSY;
}

// called when window is closed
void on_window_main_destroy()
{
    gtk_main_quit();
}

Create a new C source file called game_logic.h in the src folder. Add the following code to the file.

game_logic.h

#ifndef GAME_LOGIC_H
#define GAME_LOGIC_H

// Game can be in one of 4 states:
// Busy: 	game is new or players are busy playing
// O Won:	O player won the game
// X Won: 	X player won the game
// Tie:		game was a tie
typedef enum {GM_BUSY, GM_O_WON, GM_X_WON, GM_TIE} game_state;

game_state get_game_state(const gchar *gm_board, gint *win_blocks);

#endif // GAME_LOGIC_H

GTK C tic-tac-toe game logic that determines if the game is won or a tie is placed in a separate C file. Create a new C source file called game_logic.c in the src folder. Add the following code to the file.

game_logic.c

#include <gtk/gtk.h>
#include "game_logic.h"

// Function prototypes for private helper functions implemented at bottom of this file
static gint check_game_match(const gint lookup[][3], const gint len,
                            const gchar ch,  const char *gm_board, gint *win_blocks);
static gboolean game_board_full(const gchar *gm_board);

// Lookup tables for winning block combinations
const gint tbl_hor[3][3] = {{0, 1, 2}, {3, 4, 5}, {6, 7, 8}};	// Horizontal
const gint tbl_ver[3][3] = {{0, 3, 6}, {1, 4, 7}, {2, 5, 8}};	// Vertical
const gint tbl_diag[2][3] = {{0, 4, 8}, {2, 4, 6}};				// Diagonal

// Returns the state of the game as either busy, O won, X won or a tie
// Returns the winning block combination in win_blocks
game_state get_game_state(const gchar *gm_board, gint *win_blocks)
{
    game_state state = GM_BUSY;		// Start by assuming game is busy, not won or a tie
    gint result;					// A result of -1 means game is still busy
    
    // Determine if X won by checking for horizontal match, vertical match and diagonal match
    result = check_game_match(tbl_hor, 3, 'x', gm_board, win_blocks);
    if (result > -1) { return GM_X_WON; }
    result = check_game_match(tbl_ver, 3, 'x', gm_board, win_blocks);
    if (result > -1) { return GM_X_WON; }
    result = check_game_match(tbl_diag, 2, 'x', gm_board, win_blocks);
    if (result > -1) { return GM_X_WON; }
    // Determine if O won by checking for horizontal match, vertical match and diagonal match
    result = check_game_match(tbl_hor, 3, 'o', gm_board, win_blocks);
    if (result > -1) { return GM_O_WON; }
    result = check_game_match(tbl_ver, 3, 'o', gm_board, win_blocks);
    if (result > -1) { return GM_O_WON; }
    result = check_game_match(tbl_diag, 2, 'o', gm_board, win_blocks);
    if (result > -1) { return GM_O_WON; }
    
    // Check if game was a tie
    if (game_board_full(gm_board)) {
        state = GM_TIE;
    }
    
    return state;
}

// Checks the game board for a single match against a lookup table
static gint check_game_match(
							const gint lookup[][3],	// A pointer to one of the lookup tables
							const gint len,			// Number of sub-arrays in lookup table
                            const gchar ch,			// Character to look for (o or x)
                            const char *gm_board,	// Pointer to the game board array
                            gint *win_blocks)		// Returns winning blocks combination
{
    gint i, j;          // For loops to iterate through arrays
    gint count = 0;     // Tests for 3 matches
    
    for (i = 0; i < len; i++) {						// Go through each sub-array of lookup table
        count = 0;									// Reset match account
        for (j = 0; j < 3; j++) {					// Go through each element of sub-array
            if (ch == gm_board[lookup[i][j]]) {		// Did game board block match character? (o or x)
                win_blocks[j] = lookup[i][j];		// Save values from sub-array, if game is won then contains winning blocks
                count++;							// Keep track of number of matching blocks
                if (count == 3) {					// Did the player win the game?
                    return i;						// Return number > -1 to indicate game was won
                }
            }
        }
    }
    
    return -1;		// Game is a tie or still busy
}

// Determine if all blocks on the game board are full, used for determining if game is a tie
static gboolean game_board_full(const gchar *gm_board)
{
    int i;
    
    for (i = 0; i < 9; i++) {
        if (gm_board[i] == 0) {
            return FALSE;
        }
    }
    
    return TRUE;
}

How the GTK C Tic-Tac-Toe Game Code Works

An explanation of how the code works is not included in the text of this article. The code has many comments that explains how it works.

Watch the video embedded near the top of this GTK C tic-tac-toe game tutorial for an explanation of how the code works. Also, examine the source code and look up the various GTK functions in the GTK documentation.

Leave a Reply

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