Joseph Alan Vergara
Published © GPL3+

SNAKE OS - Interactive HMI Game on RT-Thread

A polished Snake Game on the Renesas HMI Board showcasing RT-Thread real-time power with smooth visuals, touch control, and dynamic effects.

IntermediateProtip1 hour78
SNAKE OS - Interactive HMI Game on RT-Thread

Things used in this project

Hardware components

hmi board
×1

Software apps and online services

RT-Thread IoT OS
RT-Thread IoT OS

Hand tools and fabrication machines

laptop

Story

Read more

Custom parts and enclosures

CAD - Enclosures and custom parts

Schematics

schematics and circuit diagrams

Code

SNAKE OS

C/C++
Here is a breakdown of Snake OS suitable for your documentation or presentation.

1. What is it?
Snake OS (HMI Edition) is a specialized embedded software application that turns a Renesas HMI-Board into a dedicated handheld gaming console.

Unlike a typical PC game, this is a "Bare Metal" / RTOS application. This means the code runs directly on the microcontroller hardware, managing every pixel on the screen and every touch input in real-time without an operating system like Windows or Android getting in the way.

It serves as a Technical Demonstrator to show that:

RT-Thread (Real-Time OS) can manage complex logic states seamlessly.

LVGL (Graphics Library) can render high-framerate animations (50 FPS) on embedded chips.

Hardware Interaction (Touch & Display) can be highly responsive.

2. How do you use it?
Here is the user manual for interacting with the system:

Step 1: Power On & Boot
Action: Plug the Renesas board into a USB power source.

Result: The system boots up. You will see the Splash Screen animation with the "SNAKE OS" logo and a loading bar. This initializes the hardware drivers in the background.

Step 2: The Main Menu
Action: Once the loading finishes, you are presented with the Main Menu.

Options:

PLAY GAME: Starts a new session.

SETTINGS: Takes you to a toggle switch where you can manually turn "Portal Walls" ON or OFF (for Levels 1-3).

Step 3: Playing the Game
Controls: Use the large D-Pad (Arrow Keys) at the bottom of the screen to steer the snake.

Objective:

Eat the Red Apples to grow and gain points.

Avoid hitting the Gray Obstacles or your own tail.

If Portal Mode is active (Cyan Border), you can pass through walls to teleport to the other side.

Bonus: If you see a Golden Apple, eat it quickly! It gives you +5 points and removes a wall, but it disappears after 5 seconds.

Step 4: Pausing & Quitting
Pause: Tap the Pause Icon (||) in the top-right corner. The game freezes instantly.

Resume: Tap "RESUME" to see a "3... 2... 1..." countdown before the snake moves again.

Quit: Tap "QUIT GAME" to return to the main menu (and see a "You Gave Up" message).

Step 5: Game Over & Restart
Action: If you crash, the "GAME OVER" screen appears showing your Score and High Score (saved to memory).

Result: Tap "RESTART" to play again instantly.

Autopilot (Attract Mode)
Action: Stop touching the screen for 10 seconds.

Result: The AI takes over and plays the game automatically to attract new players. Touch the screen anywhere to regain control.
#include "ui.h"
#include "game.h"
#include <stdio.h>

/* ---------------------------------------------------------- */
/* OBJECTS & VARIABLES                                        */
/* ---------------------------------------------------------- */
static lv_obj_t *scr_splash;
static lv_obj_t *scr_menu;
static lv_obj_t *scr_settings;
static lv_obj_t *scr_game;

static lv_obj_t *obj_board;
static lv_obj_t *obj_control_panel;
static lv_obj_t *label_score;
static lv_obj_t *label_high_score;
static lv_obj_t *label_level;
static lv_obj_t *label_demo;
static lv_obj_t *modal_game_over;
static lv_obj_t *modal_pause;
static lv_obj_t *modal_quit_msg;
static lv_obj_t *label_countdown;

static lv_obj_t *sw_portal_settings;
static lv_timer_t *timer_game = NULL;
static lv_timer_t *timer_countdown = NULL;

/* 50 FPS Rendering for smooth visuals */
#define RENDER_PERIOD_MS  20
static uint32_t time_accumulator = 0;

/* --- Styling --- */
#define SNAKE_COLOR_HEAD    lv_color_hex(0x4CAF50)
#define SNAKE_COLOR_TAIL    lv_color_hex(0xCCFF90)
#define FOOD_COLOR          lv_color_hex(0xFF5252)
#define GOLDEN_FOOD_COLOR   lv_color_hex(0xFFD700)
#define OBSTACLE_COLOR      lv_color_hex(0x9E9E9E)
#define BOARD_BG_COLOR      lv_color_hex(0x202020)
#define CONTROL_BG_COLOR    lv_color_hex(0x303030)
#define PORTAL_BORDER_COLOR lv_color_hex(0x00E5FF)
#define NORMAL_BORDER_COLOR lv_color_hex(0x555555)

