Tristan ListancoDja-ver Hassan
Published © Apache-2.0

Embedded HMI Tic-Tac-Toe

A minimalist touch-screen Tic-Tac-Toe game built on RT-Thread RTOS that demonstrates how to create, user-friendly embedded interfaces

IntermediateWork in progress131
Embedded HMI Tic-Tac-Toe

Things used in this project

Hardware components

RT-Thread Renesas RA6M3
×1

Software apps and online services

RT-Thread IoT OS
RT-Thread IoT OS

Story

Read more

Schematics

Flowchart

Code

SConscript

C/C++
# SConscript in the src/ directory

import os
from SCons.Script import *

# Get the list of all C source files in the current directory (src/)
src = Glob('*.c')

# Add the 'inc' directory to the compiler's include search path
CPPPATH = ['#inc']

# Return the list of files and the include path
list = [src, CPPPATH]

Return('list')

lvgl_port.c

C/C++
#include <lvgl.h>
#include "ui.h"

// This function is typically called by the board-level LVGL driver 
// after the display and input devices have been initialized.
void lv_user_gui_init(void) {
    // Call the application UI initialization function
    ui_init();
}

ui.c

C/C++
#include "ui.h"

// --- UI Objects and Constants ---
#define DISP_HOR_RES  480
#define DISP_VER_RES  272

// New smaller cell size calculations for a better fit
#define GRID_SIZE_PIXELS 70
#define GRID_PAD_PIXELS  5
#define GRID_TOTAL_SIZE  (GRID_SIZE_PIXELS * GAME_GRID_SIZE + GRID_PAD_PIXELS * (GAME_GRID_SIZE - 1) + 2 * GRID_PAD_PIXELS) // 210 + 10 + 10 = 230

static lv_obj_t *screen_main;
static lv_obj_t *grid_container;
static lv_obj_t *sidebar_container;
static lv_obj_t *grid_cells[GAME_GRID_SIZE][GAME_GRID_SIZE];
static lv_obj_t *lbl_status;
static lv_obj_t *btn_reset;

// Colors
// FIX 1: Use LV_COLOR_HEX() macro for static initialization
static const lv_color_t COLOR_BACKGROUND = LV_COLOR_HEX(0x1A202C); // Dark blue/gray
static const lv_color_t COLOR_GRID_BG    = LV_COLOR_HEX(0x2D3748); // Medium blue/gray
static const lv_color_t COLOR_CELL_BG    = LV_COLOR_HEX(0x4A5568); // Lighter blue/gray
static const lv_color_t COLOR_WIN_GLOW   = LV_COLOR_HEX(0x48BB78); // Green for win

// --- Callbacks and Event Handlers ---

/**
 * @brief Applies the green glow effect to the winning tiles.
 */
static void ui_apply_win_glow(game_win_line_t line) {
    // If a winner is present, apply glow style
    if (line.winner != GAME_PLAYER_NONE) {
        lv_obj_t *cell1 = grid_cells[line.r1][line.c1];
        lv_obj_t *cell2 = grid_cells[line.r2][line.c2];
        lv_obj_t *cell3 = grid_cells[line.r3][line.c3];

        lv_obj_set_style_bg_color(cell1, COLOR_WIN_GLOW, LV_STATE_DEFAULT);
        lv_obj_set_style_bg_color(cell2, COLOR_WIN_GLOW, LV_STATE_DEFAULT);
        lv_obj_set_style_bg_color(cell3, COLOR_WIN_GLOW, LV_STATE_DEFAULT);
        
        // Also disable ALL buttons to prevent further clicks
        int r, c;
        for (r = 0; r < GAME_GRID_SIZE; r++) {
            for (c = 0; c < GAME_GRID_SIZE; c++) {
                lv_obj_add_state(grid_cells[r][c], LV_STATE_DISABLED);
            }
        }
    }
}


