CARL HOVAN CHAN
Published

HMI Minesweeper: Tap & Clear

Classic Minesweeper adapted for RT-Thread on Renesas HMI

IntermediateShowcase (no instructions)5 hours52
HMI Minesweeper: Tap & Clear

Things used in this project

Hardware components

Renesas Ra6m3 Board
×1

Software apps and online services

RT-Thread IoT OS
RT-Thread IoT OS

Story

Read more

Code

ui.h

C/C++
#ifndef UI_H
#define UI_H

void ui_init(void);

#endif // UI_H

game.h

C/C++
#ifndef GAME_H
#define GAME_H

#include <stdint.h>
#include <stdbool.h>

// Grid Dimensions (Fit within 480x232 area)
#define GAME_COLS 10
#define GAME_ROWS 6
#define TOTAL_MINES 8

typedef enum {
    CELL_HIDDEN,
    CELL_REVEALED,
    CELL_FLAGGED
} CellState;

typedef struct {
    bool is_mine;
    int neighbor_mines; // 0-8
    CellState state;
} Cell;

typedef enum {
    GAME_PLAYING,
    GAME_WON,
    GAME_LOST
} GameStatus;

void game_logic_init(void);
void game_logic_reset(void);
GameStatus game_click_cell(int x, int y, bool flag_mode);
Cell* game_get_cell(int x, int y);
GameStatus game_get_status(void);

#endif // GAME_H

ui.c

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

// UI Objects
static lv_obj_t *lbl_status;
static lv_obj_t *btn_mode;
static lv_obj_t *lbl_mode;
static lv_obj_t *game_container;
static lv_obj_t *tile_btns[GAME_ROWS][GAME_COLS];

static bool is_flag_mode = false;

// Styles
static lv_style_t style_tile_hidden;
static lv_style_t style_tile_revealed;
static lv_style_t style_tile_mine;

// --- Helper Functions ---

static void update_ui_grid(void) {
    for (int y = 0; y < GAME_ROWS; y++) {
        for (int x = 0; x < GAME_COLS; x++) {
            Cell *c = game_get_cell(x, y);
            lv_obj_t *btn = tile_btns[y][x];

            if (c->state == CELL_HIDDEN) {
                lv_obj_add_style(btn, &style_tile_hidden, 0);
                lv_obj_remove_style(btn, &style_tile_revealed, 0);
                lv_obj_remove_style(btn, &style_tile_mine, 0);
                lv_label_set_text(lv_obj_get_child(btn, 0), "");
            }
            else if (c->state == CELL_FLAGGED) {
                lv_obj_add_style(btn, &style_tile_hidden, 0); // Keep hidden style base
                lv_label_set_text(lv_obj_get_child(btn, 0), "F");
                lv_obj_set_style_text_color(lv_obj_get_child(btn, 0), lv_palette_main(LV_PALETTE_YELLOW), 0);
            }
            else if (c->state == CELL_REVEALED) {
                if (c->is_mine) {
                    lv_obj_add_style(btn, &style_tile_mine, 0);
                    lv_label_set_text(lv_obj_get_child(btn, 0), "X");
                    lv_obj_set_style_text_color(lv_obj_get_child(btn, 0), lv_color_white(), 0);
                } else {
                    lv_obj_remove_style(btn, &style_tile_hidden, 0);
                    lv_obj_add_style(btn, &style_tile_revealed, 0);

                    if (c->neighbor_mines > 0) {
                        char buf[2];
                        snprintf(buf, sizeof(buf), "%d", c->neighbor_mines);
                        lv_label_set_text(lv_obj_get_child(btn, 0), buf);

                        // Color code numbers
                        lv_color_t num_color;
                        switch(c->neighbor_mines) {
                            case 1: num_color = lv_palette_main(LV_PALETTE_BLUE); break;
                            case 2: num_color = lv_palette_main(LV_PALETTE_GREEN); break;
                            case 3: num_color = lv_palette_main(LV_PALETTE_RED); break;
                            default: num_color = lv_palette_main(LV_PALETTE_PURPLE); break;
                        }
                        lv_obj_set_style_text_color(lv_obj_get_child(btn, 0), num_color, 0);
                    } else {
                        lv_label_set_text(lv_obj_get_child(btn, 0), "");
                    }
                }
            }
        }
    }

    GameStatus status = game_get_status();
    if (status == GAME_WON) lv_label_set_text(lbl_status, "YOU WIN!");
    else if (status == GAME_LOST) lv_label_set_text(lbl_status, "GAME OVER");
    else lv_label_set_text(lbl_status, "Minesweeper");
}

// --- Event Handlers ---

static void evt_tile_click(lv_event_t *e) {
    if (game_get_status() != GAME_PLAYING) return;

    // Retrieve coordinates stored in user_data
    intptr_t coords = (intptr_t)lv_event_get_user_data(e);
    int x = (coords >> 8) & 0xFF;
    int y = coords & 0xFF;

    game_click_cell(x, y, is_flag_mode);
    update_ui_grid();
}