/* --- Forward Declarations --- */
static void game_tick_cb(lv_timer_t * t);
static void countdown_timer_cb(lv_timer_t * t);
static void board_draw_event_cb(lv_event_t * e);
static void btn_dir_event_cb(lv_event_t * e);
static void pause_btn_event_cb(lv_event_t * e);
static void pause_modal_event_cb(lv_event_t * e);
static void quit_msg_event_cb(lv_event_t * e);
static void restart_btn_event_cb(lv_event_t * e);
static void menu_event_cb(lv_event_t * e);
static void settings_event_cb(lv_event_t * e);
static void show_game_over(void);
static void show_quit_message(void);
static void start_resume_countdown(void);
static void create_dpad(lv_obj_t * parent);
static void create_menu_screen(void);
static void create_settings_screen(void);
static void create_game_screen(void);
static void splash_anim_ready_cb(lv_anim_t * a);
static void cleanup_game_resources(void);

/* ---------------------------------------------------------- */
/* 1. INITIALIZATION (SPLASH SCREEN)                          */
/* ---------------------------------------------------------- */
void ui_init(void) {
    scr_splash = lv_obj_create(NULL);
    lv_obj_set_style_bg_color(scr_splash, lv_color_black(), 0);
    lv_scr_load(scr_splash);

    lv_obj_t * logo_cont = lv_obj_create(scr_splash);
    lv_obj_set_size(logo_cont, 180, 60);
    lv_obj_align(logo_cont, LV_ALIGN_CENTER, 0, -60);
    lv_obj_set_style_bg_color(logo_cont, lv_color_hex(0x4CAF50), 0);
    lv_obj_set_style_radius(logo_cont, 10, 0);
    lv_obj_set_style_border_width(logo_cont, 2, 0);
    lv_obj_set_style_border_color(logo_cont, lv_color_white(), 0);
    lv_obj_clear_flag(logo_cont, LV_OBJ_FLAG_SCROLLABLE);

    lv_obj_t * label_logo = lv_label_create(logo_cont);
    lv_label_set_text(label_logo, "SNAKE OS");
    lv_obj_set_style_text_font(label_logo, &lv_font_montserrat_14, 0);
    lv_obj_set_style_text_color(label_logo, lv_color_white(), 0);
    lv_obj_center(label_logo);

    lv_obj_t * eye = lv_obj_create(logo_cont);
    lv_obj_set_size(eye, 8, 8);
    lv_obj_set_style_radius(eye, 4, 0);
    lv_obj_set_style_bg_color(eye, lv_color_black(), 0);
    lv_obj_align(eye, LV_ALIGN_RIGHT_MID, -10, -5);

    lv_obj_t * label_devs = lv_label_create(scr_splash);
    lv_label_set_text(label_devs, "Developed By:\nJoseph Vergara\nJP Maluya");
    lv_obj_set_style_text_align(label_devs, LV_TEXT_ALIGN_CENTER, 0);
    lv_obj_set_style_text_color(label_devs, lv_color_white(), 0);
    lv_obj_align(label_devs, LV_ALIGN_CENTER, 0, 20);

    lv_obj_t * bar_loading = lv_bar_create(scr_splash);
    lv_obj_set_size(bar_loading, 200, 10);
    lv_obj_align(bar_loading, LV_ALIGN_BOTTOM_MID, 0, -40);
    lv_obj_set_style_bg_color(bar_loading, lv_color_hex(0x333333), LV_PART_MAIN);
    lv_obj_set_style_bg_color(bar_loading, lv_color_hex(0xFFD700), LV_PART_INDICATOR);

    lv_anim_t a;
    lv_anim_init(&a);
    lv_anim_set_var(&a, bar_loading);
    lv_anim_set_values(&a, 0, 100);
    lv_anim_set_time(&a, 3000);
    lv_anim_set_exec_cb(&a, (lv_anim_exec_xcb_t)lv_bar_set_value);
    lv_anim_set_path_cb(&a, lv_anim_path_ease_in_out);
    lv_anim_set_ready_cb(&a, splash_anim_ready_cb);
    lv_anim_start(&a);
}

static void splash_anim_ready_cb(lv_anim_t * a) {
    create_menu_screen();
    lv_scr_load_anim(scr_menu, LV_SCR_LOAD_ANIM_FADE_ON, 500, 0, true);
}

static void cleanup_game_resources(void) {
    if (timer_game != NULL) {
        lv_timer_del(timer_game);
        timer_game = NULL;
    }
    if (timer_countdown != NULL) {
        lv_timer_del(timer_countdown);
        timer_countdown = NULL;
    }
}

