From 59e9b05f60952ecc82dd1e46dde9a46cada5130f Mon Sep 17 00:00:00 2001 From: cazz Date: Fri, 9 Jan 2026 21:45:13 +0200 Subject: [PATCH 1/4] feat: add ui panel helpers --- src/modules/ui.c | 46 ++++++++++++++++++++++++++++++++++++++++++++++ src/modules/ui.h | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/src/modules/ui.c b/src/modules/ui.c index 6cddd30..ec928d6 100644 --- a/src/modules/ui.c +++ b/src/modules/ui.c @@ -56,3 +56,49 @@ bool ui_button_render(SDL_Renderer* renderer, const ui_button_t* button) { return true; } + +void ui_panel_init(ui_panel_t* panel, SDL_Color fill_color, SDL_Color border_color) { + SDL_assert(panel != NULL); + + panel->rect.x = 0.f; + panel->rect.y = 0.f; + panel->rect.w = 0.f; + panel->rect.h = 0.f; + panel->fill_color = fill_color; + panel->border_color = border_color; +} + +void ui_panel_layout_from_content(ui_panel_t* panel, const vector2i_t* screen_size, const vector2i_t* content_size, + float padding_x, float padding_y) { + SDL_assert(panel != NULL); + SDL_assert(screen_size != NULL); + SDL_assert(content_size != NULL); + + panel->rect.w = (float)content_size->x + padding_x * 2.f; + panel->rect.h = (float)content_size->y + padding_y * 2.f; + panel->rect.x = ((float)screen_size->x - panel->rect.w) * 0.5f; + panel->rect.y = ((float)screen_size->y - panel->rect.h) * 0.5f; +} + +bool ui_panel_render(SDL_Renderer* renderer, const ui_panel_t* panel) { + SDL_assert(renderer != NULL); + SDL_assert(panel != NULL); + + SDL_SetRenderDrawColor(renderer, panel->fill_color.r, panel->fill_color.g, panel->fill_color.b, + panel->fill_color.a); + if (SDL_RenderFillRect(renderer, &panel->rect) == false) { + return false; + } + + if (panel->border_color.a == 0) { + return true; + } + + SDL_SetRenderDrawColor(renderer, panel->border_color.r, panel->border_color.g, panel->border_color.b, + panel->border_color.a); + if (SDL_RenderRect(renderer, &panel->rect) == false) { + return false; + } + + return true; +} diff --git a/src/modules/ui.h b/src/modules/ui.h index a3b242f..5e5bfa5 100644 --- a/src/modules/ui.h +++ b/src/modules/ui.h @@ -12,6 +12,12 @@ typedef struct { SDL_Color border_color; } ui_button_t; +typedef struct { + SDL_FRect rect; + SDL_Color fill_color; + SDL_Color border_color; +} ui_panel_t; + /** * @brief Initialize a button with colors and zeroed geometry. * @@ -63,4 +69,34 @@ bool ui_button_contains(const ui_button_t* button, float x, float y); */ bool ui_button_render(SDL_Renderer* renderer, const ui_button_t* button); +/** + * @brief Initialize a panel with colors and zeroed geometry. + * + * @param panel Panel to initialize. + * @param fill_color Fill color for the panel body. + * @param border_color Border color for the panel outline. + */ +void ui_panel_init(ui_panel_t* panel, SDL_Color fill_color, SDL_Color border_color); + +/** + * @brief Size and center a panel based on content size and padding. + * + * @param panel Panel to size and position. + * @param screen_size Screen size in pixels. + * @param content_size Content size in pixels. + * @param padding_x Horizontal padding around the content. + * @param padding_y Vertical padding around the content. + */ +void ui_panel_layout_from_content(ui_panel_t* panel, const vector2i_t* screen_size, const vector2i_t* content_size, + float padding_x, float padding_y); + +/** + * @brief Render the panel body and optional border. + * + * @param renderer SDL renderer to draw with. + * @param panel Panel to draw. + * @return true if rendering succeeded, false otherwise. + */ +bool ui_panel_render(SDL_Renderer* renderer, const ui_panel_t* panel); + #endif // UI_H From 48df9af1c8261b927a2493aad3a6aaf26e9a2555 Mon Sep 17 00:00:00 2001 From: cazz Date: Fri, 9 Jan 2026 21:50:35 +0200 Subject: [PATCH 2/4] feat: add start menu and game state --- src/snake.c | 332 ++++++++++++++++++++++++++++++++++++---------------- src/snake.h | 7 +- 2 files changed, 237 insertions(+), 102 deletions(-) diff --git a/src/snake.c b/src/snake.c index f595f8d..8b6b784 100644 --- a/src/snake.c +++ b/src/snake.c @@ -10,15 +10,16 @@ static const SDL_Color k_color_empty = {0, 0, 0, 255}; static const SDL_Color k_color_food = {255, 0, 0, 255}; static const SDL_Color k_color_snake_head = {0, 255, 0, 255}; -static const SDL_Color k_color_pause_overlay = {0, 0, 0, 160}; -static const SDL_Color k_color_pause_panel = {25, 25, 25, 220}; -static const SDL_Color k_color_pause_button = {220, 220, 220, 255}; -static const SDL_Color k_color_pause_button_border = {180, 180, 180, 255}; +static const SDL_Color k_color_menu_overlay = {0, 0, 0, 160}; +static const SDL_Color k_color_menu_panel = {25, 25, 25, 220}; +static const SDL_Color k_color_menu_panel_border = {80, 80, 80, 255}; +static const SDL_Color k_color_menu_button = {220, 220, 220, 255}; +static const SDL_Color k_color_menu_button_border = {180, 180, 180, 255}; -static const float k_pause_panel_padding = 20.f; -static const float k_pause_text_gap = 16.f; -static const float k_pause_button_padding_x = 28.f; -static const float k_pause_button_padding_y = 12.f; +static const float k_menu_panel_padding = 20.f; +static const float k_menu_text_gap = 16.f; +static const float k_menu_button_padding_x = 28.f; +static const float k_menu_button_padding_y = 12.f; static bool update_pause_text(snake_t* snake); @@ -103,37 +104,128 @@ static bool update_pause_text(snake_t* snake) { return true; } -static void compute_pause_layout(const vector2i_t* screen_size, const vector2i_t* pause_text_size, - const vector2i_t* resume_text_size, SDL_FRect* out_panel_rect, - SDL_FRect* out_button_rect) { +typedef struct { + SDL_FRect panel_rect; + SDL_FRect button_rect; + SDL_FPoint title_pos; + SDL_FPoint subtitle_pos; + bool has_subtitle; +} menu_layout_t; + +static bool get_screen_size(snake_t* snake, vector2i_t* out_size) { + SDL_assert(snake != NULL); + SDL_assert(out_size != NULL); + + if (SDL_GetCurrentRenderOutputSize(snake->window.sdl_renderer, &out_size->x, &out_size->y) == false) { + SDL_Log("Failed to query render output size: %s", SDL_GetError()); + snake->window.is_running = false; + return false; + } + + return true; +} + +static bool get_text_size(snake_t* snake, TTF_Text* text, vector2i_t* out_size, const char* label) { + SDL_assert(snake != NULL); + SDL_assert(text != NULL); + SDL_assert(out_size != NULL); + SDL_assert(label != NULL); + + if (TTF_GetTextSize(text, &out_size->x, &out_size->y) == false) { + SDL_Log("Failed to measure %s text: %s", label, SDL_GetError()); + snake->window.is_running = false; + return false; + } + + return true; +} + +static void compute_menu_layout(const vector2i_t* screen_size, const vector2i_t* title_size, + const vector2i_t* subtitle_size, bool has_subtitle, const vector2i_t* button_label_size, + menu_layout_t* out_layout) { SDL_assert(screen_size != NULL); - SDL_assert(pause_text_size != NULL); - SDL_assert(resume_text_size != NULL); - SDL_assert(out_panel_rect != NULL); - SDL_assert(out_button_rect != NULL); + SDL_assert(title_size != NULL); + SDL_assert(button_label_size != NULL); + SDL_assert(out_layout != NULL); ui_button_t button; - ui_button_init(&button, k_color_pause_button, k_color_pause_button_border); - ui_button_layout_from_label(&button, resume_text_size, 0.f, 0.f, k_pause_button_padding_x, - k_pause_button_padding_y); + ui_button_init(&button, k_color_menu_button, k_color_menu_button_border); + ui_button_layout_from_label(&button, button_label_size, 0.f, 0.f, k_menu_button_padding_x, k_menu_button_padding_y); const float button_width = button.rect.w; const float button_height = button.rect.h; - const float panel_width = SDL_max((float)pause_text_size->x, button_width) + k_pause_panel_padding * 2.f; - const float panel_height = - (float)pause_text_size->y + k_pause_text_gap + button_height + k_pause_panel_padding * 2.f; - - out_panel_rect->x = ((float)screen_size->x - panel_width) * 0.5f; - out_panel_rect->y = ((float)screen_size->y - panel_height) * 0.5f; - out_panel_rect->w = panel_width; - out_panel_rect->h = panel_height; - - const float button_center_x = out_panel_rect->x + panel_width * 0.5f; - const float button_center_y = - out_panel_rect->y + k_pause_panel_padding + (float)pause_text_size->y + k_pause_text_gap + button_height * 0.5f; - ui_button_layout_from_label(&button, resume_text_size, button_center_x, button_center_y, k_pause_button_padding_x, - k_pause_button_padding_y); - *out_button_rect = button.rect; + float content_width = (float)title_size->x; + if (has_subtitle == true && subtitle_size != NULL) { + content_width = SDL_max(content_width, (float)subtitle_size->x); + } + content_width = SDL_max(content_width, button_width); + + float content_height = (float)title_size->y; + if (has_subtitle == true && subtitle_size != NULL) { + content_height += k_menu_text_gap + (float)subtitle_size->y; + } + content_height += k_menu_text_gap + button_height; + + vector2i_t content_size = {(int)(content_width + 0.5f), (int)(content_height + 0.5f)}; + + ui_panel_t panel; + ui_panel_init(&panel, k_color_menu_panel, k_color_menu_panel_border); + ui_panel_layout_from_content(&panel, screen_size, &content_size, k_menu_panel_padding, k_menu_panel_padding); + out_layout->panel_rect = panel.rect; + + float cursor_y = panel.rect.y + k_menu_panel_padding; + out_layout->title_pos.x = panel.rect.x + (panel.rect.w - (float)title_size->x) * 0.5f; + out_layout->title_pos.y = cursor_y; + cursor_y += (float)title_size->y; + + out_layout->has_subtitle = has_subtitle; + if (has_subtitle == true && subtitle_size != NULL) { + cursor_y += k_menu_text_gap; + out_layout->subtitle_pos.x = panel.rect.x + (panel.rect.w - (float)subtitle_size->x) * 0.5f; + out_layout->subtitle_pos.y = cursor_y; + cursor_y += (float)subtitle_size->y; + } + + cursor_y += k_menu_text_gap; + const float button_center_x = panel.rect.x + panel.rect.w * 0.5f; + const float button_center_y = cursor_y + button_height * 0.5f; + ui_button_layout_from_label(&button, button_label_size, button_center_x, button_center_y, k_menu_button_padding_x, + k_menu_button_padding_y); + out_layout->button_rect = button.rect; +} + +static bool get_menu_layout(snake_t* snake, TTF_Text* title_text, TTF_Text* subtitle_text, bool has_subtitle, + TTF_Text* button_text, menu_layout_t* out_layout) { + SDL_assert(snake != NULL); + SDL_assert(title_text != NULL); + SDL_assert(button_text != NULL); + SDL_assert(out_layout != NULL); + + vector2i_t screen_size; + if (get_screen_size(snake, &screen_size) == false) { + return false; + } + + vector2i_t title_size; + if (get_text_size(snake, title_text, &title_size, "title") == false) { + return false; + } + + vector2i_t subtitle_size = {0, 0}; + if (has_subtitle == true) { + SDL_assert(subtitle_text != NULL); + if (get_text_size(snake, subtitle_text, &subtitle_size, "subtitle") == false) { + return false; + } + } + + vector2i_t button_size; + if (get_text_size(snake, button_text, &button_size, "button") == false) { + return false; + } + + compute_menu_layout(&screen_size, &title_size, &subtitle_size, has_subtitle, &button_size, out_layout); + return true; } static bool reset(snake_t* snake) { @@ -141,8 +233,6 @@ static bool reset(snake_t* snake) { SDL_Log("Resetting game state"); - snake->is_paused = false; - dynamic_array_destroy(&snake->array_food); dynamic_array_destroy(&snake->array_body); @@ -275,11 +365,39 @@ bool snake_create(snake_t* snake, const char* title) { goto fail; } + const char* start_title = "Start Game"; + snake->text_start_title = TTF_CreateText(snake->window.ttf_text_engine, snake->window.ttf_font_default, start_title, + SDL_strlen(start_title)); + if (snake->text_start_title == NULL) { + SDL_Log("Failed to create start title text object: %s", SDL_GetError()); + goto fail; + } + + if (TTF_SetTextColor(snake->text_start_title, 255, 255, 255, 255) == false) { + SDL_Log("Failed to set start title text color: %s", SDL_GetError()); + goto fail; + } + + const char* start_label = "Start"; + snake->text_start_button = TTF_CreateText(snake->window.ttf_text_engine, snake->window.ttf_font_default, + start_label, SDL_strlen(start_label)); + if (snake->text_start_button == NULL) { + SDL_Log("Failed to create start button text object: %s", SDL_GetError()); + goto fail; + } + + if (TTF_SetTextColor(snake->text_start_button, 20, 20, 20, 255) == false) { + SDL_Log("Failed to set start button text color: %s", SDL_GetError()); + goto fail; + } + if (reset(snake) == false) { SDL_Log("Failed to initialize game state"); goto fail; } + snake->state = SNAKE_STATE_START; + SDL_Log("Snake game initialized successfully"); return snake->window.is_running; @@ -306,6 +424,16 @@ void snake_destroy(snake_t* snake) { snake->text_resume = NULL; } + if (snake->text_start_title != NULL) { + TTF_DestroyText(snake->text_start_title); + snake->text_start_title = NULL; + } + + if (snake->text_start_button != NULL) { + TTF_DestroyText(snake->text_start_button); + snake->text_start_button = NULL; + } + audio_manager_destroy(&snake->audio); window_destroy(&snake->window); @@ -322,7 +450,7 @@ void snake_destroy(snake_t* snake) { static void handle_movement_keys(snake_t* snake, SDL_Scancode scancode) { SDL_assert(snake != NULL); - if (snake->is_paused == true) { + if (snake->state != SNAKE_STATE_PLAYING) { return; } @@ -519,7 +647,7 @@ static bool test_food_collision(snake_t* snake) { void snake_update_fixed(snake_t* snake) { SDL_assert(snake != NULL); - if (snake->is_paused == true) { + if (snake->state != SNAKE_STATE_PLAYING) { return; } @@ -567,48 +695,52 @@ void snake_handle_events(snake_t* snake) { if (event.type == SDL_EVENT_KEY_DOWN) { if (event.key.scancode == SDL_SCANCODE_ESCAPE && event.key.repeat == 0) { - snake->is_paused = !snake->is_paused; - if (update_pause_text(snake) == false) { - snake->window.is_running = false; + if (snake->state == SNAKE_STATE_PLAYING) { + snake->state = SNAKE_STATE_PAUSED; + if (update_pause_text(snake) == false) { + snake->window.is_running = false; + } + } else if (snake->state == SNAKE_STATE_PAUSED) { + snake->state = SNAKE_STATE_PLAYING; } } handle_movement_keys(snake, event.key.scancode); } - if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN && snake->is_paused == true && - event.button.button == SDL_BUTTON_LEFT && event.button.down == true) { - vector2i_t screen_size; - if (SDL_GetCurrentRenderOutputSize(snake->window.sdl_renderer, &screen_size.x, &screen_size.y) == false) { - SDL_Log("Failed to query render output size: %s", SDL_GetError()); - snake->window.is_running = false; - return; - } + if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN && event.button.button == SDL_BUTTON_LEFT && + event.button.down == true) { + if (snake->state == SNAKE_STATE_PAUSED) { + menu_layout_t layout; + if (get_menu_layout(snake, snake->text_pause, NULL, false, snake->text_resume, &layout) == false) { + return; + } - vector2i_t pause_text_size; - if (TTF_GetTextSize(snake->text_pause, &pause_text_size.x, &pause_text_size.y) == false) { - SDL_Log("Failed to measure pause text: %s", SDL_GetError()); - snake->window.is_running = false; - return; - } + ui_button_t button; + ui_button_init(&button, k_color_menu_button, k_color_menu_button_border); + button.rect = layout.button_rect; - vector2i_t resume_text_size; - if (TTF_GetTextSize(snake->text_resume, &resume_text_size.x, &resume_text_size.y) == false) { - SDL_Log("Failed to measure resume text: %s", SDL_GetError()); - snake->window.is_running = false; - return; - } - - SDL_FRect panel_rect; - SDL_FRect button_rect; - compute_pause_layout(&screen_size, &pause_text_size, &resume_text_size, &panel_rect, &button_rect); + if (ui_button_contains(&button, event.button.x, event.button.y) == true) { + snake->state = SNAKE_STATE_PLAYING; + } + } else if (snake->state == SNAKE_STATE_START) { + menu_layout_t layout; + if (get_menu_layout(snake, snake->text_start_title, NULL, false, snake->text_start_button, &layout) == + false) { + return; + } - ui_button_t button; - ui_button_init(&button, k_color_pause_button, k_color_pause_button_border); - button.rect = button_rect; + ui_button_t button; + ui_button_init(&button, k_color_menu_button, k_color_menu_button_border); + button.rect = layout.button_rect; - if (ui_button_contains(&button, event.button.x, event.button.y) == true) { - snake->is_paused = false; + if (ui_button_contains(&button, event.button.x, event.button.y) == true) { + if (reset(snake) == false) { + snake->window.is_running = false; + return; + } + snake->state = SNAKE_STATE_PLAYING; + } } } } @@ -664,63 +796,61 @@ void snake_render_frame(snake_t* snake) { return; } - if (snake->is_paused == true) { - vector2i_t pause_text_size; - if (TTF_GetTextSize(snake->text_pause, &pause_text_size.x, &pause_text_size.y) == false) { - SDL_Log("Failed to measure pause text: %s", SDL_GetError()); - snake->window.is_running = false; - return; - } + if (snake->state == SNAKE_STATE_PAUSED || snake->state == SNAKE_STATE_START) { + TTF_Text* title_text = snake->state == SNAKE_STATE_PAUSED ? snake->text_pause : snake->text_start_title; + TTF_Text* button_text = snake->state == SNAKE_STATE_PAUSED ? snake->text_resume : snake->text_start_button; - vector2i_t resume_text_size; - if (TTF_GetTextSize(snake->text_resume, &resume_text_size.x, &resume_text_size.y) == false) { - SDL_Log("Failed to measure resume text: %s", SDL_GetError()); - snake->window.is_running = false; + menu_layout_t layout; + if (get_menu_layout(snake, title_text, NULL, false, button_text, &layout) == false) { return; } - SDL_FRect panel_rect; - SDL_FRect button_rect; - compute_pause_layout(&screen_size, &pause_text_size, &resume_text_size, &panel_rect, &button_rect); - if (SDL_SetRenderDrawBlendMode(snake->window.sdl_renderer, SDL_BLENDMODE_BLEND) == false) { SDL_Log("Failed to set blend mode: %s", SDL_GetError()); snake->window.is_running = false; return; } - SDL_SetRenderDrawColor(snake->window.sdl_renderer, k_color_pause_overlay.r, k_color_pause_overlay.g, - k_color_pause_overlay.b, k_color_pause_overlay.a); + SDL_SetRenderDrawColor(snake->window.sdl_renderer, k_color_menu_overlay.r, k_color_menu_overlay.g, + k_color_menu_overlay.b, k_color_menu_overlay.a); SDL_FRect overlay_rect = {0.f, 0.f, (float)screen_size.x, (float)screen_size.y}; SDL_RenderFillRect(snake->window.sdl_renderer, &overlay_rect); - SDL_SetRenderDrawColor(snake->window.sdl_renderer, k_color_pause_panel.r, k_color_pause_panel.g, - k_color_pause_panel.b, k_color_pause_panel.a); - SDL_RenderFillRect(snake->window.sdl_renderer, &panel_rect); + ui_panel_t panel; + ui_panel_init(&panel, k_color_menu_panel, k_color_menu_panel_border); + panel.rect = layout.panel_rect; + if (ui_panel_render(snake->window.sdl_renderer, &panel) == false) { + SDL_Log("Failed to render menu panel: %s", SDL_GetError()); + snake->window.is_running = false; + return; + } ui_button_t button; - ui_button_init(&button, k_color_pause_button, k_color_pause_button_border); - button.rect = button_rect; + ui_button_init(&button, k_color_menu_button, k_color_menu_button_border); + button.rect = layout.button_rect; if (ui_button_render(snake->window.sdl_renderer, &button) == false) { - SDL_Log("Failed to render pause button: %s", SDL_GetError()); + SDL_Log("Failed to render menu button: %s", SDL_GetError()); snake->window.is_running = false; return; } - const float pause_text_x = panel_rect.x + (panel_rect.w - (float)pause_text_size.x) * 0.5f; - const float pause_text_y = panel_rect.y + k_pause_panel_padding; - if (TTF_DrawRendererText(snake->text_pause, pause_text_x, pause_text_y) == false) { - SDL_Log("Failed to render pause text: %s", SDL_GetError()); + if (TTF_DrawRendererText(title_text, layout.title_pos.x, layout.title_pos.y) == false) { + SDL_Log("Failed to render menu title text: %s", SDL_GetError()); snake->window.is_running = false; return; } - float resume_text_x = 0.f; - float resume_text_y = 0.f; - ui_button_get_label_position(&button, &resume_text_size, &resume_text_x, &resume_text_y); - if (TTF_DrawRendererText(snake->text_resume, resume_text_x, resume_text_y) == false) { - SDL_Log("Failed to render resume text: %s", SDL_GetError()); + vector2i_t button_text_size; + if (get_text_size(snake, button_text, &button_text_size, "menu button") == false) { + return; + } + + float button_text_x = 0.f; + float button_text_y = 0.f; + ui_button_get_label_position(&button, &button_text_size, &button_text_x, &button_text_y); + if (TTF_DrawRendererText(button_text, button_text_x, button_text_y) == false) { + SDL_Log("Failed to render menu button text: %s", SDL_GetError()); snake->window.is_running = false; return; } diff --git a/src/snake.h b/src/snake.h index 8ec809b..4d774b1 100644 --- a/src/snake.h +++ b/src/snake.h @@ -18,6 +18,8 @@ typedef enum { SNAKE_DIRECTION_RIGHT } snake_direction_t; +typedef enum { SNAKE_STATE_START, SNAKE_STATE_PLAYING, SNAKE_STATE_PAUSED, SNAKE_STATE_GAME_OVER } snake_game_state_t; + typedef enum { SNAKE_CELL_EMPTY, SNAKE_CELL_WALL, SNAKE_CELL_FOOD, SNAKE_CELL_SNAKE } snake_cell_state_t; typedef struct { @@ -30,7 +32,7 @@ typedef struct { window_t window; audio_manager_t audio; - bool is_paused; + snake_game_state_t state; snake_direction_t current_direction; @@ -50,6 +52,9 @@ typedef struct { char text_pause_buffer[32]; TTF_Text* text_resume; + + TTF_Text* text_start_title; + TTF_Text* text_start_button; } snake_t; bool snake_create(snake_t* snake, const char* title); From 872a3ffcfff58a6a4ae6752d2ec711cf04969357 Mon Sep 17 00:00:00 2001 From: cazz Date: Fri, 9 Jan 2026 21:52:53 +0200 Subject: [PATCH 3/4] feat: add game over menu with restart --- src/snake.c | 135 +++++++++++++++++++++++++++++++++++++++++++++++++--- src/snake.h | 5 ++ 2 files changed, 133 insertions(+), 7 deletions(-) diff --git a/src/snake.c b/src/snake.c index 8b6b784..8a1167c 100644 --- a/src/snake.c +++ b/src/snake.c @@ -22,6 +22,7 @@ static const float k_menu_button_padding_x = 28.f; static const float k_menu_button_padding_y = 12.f; static bool update_pause_text(snake_t* snake); +static bool update_game_over_score_text(snake_t* snake); static bool get_random_empty_position(snake_t* snake, vector2i_t* out_position) { SDL_assert(snake != NULL); @@ -104,6 +105,25 @@ static bool update_pause_text(snake_t* snake) { return true; } +static bool update_game_over_score_text(snake_t* snake) { + SDL_assert(snake != NULL); + SDL_assert(snake->text_game_over_score != NULL); + + const int written = snprintf(snake->text_game_over_score_buffer, sizeof(snake->text_game_over_score_buffer), + "Final Score: %zu", snake->array_body.size); + if (written < 0 || (size_t)written >= sizeof(snake->text_game_over_score_buffer)) { + SDL_Log("Failed to format game over score text."); + return false; + } + + if (TTF_SetTextString(snake->text_game_over_score, snake->text_game_over_score_buffer, (size_t)written) == false) { + SDL_Log("Failed to update game over score text: %s", SDL_GetError()); + return false; + } + + return true; +} + typedef struct { SDL_FRect panel_rect; SDL_FRect button_rect; @@ -391,6 +411,51 @@ bool snake_create(snake_t* snake, const char* title) { goto fail; } + const char* game_over_title = "Game Over"; + snake->text_game_over_title = TTF_CreateText(snake->window.ttf_text_engine, snake->window.ttf_font_default, + game_over_title, SDL_strlen(game_over_title)); + if (snake->text_game_over_title == NULL) { + SDL_Log("Failed to create game over title text object: %s", SDL_GetError()); + goto fail; + } + + if (TTF_SetTextColor(snake->text_game_over_title, 255, 255, 255, 255) == false) { + SDL_Log("Failed to set game over title text color: %s", SDL_GetError()); + goto fail; + } + + const int game_over_written = + snprintf(snake->text_game_over_score_buffer, sizeof(snake->text_game_over_score_buffer), "Final Score: 0"); + if (game_over_written < 0 || (size_t)game_over_written >= sizeof(snake->text_game_over_score_buffer)) { + SDL_Log("Failed to format initial game over score text"); + goto fail; + } + + snake->text_game_over_score = TTF_CreateText(snake->window.ttf_text_engine, snake->window.ttf_font_default, + snake->text_game_over_score_buffer, (size_t)game_over_written); + if (snake->text_game_over_score == NULL) { + SDL_Log("Failed to create game over score text object: %s", SDL_GetError()); + goto fail; + } + + if (TTF_SetTextColor(snake->text_game_over_score, 255, 255, 255, 255) == false) { + SDL_Log("Failed to set game over score text color: %s", SDL_GetError()); + goto fail; + } + + const char* restart_label = "Restart"; + snake->text_restart_button = TTF_CreateText(snake->window.ttf_text_engine, snake->window.ttf_font_default, + restart_label, SDL_strlen(restart_label)); + if (snake->text_restart_button == NULL) { + SDL_Log("Failed to create restart button text object: %s", SDL_GetError()); + goto fail; + } + + if (TTF_SetTextColor(snake->text_restart_button, 20, 20, 20, 255) == false) { + SDL_Log("Failed to set restart button text color: %s", SDL_GetError()); + goto fail; + } + if (reset(snake) == false) { SDL_Log("Failed to initialize game state"); goto fail; @@ -434,6 +499,21 @@ void snake_destroy(snake_t* snake) { snake->text_start_button = NULL; } + if (snake->text_game_over_title != NULL) { + TTF_DestroyText(snake->text_game_over_title); + snake->text_game_over_title = NULL; + } + + if (snake->text_game_over_score != NULL) { + TTF_DestroyText(snake->text_game_over_score); + snake->text_game_over_score = NULL; + } + + if (snake->text_restart_button != NULL) { + TTF_DestroyText(snake->text_restart_button); + snake->text_restart_button = NULL; + } + audio_manager_destroy(&snake->audio); window_destroy(&snake->window); @@ -655,9 +735,8 @@ void snake_update_fixed(snake_t* snake) { if (test_body_collision(snake) == true) { SDL_Log("Collision detected! Score: %zu", snake->array_body.size); - // Restart the game if the snake collides with its own array_body. - if (reset(snake) == false) { - SDL_Log("Failed to reset game after collision"); + snake->state = SNAKE_STATE_GAME_OVER; + if (update_game_over_score_text(snake) == false) { snake->window.is_running = false; } return; @@ -734,6 +813,24 @@ void snake_handle_events(snake_t* snake) { ui_button_init(&button, k_color_menu_button, k_color_menu_button_border); button.rect = layout.button_rect; + if (ui_button_contains(&button, event.button.x, event.button.y) == true) { + if (reset(snake) == false) { + snake->window.is_running = false; + return; + } + snake->state = SNAKE_STATE_PLAYING; + } + } else if (snake->state == SNAKE_STATE_GAME_OVER) { + menu_layout_t layout; + if (get_menu_layout(snake, snake->text_game_over_title, snake->text_game_over_score, true, + snake->text_restart_button, &layout) == false) { + return; + } + + ui_button_t button; + ui_button_init(&button, k_color_menu_button, k_color_menu_button_border); + button.rect = layout.button_rect; + if (ui_button_contains(&button, event.button.x, event.button.y) == true) { if (reset(snake) == false) { snake->window.is_running = false; @@ -796,12 +893,28 @@ void snake_render_frame(snake_t* snake) { return; } - if (snake->state == SNAKE_STATE_PAUSED || snake->state == SNAKE_STATE_START) { - TTF_Text* title_text = snake->state == SNAKE_STATE_PAUSED ? snake->text_pause : snake->text_start_title; - TTF_Text* button_text = snake->state == SNAKE_STATE_PAUSED ? snake->text_resume : snake->text_start_button; + if (snake->state == SNAKE_STATE_PAUSED || snake->state == SNAKE_STATE_START || + snake->state == SNAKE_STATE_GAME_OVER) { + TTF_Text* title_text = NULL; + TTF_Text* subtitle_text = NULL; + bool has_subtitle = false; + TTF_Text* button_text = NULL; + + if (snake->state == SNAKE_STATE_PAUSED) { + title_text = snake->text_pause; + button_text = snake->text_resume; + } else if (snake->state == SNAKE_STATE_START) { + title_text = snake->text_start_title; + button_text = snake->text_start_button; + } else { + title_text = snake->text_game_over_title; + subtitle_text = snake->text_game_over_score; + has_subtitle = true; + button_text = snake->text_restart_button; + } menu_layout_t layout; - if (get_menu_layout(snake, title_text, NULL, false, button_text, &layout) == false) { + if (get_menu_layout(snake, title_text, subtitle_text, has_subtitle, button_text, &layout) == false) { return; } @@ -841,6 +954,14 @@ void snake_render_frame(snake_t* snake) { return; } + if (layout.has_subtitle == true) { + if (TTF_DrawRendererText(subtitle_text, layout.subtitle_pos.x, layout.subtitle_pos.y) == false) { + SDL_Log("Failed to render menu subtitle text: %s", SDL_GetError()); + snake->window.is_running = false; + return; + } + } + vector2i_t button_text_size; if (get_text_size(snake, button_text, &button_text_size, "menu button") == false) { return; diff --git a/src/snake.h b/src/snake.h index 4d774b1..4c70f6f 100644 --- a/src/snake.h +++ b/src/snake.h @@ -55,6 +55,11 @@ typedef struct { TTF_Text* text_start_title; TTF_Text* text_start_button; + + TTF_Text* text_game_over_title; + TTF_Text* text_game_over_score; + char text_game_over_score_buffer[48]; + TTF_Text* text_restart_button; } snake_t; bool snake_create(snake_t* snake, const char* title); From 84532a63929d9b61c103a4a71ef8db66abc96a17 Mon Sep 17 00:00:00 2001 From: cazz Date: Fri, 9 Jan 2026 21:58:29 +0200 Subject: [PATCH 4/4] chore: update agent notes --- AGENTS.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index b04911f..9ed28ce 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,8 +13,11 @@ ctest --test-dir build --output-on-failure - `src/main.c`: main loop (fixed-timestep update + render). - `src/snake.c`, `src/snake.h`: game state, movement, collisions, rendering. - `src/modules/window.c`, `src/modules/window.h`: SDL window/renderer + SDL_ttf init/teardown + timing. +- `src/modules/audio.c`, `src/modules/audio.h`: audio manager + sound loading/playing. +- `src/modules/ui.c`, `src/modules/ui.h`: reusable UI widgets (buttons/panels). - `src/utils/*`: `vector2i_*` + `dynamic_array_*`. -- `tests/dynamic_array_tests.c`: only unit test target (`dynamic_array_tests`). +- `tests/dynamic_array_tests.c`: unit test target (`dynamic_array_tests`). +- `tests/vector_tests.c`: unit test target (`vector_tests`). ## Conventions (follow these) @@ -24,6 +27,7 @@ ctest --test-dir build --output-on-failure - Lifetimes: `*_destroy()` must be safe after partial init; free resources once, then set pointers to `NULL`. - Avoid unbounded loops; random placement must have an upper bound + a deterministic fallback. - Prefer `snprintf` over `sprintf`; avoid implicit truncation. +- Commit messages: use Conventional Commit prefixes (`feat:`, `fix:`, `refactor:`, `chore:`, etc.) with a short summary. ## CMake notes