/**
 * @brief Updates the status label and applies the win glow if necessary.
 */
static void ui_update_status(void) {
    game_win_line_t line = game_get_win_line();
    const char *status_text;

    if (line.winner != GAME_PLAYER_NONE) {
        // Winner found
        status_text = (line.winner == GAME_PLAYER_X) ? "PLAYER X\nWINS!" : "PLAYER O\nWINS!";
        lv_obj_clear_state(btn_reset, LV_STATE_DISABLED);
        ui_apply_win_glow(line); // Apply the glow effect
    } else {
        game_player_t current_p = game_get_current_player();
        if (current_p == GAME_PLAYER_NONE) {
             // Game over (Draw)
             status_text = "DRAW!\nRESET GAME.";
             lv_obj_clear_state(btn_reset, LV_STATE_DISABLED);
        } else {
            // Game is ongoing
            status_text = (current_p == GAME_PLAYER_X) ? "X's TURN" : "O's TURN";
            lv_obj_add_state(btn_reset, LV_STATE_DISABLED);
        }
    }

    lv_label_set_text(lbl_status, status_text);
}

/**
 * @brief Event handler for the 9 grid cell buttons.
 */
static void grid_btn_event_cb(lv_event_t *e) {
    lv_obj_t *btn = lv_event_get_target(e);
    // User data is set to 10 * row + col
    // Retrieve by casting the void* pointer back to uint32_t
    uint32_t user_data = (uint32_t)(uintptr_t)lv_obj_get_user_data(btn);
    int row = user_data / 10;
    int col = user_data % 10;

    game_player_t moved_player = game_make_move(row, col);

    if (moved_player != GAME_PLAYER_NONE) {
        // Successful move: update button text and disable it
        const char *symbol = (moved_player == GAME_PLAYER_X) ? "X" : "O";
        lv_obj_t *label = lv_obj_get_child(btn, 0);
        lv_label_set_text(label, symbol);

        // Disable the button after it has been played
        lv_obj_add_state(btn, LV_STATE_DISABLED);

        ui_update_status();
    }
}

/**
 * @brief Event handler for the reset button.
 */
static void reset_btn_event_cb(lv_event_t *e) {
    // 1. Reset Game Logic
    game_reset();

    // 2. Reset UI state and remove the glow style
    int r, c;
    for (r = 0; r < GAME_GRID_SIZE; r++) {
        for (c = 0; c < GAME_GRID_SIZE; c++) {
            lv_obj_t *btn = grid_cells[r][c];
            lv_obj_t *label = lv_obj_get_child(btn, 0);

            // Clear the label text
            lv_label_set_text(label, "");

            // Re-enable the button and reset its background color
            lv_obj_clear_state(btn, LV_STATE_DISABLED);
            lv_obj_set_style_bg_color(btn, COLOR_CELL_BG, LV_STATE_DEFAULT);
        }
    }

    // 3. Update Status
    ui_update_status();
}

// --- UI Construction Functions ---

/**
 * @brief Create the main screen and base styles.
 */
static void ui_create_screen(void) {
    screen_main = lv_obj_create(NULL);
    lv_obj_set_style_bg_color(screen_main, COLOR_BACKGROUND, LV_STATE_DEFAULT);
    lv_obj_set_style_border_width(screen_main, 0, LV_STATE_DEFAULT);
    lv_scr_load(screen_main);
}

/**
 * @brief Creates the sidebar for status and reset button (Text to the side).
 */