/* ---------------------------------------------------------- */
/* 2. MENU SCREEN                                             */
/* ---------------------------------------------------------- */
static void create_menu_screen(void) {
    game.state = GAME_STATE_MENU;

    scr_menu = lv_obj_create(NULL);
    lv_obj_set_style_bg_color(scr_menu, BOARD_BG_COLOR, 0);

    lv_obj_t * lbl_title = lv_label_create(scr_menu);
    lv_label_set_text(lbl_title, "SNAKE OS");
    lv_obj_set_style_text_font(lbl_title, &lv_font_montserrat_14, 0);
    lv_obj_set_style_text_color(lbl_title, lv_color_hex(0x4CAF50), 0);
    lv_obj_align(lbl_title, LV_ALIGN_TOP_MID, 0, 30);

    lv_obj_t * btn_play = lv_btn_create(scr_menu);
    lv_obj_set_size(btn_play, 160, 50);
    lv_obj_align(btn_play, LV_ALIGN_CENTER, 0, -20);
    lv_obj_add_event_cb(btn_play, menu_event_cb, LV_EVENT_CLICKED, (void*)1);
    lv_obj_t * lbl_play = lv_label_create(btn_play);
    lv_label_set_text(lbl_play, "PLAY GAME");
    lv_obj_center(lbl_play);

    lv_obj_t * btn_settings = lv_btn_create(scr_menu);
    lv_obj_set_size(btn_settings, 160, 50);
    lv_obj_align(btn_settings, LV_ALIGN_CENTER, 0, 40);
    lv_obj_set_style_bg_color(btn_settings, lv_color_hex(0x555555), 0);
    lv_obj_add_event_cb(btn_settings, menu_event_cb, LV_EVENT_CLICKED, (void*)2);
    lv_obj_t * lbl_set = lv_label_create(btn_settings);
    lv_label_set_text(lbl_set, "SETTINGS");
    lv_obj_center(lbl_set);
}

static void menu_event_cb(lv_event_t * e) {
    int code = (int)(intptr_t)lv_event_get_user_data(e);
    if (code == 1) {
        create_game_screen();
        lv_scr_load_anim(scr_game, LV_SCR_LOAD_ANIM_MOVE_LEFT, 300, 0, true);
    } else if (code == 2) {
        create_settings_screen();
        lv_scr_load_anim(scr_settings, LV_SCR_LOAD_ANIM_MOVE_LEFT, 300, 0, true);
    }
}

/* ---------------------------------------------------------- */
/* 3. SETTINGS SCREEN                                         */
/* ---------------------------------------------------------- */
static void create_settings_screen(void) {
    game.state = GAME_STATE_SETTINGS;

    scr_settings = lv_obj_create(NULL);
    lv_obj_set_style_bg_color(scr_settings, BOARD_BG_COLOR, 0);

    lv_obj_t * lbl = lv_label_create(scr_settings);
    lv_label_set_text(lbl, "SETTINGS");
    lv_obj_set_style_text_color(lbl, lv_color_white(), 0);
    lv_obj_align(lbl, LV_ALIGN_TOP_MID, 0, 20);

    lv_obj_t * panel = lv_obj_create(scr_settings);
    lv_obj_set_size(panel, 240, 60);
    lv_obj_align(panel, LV_ALIGN_CENTER, 0, -30);
    lv_obj_set_style_bg_color(panel, CONTROL_BG_COLOR, 0);
    lv_obj_set_style_border_width(panel, 0, 0);

    lv_obj_t * lbl_sw = lv_label_create(panel);
    lv_label_set_text(lbl_sw, "Portal Walls");
    lv_obj_set_style_text_color(lbl_sw, lv_color_white(), 0);
    lv_obj_align(lbl_sw, LV_ALIGN_LEFT_MID, 0, 0);

    sw_portal_settings = lv_switch_create(panel);
    if (game.user_portal_setting) lv_obj_add_state(sw_portal_settings, LV_STATE_CHECKED);
    lv_obj_align(sw_portal_settings, LV_ALIGN_RIGHT_MID, 0, 0);
    lv_obj_add_event_cb(sw_portal_settings, settings_event_cb, LV_EVENT_VALUE_CHANGED, NULL);

    lv_obj_t * btn_back = lv_btn_create(scr_settings);
    lv_obj_set_size(btn_back, 100, 40);
    lv_obj_align(btn_back, LV_ALIGN_BOTTOM_MID, 0, -30);
    lv_obj_add_event_cb(btn_back, settings_event_cb, LV_EVENT_CLICKED, NULL);
    lv_obj_t * lbl_back = lv_label_create(btn_back);
    lv_label_set_text(lbl_back, "BACK");
    lv_obj_center(lbl_back);
}

static void settings_event_cb(lv_event_t * e) {
    lv_event_code_t code = lv_event_get_code(e);
    lv_obj_t * obj = lv_event_get_target(e);

    if (code == LV_EVENT_VALUE_CHANGED) {
        bool active = lv_obj_has_state(obj, LV_STATE_CHECKED);
        game_set_portal_mode(active);
    }
    else if (code == LV_EVENT_CLICKED) {
        create_menu_screen();
        lv_scr_load_anim(scr_menu, LV_SCR_LOAD_ANIM_MOVE_RIGHT, 300, 0, true);
    }
}

