Implementing a Terminal-Based Gomoku Game in C on Linux

Terminal-based board games in C require direct manipulation of standard I/O, raw input processing, and efficient state evaluation. The architecture for a 15x15 Gomoku game on Linux centers on a matrix for piece tracking, POSIX terminal configuration for instant key capture, and directional scanning for win validation.

Board State and Cursor Tracking

The game board is represented as a 15x15 matrix. Each cell holds a character representing an empty space, a black piece, or a white piece. Two integer variables track the active cursor coordinates, allowing players to navigate before confirming a move.

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <termios.h>
#include <unistd.h>

#define BOARD_SIZE 15
#define EMPTY_SLOT '.'
#define PLAYER_ONE 'X'
#define PLAYER_TWO 'O'

char grid[BOARD_SIZE][BOARD_SIZE];
int cursor_row = BOARD_SIZE / 2;
int cursor_col = BOARD_SIZE / 2;

void init_board(void) {
    for (int r = 0; r < BOARD_SIZE; r++) {
        for (int c = 0; c < BOARD_SIZE; c++) {
            grid[r][c] = EMPTY_SLOT;
        }
    }
}

Raw Terminal Input Configuration

Standard C input functions like getchar() wait for a newline and echo characters to the screen. For real-time cursor movement, the terminal must be switched to raw mode using the termios API. This disables canonical mode and echo, allowing immediate processing of arrow keys and the Enter key.

struct termios original_term;

void enable_raw_mode(void) {
    tcgetattr(STDIN_FILENO, &original_term);
    struct termios raw = original_term;
    raw.c_lflag &= ~(ICANON | ECHO);
    raw.c_cc[VMIN] = 1;
    raw.c_cc[VTIME] = 0;
    tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);
}

void disable_raw_mode(void) {
    tcsetattr(STDIN_FILENO, TCSAFLUSH, &original_term);
}

char read_input(void) {
    char c = getchar();
    if (c == '\x1b') { // Escape sequence for arrow keys
        getchar(); // Skip '['
        char dir = getchar();
        switch (dir) {
            case 'A': return 'U'; // Up
            case 'B': return 'D'; // Down
            case 'C': return 'R'; // Right
            case 'D': return 'L'; // Left
        }
    }
    return c;
}

Rendering the Grid

The display function clears the screen and iterates through the matrix. When the loop reaches the current cursor coordinates, it highlights the position by printing the active player's symbol inside brackets. ANSI escape codes handle screen clearing and cursor resetting.

void render_board(char active_turn) {
    printf("\033[2J\033[H"); // Clear screen and move to home
    printf("  ");
    for (int c = 0; c < BOARD_SIZE; c++) printf("%2d", c);
    printf("\n");

    for (int r = 0; r < BOARD_SIZE; r++) {
        printf("%2d", r);
        for (int c = 0; c < BOARD_SIZE; c++) {
            if (r == cursor_row && c == cursor_col) {
                printf("[%c]", active_turn);
            } else {
                printf(" %c ", grid[r][c]);
            }
        }
        printf("\n");
    }
}

Win Condition Evaluation

Victory is determined by checking four axes from the most recently placed piece: horizontal, vertical, main diagonal, and anti-diagonal. A helper function counts consecutive identical symbols in a given direction. If the sum of both directions along an axis plus the placed piece equals or exceeds five, the game ends.

int scan_axis(int start_r, int start_c, int dr, int dc, char symbol) {
    int count = 0;
    int r = start_r + dr;
    int c = start_c + dc;

    while (r >= 0 && r < BOARD_SIZE && c >= 0 && c < BOARD_SIZE && grid[r][c] == symbol) {
        count++;
        r += dr;
        c += dc;
    }
    return count;
}

bool check_victory(int placed_r, int placed_c, char symbol) {
    int directions[4][2] = {{0, 1}, {1, 0}, {1, 1}, {1, -1}};

    for (int i = 0; i < 4; i++) {
        int forward = scan_axis(placed_r, placed_c, directions[i][0], directions[i][1], symbol);
        int backward = scan_axis(placed_r, placed_c, -directions[i][0], -directions[i][1], symbol);
        if (forward + backward + 1 >= 5) {
            return true;
        }
    }
    return false;
}

Main Execution Loop

The core loop alternates turns, processes input, updates cursor boundaries, places pieces, and evaluates win conditions. The loop terminates when check_victory returns true or the board fills completely. Terminal settings are restored upon exit to prevent shell corruption.

int main(void) {
    init_board();
    enable_raw_mode();
    atexit(disable_raw_mode);

    char turn = PLAYER_ONE;
    bool running = true;
    int moves = 0;

    while (running) {
        render_board(turn);
        char key = read_input();

        switch (key) {
            case 'U': if (cursor_row > 0) cursor_row--; break;
            case 'D': if (cursor_row < BOARD_SIZE - 1) cursor_row++; break;
            case 'L': if (cursor_col > 0) cursor_col--; break;
            case 'R': if (cursor_col < BOARD_SIZE - 1) cursor_col++; break;
            case '\n':
            case ' ':
                if (grid[cursor_row][cursor_col] == EMPTY_SLOT) {
                    grid[cursor_row][cursor_col] = turn;
                    moves++;
                    if (check_victory(cursor_row, cursor_col, turn)) {
                        render_board(turn);
                        printf("\nPlayer %c wins!\n", turn);
                        running = false;
                    } else if (moves == BOARD_SIZE * BOARD_SIZE) {
                        render_board(turn);
                        printf("\nDraw!\n");
                        running = false;
                    } else {
                        turn = (turn == PLAYER_ONE) ? PLAYER_TWO : PLAYER_ONE;
                    }
                }
                break;
            case 'q':
                running = false;
                break;
        }
    }
    return 0;
}

Tags: C Linux game development Terminal UI POSIX

Posted on Tue, 23 Jun 2026 16:41:09 +0000 by angulion