static void evt_reset(lv_event_t *e) {
    game_logic_reset();
    lv_label_set_text(lbl_status, "Minesweeper");
    update_ui_grid();
}

static void evt_mode_toggle(lv_event_t *e) {
    is_flag_mode = !is_flag_mode;
    if (is_flag_mode) {
        lv_label_set_text(lbl_mode, "MODE: FLAG");
        lv_obj_set_style_bg_color(btn_mode, lv_palette_main(LV_PALETTE_ORANGE), 0);
    } else {
        lv_label_set_text(lbl_mode, "MODE: DIG");
        lv_obj_set_style_bg_color(btn_mode, lv_palette_main(LV_PALETTE_BLUE), 0);
    }
}

// --- Init ---

void ui_init(void) {
    game_logic_init();

    // 1. Initialize Styles
    lv_style_init(&style_tile_hidden);
    lv_style_set_bg_color(&style_tile_hidden, lv_palette_main(LV_PALETTE_GREY));
    lv_style_set_border_width(&style_tile_hidden, 2);
    lv_style_set_border_color(&style_tile_hidden, lv_palette_darken(LV_PALETTE_GREY, 2));

    lv_style_init(&style_tile_revealed);
    lv_style_set_bg_color(&style_tile_revealed, lv_color_white());
    lv_style_set_border_width(&style_tile_revealed, 1);
    lv_style_set_border_color(&style_tile_revealed, lv_palette_lighten(LV_PALETTE_GREY, 2));

    lv_style_init(&style_tile_mine);
    lv_style_set_bg_color(&style_tile_mine, lv_palette_main(LV_PALETTE_RED));

    // 2. Top Bar
    lv_obj_t *top_bar = lv_obj_create(lv_scr_act());
    lv_obj_set_size(top_bar, 480, 50);
    lv_obj_set_pos(top_bar, 0, 0);
    lv_obj_set_flex_flow(top_bar, LV_FLEX_FLOW_ROW);
    lv_obj_set_flex_align(top_bar, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
    lv_obj_clear_flag(top_bar, LV_OBJ_FLAG_SCROLLABLE);

    // Status Label
    lbl_status = lv_label_create(top_bar);
    lv_label_set_text(lbl_status, "Minesweeper");

    // Mode Button
    btn_mode = lv_btn_create(top_bar);
    lv_obj_add_event_cb(btn_mode, evt_mode_toggle, LV_EVENT_CLICKED, NULL);
    lv_obj_set_width(btn_mode, 120);
    lbl_mode = lv_label_create(btn_mode);
    lv_label_set_text(lbl_mode, "MODE: DIG");
    lv_obj_center(lbl_mode);

    // Reset Button
    lv_obj_t *btn_rst = lv_btn_create(top_bar);
    lv_obj_add_event_cb(btn_rst, evt_reset, LV_EVENT_CLICKED, NULL);
    lv_obj_t *l = lv_label_create(btn_rst);
    lv_label_set_text(l, "Restart");

    // 3. Game Grid Container
    game_container = lv_obj_create(lv_scr_act());
    lv_obj_set_size(game_container, 480, 222);
    lv_obj_set_pos(game_container, 0, 50);
    lv_obj_set_style_pad_all(game_container, 2, 0);
    lv_obj_clear_flag(game_container, LV_OBJ_FLAG_SCROLLABLE);

    // Calculate tile size
    // Width: (480 - padding) / 10 cols approx 46px
    // Height: (222 - padding) / 6 rows approx 35px
    int w = 46;
    int h = 34;

    for (int y = 0; y < GAME_ROWS; y++) {
        for (int x = 0; x < GAME_COLS; x++) {
            tile_btns[y][x] = lv_btn_create(game_container);
            lv_obj_set_size(tile_btns[y][x], w, h);
            lv_obj_set_pos(tile_btns[y][x], x * w + 5, y * h + 5); // Simple manual grid placement
            lv_obj_add_style(tile_btns[y][x], &style_tile_hidden, 0);

            // Store coordinates in user_data (Upper byte X, Lower byte Y)
            intptr_t coords = (x << 8) | y;
            lv_obj_add_event_cb(tile_btns[y][x], evt_tile_click, LV_EVENT_CLICKED, (void*)coords);

            // Label for number/icon
            lv_obj_t *lbl = lv_label_create(tile_btns[y][x]);
            lv_label_set_text(lbl, "");
            lv_obj_center(lbl);
        }
    }

    update_ui_grid();
}

game.c

C/C++
#include "game.h"
#include <stdlib.h>
#include <rtthread.h> // For rt_tick_get() seeding

static Cell grid[GAME_ROWS][GAME_COLS];
static GameStatus current_status;
static int cells_revealed;

// Helper: Check bounds
static bool is_valid(int x, int y) {
    return (x >= 0 && x < GAME_COLS && y >= 0 && y < GAME_ROWS);
}

// Calculate neighbor mines for a cell
static void calculate_neighbors(void) {
    for (int y = 0; y < GAME_ROWS; y++) {
        for (int x = 0; x < GAME_COLS; x++) {
            if (grid[y][x].is_mine) continue;

            int count = 0;
            for (int dy = -1; dy <= 1; dy++) {
                for (int dx = -1; dx <= 1; dx++) {
                    if (dx == 0 && dy == 0) continue;
                    if (is_valid(x + dx, y + dy) && grid[y + dy][x + dx].is_mine) {
                        count++;
                    }
                }
            }
            grid[y][x].neighbor_mines = count;
        }
    }
}

void game_logic_init(void) {
    srand(rt_tick_get()); // Seed using RT-Thread tick
    game_logic_reset();
}

void game_logic_reset(void) {
    current_status = GAME_PLAYING;
    cells_revealed = 0;

    // Reset grid
    for (int y = 0; y < GAME_ROWS; y++) {
        for (int x = 0; x < GAME_COLS; x++) {
            grid[y][x].is_mine = false;
            grid[y][x].state = CELL_HIDDEN;
            grid[y][x].neighbor_mines = 0;
        }
    }

    // Place Mines
    int mines_placed = 0;
    while (mines_placed < TOTAL_MINES) {
        int rx = rand() % GAME_COLS;
        int ry = rand() % GAME_ROWS;
        if (!grid[ry][rx].is_mine) {
            grid[ry][rx].is_mine = true;
            mines_placed++;
        }
    }

    calculate_neighbors();
}

// Recursive flood fill for empty cells
static void reveal_recursive(int x, int y) {
    if (!is_valid(x, y) || grid[y][x].state != CELL_HIDDEN) return;

    grid[y][x].state = CELL_REVEALED;
    cells_revealed++;

    if (grid[y][x].neighbor_mines > 0) return; // Stop at number border

    // Recurse neighbors
    for (int dy = -1; dy <= 1; dy++) {
        for (int dx = -1; dx <= 1; dx++) {
            reveal_recursive(x + dx, y + dy);
        }
    }
}

GameStatus game_click_cell(int x, int y, bool flag_mode) {
    if (current_status != GAME_PLAYING || !is_valid(x, y)) return current_status;

    Cell *c = &grid[y][x];

    if (flag_mode) {
        if (c->state == CELL_HIDDEN) c->state = CELL_FLAGGED;
        else if (c->state == CELL_FLAGGED) c->state = CELL_HIDDEN;
        return GAME_PLAYING;
    }

    // Dig Mode
    if (c->state == CELL_FLAGGED || c->state == CELL_REVEALED) return GAME_PLAYING;

    if (c->is_mine) {
        c->state = CELL_REVEALED;
        current_status = GAME_LOST;
        // Reveal all mines
        for(int i=0; i<GAME_ROWS; i++) {
            for(int j=0; j<GAME_COLS; j++) {
                if(grid[i][j].is_mine) grid[i][j].state = CELL_REVEALED;
            }
        }
    } else {
        reveal_recursive(x, y);
        // Check win condition
        if (cells_revealed >= (GAME_COLS * GAME_ROWS - TOTAL_MINES)) {
            current_status = GAME_WON;
        }
    }

    return current_status;
}

Cell* game_get_cell(int x, int y) {
    if (!is_valid(x, y)) return NULL;
    return &grid[y][x];
}

GameStatus game_get_status(void) {
    return current_status;
}

lvgl-port.c

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

/* * This function is called by the board initialization process
 * (usually in board/lvgl/lv_port_disp.c or similar)
 */
void lv_user_gui_init(void)
{
    ui_init();
}

main.c

C/C++
#include <rtthread.h>
#include <rtdevice.h>
#include "hal_data.h"

/* defined in Renesas FSP BSP, but re-defining just in case to match your context */
#ifndef BSP_IO_PORT_06_PIN_00
#define BSP_IO_PORT_06_PIN_00    (0x0600)
#endif

#define LCD_BACKLIGHT_PIN    BSP_IO_PORT_06_PIN_00

int main(void)
{
    /* 1. Hardware Setup: Turn on LCD Backlight
     * The display driver initializes the screen data, but the
     * physical backlight often requires this manual GPIO high.
     */
    rt_pin_mode(LCD_BACKLIGHT_PIN, PIN_MODE_OUTPUT);
    rt_pin_write(LCD_BACKLIGHT_PIN, PIN_HIGH);

    rt_kprintf("System Started: LCD Backlight ON\n");

    /* * 2. Main Loop
     * Since LVGL runs in its own thread (initialized by board/lvgl drivers),
     * and your game logic is hooked via lv_user_gui_init,
     * the main thread just needs to stay alive or yield.
     * * We blink the built-in LED (if available) or just sleep
     * to indicate the system is running without freezing.
     */
    while (1)
    {
        rt_thread_mdelay(1000);
    }

    return RT_EOK;
}

SConscript

C/C++
from building import *

cwd = GetCurrentDir()

# Include current directory for headers
CPPPATH = [cwd + '/../inc']

# Compile all .c files in src/
src = Glob('*.c')

group = DefineGroup('User_Game', src, depend = [''], CPPPATH = CPPPATH)

Return('group')

Credits

CARL HOVAN CHAN
1 project • 0 followers

Comments