/* ---------------------------------------------------------- */
/* 4. GAME SCREEN                                             */
/* ---------------------------------------------------------- */
static void create_game_screen(void) {
    cleanup_game_resources();

    game_reset();
    game.state = GAME_STATE_IDLE;
    time_accumulator = 0;

    scr_game = lv_obj_create(NULL);
    lv_obj_set_style_bg_color(scr_game, lv_color_black(), 0);

    /* Board Area */
    obj_board = lv_obj_create(scr_game);
    lv_obj_set_size(obj_board, 300, 272);
    lv_obj_align(obj_board, LV_ALIGN_LEFT_MID, 0, 0);
    lv_obj_set_style_bg_color(obj_board, BOARD_BG_COLOR, 0);
    lv_obj_set_style_border_width(obj_board, 2, 0);

    if(game.portal_mode) lv_obj_set_style_border_color(obj_board, PORTAL_BORDER_COLOR, 0);
    else lv_obj_set_style_border_color(obj_board, NORMAL_BORDER_COLOR, 0);

    lv_obj_set_scrollbar_mode(obj_board, LV_SCROLLBAR_MODE_OFF);
    lv_obj_clear_flag(obj_board, LV_OBJ_FLAG_SCROLLABLE);
    lv_obj_add_event_cb(obj_board, board_draw_event_cb, LV_EVENT_DRAW_MAIN, NULL);

    /* Auto Pilot Label */
    label_demo = lv_label_create(scr_game);
    lv_label_set_text(label_demo, "AUTO PILOT");
    lv_obj_set_style_text_font(label_demo, &lv_font_montserrat_14, 0);
    lv_obj_set_style_text_color(label_demo, lv_color_hex(0xFFD700), 0);
    lv_obj_set_style_bg_color(label_demo, lv_color_black(), 0);
    lv_obj_set_style_bg_opa(label_demo, LV_OPA_COVER, 0);
    lv_obj_set_style_pad_all(label_demo, 8, 0);
    lv_obj_set_style_radius(label_demo, 4, 0);
    lv_obj_set_style_border_width(label_demo, 1, 0);
    lv_obj_set_style_border_color(label_demo, lv_color_hex(0xFFD700), 0);
    lv_obj_align_to(label_demo, obj_board, LV_ALIGN_TOP_MID, 0, 15);
    lv_obj_add_flag(label_demo, LV_OBJ_FLAG_HIDDEN);

    /* Control Panel */
    obj_control_panel = lv_obj_create(scr_game);
    lv_obj_set_size(obj_control_panel, 180, 272);
    lv_obj_align(obj_control_panel, LV_ALIGN_RIGHT_MID, 0, 0);
    lv_obj_set_style_bg_color(obj_control_panel, CONTROL_BG_COLOR, 0);
    lv_obj_set_style_border_width(obj_control_panel, 0, 0);

    label_score = lv_label_create(obj_control_panel);
    lv_label_set_text(label_score, "Score: 0");
    lv_obj_set_style_text_font(label_score, &lv_font_montserrat_14, 0);
    lv_obj_set_style_text_color(label_score, lv_color_white(), 0);
    lv_obj_align(label_score, LV_ALIGN_TOP_LEFT, 10, 10);

    label_high_score = lv_label_create(obj_control_panel);
    lv_label_set_text(label_high_score, "Best: 0");
    lv_obj_set_style_text_font(label_high_score, &lv_font_montserrat_14, 0);
    lv_obj_set_style_text_color(label_high_score, lv_color_hex(0xFFD700), 0);
    lv_obj_align(label_high_score, LV_ALIGN_TOP_LEFT, 10, 30);

    label_level = lv_label_create(obj_control_panel);
    lv_label_set_text(label_level, "Level 1");
    lv_obj_set_style_text_font(label_level, &lv_font_montserrat_14, 0);
    lv_obj_set_style_text_color(label_level, lv_color_hex(0x00E5FF), 0);
    lv_obj_align(label_level, LV_ALIGN_TOP_LEFT, 10, 50);

    /* Pause Button (Top Right) */
    lv_obj_t * btn_pause = lv_btn_create(obj_control_panel);
    lv_obj_set_size(btn_pause, 40, 40);
    lv_obj_align(btn_pause, LV_ALIGN_TOP_RIGHT, -10, 10);
    lv_obj_set_style_bg_color(btn_pause, lv_color_hex(0x555555), 0);
    lv_obj_add_event_cb(btn_pause, pause_btn_event_cb, LV_EVENT_CLICKED, NULL);
    lv_obj_t * lbl_pause = lv_label_create(btn_pause);
    lv_label_set_text(lbl_pause, LV_SYMBOL_PAUSE);
    lv_obj_center(lbl_pause);

    create_dpad(obj_control_panel);

    timer_game = lv_timer_create(game_tick_cb, RENDER_PERIOD_MS, NULL);
}

