Hardware components | ||||||
| × | 1 | ||||
Software apps and online services | ||||||
![]() |
| |||||
This project is a two-player strategy game called Sweet Sabotage, developed on the Renesas HMI-Board using RT-Thread and the LVGL graphics library. I created this project to learn how to build a fully interactive embedded interface, combine touchscreen input with a real-time operating system, and design a simple but engaging game using LVGL. The game works by presenting a 5x5 grid of candies where two players take turns selecting tiles while trying to avoid hidden poison placed during a secret setup phase. Each turn reveals whether the selected candy is safe or lethal, and the game manages all logic, display updates, and state transitions through a structured C-based architecture.
MAIN SCREEN
PLAY SCREEN
#include "game.h"
#include <stdio.h>
static game_phase_t current_phase;
static int current_player;
static int8_t poison_p1_idx;
static int8_t poison_p2_idx;
static bool taken[GRID_SIZE];
static int turn_count;
static int winner;
/* Initialize game state */
void game_init(void)
{
game_reset();
}
void game_reset(void)
{
current_phase = GAME_PHASE_SETUP_P1;
current_player = 1;
poison_p1_idx = -1;
poison_p2_idx = -1;
turn_count = 0;
winner = 0;
for (int i = 0; i < GRID_SIZE; i++) {
taken[i] = false;
}
ui_update_view();
ui_show_modal("Setup Phase", "Player 1:\nSelect a candy to hide your poison!");
}
/* Main Interaction Logic */
void game_on_cell_tap(uint8_t idx)
{
if (idx >= GRID_SIZE) return;
/* --- Setup Phase Player 1 --- */
if (current_phase == GAME_PHASE_SETUP_P1) {
poison_p1_idx = (int8_t)idx;
/* Transition to P2 */
current_phase = GAME_PHASE_SETUP_P2;
current_player = 2;
ui_update_view(); /* Clears board so P2 can't see P1's pick */
ui_show_modal("Pass Device", "Pass device to Player 2.\nPlayer 2: Hide your poison!");
return;
}
/* --- Setup Phase Player 2 --- */
if (current_phase == GAME_PHASE_SETUP_P2) {
/* FIX: Removed the check 'if (idx == poison_p1_idx)'
Now P2 CAN pick the same spot as P1 (Double Poison Strategy) */
poison_p2_idx = (int8_t)idx;
/* Start Game */
current_phase = GAME_PHASE_PLAY;
current_player = 1; /* P1 starts */
ui_update_view();
ui_show_modal("Game Start!", "Grid is armed.\nPlayer 1, eat a candy... if you dare.");
return;
}
/* --- Main Gameplay Phase --- */
if (current_phase == GAME_PHASE_PLAY) {
if (taken[idx]) return; /* Already eaten */
taken[idx] = true;
turn_count++;
/* Check for Death (Poison) */
bool is_poison = (idx == poison_p1_idx || idx == poison_p2_idx);
if (is_poison) {
/* Current player ate poison -> Opponent wins */
winner = (current_player == 1) ? 2 : 1;
current_phase = GAME_PHASE_GAME_OVER;
char msg[64];
snprintf(msg, sizeof(msg), "Player %d ate poison!", current_player);
ui_update_view(); /* Reveal poisons */
ui_show_modal(winner == 1 ? "Player 1 Wins!" : "Player 2 Wins!", msg);
}
else {
/* Check Draw Condition */
int safe_count = 0;
/* Calculate total safe candies dynamically based on overlap */
int total_poisons = (poison_p1_idx == poison_p2_idx) ? 1 : 2;
int target_safe_eaten = GRID_SIZE - total_poisons;
for (int i = 0; i < GRID_SIZE; i++) {
if (taken[i] && i != poison_p1_idx && i != poison_p2_idx) {
safe_count++;
}
}
/* If all safe candies are eaten */
if (safe_count >= target_safe_eaten) {
winner = 0;
current_phase = GAME_PHASE_GAME_OVER;
ui_update_view();
ui_show_modal("DRAW!", "All safe candies eaten.\nYou both survived!");
} else {
/* Switch Turn */
current_player = (current_player == 1) ? 2 : 1;
ui_update_view();
}
}
}
}
/* Getters */
game_phase_t game_get_phase(void) { return current_phase; }
int game_get_current_player(void) { return current_player; }
bool game_is_cell_taken(uint8_t idx) { return taken[idx]; }
bool game_is_p1_poison(uint8_t idx) { return (int8_t)idx == poison_p1_idx; }
bool game_is_p2_poison(uint8_t idx) { return (int8_t)idx == poison_p2_idx; }
int game_get_score_p1(void) { return (winner == 1) ? 1 : 0; }
int game_get_score_p2(void) { return (winner == 2) ? 1 : 0; }
#ifndef GAME_H
#define GAME_H
#include <stdint.h>
#include <stdbool.h>
/* 5x5 Grid settings */
#define GRID_ROWS 5
#define GRID_COLS 5
#define GRID_SIZE (GRID_ROWS * GRID_COLS)
/* Game Phases */
typedef enum {
GAME_PHASE_SETUP_P1, // Player 1 hiding poison
GAME_PHASE_SETUP_P2, // Player 2 hiding poison
GAME_PHASE_PLAY, // Main gameplay loop
GAME_PHASE_GAME_OVER // Match finished
} game_phase_t;
/* Core API */
void game_init(void);
void game_reset(void);
void game_on_cell_tap(uint8_t idx);
/* State Getters for UI */
game_phase_t game_get_phase(void);
int game_get_current_player(void);
bool game_is_cell_taken(uint8_t idx);
bool game_is_p1_poison(uint8_t idx);
bool game_is_p2_poison(uint8_t idx);
int game_get_score_p1(void); // Used as win counter or dummy
int game_get_score_p2(void);
/* Callbacks implemented in UI */
extern void ui_show_modal(const char *title, const char *msg);
extern void ui_update_view(void);
#endif
#ifndef UI_H__
#define UI_H__
#include <lvgl.h>
#include "game.h"
/* Initialize UI (create screens, widgets) */
void ui_init(void);
/* Call frequently from main loop */
void ui_task_handler(void);
/* Refresh a single cell's visuals (call after state changes) */
void ui_refresh_cell(uint8_t idx);
/* Show confirm popup during poison selection (player 1 or 2) */
void ui_confirm_selection(uint8_t player, uint8_t idx);
/* Show final game over overlay */
void ui_show_game_over(int winner);
/* Request restart (from UI or popup) */
void ui_request_restart(void);
#endif /* UI_H__ */
#include "game.h"
#include <stdio.h>
static game_phase_t current_phase;
static int current_player;
static int8_t poison_p1_idx;
static int8_t poison_p2_idx;
static bool taken[GRID_SIZE];
static int turn_count;
static int winner;
/* Initialize game state */
void game_init(void)
{
game_reset();
}
void game_reset(void)
{
current_phase = GAME_PHASE_SETUP_P1;
current_player = 1;
poison_p1_idx = -1;
poison_p2_idx = -1;
turn_count = 0;
winner = 0;
for (int i = 0; i < GRID_SIZE; i++) {
taken[i] = false;
}
ui_update_view();
ui_show_modal("Setup Phase", "Player 1:\nSelect a candy to hide your poison!");
}
/* Main Interaction Logic */
void game_on_cell_tap(uint8_t idx)
{
if (idx >= GRID_SIZE) return;
/* --- Setup Phase Player 1 --- */
if (current_phase == GAME_PHASE_SETUP_P1) {
poison_p1_idx = (int8_t)idx;
/* Transition to P2 */
current_phase = GAME_PHASE_SETUP_P2;
current_player = 2;
ui_update_view(); /* Clears board so P2 can't see P1's pick */
ui_show_modal("Pass Device", "Pass device to Player 2.\nPlayer 2: Hide your poison!");
return;
}
/* --- Setup Phase Player 2 --- */
if (current_phase == GAME_PHASE_SETUP_P2) {
/* FIX: Removed the check 'if (idx == poison_p1_idx)'
Now P2 CAN pick the same spot as P1 (Double Poison Strategy) */
poison_p2_idx = (int8_t)idx;
/* Start Game */
current_phase = GAME_PHASE_PLAY;
current_player = 1; /* P1 starts */
ui_update_view();
ui_show_modal("Game Start!", "Grid is armed.\nPlayer 1, eat a candy... if you dare.");
return;
}
/* --- Main Gameplay Phase --- */
if (current_phase == GAME_PHASE_PLAY) {
if (taken[idx]) return; /* Already eaten */
taken[idx] = true;
turn_count++;
/* Check for Death (Poison) */
bool is_poison = (idx == poison_p1_idx || idx == poison_p2_idx);
if (is_poison) {
/* Current player ate poison -> Opponent wins */
winner = (current_player == 1) ? 2 : 1;
current_phase = GAME_PHASE_GAME_OVER;
char msg[64];
snprintf(msg, sizeof(msg), "Player %d ate poison!", current_player);
ui_update_view(); /* Reveal poisons */
ui_show_modal(winner == 1 ? "Player 1 Wins!" : "Player 2 Wins!", msg);
}
else {
/* Check Draw Condition */
int safe_count = 0;
/* Calculate total safe candies dynamically based on overlap */
int total_poisons = (poison_p1_idx == poison_p2_idx) ? 1 : 2;
int target_safe_eaten = GRID_SIZE - total_poisons;
for (int i = 0; i < GRID_SIZE; i++) {
if (taken[i] && i != poison_p1_idx && i != poison_p2_idx) {
safe_count++;
}
}
/* If all safe candies are eaten */
if (safe_count >= target_safe_eaten) {
winner = 0;
current_phase = GAME_PHASE_GAME_OVER;
ui_update_view();
ui_show_modal("DRAW!", "All safe candies eaten.\nYou both survived!");
} else {
/* Switch Turn */
current_player = (current_player == 1) ? 2 : 1;
ui_update_view();
}
}
}
}
/* Getters */
game_phase_t game_get_phase(void) { return current_phase; }
int game_get_current_player(void) { return current_player; }
bool game_is_cell_taken(uint8_t idx) { return taken[idx]; }
bool game_is_p1_poison(uint8_t idx) { return (int8_t)idx == poison_p1_idx; }
bool game_is_p2_poison(uint8_t idx) { return (int8_t)idx == poison_p2_idx; }
int game_get_score_p1(void) { return (winner == 1) ? 1 : 0; }
int game_get_score_p2(void) { return (winner == 2) ? 1 : 0; }
#include <lvgl.h>
#include "ui.h"
#include "game.h"
/* * The display and input drivers (lv_port_disp_init, lv_port_indev_init)
* are provided by the board drivers in the "board/lvgl" folder.
* * This file only needs to define the user GUI entry point.
*/
void lv_user_gui_init(void)
{
/* * FIX: Only call ui_init().
* * We DO NOT call game_init() here because ui_init() inside 'src/ui.c'
* already calls game_init() at the very end.
* * Calling it twice here was causing the crash (black screen) because
* the game tried to update buttons that hadn't been created yet.
*/
ui_init();
}
#include <rtthread.h>
#include <rtdevice.h>
#include <board.h>
/* * NOTE: We do NOT include lvgl.h or ui.h here.
* The LVGL thread starts automatically in the background
* and calls lv_user_gui_init() in src/lvgl_port.c
*/
int main(void)
{
/* LED setup (optional, just to verify board is alive) */
#ifdef LED_BLUE_PIN
rt_pin_mode(LED_BLUE_PIN, PIN_MODE_OUTPUT);
#endif
while (1)
{
/* Optional: Blink LED to show system is running */
#ifdef LED_BLUE_PIN
rt_pin_write(LED_BLUE_PIN, PIN_HIGH);
rt_thread_mdelay(500);
rt_pin_write(LED_BLUE_PIN, PIN_LOW);
rt_thread_mdelay(500);
#else
/* Just sleep to let the idle task run */
rt_thread_mdelay(1000);
#endif
}
return 0;
}
#include "ui.h"
#include "game.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
/* --- EMBEDDED CANDY ICON (Alpha Map) --- */
/* This creates a "Candy" shape directly in code so you don't need external files/fonts */
#if LV_COLOR_DEPTH == 16 || LV_COLOR_DEPTH == 32
static const uint8_t candy_icon_map[] = {
0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00,
0x00, 0x00, 0xff, 0xcc, 0xff, 0x00, 0x00, 0x00, 0xff, 0xcc, 0xff, 0x00,
0x00, 0xff, 0xcc, 0x00, 0xcc, 0xff, 0xff, 0xff, 0x00, 0xcc, 0xff, 0x00,
0xff, 0xcc, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x00, 0x00, 0xcc, 0xff,
0x00, 0xff, 0xcc, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xcc, 0xff,
0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00,
0x00, 0x00, 0xff, 0xff, 0xff, 0xe0, 0xe0, 0xff, 0xff, 0xff, 0x00, 0x00,
0x00, 0x00, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0xff, 0x00, 0x00,
0x00, 0x00, 0xff, 0xff, 0xff, 0xe0, 0xe0, 0xff, 0xff, 0xff, 0x00, 0x00,
0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00,
0x00, 0xff, 0xcc, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0xcc, 0xff,
0xff, 0xcc, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x00, 0x00, 0xcc, 0xff,
0x00, 0xff, 0xcc, 0x00, 0xcc, 0xff, 0xff, 0xff, 0x00, 0xcc, 0xff, 0x00,
0x00, 0x00, 0xff, 0xcc, 0xff, 0x00, 0x00, 0x00, 0xff, 0xcc, 0xff, 0x00,
0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00,
};
const lv_img_dsc_t candy_icon = {
.header.cf = LV_IMG_CF_ALPHA_8BIT,
.header.always_zero = 0,
.header.reserved = 0,
.header.w = 12,
.header.h = 15,
.data_size = 180,
.data = candy_icon_map,
};
#endif
/* Layout for 480x272 - 5x5 Grid */
#define CELL_W 54
#define CELL_H 38
#define CELL_GAP 6
#define GRID_X 93
#define GRID_Y 40
/* UI objects */
static lv_obj_t *screen_obj;
static lv_obj_t *title_screen_obj;
static lv_obj_t *game_screen_obj;
static lv_obj_t *cells[GRID_SIZE];
static lv_obj_t *lbl_status;
static lv_obj_t *btn_restart;
static lv_obj_t *modal_obj = NULL;
/* Storage for visual state */
static lv_color_t cell_save_colors[GRID_SIZE];
/* Styles */
static lv_style_t style_bg;
static lv_style_t style_cell;
static lv_style_t style_text;
static lv_style_t style_btn;
static lv_style_t style_title_text;
static lv_style_t style_subtitle_text;
/* Candy Colors Palette */
static uint32_t candy_colors[] = {
0xFF5252, // Red
0x448AFF, // Blue
0x69F0AE, // Green
0xFFD740, // Yellow
0xE040FB, // Purple
0xFFAB40, // Orange
0xFF4081 // Pink
};
/* Forward declarations */
static void cell_event_cb(lv_event_t * e);
static void restart_event_cb(lv_event_t * e);
static void start_setup_event_cb(lv_event_t * e);
static void modal_close_cb(lv_event_t * e);
void ui_show_game_screen(void);
static void update_labels(void)
{
char buf[64];
game_phase_t phase = game_get_phase();
int p = game_get_current_player();
if (phase == GAME_PHASE_SETUP_P1) {
snprintf(buf, sizeof(buf), "Setup: Player 1 Hide Poison");
} else if (phase == GAME_PHASE_SETUP_P2) {
snprintf(buf, sizeof(buf), "Setup: Player 2 Hide Poison");
} else if (phase == GAME_PHASE_PLAY) {
snprintf(buf, sizeof(buf), "Turn: Player %d (Eat!)", p);
} else {
snprintf(buf, sizeof(buf), "Game Over");
}
if (lbl_status) lv_label_set_text(lbl_status, buf);
}
/* Modal functions */
void ui_show_modal(const char *title, const char *msg)
{
if (modal_obj) lv_obj_del(modal_obj);
modal_obj = lv_obj_create(lv_scr_act());
lv_obj_set_size(modal_obj, 300, 180);
lv_obj_center(modal_obj);
lv_obj_set_style_bg_color(modal_obj, lv_color_hex(0xFFFFFF), LV_PART_MAIN);
lv_obj_set_style_radius(modal_obj, 12, LV_PART_MAIN);
lv_obj_set_style_border_width(modal_obj, 4, LV_PART_MAIN);
lv_obj_set_style_border_color(modal_obj, lv_color_hex(0xFF80AB), LV_PART_MAIN);
lv_obj_t *l_title = lv_label_create(modal_obj);
lv_label_set_text(l_title, title);
lv_obj_set_style_text_font(l_title, &lv_font_montserrat_14, 0);
lv_obj_set_style_text_color(l_title, lv_color_hex(0xC51162), 0);
lv_obj_align(l_title, LV_ALIGN_TOP_MID, 0, 5);
lv_obj_t *l_msg = lv_label_create(modal_obj);
lv_label_set_text(l_msg, msg);
lv_label_set_long_mode(l_msg, LV_LABEL_LONG_WRAP);
lv_obj_set_width(l_msg, 260);
lv_obj_set_style_text_align(l_msg, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_align(l_msg, LV_ALIGN_CENTER, 0, -10);
lv_obj_t *btn = lv_btn_create(modal_obj);
lv_obj_set_size(btn, 100, 40);
lv_obj_align(btn, LV_ALIGN_BOTTOM_MID, 0, -10);
lv_obj_set_style_bg_color(btn, lv_color_hex(0xFF4081), LV_PART_MAIN);
lv_obj_add_event_cb(btn, modal_close_cb, LV_EVENT_CLICKED, NULL);
lv_obj_t *l_btn = lv_label_create(btn);
lv_label_set_text(l_btn, "OK");
lv_obj_center(l_btn);
}
static void modal_close_cb(lv_event_t * e)
{
if (modal_obj) {
lv_obj_del(modal_obj);
modal_obj = NULL;
}
}
void ui_update_view(void)
{
if (!game_screen_obj) return;
update_labels();
game_phase_t phase = game_get_phase();
for (uint8_t i = 0; i < GRID_SIZE; i++)
{
lv_obj_t *btn = cells[i];
if (!btn) continue;
/* Get the Image Object (Child 0) which replaced the label/circle */
lv_obj_t *img_icon = lv_obj_get_child(btn, 0);
/* 1. Reset State */
lv_obj_clear_state(btn, LV_STATE_DISABLED);
/* 2. Reset Visuals */
lv_color_t color = cell_save_colors[i];
lv_obj_set_style_bg_opa(btn, LV_OPA_COVER, LV_PART_MAIN);
lv_obj_set_style_bg_color(btn, color, LV_PART_MAIN);
lv_obj_set_style_bg_grad_color(btn, lv_color_darken(color, 50), LV_PART_MAIN);
lv_obj_set_style_border_color(btn, lv_color_lighten(color, 50), LV_PART_MAIN);
lv_obj_set_style_border_width(btn, 2, LV_PART_MAIN);
/* Restore Candy Icon: White, Visible */
if(img_icon) {
lv_obj_set_style_img_recolor(img_icon, lv_color_white(), LV_PART_MAIN);
lv_obj_set_style_img_recolor_opa(img_icon, LV_OPA_COVER, LV_PART_MAIN);
lv_obj_clear_flag(img_icon, LV_OBJ_FLAG_HIDDEN);
}
/* 3. Apply Game Logic */
if (phase == GAME_PHASE_GAME_OVER) {
bool p1_trap = game_is_p1_poison(i);
bool p2_trap = game_is_p2_poison(i);
if (p1_trap || p2_trap) {
/* Reveal Poison: Turn button dark grey */
lv_obj_set_style_bg_color(btn, lv_color_hex(0x222222), LV_PART_MAIN);
lv_obj_set_style_bg_grad_color(btn, lv_color_hex(0x000000), LV_PART_MAIN);
lv_obj_set_style_border_color(btn, lv_color_hex(0xFF0000), LV_PART_MAIN);
/* Turn Icon Black to look like a "skull/hole" */
if(img_icon) {
lv_obj_set_style_img_recolor(img_icon, lv_color_black(), LV_PART_MAIN);
}
} else if (game_is_cell_taken(i)) {
lv_obj_set_style_bg_opa(btn, LV_OPA_20, LV_PART_MAIN);
}
lv_obj_add_state(btn, LV_STATE_DISABLED);
if (btn_restart) lv_obj_clear_flag(btn_restart, LV_OBJ_FLAG_HIDDEN);
}
else {
/* Play/Setup Phase */
if (btn_restart) lv_obj_add_flag(btn_restart, LV_OBJ_FLAG_HIDDEN);
if (game_is_cell_taken(i)) {
/* Eaten candy - Make invisible */
lv_obj_set_style_bg_opa(btn, LV_OPA_0, LV_PART_MAIN);
lv_obj_set_style_border_width(btn, 0, LV_PART_MAIN);
/* Hide Icon */
if(img_icon) lv_obj_add_flag(img_icon, LV_OBJ_FLAG_HIDDEN);
}
}
}
}
static void cell_event_cb(lv_event_t * e)
{
lv_obj_t *target = lv_event_get_target(e);
uintptr_t ud = (uintptr_t)lv_event_get_user_data(e);
uint8_t idx = (uint8_t)(ud & 0xFF);
lv_obj_set_style_transform_zoom(target, 280, LV_PART_MAIN);
game_on_cell_tap(idx);
lv_obj_set_style_transform_zoom(target, 256, LV_PART_MAIN);
}
static void restart_event_cb(lv_event_t * e)
{
game_reset();
}
static void start_setup_event_cb(lv_event_t * e)
{
if (title_screen_obj) lv_obj_add_flag(title_screen_obj, LV_OBJ_FLAG_HIDDEN);
if (game_screen_obj) {
lv_obj_clear_flag(game_screen_obj, LV_OBJ_FLAG_HIDDEN);
} else {
ui_show_game_screen();
}
game_init();
}
void ui_show_title_screen(void)
{
title_screen_obj = lv_obj_create(screen_obj);
lv_obj_set_size(title_screen_obj, LV_PCT(100), LV_PCT(100));
lv_obj_set_style_bg_opa(title_screen_obj, LV_OPA_TRANSP, 0);
lv_obj_clear_flag(title_screen_obj, LV_OBJ_FLAG_SCROLLABLE);
/* Overlay */
lv_obj_t *overlay = lv_obj_create(title_screen_obj);
lv_obj_set_size(overlay, LV_PCT(100), LV_PCT(100));
lv_obj_set_style_bg_color(overlay, lv_color_hex(0x5E1224), 0);
lv_obj_set_style_bg_opa(overlay, LV_OPA_60, 0);
lv_obj_clear_flag(overlay, LV_OBJ_FLAG_SCROLLABLE);
/* Title */
lv_obj_t *l_title = lv_label_create(title_screen_obj);
lv_label_set_text(l_title, "Candy Poison");
lv_obj_add_style(l_title, &style_title_text, 0);
lv_obj_align(l_title, LV_ALIGN_CENTER, 0, -40);
/* Subtitle */
lv_obj_t *l_sub = lv_label_create(title_screen_obj);
lv_label_set_text(l_sub, "5x5 Grid. 2 Poisons. 2 Players.");
lv_obj_add_style(l_sub, &style_subtitle_text, 0);
lv_obj_align(l_sub, LV_ALIGN_CENTER, 0, -10);
/* Start Button */
lv_obj_t *btn_start = lv_btn_create(title_screen_obj);
lv_obj_add_style(btn_start, &style_btn, LV_PART_MAIN);
lv_obj_set_size(btn_start, 160, 50);
lv_obj_align(btn_start, LV_ALIGN_CENTER, 0, 50);
lv_obj_add_event_cb(btn_start, start_setup_event_cb, LV_EVENT_CLICKED, NULL);
lv_obj_t *l_btn = lv_label_create(btn_start);
lv_label_set_text(l_btn, "Start Setup");
lv_obj_set_style_text_font(l_btn, &lv_font_montserrat_14, 0);
lv_obj_center(l_btn);
}
void ui_show_game_screen(void)
{
game_screen_obj = lv_obj_create(screen_obj);
lv_obj_set_size(game_screen_obj, LV_PCT(100), LV_PCT(100));
lv_obj_set_style_bg_opa(game_screen_obj, LV_OPA_TRANSP, 0);
lv_obj_clear_flag(game_screen_obj, LV_OBJ_FLAG_SCROLLABLE);
lbl_status = lv_label_create(game_screen_obj);
lv_obj_add_style(lbl_status, &style_text, 0);
lv_obj_align(lbl_status, LV_ALIGN_TOP_MID, 0, 12);
/* 5x5 Grid */
for (int r = 0; r < GRID_ROWS; r++) {
for (int c = 0; c < GRID_COLS; c++) {
uint8_t idx = (uint8_t)(r * GRID_COLS + c);
lv_obj_t *btn = lv_btn_create(game_screen_obj);
lv_obj_add_style(btn, &style_cell, LV_PART_MAIN);
/* Randomize & Save Color */
int color_idx = rand() % (sizeof(candy_colors)/sizeof(candy_colors[0]));
lv_color_t color = lv_color_hex(candy_colors[color_idx]);
cell_save_colors[idx] = color;
lv_obj_set_style_bg_color(btn, color, LV_PART_MAIN);
lv_obj_set_style_bg_grad_color(btn, lv_color_darken(color, 50), LV_PART_MAIN);
lv_obj_set_style_bg_grad_dir(btn, LV_GRAD_DIR_VER, LV_PART_MAIN);
lv_obj_set_style_border_color(btn, lv_color_lighten(color, 50), LV_PART_MAIN);
lv_obj_set_size(btn, CELL_W, CELL_H);
lv_obj_set_pos(btn, GRID_X + c * (CELL_W + CELL_GAP), GRID_Y + r * (CELL_H + CELL_GAP));
lv_obj_add_event_cb(btn, cell_event_cb, LV_EVENT_CLICKED, (void*)(uintptr_t)idx);
/* REPLACED CIRCLE WITH IMAGE OBJECT */
/* Uses the embedded 'candy_icon' array defined at top of file */
#if LV_COLOR_DEPTH == 16 || LV_COLOR_DEPTH == 32
lv_obj_t *img_icon = lv_img_create(btn);
lv_img_set_src(img_icon, &candy_icon);
lv_obj_center(img_icon);
/* Tint the icon white initially so it looks like a generic candy */
lv_obj_set_style_img_recolor(img_icon, lv_color_white(), LV_PART_MAIN);
lv_obj_set_style_img_recolor_opa(img_icon, LV_OPA_COVER, LV_PART_MAIN);
#endif
cells[idx] = btn;
}
}
/* Restart Button */
btn_restart = lv_btn_create(game_screen_obj);
lv_obj_add_style(btn_restart, &style_btn, LV_PART_MAIN);
lv_obj_set_size(btn_restart, 120, 42);
lv_obj_align(btn_restart, LV_ALIGN_BOTTOM_MID, 0, -10);
lv_obj_add_event_cb(btn_restart, restart_event_cb, LV_EVENT_CLICKED, NULL);
lv_obj_add_flag(btn_restart, LV_OBJ_FLAG_HIDDEN);
lv_obj_t *lr = lv_label_create(btn_restart);
lv_label_set_text(lr, "Play Again");
lv_obj_set_style_text_font(lr, &lv_font_montserrat_14, 0);
lv_obj_center(lr);
}
void ui_init(void)
{
screen_obj = lv_scr_act();
/* Background Style */
lv_style_init(&style_bg);
lv_style_set_bg_color(&style_bg, lv_color_hex(0x880E4F));
lv_style_set_bg_grad_color(&style_bg, lv_color_hex(0x2C041C));
lv_style_set_bg_grad_dir(&style_bg, LV_GRAD_DIR_VER);
lv_obj_add_style(screen_obj, &style_bg, 0);
lv_style_init(&style_text);
lv_style_set_text_color(&style_text, lv_color_hex(0xFFFFFF));
lv_style_set_text_font(&style_text, &lv_font_montserrat_14);
lv_style_init(&style_title_text);
lv_style_set_text_color(&style_title_text, lv_color_hex(0xFFFFFF));
lv_style_set_text_font(&style_title_text, &lv_font_montserrat_14);
lv_style_init(&style_subtitle_text);
lv_style_set_text_color(&style_subtitle_text, lv_color_hex(0xE0E0E0));
lv_style_set_text_font(&style_subtitle_text, &lv_font_montserrat_14);
/* Cell Style */
lv_style_init(&style_cell);
lv_style_set_radius(&style_cell, 12);
lv_style_set_border_width(&style_cell, 2);
lv_style_set_border_opa(&style_cell, LV_OPA_80);
lv_style_set_shadow_width(&style_cell, 8);
lv_style_set_shadow_ofs_y(&style_cell, 3);
lv_style_set_shadow_color(&style_cell, lv_color_hex(0x000000));
lv_style_set_shadow_opa(&style_cell, LV_OPA_50);
/* Button Style */
lv_style_init(&style_btn);
lv_style_set_bg_color(&style_btn, lv_color_hex(0xFFFFFF));
lv_style_set_text_color(&style_btn, lv_color_hex(0xC51162));
lv_style_set_radius(&style_btn, 25);
lv_style_set_shadow_width(&style_btn, 10);
lv_style_set_shadow_ofs_y(&style_btn, 4);
lv_style_set_shadow_color(&style_btn, lv_color_hex(0x000000));
lv_style_set_shadow_opa(&style_btn, LV_OPA_30);
ui_show_title_screen();
}
void ui_task_handler(void) {
lv_timer_handler();
}







Comments