static void ui_create_sidebar(void) {
    sidebar_container = lv_obj_create(screen_main);
    lv_obj_set_size(sidebar_container, DISP_HOR_RES - GRID_TOTAL_SIZE - 20, DISP_VER_RES); // Use remaining space
    lv_obj_align(sidebar_container, LV_ALIGN_RIGHT_MID, -5, 0);
    lv_obj_set_style_bg_color(sidebar_container, COLOR_BACKGROUND, LV_STATE_DEFAULT);
    lv_obj_set_style_border_width(sidebar_container, 0, LV_STATE_DEFAULT);
    lv_obj_set_style_pad_all(sidebar_container, 0, LV_STATE_DEFAULT);
    
    // Use flex layout for vertical alignment (Status Label -> Reset Button)
    lv_obj_set_flex_flow(sidebar_container, LV_FLEX_FLOW_COLUMN);
    lv_obj_set_flex_align(sidebar_container, LV_FLEX_ALIGN_SPACE_AROUND, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
    
    // --- 1. Status Label ---
    lbl_status = lv_label_create(sidebar_container);
    lv_obj_set_style_text_font(lbl_status, &lv_font_montserrat_14, LV_STATE_DEFAULT);
    lv_obj_set_style_text_color(lbl_status, lv_color_white(), LV_STATE_DEFAULT);
    lv_obj_set_width(lbl_status, LV_SIZE_CONTENT);
    lv_label_set_recolor(lbl_status, true); // Allow coloring text if needed
    
    // FIX 2: Use style setter for text alignment
    lv_obj_set_style_text_align(lbl_status, LV_TEXT_ALIGN_CENTER, LV_STATE_DEFAULT); 
    
    // --- 2. Reset Button ---
    btn_reset = lv_btn_create(sidebar_container);
    lv_obj_set_size(btn_reset, 100, 30); // Smaller button
    
    // Style and Text
    lv_obj_set_style_bg_color(btn_reset, LV_COLOR_HEX(0xC53030), LV_STATE_DEFAULT); // Red color
    lv_obj_set_style_bg_color(btn_reset, LV_COLOR_HEX(0x9B2C2C), LV_STATE_PRESSED);
    lv_obj_add_state(btn_reset, LV_STATE_DISABLED);
    lv_obj_set_style_bg_color(btn_reset, LV_COLOR_HEX(0x606060), LV_STATE_DISABLED);
    
    lv_obj_t *label_reset = lv_label_create(btn_reset);
    lv_label_set_text(label_reset, "RESET");
    lv_obj_set_style_text_font(label_reset, &lv_font_montserrat_14, LV_STATE_DEFAULT);
    lv_obj_set_style_text_color(label_reset, lv_color_white(), LV_STATE_DEFAULT);
    lv_obj_center(label_reset);

    lv_obj_add_event_cb(btn_reset, reset_btn_event_cb, LV_EVENT_CLICKED, NULL);
}

/**
 * @brief Create the 3x3 grid container and its cell buttons (smaller board).
 */
static void ui_create_grid(void) {
    // --- 3. Grid Container ---
    grid_container = lv_obj_create(screen_main);
    // Use the calculated total size
    lv_obj_set_size(grid_container, GRID_TOTAL_SIZE, GRID_TOTAL_SIZE);
    // Align left-center
    lv_obj_align(grid_container, LV_ALIGN_LEFT_MID, 10, 0);
    lv_obj_set_style_bg_color(grid_container, COLOR_GRID_BG, LV_STATE_DEFAULT);
    lv_obj_set_style_border_width(grid_container, 0, LV_STATE_DEFAULT);
    lv_obj_set_style_pad_all(grid_container, GRID_PAD_PIXELS, LV_STATE_DEFAULT);

    // Use LVGL Grid layout
    static lv_coord_t col_dsc[] = {GRID_SIZE_PIXELS, GRID_SIZE_PIXELS, GRID_SIZE_PIXELS, LV_GRID_TEMPLATE_LAST};
    static lv_coord_t row_dsc[] = {GRID_SIZE_PIXELS, GRID_SIZE_PIXELS, GRID_SIZE_PIXELS, LV_GRID_TEMPLATE_LAST};
    lv_obj_set_grid_dsc_array(grid_container, col_dsc, row_dsc);

    // --- 4. Grid Cells (Buttons) ---
    int r, c;
    for (r = 0; r < GAME_GRID_SIZE; r++) {
        for (c = 0; c < GAME_GRID_SIZE; c++) {
            lv_obj_t *btn = lv_btn_create(grid_container);
            grid_cells[r][c] = btn;

            lv_obj_set_grid_cell(btn, LV_GRID_ALIGN_STRETCH, c, 1,
                                 LV_GRID_ALIGN_STRETCH, r, 1);

            // Style the button
            lv_obj_set_style_radius(btn, 10, LV_STATE_DEFAULT);
            lv_obj_set_style_bg_color(btn, COLOR_CELL_BG, LV_STATE_DEFAULT);
            lv_obj_set_style_border_color(btn, COLOR_GRID_BG, LV_STATE_DEFAULT);
            lv_obj_set_style_border_width(btn, 2, LV_STATE_DEFAULT);
            lv_obj_set_style_shadow_width(btn, 0, LV_STATE_DEFAULT);

            // Add text label to the button
            lv_obj_t *label = lv_label_create(btn);
            lv_label_set_text(label, "");
            lv_obj_set_style_text_font(label, &lv_font_montserrat_14, LV_STATE_DEFAULT); // Using enabled font
            lv_obj_set_style_text_color(label, lv_color_white(), LV_STATE_DEFAULT);
            lv_obj_center(label);

            // Store position data (R*10 + C)
            // FIX 3: Explicitly cast integer to void* to silence the warning
            lv_obj_set_user_data(btn, (void *)(uint32_t)(r * 10 + c));

            // Register event handler
            lv_obj_add_event_cb(btn, grid_btn_event_cb, LV_EVENT_CLICKED, NULL);
        }
    }
}

// --- Public Entry Point ---

/**
 * @brief Initializes the UI and the game logic.
 */
void ui_init(void) {
    // 1. Initialize Game Logic
    game_init();

    // 2. Build UI Elements
    ui_create_screen();
    ui_create_sidebar(); // Sidebar first (status and reset)
    ui_create_grid();    // Grid on the left

    // 3. Set Initial Status
    ui_update_status();
}

ui.h

C/C++
#ifndef __UI_H__
#define __UI_H__

#include "lvgl.h"
#include "game.h"
#include <stdint.h> // Ensure uint32_t is available for LVGL user data

// Function Prototypes
void ui_init(void);

#endif // __UI_H__

game.c

C/C++
#include "game.h"

// --- Private Game State ---
static game_player_t board[GAME_GRID_SIZE][GAME_GRID_SIZE];
static game_player_t current_player;
static uint8_t moves_count;
static game_win_line_t win_line; // Now stores the winning coordinates

/**
 * @brief Initialize the game state.
 */
void game_init(void) {
    game_reset();
}

/**
 * @brief Reset the game board and state for a new game.
 */
void game_reset(void) {
    int r, c;
    for (r = 0; r < GAME_GRID_SIZE; r++) {
        for (c = 0; c < GAME_GRID_SIZE; c++) {
            board[r][c] = GAME_PLAYER_NONE;
        }
    }
    current_player = GAME_PLAYER_X;
    moves_count = 0;
    // Reset win line structure
    win_line.winner = GAME_PLAYER_NONE;
}

/**
 * @brief Check if there is a winner and store the winning line coordinates.
 * @return The winning player, or GAME_PLAYER_NONE.
 */
game_player_t game_check_win(void) {
    // If a winner is already determined, return it immediately
    if (win_line.winner != GAME_PLAYER_NONE) {
        return win_line.winner;
    }

    int i;
    game_player_t current_winner = GAME_PLAYER_NONE;

    // Check rows and columns
    for (i = 0; i < GAME_GRID_SIZE; i++) {
        // Check row i
        if (board[i][0] != GAME_PLAYER_NONE &&
            board[i][0] == board[i][1] &&
            board[i][1] == board[i][2]) {
            
            current_winner = board[i][0];
            win_line = (game_win_line_t){i, 0, i, 1, i, 2, current_winner};
            return current_winner;
        }

        // Check column i
        if (board[0][i] != GAME_PLAYER_NONE &&
            board[0][i] == board[1][i] &&
            board[1][i] == board[2][i]) {

            current_winner = board[0][i];
            win_line = (game_win_line_t){0, i, 1, i, 2, i, current_winner};
            return current_winner;
        }
    }

    // Check diagonals
    // Top-Left to Bottom-Right
    if (board[0][0] != GAME_PLAYER_NONE &&
        board[0][0] == board[1][1] &&
        board[1][1] == board[2][2]) {
        
        current_winner = board[0][0];
        win_line = (game_win_line_t){0, 0, 1, 1, 2, 2, current_winner};
        return current_winner;
    }

    // Top-Right to Bottom-Left
    if (board[0][2] != GAME_PLAYER_NONE &&
        board[0][2] == board[1][1] &&
        board[1][1] == board[2][0]) {
        
        current_winner = board[0][2];
        win_line = (game_win_line_t){0, 2, 1, 1, 2, 0, current_winner};
        return current_winner;
    }

    return GAME_PLAYER_NONE; // Game is still ongoing or a draw
}

/**
 * @brief Attempt to make a move on the board.
 */
game_player_t game_make_move(int row, int col) {
    // Check bounds 
    if (row < 0 || row >= GAME_GRID_SIZE || col < 0 || col >= GAME_GRID_SIZE) {
        return GAME_PLAYER_NONE;
    }

    // 1. Check if the game is already over
    if (win_line.winner != GAME_PLAYER_NONE || moves_count == 9) {
        return GAME_PLAYER_NONE;
    }

    // 2. Check if the cell is empty
    if (board[row][col] == GAME_PLAYER_NONE) {

        // Make the move
        board[row][col] = current_player;
        moves_count++;

        game_player_t moved_player = current_player;

        // Check for win/draw *after* the move
        game_check_win();

        // Switch player only if no winner yet and not a draw
        if (win_line.winner == GAME_PLAYER_NONE && moves_count < 9) {
            current_player = (current_player == GAME_PLAYER_X) ? GAME_PLAYER_O : GAME_PLAYER_X;
        }

        return moved_player; // Successful move
    }

    return GAME_PLAYER_NONE; // Invalid move (cell taken)
}

/**
 * @brief Get the player whose turn it is.
 */
game_player_t game_get_current_player(void) {
    // If the game is over, return NONE
    if (win_line.winner != GAME_PLAYER_NONE || moves_count == 9) {
        return GAME_PLAYER_NONE;
    }
    return current_player;
}

/**
 * @brief Get the coordinates of the winning line.
 */
game_win_line_t game_get_win_line(void) {
    return win_line;
}

game.h

C/C++
#ifndef __GAME_H__
#define __GAME_H__

#include <stdint.h>
#include <rtthread.h> 

// Game state constants
#define GAME_GRID_SIZE 3

typedef enum {
    GAME_PLAYER_NONE = 0, // Empty cell / No winner
    GAME_PLAYER_X    = 1, // Player X
    GAME_PLAYER_O    = 2  // Player O
} game_player_t;

// Structure to hold the coordinates of the three winning tiles
typedef struct {
    int r1, c1;
    int r2, c2;
    int r3, c3;
    game_player_t winner;
} game_win_line_t;

// Function Prototypes
void game_init(void);
game_player_t game_make_move(int row, int col);
game_player_t game_check_win(void);
void game_reset(void);
game_player_t game_get_current_player(void);
game_win_line_t game_get_win_line(void); // Getter for winning line coordinates

#endif // __GAME_H__

Credits

Tristan Listanco
1 project • 1 follower
Dja-ver Hassan
1 project • 0 followers

Comments