static void create_dpad(lv_obj_t * parent) {
    const int btn_size = 48;
    const int center_offset_y = 30;

    lv_obj_t * btn_up = lv_btn_create(parent);
    lv_obj_set_size(btn_up, btn_size, btn_size);
    lv_obj_align(btn_up, LV_ALIGN_CENTER, 0, center_offset_y - btn_size);
    lv_obj_set_style_bg_color(btn_up, lv_palette_main(LV_PALETTE_BLUE), 0);
    lv_obj_t * lbl = lv_label_create(btn_up);
    lv_label_set_text(lbl, LV_SYMBOL_UP);
    lv_obj_center(lbl);
    lv_obj_add_event_cb(btn_up, btn_dir_event_cb, LV_EVENT_CLICKED, (void*)(intptr_t)DIR_UP);

    lv_obj_t * btn_down = lv_btn_create(parent);
    lv_obj_set_size(btn_down, btn_size, btn_size);
    lv_obj_align(btn_down, LV_ALIGN_CENTER, 0, center_offset_y + btn_size);
    lv_obj_set_style_bg_color(btn_down, lv_palette_main(LV_PALETTE_BLUE), 0);
    lbl = lv_label_create(btn_down);
    lv_label_set_text(lbl, LV_SYMBOL_DOWN);
    lv_obj_center(lbl);
    lv_obj_add_event_cb(btn_down, btn_dir_event_cb, LV_EVENT_CLICKED, (void*)(intptr_t)DIR_DOWN);

    lv_obj_t * btn_left = lv_btn_create(parent);
    lv_obj_set_size(btn_left, btn_size, btn_size);
    lv_obj_align(btn_left, LV_ALIGN_CENTER, -btn_size, center_offset_y);
    lv_obj_set_style_bg_color(btn_left, lv_palette_main(LV_PALETTE_BLUE), 0);
    lbl = lv_label_create(btn_left);
    lv_label_set_text(lbl, LV_SYMBOL_LEFT);
    lv_obj_center(lbl);
    lv_obj_add_event_cb(btn_left, btn_dir_event_cb, LV_EVENT_CLICKED, (void*)(intptr_t)DIR_LEFT);

    lv_obj_t * btn_right = lv_btn_create(parent);
    lv_obj_set_size(btn_right, btn_size, btn_size);
    lv_obj_align(btn_right, LV_ALIGN_CENTER, btn_size, center_offset_y);
    lv_obj_set_style_bg_color(btn_right, lv_palette_main(LV_PALETTE_BLUE), 0);
    lbl = lv_label_create(btn_right);
    lv_label_set_text(lbl, LV_SYMBOL_RIGHT);
    lv_obj_center(lbl);
    lv_obj_add_event_cb(btn_right, btn_dir_event_cb, LV_EVENT_CLICKED, (void*)(intptr_t)DIR_RIGHT);
}

/* --- PAUSE SYSTEM --- */
static void pause_btn_event_cb(lv_event_t * e) {
    if (game.state != GAME_STATE_PLAYING && game.state != GAME_STATE_PAUSED) return;

    game_toggle_pause();

    if (game.state == GAME_STATE_PAUSED) {
        modal_pause = lv_obj_create(scr_game);
        lv_obj_set_size(modal_pause, 200, 160);
        lv_obj_center(modal_pause);
        lv_obj_set_style_bg_color(modal_pause, lv_color_hex(0x444444), 0);
        lv_obj_set_style_border_color(modal_pause, lv_color_white(), 0);

        lv_obj_t * lbl = lv_label_create(modal_pause);
        lv_label_set_text(lbl, "PAUSED");
        lv_obj_set_style_text_font(lbl, &lv_font_montserrat_14, 0);
        lv_obj_set_style_text_color(lbl, lv_color_white(), 0);
        lv_obj_align(lbl, LV_ALIGN_TOP_MID, 0, 10);

        lv_obj_t * btn_res = lv_btn_create(modal_pause);
        lv_obj_set_size(btn_res, 140, 40);
        lv_obj_align(btn_res, LV_ALIGN_CENTER, 0, 0);
        lv_obj_add_event_cb(btn_res, pause_modal_event_cb, LV_EVENT_CLICKED, (void*)1);
        lv_obj_t * lbl_res = lv_label_create(btn_res);
        lv_label_set_text(lbl_res, "RESUME");
        lv_obj_center(lbl_res);

        lv_obj_t * btn_quit = lv_btn_create(modal_pause);
        lv_obj_set_size(btn_quit, 140, 40);
        lv_obj_align(btn_quit, LV_ALIGN_BOTTOM_MID, 0, -10);
        lv_obj_set_style_bg_color(btn_quit, lv_color_hex(0xFF5252), 0);
        lv_obj_add_event_cb(btn_quit, pause_modal_event_cb, LV_EVENT_CLICKED, (void*)2);
        lv_obj_t * lbl_quit = lv_label_create(btn_quit);
        lv_label_set_text(lbl_quit, "QUIT GAME");
        lv_obj_center(lbl_quit);
    } else {
        if(modal_pause) {
            lv_obj_del(modal_pause);
            modal_pause = NULL;
        }
    }
}

