/** * @file SnakeUi.c * @brief LVGL widget implementation for the Snake game */ #include "SnakeUi.h" #include "SnakeLogic.h" #include #include #include #include // Forward declarations static void game_play_event(lv_event_t* e); static void snake_timer_cb(lv_timer_t* timer); static void delete_event(lv_event_t* e); static void reenter_key_mode_event(lv_event_t* e); static void snake_draw(snake_game_t* game); static void snake_create_segment_objects(snake_game_t* game); static void snake_delete_segment_objects(snake_game_t* game); // Static flag to ensure srand is only called once static bool srand_initialized = false; /** * @brief Free all resources for the snake game object */ static void delete_event(lv_event_t* e) { lv_obj_t* obj = lv_event_get_target_obj(e); snake_game_t* game = (snake_game_t*)lv_obj_get_user_data(obj); if (game) { // Stop timer first if (game->timer) { lv_timer_delete(game->timer); game->timer = NULL; } // Restore edit mode and remove from group before cleanup if (tt_lvgl_hardware_keyboard_is_available()) { lv_group_t* group = lv_group_get_default(); if (group) lv_group_set_editing(group, false); lv_group_remove_obj(game->container); } // Delete LVGL objects for snake segments snake_delete_segment_objects(game); // Free snake body linked list snake_free_body(game->head); game->head = NULL; // Food object is a child of container, will be deleted automatically // Container is a child of obj, will be deleted automatically lv_free(game); lv_obj_set_user_data(obj, NULL); } } /** * @brief Re-enter key mode when the game area is clicked after Q/Esc exit */ static void reenter_key_mode_event(lv_event_t* e) { lv_obj_t* container = lv_event_get_current_target_obj(e); lv_group_t* group = lv_group_get_default(); if (!group) return; if (lv_obj_get_group(container) == NULL) { lv_group_add_obj(group, container); } lv_group_focus_obj(container); lv_group_set_editing(group, true); } /** * @brief Delete LVGL objects for all snake segments */ static void snake_delete_segment_objects(snake_game_t* game) { if (!game || !game->head) return; snake_segment_t* segment = game->head; while (segment != NULL) { if (segment->obj) { lv_obj_delete(segment->obj); segment->obj = NULL; } segment = segment->next; } } /** * @brief Create LVGL objects for all snake segments */ static void snake_create_segment_objects(snake_game_t* game) { if (!game || !game->head || !game->container) return; snake_segment_t* segment = game->head; bool is_head = true; while (segment != NULL) { segment->obj = lv_obj_create(game->container); lv_obj_set_size(segment->obj, game->cell_size - 2, game->cell_size - 2); lv_obj_set_style_radius(segment->obj, SNAKE_CELL_RADIUS, LV_PART_MAIN); lv_obj_set_style_border_width(segment->obj, 0, LV_PART_MAIN); if (is_head) { lv_obj_set_style_bg_color(segment->obj, SNAKE_HEAD_COLOR, LV_PART_MAIN); is_head = false; } else { lv_obj_set_style_bg_color(segment->obj, SNAKE_BODY_COLOR, LV_PART_MAIN); } // Position will be set by snake_draw() segment = segment->next; } } /** * @brief Update LVGL object positions based on game state */ static void snake_draw(snake_game_t* game) { if (!game || !game->container) return; // Update snake segment positions snake_segment_t* segment = game->head; while (segment != NULL) { if (segment->obj) { lv_coord_t px = segment->x * game->cell_size + 1; lv_coord_t py = segment->y * game->cell_size + 1; lv_obj_set_pos(segment->obj, px, py); } segment = segment->next; } // Update food position if (game->food) { lv_coord_t fx = game->food_x * game->cell_size + 1; lv_coord_t fy = game->food_y * game->cell_size + 1; lv_obj_set_pos(game->food, fx, fy); } } /** * @brief Timer callback - moves snake and updates display */ static void snake_timer_cb(lv_timer_t* timer) { snake_game_t* game = (snake_game_t*)lv_timer_get_user_data(timer); if (!game || game->game_over || game->paused) return; // Move snake bool move_ok = snake_move(game); if (!move_ok) { // Collision occurred, game over game->game_over = true; lv_obj_send_event(game->widget, LV_EVENT_VALUE_CHANGED, NULL); return; } // Check food collision if (snake_check_food_collision(game)) { // Grow snake and update score if (!snake_grow(game->head)) { // Memory allocation failed - treat as game over game->game_over = true; lv_obj_send_event(game->widget, LV_EVENT_VALUE_CHANGED, NULL); return; } game->score++; game->length++; // Create LVGL object for new segment snake_segment_t* tail = game->head; while (tail->next != NULL) { tail = tail->next; } if (tail && !tail->obj && game->container) { tail->obj = lv_obj_create(game->container); if (!tail->obj) { game->game_over = true; lv_obj_send_event(game->widget, LV_EVENT_VALUE_CHANGED, NULL); return; } lv_obj_set_size(tail->obj, game->cell_size - 2, game->cell_size - 2); lv_obj_set_style_radius(tail->obj, SNAKE_CELL_RADIUS, LV_PART_MAIN); lv_obj_set_style_border_width(tail->obj, 0, LV_PART_MAIN); lv_obj_set_style_bg_color(tail->obj, SNAKE_BODY_COLOR, LV_PART_MAIN); } // Spawn new food - if fails, grid is full (win!) if (!snake_spawn_food(game)) { // Hide food since there's no valid position if (game->food) { lv_obj_add_flag(game->food, LV_OBJ_FLAG_HIDDEN); } // Player won - end the game game->game_over = true; lv_obj_send_event(game->widget, LV_EVENT_VALUE_CHANGED, NULL); return; } // Increase speed (decrease timer period) as snake grows uint32_t foods_eaten = game->length - SNAKE_INITIAL_LENGTH; uint32_t speed_reduction = foods_eaten * SNAKE_SPEED_DECREASE_MS; uint32_t new_period = SNAKE_GAME_SPEED_MS; if (speed_reduction < (SNAKE_GAME_SPEED_MS - SNAKE_MIN_SPEED_MS)) { new_period = SNAKE_GAME_SPEED_MS - speed_reduction; } else { new_period = SNAKE_MIN_SPEED_MS; } lv_timer_set_period(game->timer, new_period); // Notify score change lv_obj_send_event(game->widget, LV_EVENT_VALUE_CHANGED, NULL); } // Update display snake_draw(game); } /** * @brief Event callback for game play (gesture/key) */ static void game_play_event(lv_event_t* e) { lv_event_code_t code = lv_event_get_code(e); lv_obj_t* obj = (lv_obj_t*)lv_event_get_user_data(e); snake_game_t* game = (snake_game_t*)lv_obj_get_user_data(obj); if (!game || game->game_over) return; if (code == LV_EVENT_GESTURE) { lv_dir_t dir = lv_indev_get_gesture_dir(lv_indev_active()); switch (dir) { case LV_DIR_TOP: snake_set_direction(game, SNAKE_DIR_UP); break; case LV_DIR_BOTTOM: snake_set_direction(game, SNAKE_DIR_DOWN); break; case LV_DIR_LEFT: snake_set_direction(game, SNAKE_DIR_LEFT); break; case LV_DIR_RIGHT: snake_set_direction(game, SNAKE_DIR_RIGHT); break; default: break; } } else if (code == LV_EVENT_CLICKED) { lv_indev_t* indev = lv_indev_active(); if (indev) { lv_point_t point; lv_indev_get_point(indev, &point); lv_area_t coords; lv_obj_get_coords(game->container, &coords); lv_coord_t w = coords.x2 - coords.x1 + 1; lv_coord_t h = coords.y2 - coords.y1 + 1; lv_coord_t cx = coords.x1 + w / 2; lv_coord_t cy = coords.y1 + h / 2; lv_coord_t rx = point.x - cx; lv_coord_t ry = point.y - cy; if (rx != 0 || ry != 0) { if (abs(rx) * h > abs(ry) * w) { // Horizontal touch if (rx > 0) { snake_set_direction(game, SNAKE_DIR_RIGHT); } else { snake_set_direction(game, SNAKE_DIR_LEFT); } } else { // Vertical touch if (ry > 0) { snake_set_direction(game, SNAKE_DIR_DOWN); } else { snake_set_direction(game, SNAKE_DIR_UP); } } } } } else if (code == LV_EVENT_KEY) { uint32_t key = lv_event_get_key(e); // Arrow keys, WASD, and punctuation keys for cardputer switch (key) { case LV_KEY_UP: case 'w': case 'W': case ';': snake_set_direction(game, SNAKE_DIR_UP); break; case LV_KEY_DOWN: case 's': case 'S': case '.': snake_set_direction(game, SNAKE_DIR_DOWN); break; case LV_KEY_LEFT: case 'a': case 'A': case ',': snake_set_direction(game, SNAKE_DIR_LEFT); break; case LV_KEY_RIGHT: case 'd': case 'D': case '/': snake_set_direction(game, SNAKE_DIR_RIGHT); break; case LV_KEY_ESC: case 'q': case 'Q': { // Exit key/edit mode - remove from group so navigation is restored // Re-entry: tap/click the game area to return to key mode lv_group_t* grp = lv_group_get_default(); if (grp) lv_group_set_editing(grp, false); lv_group_remove_obj(lv_event_get_current_target_obj(e)); break; } default: break; } } } /** * @brief Create a new Snake game widget */ lv_obj_t* snake_create(lv_obj_t* parent, uint16_t cell_size, bool wall_collision) { // Create main object lv_obj_t* obj = lv_obj_create(parent); if (!obj) return NULL; // Allocate game state snake_game_t* game = (snake_game_t*)lv_malloc(sizeof(snake_game_t)); if (!game) { lv_obj_delete(obj); return NULL; } memset(game, 0, sizeof(snake_game_t)); lv_obj_set_user_data(obj, game); // Store widget reference for events game->widget = obj; // Initialize game state game->cell_size = cell_size; game->score = 0; game->length = SNAKE_INITIAL_LENGTH; game->direction = SNAKE_DIR_RIGHT; game->next_direction = SNAKE_DIR_RIGHT; game->game_over = false; game->paused = false; game->wall_collision_enabled = wall_collision; // Set up main object lv_obj_set_size(obj, LV_PCT(100), LV_PCT(100)); lv_obj_remove_flag(obj, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_style_pad_all(obj, 0, LV_PART_MAIN); lv_obj_set_style_border_width(obj, 0, LV_PART_MAIN); lv_obj_set_style_bg_opa(obj, LV_OPA_TRANSP, LV_PART_MAIN); // Create game container (the actual playing field) game->container = lv_obj_create(obj); lv_obj_remove_flag(game->container, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_style_bg_color(game->container, SNAKE_BG_COLOR, LV_PART_MAIN); lv_obj_set_style_border_width(game->container, 1, LV_PART_MAIN); lv_obj_set_style_border_color(game->container, SNAKE_GRID_COLOR, LV_PART_MAIN); lv_obj_set_style_pad_all(game->container, 0, LV_PART_MAIN); lv_group_remove_obj(game->container); lv_obj_remove_flag(game->container, LV_OBJ_FLAG_GESTURE_BUBBLE); // Calculate grid dimensions based on available space (non-square) lv_coord_t available_w = lv_obj_get_content_width(parent) - 6; lv_coord_t available_h = lv_obj_get_content_height(parent) - 6; if (available_w < 50) available_w = 50; if (available_h < 50) available_h = 50; // Calculate how many cells fit in each dimension game->grid_width = available_w / cell_size; game->grid_height = available_h / cell_size; // Ensure minimum grid size (3x3 minimum for playable game) if (game->grid_width < 3) game->grid_width = 3; if (game->grid_height < 3) game->grid_height = 3; // Calculate actual field size lv_coord_t field_w = game->cell_size * game->grid_width; lv_coord_t field_h = game->cell_size * game->grid_height; lv_obj_set_size(game->container, field_w, field_h); lv_obj_center(game->container); // Initialize snake body at center int16_t start_x = game->grid_width / 2; int16_t start_y = game->grid_height / 2; game->head = snake_init_body(SNAKE_INITIAL_LENGTH, start_x, start_y); if (!game->head) { lv_obj_set_user_data(obj, NULL); lv_free(game); lv_obj_delete(obj); return NULL; } // Create LVGL objects for snake segments snake_create_segment_objects(game); // Create food object game->food = lv_obj_create(game->container); lv_obj_set_size(game->food, game->cell_size - 2, game->cell_size - 2); lv_obj_set_style_radius(game->food, game->cell_size / 2, LV_PART_MAIN); lv_obj_set_style_border_width(game->food, 0, LV_PART_MAIN); lv_obj_set_style_bg_color(game->food, SNAKE_FOOD_COLOR, LV_PART_MAIN); // Initialize random seed only once if (!srand_initialized) { srand((unsigned int)time(NULL)); srand_initialized = true; } // Spawn initial food snake_spawn_food(game); // Initial draw snake_draw(game); // Add event callbacks for touch gestures and keyboard lv_obj_add_event_cb(game->container, game_play_event, LV_EVENT_GESTURE, obj); lv_obj_add_event_cb(game->container, game_play_event, LV_EVENT_KEY, obj); lv_obj_add_event_cb(game->container, game_play_event, LV_EVENT_CLICKED, obj); lv_obj_add_event_cb(obj, delete_event, LV_EVENT_DELETE, NULL); // Set up keyboard focus if available if (tt_lvgl_hardware_keyboard_is_available()) { lv_group_t* group = lv_group_get_default(); if (group) { lv_group_add_obj(group, game->container); lv_obj_add_event_cb(game->container, reenter_key_mode_event, LV_EVENT_CLICKED, NULL); lv_group_focus_obj(game->container); lv_group_set_editing(group, true); } } // Start game timer game->timer = lv_timer_create(snake_timer_cb, SNAKE_GAME_SPEED_MS, game); return obj; } /** * @brief Reset the game to start a new game */ void snake_set_new_game(lv_obj_t* obj) { snake_game_t* game = (snake_game_t*)lv_obj_get_user_data(obj); if (!game) return; // Stop timer during reset if (game->timer) { lv_timer_pause(game->timer); } // Delete old snake segment objects snake_delete_segment_objects(game); // Free old snake body snake_free_body(game->head); game->head = NULL; // Reset game state game->score = 0; game->length = SNAKE_INITIAL_LENGTH; game->direction = SNAKE_DIR_RIGHT; game->next_direction = SNAKE_DIR_RIGHT; game->game_over = false; game->paused = false; // Create new snake body at center int16_t start_x = game->grid_width / 2; int16_t start_y = game->grid_height / 2; game->head = snake_init_body(SNAKE_INITIAL_LENGTH, start_x, start_y); if (!game->head) { // Memory allocation failed - keep game in over state game->game_over = true; lv_obj_send_event(obj, LV_EVENT_VALUE_CHANGED, NULL); return; } // Create new segment objects snake_create_segment_objects(game); // Spawn new food and unhide it if (snake_spawn_food(game)) { if (game->food) { lv_obj_remove_flag(game->food, LV_OBJ_FLAG_HIDDEN); } } // Draw snake_draw(game); // Resume timer if (game->timer) { lv_timer_resume(game->timer); } // Notify change lv_obj_send_event(obj, LV_EVENT_VALUE_CHANGED, NULL); } /** * @brief Get the current score */ uint16_t snake_get_score(lv_obj_t* obj) { snake_game_t* game = (snake_game_t*)lv_obj_get_user_data(obj); if (!game) return 0; return game->score; } /** * @brief Get the current snake length */ uint16_t snake_get_length(lv_obj_t* obj) { snake_game_t* game = (snake_game_t*)lv_obj_get_user_data(obj); if (!game) return 0; return game->length; } /** * @brief Check if game is over */ bool snake_get_game_over(lv_obj_t* obj) { snake_game_t* game = (snake_game_t*)lv_obj_get_user_data(obj); if (!game) return true; return game->game_over; }