// Jeux mode for Lisael Box: a small menu -> Memory, Compter, (Coloriage stub). // // Design rule: no losing, no scores that shame — only positive feedback (a star // / "Bravo !"). All games are LVGL-only and fit the 320x240 screen. // // * Memory : 3x2 grid of 6 cards (3 emoji pairs). Flip two; if they match // they stay revealed; otherwise they flip back. Win = all matched. // * Compter : show N emoji objects, offer 3 number buttons, tap the right // count -> "Bravo !". New round on success. // * Coloriage: documented STUB (free-draw on a canvas is heavier on 320x240 // and needs an lv_canvas + touch drawing loop; left as a labelled // placeholder screen). See note in build_coloring_stub(). #include "ui/ui.h" #include "ui/icons.h" #include "ui/assets/lisael_icons.h" #include "ui/fonts/lisael_fonts.h" #include #include #include #include "esp_log.h" #include "esp_random.h" static const char *TAG = "ui.games"; static lv_obj_t *s_menu_root; // --------------------------------------------------------------------------- // Shared: a positive-feedback toast ("Bravo ! ⭐"). // --------------------------------------------------------------------------- static void show_bravo(lv_obj_t *parent) { lv_obj_t *toast = lv_obj_create(parent); lv_obj_set_size(toast, 200, 70); lv_obj_center(toast); lv_obj_set_style_bg_color(toast, lv_color_hex(0xFFF3BF), 0); lv_obj_set_style_radius(toast, 16, 0); lv_obj_set_flex_flow(toast, LV_FLEX_FLOW_ROW); lv_obj_set_flex_align(toast, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_set_style_pad_column(toast, 8, 0); lv_obj_t *lbl = lv_label_create(toast); lv_label_set_text(lbl, "Bravo !"); lv_obj_set_style_text_font(lbl, &lisael_font_24, 0); lv_obj_t *star = lv_image_create(toast); lv_image_set_src(star, &lisael_ic_star); // Auto-dismiss after 1.5 s. lv_obj_delete_delayed(toast, 1500); } // =========================================================================== // MEMORY // =========================================================================== #define MEM_PAIRS 3 #define MEM_CARDS (MEM_PAIRS * 2) static const lv_image_dsc_t *const MEM_FACES[MEM_PAIRS] = { &lisael_ic_cat, // 🐱 &lisael_ic_dog, // 🐶 &lisael_ic_frog, // 🐸 }; typedef struct { lv_obj_t *cards[MEM_CARDS]; int face[MEM_CARDS]; // index into MEM_FACES bool matched[MEM_CARDS]; int first; // currently revealed card, or -1 int matched_count; lv_obj_t *root; } memory_state_t; static void mem_set_revealed(memory_state_t *st, int i, bool revealed) { lv_obj_t *img = lv_obj_get_child(st->cards[i], 0); if (revealed) { lv_image_set_src(img, MEM_FACES[st->face[i]]); lv_obj_set_style_image_opa(img, LV_OPA_COVER, 0); } else { lv_obj_set_style_image_opa(img, LV_OPA_TRANSP, 0); // face down } lv_obj_set_style_bg_color(st->cards[i], revealed ? lv_color_hex(0xB2F2BB) : lv_color_hex(0x99C8FF), 0); } static void mem_flip_back_cb(lv_timer_t *t) { memory_state_t *st = lv_timer_get_user_data(t); for (int i = 0; i < MEM_CARDS; i++) { if (!st->matched[i]) { mem_set_revealed(st, i, false); } } st->first = -1; lv_timer_delete(t); } static void mem_card_cb(lv_event_t *e) { memory_state_t *st = lv_event_get_user_data(e); lv_obj_t *card = lv_event_get_target_obj(e); int idx = -1; for (int i = 0; i < MEM_CARDS; i++) { if (st->cards[i] == card) { idx = i; break; } } if (idx < 0 || st->matched[idx] || idx == st->first) { return; // ignore taps on matched / already-open card } mem_set_revealed(st, idx, true); if (st->first < 0) { st->first = idx; // first of the pair return; } // Second card: check match. if (st->face[idx] == st->face[st->first]) { st->matched[idx] = st->matched[st->first] = true; st->matched_count += 2; st->first = -1; if (st->matched_count == MEM_CARDS) { show_bravo(st->root); } } else { // Flip both back after a short delay. lv_timer_t *t = lv_timer_create(mem_flip_back_cb, 800, st); lv_timer_set_repeat_count(t, 1); } } static void mem_build(lv_obj_t *parent) { memory_state_t *st = lv_malloc(sizeof(memory_state_t)); memset(st, 0, sizeof(*st)); st->first = -1; st->root = parent; // Build and shuffle the deck. for (int i = 0; i < MEM_CARDS; i++) { st->face[i] = i % MEM_PAIRS; } for (int i = MEM_CARDS - 1; i > 0; i--) { int j = (int)(esp_random() % (uint32_t)(i + 1)); int tmp = st->face[i]; st->face[i] = st->face[j]; st->face[j] = tmp; } lv_obj_t *grid = lv_obj_create(parent); lv_obj_remove_style_all(grid); lv_obj_set_size(grid, LV_PCT(100), 180); lv_obj_align(grid, LV_ALIGN_BOTTOM_MID, 0, -4); lv_obj_set_flex_flow(grid, LV_FLEX_FLOW_ROW_WRAP); lv_obj_set_flex_align(grid, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); for (int i = 0; i < MEM_CARDS; i++) { lv_obj_t *card = lv_button_create(grid); lv_obj_set_size(card, 90, 80); lv_obj_set_style_radius(card, 12, 0); lv_obj_add_event_cb(card, mem_card_cb, LV_EVENT_CLICKED, st); lv_obj_t *img = lv_image_create(card); lv_obj_center(img); st->cards[i] = card; mem_set_revealed(st, i, false); } // NOTE: `st` intentionally lives for the screen's lifetime (freed when the // screen object is deleted). For a long-lived single-screen app this is // fine; if screens are recreated, attach a delete handler to free `st`. } // =========================================================================== // COMPTER ("combien d'objets ?") // =========================================================================== typedef struct { lv_obj_t *objects_box; lv_obj_t *buttons[3]; int answer; lv_obj_t *root; } count_state_t; static void count_new_round(count_state_t *st); static void count_btn_cb(lv_event_t *e) { count_state_t *st = lv_event_get_user_data(e); lv_obj_t *btn = lv_event_get_target_obj(e); lv_obj_t *lbl = lv_obj_get_child(btn, 0); int chosen = atoi(lv_label_get_text(lbl)); if (chosen == st->answer) { show_bravo(st->root); count_new_round(st); } // Wrong answer: no penalty, no message — child just tries again. } static void count_new_round(count_state_t *st) { // 1..5 objects. st->answer = 1 + (int)(esp_random() % 5); // Render the objects. lv_obj_clean(st->objects_box); for (int i = 0; i < st->answer; i++) { lv_obj_t *o = lv_image_create(st->objects_box); // 🍎 apple icon lv_image_set_src(o, &lisael_ic_apple); } // Offer the right answer + two distractors in a shuffled set. int opts[3]; opts[0] = st->answer; for (int i = 1; i < 3; i++) { int v; do { v = 1 + (int)(esp_random() % 5); } while (v == opts[0] || (i == 2 && v == opts[1])); opts[i] = v; } for (int i = 2; i > 0; i--) { int j = (int)(esp_random() % (uint32_t)(i + 1)); int tmp = opts[i]; opts[i] = opts[j]; opts[j] = tmp; } for (int i = 0; i < 3; i++) { lv_obj_t *lbl = lv_obj_get_child(st->buttons[i], 0); lv_label_set_text_fmt(lbl, "%d", opts[i]); } } static void count_build(lv_obj_t *parent) { count_state_t *st = lv_malloc(sizeof(count_state_t)); memset(st, 0, sizeof(*st)); st->root = parent; lv_obj_t *q = lv_label_create(parent); lv_label_set_text(q, "Combien ?"); lv_obj_align(q, LV_ALIGN_TOP_MID, 0, 50); st->objects_box = lv_obj_create(parent); lv_obj_remove_style_all(st->objects_box); lv_obj_set_size(st->objects_box, LV_PCT(90), 80); lv_obj_align(st->objects_box, LV_ALIGN_CENTER, 0, -10); lv_obj_set_flex_flow(st->objects_box, LV_FLEX_FLOW_ROW_WRAP); lv_obj_set_flex_align(st->objects_box, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_t *bar = lv_obj_create(parent); lv_obj_remove_style_all(bar); lv_obj_set_size(bar, LV_PCT(100), 64); lv_obj_align(bar, LV_ALIGN_BOTTOM_MID, 0, -6); lv_obj_set_flex_flow(bar, LV_FLEX_FLOW_ROW); lv_obj_set_flex_align(bar, LV_FLEX_ALIGN_SPACE_EVENLY, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); for (int i = 0; i < 3; i++) { lv_obj_t *b = lv_button_create(bar); lv_obj_add_style(b, lisael_ui_style_button(), 0); lv_obj_set_size(b, 72, 56); lv_obj_add_event_cb(b, count_btn_cb, LV_EVENT_CLICKED, st); lv_obj_t *l = lv_label_create(b); lv_obj_set_style_text_font(l, &lv_font_montserrat_28, 0); lv_label_set_text(l, "0"); lv_obj_center(l); st->buttons[i] = b; } count_new_round(st); } // =========================================================================== // COLORIAGE (documented stub) // =========================================================================== static void build_coloring_stub(lv_obj_t *parent) { // STUB: a real coloring game would use an lv_canvas (e.g. 280x180 in PSRAM) // plus an LV_EVENT_PRESSING handler reading the touch point and painting a // brush of the selected color via lv_canvas_set_px / lv_draw on the canvas // buffer. That is doable but heavier on a 320x240 screen and needs a tuned // brush/undo UX for small children, so it is intentionally deferred. lv_obj_t *lbl = lv_label_create(parent); lv_label_set_text(lbl, "Coloriage\n(bientôt)\n\n🎨"); lv_obj_set_style_text_align(lbl, LV_TEXT_ALIGN_CENTER, 0); lv_obj_center(lbl); } // =========================================================================== // Game sub-screen wrapper + games menu // =========================================================================== typedef void (*game_builder_t)(lv_obj_t *parent); static void open_game(game_builder_t builder, const char *title) { lv_obj_t *scr = lv_obj_create(NULL); lv_obj_set_style_bg_color(scr, lv_color_hex(0xF3F0FF), 0); lisael_ui_add_back_button(scr); // returns HOME lv_obj_t *t = lv_label_create(scr); lv_label_set_text(t, title); lv_obj_align(t, LV_ALIGN_TOP_MID, 0, 16); builder(scr); lv_screen_load_anim(scr, LV_SCR_LOAD_ANIM_MOVE_LEFT, 250, 0, false); ESP_LOGI(TAG, "game open: %s", title); } static void memory_cb(lv_event_t *e) { (void)e; open_game(mem_build, "Memory"); } static void count_cb(lv_event_t *e) { (void)e; open_game(count_build, "Compter"); } static void color_cb(lv_event_t *e) { (void)e; open_game(build_coloring_stub, "Coloriage"); } static void balloons_cb(lv_event_t *e) { (void)e; // The balloons screen is self-contained (own back button to HOME). lv_screen_load_anim(lisael_screen_balloons(), LV_SCR_LOAD_ANIM_MOVE_LEFT, 250, 0, false); } lv_obj_t *lisael_screen_games(void) { if (s_menu_root) { return s_menu_root; } s_menu_root = lv_obj_create(NULL); lv_obj_set_style_bg_color(s_menu_root, lv_color_hex(0xF3F0FF), 0); lisael_ui_add_back_button(s_menu_root); lv_obj_t *title = lv_label_create(s_menu_root); lv_label_set_text(title, "Jeux"); lv_obj_set_style_text_font(title, &lisael_font_24, 0); lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 16); lv_obj_t *list = lv_obj_create(s_menu_root); lv_obj_remove_style_all(list); lv_obj_set_size(list, LV_PCT(100), 150); lv_obj_align(list, LV_ALIGN_CENTER, 0, 20); lv_obj_set_flex_flow(list, LV_FLEX_FLOW_COLUMN); lv_obj_set_flex_align(list, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_set_style_pad_row(list, 10, 0); struct { const char *txt; const lv_image_dsc_t *icon; lv_event_cb_t cb; } games[] = { { "Memory", &lisael_ic_cat, memory_cb }, { "Compter", &lisael_ic_apple, count_cb }, { "Calme ta colère", &lisael_ic_balloon, balloons_cb }, { "Coloriage", &lisael_ic_palette, color_cb }, }; for (size_t i = 0; i < sizeof(games) / sizeof(games[0]); i++) { lv_obj_t *b = lv_button_create(list); lv_obj_add_style(b, lisael_ui_style_button(), 0); lv_obj_set_width(b, LV_PCT(82)); lv_obj_set_flex_flow(b, LV_FLEX_FLOW_ROW); lv_obj_set_flex_align(b, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER); lv_obj_set_style_pad_column(b, 12, 0); lv_obj_add_event_cb(b, games[i].cb, LV_EVENT_CLICKED, NULL); lv_obj_t *ic = lv_image_create(b); lv_image_set_src(ic, games[i].icon); lv_obj_t *l = lv_label_create(b); lv_label_set_text(l, games[i].txt); } return s_menu_root; }