static void pause_modal_event_cb(lv_event_t * e) {
    int code = (int)(intptr_t)lv_event_get_user_data(e);
    lv_obj_del(modal_pause);
    modal_pause = NULL;

    if (code == 1) { /* Resume */
        start_resume_countdown();
    } else if (code == 2) { /* Quit */
        show_quit_message();
    }
}

static void start_resume_countdown(void) {
    game.state = GAME_STATE_RESUME_COUNTDOWN;
    game.countdown_val = 3;

    label_countdown = lv_label_create(scr_game);
    lv_obj_set_style_text_font(label_countdown, &lv_font_montserrat_14, 0);
    lv_obj_set_style_transform_zoom(label_countdown, 1024, 0);

    lv_obj_set_style_text_color(label_countdown, lv_color_hex(0xFFD700), 0);
    lv_label_set_text(label_countdown, "3");
    lv_obj_align_to(label_countdown, obj_board, LV_ALIGN_CENTER, 0, 0);

    timer_countdown = lv_timer_create(countdown_timer_cb, 1000, NULL);
}

static void countdown_timer_cb(lv_timer_t * t) {
    game.countdown_val--;

    if (game.countdown_val > 0) {
        lv_label_set_text_fmt(label_countdown, "%d", game.countdown_val);
    } else {
        lv_obj_del(label_countdown);
        lv_timer_del(timer_countdown);
        timer_countdown = NULL;
        game.state = GAME_STATE_PLAYING;
        time_accumulator = 0;
    }
}

static void show_quit_message(void) {
    modal_quit_msg = lv_obj_create(scr_game);
    lv_obj_set_size(modal_quit_msg, 220, 120);
    lv_obj_center(modal_quit_msg);
    lv_obj_set_style_bg_color(modal_quit_msg, lv_color_hex(0x000000), 0);
    lv_obj_set_style_border_width(modal_quit_msg, 2, 0);
    lv_obj_set_style_border_color(modal_quit_msg, lv_color_hex(0xFF5252), 0);

    lv_obj_t * lbl = lv_label_create(modal_quit_msg);
    lv_label_set_text(lbl, "YOU GAVE UP!\n(Weak...)");
    lv_obj_set_style_text_align(lbl, LV_TEXT_ALIGN_CENTER, 0);
    lv_obj_set_style_text_color(lbl, lv_color_hex(0xFF5252), 0);
    lv_obj_center(lbl);

    lv_obj_t * btn = lv_btn_create(modal_quit_msg);
    lv_obj_set_size(btn, 220, 120);
    lv_obj_center(btn);
    lv_obj_set_style_bg_opa(btn, LV_OPA_TRANSP, 0);
    lv_obj_add_event_cb(btn, quit_msg_event_cb, LV_EVENT_CLICKED, NULL);
}

static void quit_msg_event_cb(lv_event_t * e) {
    cleanup_game_resources();
    create_menu_screen();
    lv_scr_load_anim(scr_menu, LV_SCR_LOAD_ANIM_FADE_ON, 500, 0, true);
}

static void game_tick_cb(lv_timer_t * t) {
    if (game.state == GAME_STATE_IDLE || game.state == GAME_STATE_OVER) {
        game_process_idle();
    }

    game_update_particles();

    if (game.state == GAME_STATE_AI_PLAYING && modal_game_over) {
        lv_obj_del(modal_game_over);
        modal_game_over = NULL;
    }

    if (game.state == GAME_STATE_AI_PLAYING) {
        lv_obj_clear_flag(label_demo, LV_OBJ_FLAG_HIDDEN);
        lv_obj_move_foreground(label_demo);
    } else if (label_demo) {
        lv_obj_add_flag(label_demo, LV_OBJ_FLAG_HIDDEN);
    }

    if (obj_board) {
        if(game.portal_mode) lv_obj_set_style_border_color(obj_board, PORTAL_BORDER_COLOR, 0);
        else lv_obj_set_style_border_color(obj_board, NORMAL_BORDER_COLOR, 0);
    }

    if (game.state == GAME_STATE_PLAYING || game.state == GAME_STATE_AI_PLAYING) {

        time_accumulator += RENDER_PERIOD_MS;

        int catch_up_limit = 0;
        while (time_accumulator >= game.speed_ms && catch_up_limit < 2) {
            game_update();
            time_accumulator -= game.speed_ms;
            catch_up_limit++;
        }
        if (time_accumulator > 300) time_accumulator = 0;

        lv_label_set_text_fmt(label_score, "Score: %d", (int)game.score);
        lv_label_set_text_fmt(label_high_score, "Best: %d", (int)game.high_score);
        lv_label_set_text_fmt(label_level, "Level %d", (int)game.level);

        lv_obj_invalidate(obj_board);

        if (game.state == GAME_STATE_OVER) {
            show_game_over();
        }
    }
}

static void btn_dir_event_cb(lv_event_t * e) {
    if (game.state == GAME_STATE_MENU || game.state == GAME_STATE_SETTINGS ||
        game.state == GAME_STATE_PAUSED || game.state == GAME_STATE_RESUME_COUNTDOWN) return;

    if (game.state == GAME_STATE_IDLE || game.state == GAME_STATE_OVER || game.state == GAME_STATE_AI_PLAYING) {
        if(game.state == GAME_STATE_OVER) game_reset();
        if(game.state == GAME_STATE_AI_PLAYING) game_reset();
        if (modal_game_over) {
            lv_obj_del(modal_game_over);
            modal_game_over = NULL;
        }
        game.state = GAME_STATE_PLAYING;
    }
    direction_t dir = (direction_t)(intptr_t)lv_event_get_user_data(e);
    game_set_direction(dir);
}

static void restart_btn_event_cb(lv_event_t * e) {
    if (lv_event_get_code(e) == LV_EVENT_CLICKED) {
        game_reset();
        game.state = GAME_STATE_PLAYING;
        if (modal_game_over) {
            lv_obj_del(modal_game_over);
            modal_game_over = NULL;
        }
        lv_label_set_text(label_score, "Score: 0");
    }
}

static void board_draw_event_cb(lv_event_t * e) {
    lv_draw_ctx_t * draw_ctx = lv_event_get_draw_ctx(e);
    lv_draw_rect_dsc_t rect_dsc;
    lv_draw_rect_dsc_init(&rect_dsc);

    lv_obj_t * obj = lv_event_get_target(e);
    lv_area_t obj_coords;
    lv_obj_get_coords(obj, &obj_coords);
    int offset_x = obj_coords.x1;
    int offset_y = obj_coords.y1 + GRID_OFFSET_Y;

    rect_dsc.bg_color = FOOD_COLOR;
    rect_dsc.radius = CELL_SIZE / 2;
    lv_area_t area;
    area.x1 = offset_x + game.food.x * CELL_SIZE;
    area.y1 = offset_y + game.food.y * CELL_SIZE;
    area.x2 = area.x1 + CELL_SIZE - 2;
    area.y2 = area.y1 + CELL_SIZE - 2;
    lv_draw_rect(draw_ctx, &rect_dsc, &area);

    if (game.golden_active) {
        rect_dsc.bg_color = GOLDEN_FOOD_COLOR;
        area.x1 = offset_x + game.golden_food.x * CELL_SIZE;
        area.y1 = offset_y + game.golden_food.y * CELL_SIZE;
        area.x2 = area.x1 + CELL_SIZE - 2;
        area.y2 = area.y1 + CELL_SIZE - 2;
        lv_draw_rect(draw_ctx, &rect_dsc, &area);
    }

    rect_dsc.bg_color = OBSTACLE_COLOR;
    rect_dsc.radius = 2;
    for (int i = 0; i < game.obstacle_count; i++) {
        area.x1 = offset_x + game.obstacles[i].x * CELL_SIZE;
        area.y1 = offset_y + game.obstacles[i].y * CELL_SIZE;
        area.x2 = area.x1 + CELL_SIZE - 2;
        area.y2 = area.y1 + CELL_SIZE - 2;
        lv_draw_rect(draw_ctx, &rect_dsc, &area);
    }

    rect_dsc.radius = 4;
    for (int i = 0; i < game.length; i++) {
        uint8_t ratio = 0;
        if (game.length > 1) ratio = (i * 255) / (game.length - 1);
        rect_dsc.bg_color = lv_color_mix(SNAKE_COLOR_TAIL, SNAKE_COLOR_HEAD, ratio);

        area.x1 = offset_x + game.body[i].x * CELL_SIZE;
        area.y1 = offset_y + game.body[i].y * CELL_SIZE;
        area.x2 = area.x1 + CELL_SIZE - 2;
        area.y2 = area.y1 + CELL_SIZE - 2;
        lv_draw_rect(draw_ctx, &rect_dsc, &area);

        if (i == 0) {
            lv_draw_rect_dsc_t eye_dsc;
            lv_draw_rect_dsc_init(&eye_dsc);
            eye_dsc.bg_color = lv_color_white();
            eye_dsc.radius = 10;
            lv_draw_rect_dsc_t pupil_dsc;
            lv_draw_rect_dsc_init(&pupil_dsc);
            pupil_dsc.bg_color = lv_color_black();
            pupil_dsc.radius = 10;

            int hx = area.x1; int hy = area.y1;
            int eye_size = 6; int pupil_size = 2;
            lv_area_t eye1, eye2, p1, p2;
            int e1_x, e1_y, e2_x, e2_y, p_off_x, p_off_y;

            switch(game.current_dir) {
                case DIR_UP: e1_x = hx+2; e1_y = hy+2; e2_x = hx+12; e2_y = hy+2; p_off_x = 2; p_off_y = 0; break;
                case DIR_DOWN: e1_x = hx+2; e1_y = hy+10; e2_x = hx+12; e2_y = hy+10; p_off_x = 2; p_off_y = 4; break;
                case DIR_LEFT: e1_x = hx+2; e1_y = hy+2; e2_x = hx+2; e2_y = hy+12; p_off_x = 0; p_off_y = 2; break;
                case DIR_RIGHT: default: e1_x = hx+10; e1_y = hy+2; e2_x = hx+10; e2_y = hy+12; p_off_x = 4; p_off_y = 2; break;
            }

            eye1.x1=e1_x; eye1.y1=e1_y; eye1.x2=e1_x+eye_size; eye1.y2=e1_y+eye_size;
            eye2.x1=e2_x; eye2.y1=e2_y; eye2.x2=e2_x+eye_size; eye2.y2=e2_y+eye_size;
            p1.x1=e1_x+(eye_size/2)-(pupil_size/2)+(p_off_x-2); p1.y1=e1_y+(eye_size/2)-(pupil_size/2)+(p_off_y-2);
            p1.x2=p1.x1+pupil_size; p1.y2=p1.y1+pupil_size;
            p2.x1=e2_x+(eye_size/2)-(pupil_size/2)+(p_off_x-2); p2.y1=e2_y+(eye_size/2)-(pupil_size/2)+(p_off_y-2);
            p2.x2=p2.x1+pupil_size; p2.y2=p2.y1+pupil_size;

            lv_draw_rect(draw_ctx, &eye_dsc, &eye1);
            lv_draw_rect(draw_ctx, &eye_dsc, &eye2);
            lv_draw_rect(draw_ctx, &pupil_dsc, &p1);
            lv_draw_rect(draw_ctx, &pupil_dsc, &p2);
        }
    }

    for (int i = 0; i < MAX_PARTICLES; i++) {
        if (game.particles[i].active) {
            rect_dsc.bg_color = lv_color_hex(game.particles[i].color);
            rect_dsc.radius = 0;
            int px = offset_x + game.particles[i].x;
            int py = offset_y + game.particles[i].y;
            int size = 4;
            area.x1 = px; area.y1 = py; area.x2 = px + size; area.y2 = py + size;
            lv_draw_rect(draw_ctx, &rect_dsc, &area);
        }
    }
}

static void show_game_over(void) {
    if (modal_game_over) return;
    modal_game_over = lv_obj_create(scr_game);
    lv_obj_set_size(modal_game_over, 220, 180);
    lv_obj_center(modal_game_over);
    lv_obj_set_style_bg_color(modal_game_over, lv_color_hex(0x444444), 0);
    lv_obj_set_style_border_color(modal_game_over, lv_color_white(), 0);

    lv_obj_t * lbl = lv_label_create(modal_game_over);
    lv_label_set_text(lbl, "GAME OVER");
    lv_obj_set_style_text_font(lbl, &lv_font_montserrat_14, 0);
    lv_obj_set_style_text_color(lbl, lv_color_hex(0xFF5252), 0);
    lv_obj_align(lbl, LV_ALIGN_TOP_MID, 0, 10);

    lv_obj_t * score_lbl = lv_label_create(modal_game_over);
    lv_label_set_text_fmt(score_lbl, "Score: %d\nBest: %d", (int)game.score, (int)game.high_score);
    lv_obj_set_style_text_align(score_lbl, LV_TEXT_ALIGN_CENTER, 0);
    lv_obj_set_style_text_color(score_lbl, lv_color_white(), 0);
    lv_obj_align(score_lbl, LV_ALIGN_TOP_MID, 0, 50);

    lv_obj_t * btn = lv_btn_create(modal_game_over);
    lv_obj_set_size(btn, 120, 50);
    lv_obj_align(btn, LV_ALIGN_BOTTOM_MID, 0, -10);
    lv_obj_add_event_cb(btn, restart_btn_event_cb, LV_EVENT_CLICKED, NULL);

    lv_obj_t * btn_lbl = lv_label_create(btn);
    lv_label_set_text(btn_lbl, "RESTART");
    lv_obj_center(btn_lbl);
}

Credits

Joseph Alan Vergara
1 project • 0 followers
Thanks to gemini AI.

Comments