From 92cb9f98835b84deea2b111c5617740bcc8df9e9 Mon Sep 17 00:00:00 2001 From: Rickey Date: Fri, 22 May 2026 18:06:13 -0700 Subject: [PATCH] Refactor/state machine (#470) * initial additions * Move current_blind to game vars * tmp * tmp * tmp * tmp * tmp * tmp * tmp * Compile but is broken * temp fix for token location * tmp * Working, lets clean up * remove todo * Remove odd reroll blind variable * fix skipping blind screen * fix reroll * remove magic for blind token locations * clang format * Cleanup button highlight in blind menu * some cleanup * Add layout file for global UI rects * remove dead code * Clang format * clang format it up * Move reset top left funct to gfx utils * make note of soon-to-be removed vars * Cleanup gamevars declaration * Cleanup layout file * Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Add include for stdbool.h in common_ui.h * Cleanup rect declaration * Start moving state machine stuff to new file * quick tmp * oops, add new files * working call list * fixed pointer stuff * working as per norm * working replacement * move defines to static const * fix compile issue * Add some missing "void"s * Working new substates * add statemachine to blind select * Fix game restart statemachine leak * clang-format * Clang format * Rework shop exit logic * Use new state machine in shop * Remove substates entirely * Remove debug stuff * Clang format * document state machine better * update list tests * clang-format * Update for PR * Claaaaang format * another clang-format * Remove extra state in round_end * cleanup docs for removals in lists * clang-format... again * update doxygen format * Clang format * Update list docs * Update lists tests * Fix bad conflict resolution --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- include/game.h | 9 ++-- include/list.h | 35 ++++++++++-- include/state_machine.h | 107 +++++++++++++++++++++++++++++++++++++ source/blind.c | 4 +- source/game.c | 77 +++++++++----------------- source/game/blind_select.c | 65 ++++++++++++---------- source/game/round_end.c | 61 ++++++++++----------- source/game/shop.c | 50 +++++++++-------- source/list.c | 27 +++++++++- source/state_machine.c | 63 ++++++++++++++++++++++ tests/list/list_test.c | 95 +++++++++++++++++++++++++++----- 11 files changed, 428 insertions(+), 165 deletions(-) create mode 100644 include/state_machine.h create mode 100644 source/state_machine.c diff --git a/include/game.h b/include/game.h index 456e29a..20d0149 100644 --- a/include/game.h +++ b/include/game.h @@ -40,9 +40,6 @@ typedef struct CardObject CardObject; typedef struct Card Card; typedef struct JokerObject JokerObject; -typedef void (*GameStateCallback)(void); -typedef void (*SubStateActionFn)(void); - // Enum value names in ../include/def_state_info_table.h enum GameState { @@ -67,7 +64,7 @@ enum PlayState }; // Game functions -void game_init(); +void game_init(void); /** * @brief Called when exiting the Game Over screen (both win or lose) to reset game variables @@ -77,9 +74,9 @@ void game_init(); * and shouldn't be called from other states, otherwise some data such as shop jokers * may not be properly reset. */ -void game_reset(); +void game_reset(void); -void game_update(); +void game_update(void); void game_change_state(enum GameState new_game_state); CardObject** get_played_array(void); diff --git a/include/list.h b/include/list.h index 9f160b6..4e1a274 100644 --- a/include/list.h +++ b/include/list.h @@ -23,6 +23,13 @@ */ #define MAX_LIST_NODES 128 +/** + * @brief Default list declaration for empty lists + */ +// clang-format off +#define LIST_DEFAULT { .head = NULL, .tail = NULL, .len = 0 } +// clang-format on + typedef struct ListNode ListNode; /** @@ -106,15 +113,16 @@ typedef struct } ListItr; /** - * Create a list. + * Initialize a list. * - * While this function does not allocate memory for the list itself, the list does allocate memory - * for each element. So every created list must be freed with @ref list_clear to ensure the list's - * nodes are deleted properly. + * Set the values of a list to default. + * + * If using this function to reset a list, the list must be freed with @ref list_clear to ensure the + * list's nodes are deleted properly. * * @return A @ref List with head and tail reset. */ -List list_create(void); +List list_init(void); /** * Clear a list. @@ -122,6 +130,8 @@ List list_create(void); * Go through the list and free each node and set the `head` and `tail` to `NULL`. * Note, it doesn't "free" the data at the node. * + * @note To reset an existing list to default values, first call `list_clear` then @ref list_init + * * @param list pointer to a @ref List to clear */ void list_clear(List* list); @@ -222,6 +232,18 @@ void* list_get_at_idx(List* list, unsigned int idx); */ bool list_remove_at_idx(List* list, unsigned int idx); +/** + * Remove a List's node with the matching pointer + * + * @param list pointer to a @ref List + * @param data pointer to data in node in list + * + * @return `true` if successfully removed, `false` otherwise + * + * @note When working with @ref ListItr, use @ref list_itr_remove_current_node() + */ +bool list_remove_data(List* list, void* data); + /** * Get the number of elements in a @ref List * @@ -265,6 +287,9 @@ void* list_itr_next(ListItr* itr); * most recently returned valu from @ref list_itr_next() * * @param itr pointer to the @ref ListItr + * + * @note When working with @ref ListItr, use this and not @ref list_remove_at() as it will + * "break" the iterator. */ void list_itr_remove_current_node(ListItr* itr); diff --git a/include/state_machine.h b/include/state_machine.h new file mode 100644 index 0000000..27d6eb8 --- /dev/null +++ b/include/state_machine.h @@ -0,0 +1,107 @@ +/** + * @file state_machine.h + * + * @brief State Machine + * + * This file is the interface into a generic state machine system. + * + * State machines are defined as an array of function callbacks where each state + * is an index in the array with three functions per state: `on_init()`, + * `on_update()`, and `on_exit()`. + * + * **`on_init()`** Ran once when transitioning into the new state + * **`on_update()`** Ran once every frame + * **`on_exit()`** Ran once when exiting a state, for cleanup. + * + * The state machine `on_update()` function is "registered" to a linked-list of + * other state machine update functions. This list will call the state machines + * active `on_update()` function. This allows much of the complexity of state + * transitions to remain central to `state_machine.c`. Also, multiple state + * machines can be registered to this list. This allows using substates within + * states, or have one off state machines like animation controllers. + * + * When a state machine is finished, it can "remove" itself from the main update + * callback list. This can be done within the update method of its own state + * machine. This can be used to start a self destructing state machine. + */ +#ifndef STATE_MACHINE_H +#define STATE_MACHINE_H + +/** + * @brief State machine callback function pointer type + */ +typedef void (*StateCallback)(void); + +/** + * @brief State machine callbacks + */ +typedef struct +{ + StateCallback on_init; + StateCallback on_update; + StateCallback on_exit; +} StateInfo; + +/** + * @brief State machine instance + */ +typedef struct +{ + /** + * @brief Pointer to the active update function in `state_infos` + */ + StateCallback active_update; + + /** + * @brief Array of @ref StateCallbacks , one entry per state + */ + StateInfo* state_infos; + + /** + * @brief Number of elements in the `state_infos` array + */ + unsigned int num_infos; + + /** + * @brief The current state of the state machine, the offset into state_infos + */ + int state; +} StateMachine; + +/** + * @brief Register a statemachine to run it's update function once per frame + * + * @param state_machine pointer to @ref StateMachine to register, cannot be NULL + */ +void state_machine_register(StateMachine* state_machine); + +/** + * @brief Remove a statemachine's update function + * + * @param state_machine pointer to @ref StateMachine to remove, cannot be NULL + */ +void state_machine_remove(StateMachine* state_machine); + +/** + * @brief Update registered state machines' update functions + */ +void state_machine_update(void); + +/** + * @brief Calls the current state's on_exit, the new state's on_init, and sets the active update fn + * + * @param state_machine pointer to @ref StateMachine, cannot be NULL + * @param new_state offset into `state_infos` array to transition to + */ +void state_machine_change_state(StateMachine* state_machine, int new_state); + +/** + * @brief no operation + */ +void noop(void); + +// clang-format off +#define STATE_INFO_UPDATE_FN_ONLY(fn) {.on_init = noop, .on_update = fn, .on_exit = noop} +// clang-format on + +#endif // STATE_MACHINE_H diff --git a/source/blind.c b/source/blind.c index 0a6410a..ef3910b 100644 --- a/source/blind.c +++ b/source/blind.c @@ -111,8 +111,8 @@ void init_unbeaten_blinds_list(bool showdown) if (!init) { init = true; - unbeaten_showdown_blinds = list_create(); - unbeaten_boss_blinds = list_create(); + unbeaten_showdown_blinds = list_init(); + unbeaten_boss_blinds = list_init(); } List* p_unbeaten_blinds = showdown ? &unbeaten_showdown_blinds : &unbeaten_boss_blinds; diff --git a/source/game.c b/source/game.c index 210ed17..e3ab495 100644 --- a/source/game.c +++ b/source/game.c @@ -27,6 +27,7 @@ #include "soundbank.h" #include "splash_screen.h" #include "sprite.h" +#include "state_machine.h" #include "timer.h" #include "tonc_memdef.h" #include "util.h" @@ -86,15 +87,6 @@ #define EXPIRE_ANIMATION_FRAME_COUNT 3 -// Used as a No Operation for game states that have no init and/or exit function. -// ricfehr3 did the work of determining whether a noop or a NULL check was more -// efficient. Well, this is the answer. -// Thanks! -// https://github.com/cellos51/balatro-gba/issues/137#issuecomment-3322485129 -static void noop(void) -{ -} - // These functions need to be forward declared // so they're visible to the state_info array, // and the sub-state function tables. @@ -212,8 +204,20 @@ static const BG_POINT HAND_PLAY_POS = {120, 70}; // variable and handling in common.c once the related refactor is finished static enum BackgroundId background_legacy = BG_NONE; +static StateInfo state_info[] = { +#define DEF_STATE_INFO(stateEnum, init_fn, update_fn, exit_fn) \ + {.on_init = init_fn, .on_update = update_fn, .on_exit = exit_fn}, +#include "../include/def_state_info_table.h" +#undef DEF_STATE_INFO +}; + +static StateMachine game_sm = { + .state_infos = &state_info[0], + .num_infos = GAME_STATE_MAX, +}; + // clang-format off -SelectionGridRow game_playing_selection_rows[] = { +static SelectionGridRow game_playing_selection_rows[] = { { 0, jokers_sel_row_get_size, @@ -240,14 +244,14 @@ SelectionGridRow game_playing_selection_rows[] = { static const Selection GAME_PLAYING_INIT_SEL = {0, 1}; -SelectionGrid game_playing_selection_grid = { +static SelectionGrid game_playing_selection_grid = { game_playing_selection_rows, NUM_ELEM_IN_ARR(game_playing_selection_rows), GAME_PLAYING_INIT_SEL }; // Array of buttons by horizontal selection index (x) -Button game_playing_buttons[] = { +static Button game_playing_buttons[] = { {PLAY_HAND_BTN_BORDER_PAL_IDX, PLAY_HAND_BTN_PAL_IDX, game_playing_play_hand_on_pressed, can_play_hand }, {SORT_BY_RANK_BTN_BORDER_PAL_IDX, SORT_BTNS_PAL_IDX, game_playing_sort_by_rank_on_pressed, NULL }, {SORT_BY_SUIT_BTN_BORDER_PAL_IDX, SORT_BTNS_PAL_IDX, game_playing_sort_by_suit_on_pressed, NULL }, @@ -258,8 +262,6 @@ Button game_playing_buttons[] = { static const int HAND_SPACING_LUT[MAX_HAND_SIZE] = {28, 28, 28, 28, 27, 21, 18, 15, 13, 12, 10, 9, 9, 8, 8, 7}; -// The current game state, this is used to determine what the game is doing at any given time -static enum GameState game_state = GAME_STATE_UNDEFINED; static enum PlayState play_state = PLAY_STARTING; // Initialization of the global vars @@ -293,28 +295,6 @@ GameVariables g_game_vars = { }; // clang-format on -typedef struct -{ - int substate; - GameStateCallback on_init; - GameStateCallback on_update; - GameStateCallback on_exit; -} StateInfo; - -StateInfo state_info[] = { -#define DEF_STATE_INFO(stateEnum, init_fn, update_fn, exit_fn) \ - {.on_init = init_fn, .on_update = update_fn, .on_exit = exit_fn, .substate = 0}, -#include "../include/def_state_info_table.h" -#undef DEF_STATE_INFO -}; - -// The sprite that displays the blind when in "GAME_PLAYING/GAME_ROUND_END" state - -// The sprite that displays the blind when in "GAME_ROUND_END" state - -// Red deck default (can later be moved to a deck.h file or something) -// Set in game_init and game_round_init - static u32 temp_score = 0; // This is the score that shows in the same spot as the hand type. static bool score_flames_active = false; static FIXED lerped_score = 0; @@ -425,11 +405,13 @@ static inline void jokers_available_to_shop_init(void) void game_init() { + state_machine_remove(&game_sm); + state_machine_register(&game_sm); // Initialize all jokers list once - _owned_jokers_list = list_create(); - _discarded_jokers_list = list_create(); - _expired_jokers_list = list_create(); - _shop_jokers_list = list_create(); + _owned_jokers_list = list_init(); + _discarded_jokers_list = list_init(); + _expired_jokers_list = list_init(); + _shop_jokers_list = list_init(); // TODO: Move this to an initialization of the play scoring states _joker_scored_itr = list_itr_create(&_owned_jokers_list); @@ -595,25 +577,14 @@ void game_update() jokers_update_loop(); - state_info[game_state].on_update(); + state_machine_update(); } void game_change_state(enum GameState new_game_state) { g_game_vars.timer = TM_ZERO; // Reset the timer - if (game_state >= 0 && game_state < GAME_STATE_MAX) - { - state_info[game_state].substate = 0; - state_info[game_state].on_exit(); - } - - if (new_game_state >= 0 && new_game_state < GAME_STATE_MAX) - { - state_info[new_game_state].on_init(); - - game_state = new_game_state; - } + state_machine_change_state(&game_sm, new_game_state); } CardObject** get_played_array(void) diff --git a/source/game/blind_select.c b/source/game/blind_select.c index a3d374a..bc99f84 100644 --- a/source/game/blind_select.c +++ b/source/game/blind_select.c @@ -11,17 +11,19 @@ #include "layout.h" #include "soundbank.h" #include "sprite.h" +#include "state_machine.h" #include "timer.h" #include "util.h" #include -#define BLIND_SELECT_BTN_PID 15 -#define TM_DISP_BLIND_PANEL_FINISH 7 -#define TM_DISP_BLIND_PANEL_START 1 -#define BLIND_SKIP_BTN_PID 5 -#define BLIND_SKIP_BTN_SELECTED_BORDER_PID 10 -#define BLIND_SELECT_BTN_SELECTED_BORDER_PID 18 +static const u32 BLIND_SELECT_BTN_PID = 15; +static const u32 BLIND_SKIP_BTN_PID = 5; +static const u32 BLIND_SKIP_BTN_SELECTED_BORDER_PID = 10; +static const u32 BLIND_SELECT_BTN_SELECTED_BORDER_PID = 18; + +static const u32 TM_DISP_BLIND_PANEL_FINISH = 7; +static const u32 TM_DISP_BLIND_PANEL_START = 1; static int timer; @@ -29,6 +31,7 @@ static void game_blind_select_start_anim_seq(void); static void game_blind_select_handle_input(void); static void game_blind_select_selected_anim_seq(void); static void game_blind_select_display_blind_panel(void); +static void game_blind_select_exit(void); static Rect game_blind_select_get_req_score_rect(enum BlindTokens blind); static void game_blind_select_print_blinds_reqs_and_rewards(void); static enum BlindType get_blind_type_from_token(enum BlindTokens blind); @@ -40,15 +43,22 @@ enum BlindSelectState BLIND_SELECT, BLIND_SELECTED_ANIM_SEQ, DISPLAY_BLIND_PANEL, - BLIND_SELECT_MAX + BLIND_SELECT_EXIT, + BLIND_SELECT_MAX, }; // TODO: this will be refactored into common state machine -static const SubStateActionFn blind_select_state_actions[] = { - game_blind_select_start_anim_seq, - game_blind_select_handle_input, - game_blind_select_selected_anim_seq, - game_blind_select_display_blind_panel +static StateInfo state_info[] = { + STATE_INFO_UPDATE_FN_ONLY(game_blind_select_start_anim_seq), + STATE_INFO_UPDATE_FN_ONLY(game_blind_select_handle_input), + STATE_INFO_UPDATE_FN_ONLY(game_blind_select_selected_anim_seq), + STATE_INFO_UPDATE_FN_ONLY(game_blind_select_display_blind_panel), + STATE_INFO_UPDATE_FN_ONLY(game_blind_select_exit), +}; + +static StateMachine blind_select_sm = { + .state_infos = &state_info[0], + .num_infos = BLIND_SELECT_MAX, }; // clang-format off @@ -74,8 +84,6 @@ static const u32 SKIP_ROW = 1; static int selection_x = 0; static int selection_y = 0; -static enum BlindSelectState substate; - static Sprite* blind_select_tokens[NUM_BLINDS_PER_ANTE] = {NULL}; static void game_blind_select_start_anim_seq() @@ -95,7 +103,7 @@ static void game_blind_select_start_anim_seq() if (timer == TM_END_ANIM_SEQ) { game_blind_select_print_blinds_reqs_and_rewards(); - substate = BLIND_SELECT; + state_machine_change_state(&blind_select_sm, BLIND_SELECT); timer = TM_ZERO; // Reset the timer } } @@ -191,7 +199,7 @@ static void game_blind_select_handle_input() { case BLIND_ROW: play_sfx(SFX_BUTTON, MM_BASE_PITCH_RATE, BUTTON_SFX_VOLUME); - substate = BLIND_SELECTED_ANIM_SEQ; + state_machine_change_state(&blind_select_sm, BLIND_SELECTED_ANIM_SEQ); timer = TM_ZERO; ++g_game_vars.round; display_round(); @@ -258,8 +266,8 @@ static void game_blind_select_selected_anim_seq() obj_hide(blind_select_tokens[i]->obj); } - substate = DISPLAY_BLIND_PANEL; // Reset the state - timer = TM_ZERO; // Reset the timer + timer = TM_ZERO; + state_machine_change_state(&blind_select_sm, DISPLAY_BLIND_PANEL); } } @@ -267,7 +275,7 @@ static void game_blind_select_display_blind_panel() { if (timer >= TM_DISP_BLIND_PANEL_FINISH) { - substate = BLIND_SELECT_MAX; + state_machine_change_state(&blind_select_sm, BLIND_SELECT_EXIT); return; } @@ -302,6 +310,12 @@ static void game_blind_select_display_blind_panel() } } +static void game_blind_select_exit(void) +{ + reset_background(); + game_change_state(GAME_STATE_PLAYING); +} + static Rect game_blind_select_get_req_score_rect(enum BlindTokens blind) { Rect blind_req_score_rect = SINGLE_BLIND_SEL_REQ_SCORE_RECT; @@ -441,7 +455,8 @@ static void blind_tokens_init() void game_blind_select_on_init(void) { timer = TM_ZERO; - substate = START_ANIM_SEQ; + state_machine_register(&blind_select_sm); + state_machine_change_state(&blind_select_sm, START_ANIM_SEQ); selection_x = 0; selection_y = 0; @@ -461,14 +476,6 @@ void game_blind_select_on_init(void) void game_blind_select_on_update(void) { timer++; - if (substate == BLIND_SELECT_MAX) - { - reset_background(); - game_change_state(GAME_STATE_PLAYING); - return; - } - - blind_select_state_actions[substate](); } void game_blind_select_on_exit(void) @@ -483,7 +490,7 @@ void game_blind_select_on_exit(void) reset_background(); selection_y = 0; - g_game_vars.timer = TM_ZERO; + state_machine_remove(&blind_select_sm); } void game_blind_select_change_background(void) diff --git a/source/game/round_end.c b/source/game/round_end.c index 120821a..f33abc7 100644 --- a/source/game/round_end.c +++ b/source/game/round_end.c @@ -5,6 +5,7 @@ #include "game.h" #include "game_variables.h" #include "layout.h" +#include "state_machine.h" #include "timer.h" #include "util.h" @@ -19,7 +20,7 @@ enum GameRoundEndStates DISPLAY_REWARDS, DISPLAY_CASHOUT, DISMISS_ROUND_END_PANEL, - ROUND_END_EXIT + ROUND_END_STATES_MAX }; static const u32 TM_RESET_STATIC_VARS = 30; @@ -46,7 +47,6 @@ static const Rect ROUND_END_MENU_RECT = {9, 7, 24, 20 static const BG_POINT CASHOUT_SRC_3X3_RECT_POS = {5, 29}; // clang-format on -static int substate; static int blind_reward = 0; static int hand_reward = 0; static int interest_reward = 0; @@ -67,16 +67,21 @@ static void game_round_end_dismiss_round_end_panel(void); static void game_round_end_extend_black_panel_down(int black_panel_bottom); -static const SubStateActionFn round_end_state_actions[] = { - game_round_end_start, - game_round_end_start_expand_popup, - game_round_end_display_finished_blind, - game_round_end_display_score_min, - game_round_end_update_blind_reward, - game_round_end_panel_exit, - game_round_end_display_rewards, - game_round_end_display_cashout, - game_round_end_dismiss_round_end_panel +static StateInfo state_info[] = { + STATE_INFO_UPDATE_FN_ONLY(game_round_end_start), + STATE_INFO_UPDATE_FN_ONLY(game_round_end_start_expand_popup), + STATE_INFO_UPDATE_FN_ONLY(game_round_end_display_finished_blind), + STATE_INFO_UPDATE_FN_ONLY(game_round_end_display_score_min), + STATE_INFO_UPDATE_FN_ONLY(game_round_end_update_blind_reward), + STATE_INFO_UPDATE_FN_ONLY(game_round_end_panel_exit), + STATE_INFO_UPDATE_FN_ONLY(game_round_end_display_rewards), + STATE_INFO_UPDATE_FN_ONLY(game_round_end_display_cashout), + STATE_INFO_UPDATE_FN_ONLY(game_round_end_dismiss_round_end_panel), +}; + +static StateMachine round_end_sm = { + .state_infos = &state_info[0], + .num_infos = ROUND_END_STATES_MAX, }; static int calculate_interest_reward(void) @@ -93,8 +98,8 @@ static void game_round_end_start(void) if (g_game_vars.timer == TM_RESET_STATIC_VARS) { change_background(BG_ROUND_END, false); // Change the background to the round end background - substate = START_EXPAND_POPUP; // Change the state to the next one - g_game_vars.timer = TM_ZERO; // Reset the timer + state_machine_change_state(&round_end_sm, START_EXPAND_POPUP); + g_game_vars.timer = TM_ZERO; // Reset the timer blind_reward = blind_get_reward(g_game_vars.current_blind); hand_reward = g_game_vars.hands; interest_reward = calculate_interest_reward(); @@ -109,7 +114,7 @@ static void game_round_end_start_expand_popup(void) if (g_game_vars.timer == TM_END_POP_MENU_ANIM) { - substate = DISPLAY_FINISHED_BLIND; + state_machine_change_state(&round_end_sm, DISPLAY_FINISHED_BLIND); g_game_vars.timer = TM_ZERO; } } @@ -151,7 +156,7 @@ static void game_round_end_display_finished_blind(void) if (g_game_vars.timer >= TM_END_DISPLAY_FIN_BLIND) { - substate = DISPLAY_SCORE_MIN; + state_machine_change_state(&round_end_sm, DISPLAY_SCORE_MIN); g_game_vars.timer = TM_ZERO; } } @@ -172,7 +177,7 @@ static void game_round_end_display_score_min(void) if (g_game_vars.timer >= TM_END_DISPLAY_SCORE_MIN) { - substate = UPDATE_BLIND_REWARD; + state_machine_change_state(&round_end_sm, UPDATE_BLIND_REWARD); g_game_vars.timer = TM_ZERO; } } @@ -208,7 +213,7 @@ static void game_round_end_update_blind_reward(void) tte_erase_rect_wrapper(BLIND_REQ_TEXT_RECT); obj_hide(g_game_vars.playing_blind_token->obj); affine_background_load_palette(affine_background_gfxPal); - substate = BLIND_PANEL_EXIT; + state_machine_change_state(&round_end_sm, BLIND_PANEL_EXIT); g_game_vars.timer = TM_ZERO; } } @@ -236,7 +241,7 @@ static void game_round_end_panel_exit(void) else if (g_game_vars.timer > FRAMES(20)) { memset16(&pal_bg_mem[REWARD_PANEL_BORDER_PID], 0x1483, 1); - substate = DISPLAY_REWARDS; + state_machine_change_state(&round_end_sm, DISPLAY_REWARDS); g_game_vars.timer = TM_ZERO; } } @@ -342,7 +347,7 @@ static void game_round_end_display_rewards(void) if (hand_reward <= 0 && interest_to_count <= 0) { g_game_vars.timer = TM_ZERO; - substate = DISPLAY_CASHOUT; + state_machine_change_state(&round_end_sm, DISPLAY_CASHOUT); } else if (g_game_vars.timer == TM_START_ROUND_END_REWARDS_ANIM) { @@ -406,7 +411,7 @@ static void game_round_end_display_cashout() { game_round_end_cashout(); - substate = DISMISS_ROUND_END_PANEL; // Go to the next state + state_machine_change_state(&round_end_sm, DISMISS_ROUND_END_PANEL); g_game_vars.timer = TM_ZERO; obj_hide(g_game_vars.round_end_blind_token->obj); // Hide the blind token object @@ -423,7 +428,7 @@ static void game_round_end_dismiss_round_end_panel(void) if (g_game_vars.timer >= TM_DISMISS_ROUND_END_TM) { g_game_vars.timer = TM_ZERO; - substate = ROUND_END_EXIT; + game_change_state(GAME_STATE_SHOP); } } @@ -446,19 +451,14 @@ void game_round_end_change_background(void) void game_round_end_on_init(void) { - substate = ROUND_END_START; g_game_vars.timer = 0; + state_machine_register(&round_end_sm); + state_machine_change_state(&round_end_sm, ROUND_END_START); } void game_round_end_on_update(void) { - if (substate == ROUND_END_EXIT) - { - game_change_state(GAME_STATE_SHOP); - return; - } - - round_end_state_actions[substate](); + // Substate logic only } void game_round_end_on_exit(void) @@ -470,5 +470,6 @@ void game_round_end_on_exit(void) interest_reward = 0; sprite_destroy(&g_game_vars.playing_blind_token); sprite_destroy(&g_game_vars.round_end_blind_token); + state_machine_remove(&round_end_sm); // TODO: Reuse sprites for blind selection? } diff --git a/source/game/shop.c b/source/game/shop.c index b68966c..4c09490 100644 --- a/source/game/shop.c +++ b/source/game/shop.c @@ -20,6 +20,7 @@ #include "random.h" #include "save.h" #include "soundbank.h" +#include "state_machine.h" #include "timer.h" #include "util.h" @@ -62,8 +63,6 @@ static const Rect SHOP_REROLL_RECT = { 88, 96, UNDEFINED, UNDEFINED }; static const BG_POINT SHOP_JOKER_SPRITES_INIT_POS = {120, 160}; // clang-format on -// Shop Substates - enum GameShopStates { GAME_SHOP_INTRO, @@ -72,14 +71,19 @@ enum GameShopStates GAME_SHOP_MAX }; -static void game_shop_intro(); -static void game_shop_process_user_input(); -static void game_shop_outro(); +static void game_shop_intro(void); +static void game_shop_process_user_input(void); +static void game_shop_outro(void); -static const SubStateActionFn shop_state_actions[] = { - game_shop_intro, - game_shop_process_user_input, - game_shop_outro +static StateInfo shop_state_actions[] = { + STATE_INFO_UPDATE_FN_ONLY(game_shop_intro), + STATE_INFO_UPDATE_FN_ONLY(game_shop_process_user_input), + STATE_INFO_UPDATE_FN_ONLY(game_shop_outro), +}; + +static StateMachine shop_sm = { + .state_infos = &shop_state_actions[0], + .num_infos = GAME_SHOP_MAX, }; // Shop SelectionGrid @@ -132,7 +136,6 @@ static Button reroll_button = { // Shop internal variables static int timer; -static enum GameShopStates substate; static int reroll_cost = REROLL_BASE_COST; @@ -165,7 +168,9 @@ void game_shop_on_init(void) game_shop_change_background(); timer = TM_ZERO; - substate = GAME_SHOP_INTRO; + + state_machine_register(&shop_sm); + state_machine_change_state(&shop_sm, GAME_SHOP_INTRO); // The selection grid is initialized outside of bounds and moved // to trigger the selection change so the initial selection is visible @@ -245,7 +250,7 @@ static void game_shop_create_items(void) List* shop_jokers_list = get_shop_jokers_list(); list_clear(shop_jokers_list); - *shop_jokers_list = list_create(); + *shop_jokers_list = list_init(); for (int i = 0; i < MAX_SHOP_JOKERS; i++) { @@ -326,7 +331,7 @@ static void game_shop_intro() if (timer == TM_END_GAME_SHOP_INTRO) { - substate = GAME_SHOP_ACTIVE; + state_machine_change_state(&shop_sm, GAME_SHOP_ACTIVE); timer = TM_ZERO; // Reset the timer // print initial reroll cost only when the panel is in place @@ -515,7 +520,7 @@ static inline void game_shop_reroll(int* reroll_cost) } list_clear(shop_jokers_list); - *shop_jokers_list = list_create(); + *shop_jokers_list = list_init(); game_shop_create_items(); @@ -559,7 +564,7 @@ static void shop_reroll_row_on_key_transit(SelectionGrid* selection_grid, Select static void next_round_on_pressed(void) { // Go to next blind selection game state - substate = GAME_SHOP_EXIT; // Go to the outro sequence state + state_machine_change_state(&shop_sm, GAME_SHOP_EXIT); timer = TM_ZERO; reroll_cost = REROLL_BASE_COST; @@ -624,8 +629,7 @@ static void game_shop_outro() if (timer >= MENU_POP_OUT_ANIM_FRAMES) { - substate = GAME_SHOP_MAX; // Go to the next state - timer = TM_ZERO; // Reset the timer + game_change_state(GAME_STATE_BLIND_SELECT); } } @@ -681,14 +685,6 @@ void game_shop_on_update(void) { game_shop_lights_anim_frame(); } - - if (substate == GAME_SHOP_MAX) - { - game_change_state(GAME_STATE_BLIND_SELECT); - return; - } - - shop_state_actions[substate](); } void game_shop_on_exit(void) @@ -711,5 +707,7 @@ void game_shop_on_exit(void) increment_blind(BLIND_STATE_DEFEATED); // TODO: Move to game_round_end()? + state_machine_remove(&shop_sm); + save_game(); -} \ No newline at end of file +} diff --git a/source/list.c b/source/list.c index d1e75b7..4e3e63f 100644 --- a/source/list.c +++ b/source/list.c @@ -1,3 +1,9 @@ +/** + * @file list.c + * + * @brief List functions implementation. + */ + #include "list.h" #include "pool.h" @@ -29,9 +35,9 @@ static void s_list_remove_node(List* list, ListNode* node); */ static ListNode* s_list_itr_node_next(ListItr* itr); -List list_create(void) +List list_init(void) { - List list = {.head = NULL, .tail = NULL, .len = 0}; + List list = LIST_DEFAULT; return list; } @@ -306,3 +312,20 @@ void list_itr_remove_current_node(ListItr* itr) s_list_remove_node(itr->list, itr->current_node); itr->current_node = tmp_prev; } + +bool list_remove_data(List* list, void* data) +{ + ListItr itr = list_itr_create(list); + ListNode* ln; + + while ((ln = s_list_itr_node_next(&itr))) + { + if (ln->data == data) + { + s_list_remove_node(list, ln); + return true; + } + } + + return false; +} diff --git a/source/state_machine.c b/source/state_machine.c new file mode 100644 index 0000000..0286412 --- /dev/null +++ b/source/state_machine.c @@ -0,0 +1,63 @@ +#include "state_machine.h" + +#include "game.h" +#include "list.h" +#include "util.h" + +static List update_cbs = LIST_DEFAULT; + +// Used as a No Operation for game states that have no init and/or exit function. +// ricfehr3 did the work of determining whether a noop or a NULL check was more +// efficient. Well, this is the answer. +// Thanks! +// https://github.com/cellos51/balatro-gba/issues/137#issuecomment-3322485129 +void noop(void) {}; + +void state_machine_register(StateMachine* state_machine) +{ + // Always try to remove the state machine first. Only one can exist at a time + // So ensure that calling this function doesn't add two update functions + state_machine_remove(state_machine); + + state_machine->active_update = noop; + state_machine->state = UNDEFINED; + + list_push_back(&update_cbs, &state_machine->active_update); +} + +void state_machine_remove(StateMachine* state_machine) +{ + list_remove_data(&update_cbs, &state_machine->active_update); +} + +void state_machine_update(void) +{ + ListItr itr = list_itr_create(&update_cbs); + StateCallback* cb; + while ((cb = list_itr_next(&itr))) + { + (*cb)(); + } +} + +void state_machine_change_state(StateMachine* state_machine, int new_state) +{ + if (state_machine->state >= 0 && state_machine->state < state_machine->num_infos) + { + state_machine->state_infos[state_machine->state].on_exit(); + } + + if (new_state >= 0 && new_state < state_machine->num_infos) + { + state_machine->state_infos[new_state].on_init(); + + state_machine->active_update = state_machine->state_infos[new_state].on_update; + + state_machine->state = new_state; + } + else + { + state_machine->active_update = noop; + state_machine->state = UNDEFINED; + } +} diff --git a/tests/list/list_test.c b/tests/list/list_test.c index eafc13e..643b89a 100644 --- a/tests/list/list_test.c +++ b/tests/list/list_test.c @@ -5,13 +5,13 @@ #include // As simple as it gets, just needs to be initialized correctly -// - list_create +// - list_init // - list_is_empty // - list_get_len // - list_clear void create_and_clear_list(void) { - List my_cool_list = list_create(); + List my_cool_list = list_init(); // verify no data assert(my_cool_list.head == NULL); @@ -26,7 +26,7 @@ void create_and_clear_list(void) // Push back one entry, make sure it looks as expected // tests: -// - list_create +// - list_init // - list_push_back // - list_is_empty // - list_get_len @@ -34,7 +34,7 @@ void create_and_clear_list(void) // - list_clear void push_back_one_entry(void) { - List my_cool_list = list_create(); + List my_cool_list = list_init(); // verify no data assert(my_cool_list.head == NULL); @@ -69,7 +69,7 @@ void push_back_one_entry(void) } // Push front one entry, make sure it looks as expected -// - list_create +// - list_init // - list_push_front // - list_is_empty // - list_get_len @@ -77,7 +77,7 @@ void push_back_one_entry(void) // - list_clear void push_front_one_entry(void) { - List my_cool_list = list_create(); + List my_cool_list = list_init(); // verify no data assert(my_cool_list.head == NULL); @@ -123,7 +123,7 @@ void push_front_one_entry(void) // Lastly, push to the front the same 3 entries and verify the state of the list // before detroying. // -// - list_create +// - list_init // - list_push_front // - list_is_empty // - list_get_len @@ -135,7 +135,7 @@ void push_front_one_entry(void) // - list_itr_remove_node_current; void push_back_three_remove_push_front_three_entries(void) { - List my_cool_list = list_create(); + List my_cool_list = list_init(); // verify no data assert(my_cool_list.head == NULL); @@ -287,7 +287,7 @@ void push_back_three_remove_push_front_three_entries(void) } // Test inserting at head, middle, and tail of list -// - list_create +// - list_init // - list_push_back // - list_insert // - list_is_empty @@ -299,7 +299,7 @@ void push_back_three_remove_push_front_three_entries(void) // - list_get_at_idx void test_list_insertion(void) { - List my_cool_list = list_create(); + List my_cool_list = list_init(); // verify no data assert(my_cool_list.head == NULL); @@ -515,7 +515,7 @@ void test_list_insertion(void) } // Test inserting at head, middle, and tail of list -// - list_create +// - list_init // - list_push_back // - list_swap // - list_is_empty @@ -526,7 +526,7 @@ void test_list_insertion(void) // - list_get_at_idx void test_list_swap(void) { - List my_cool_list = list_create(); + List my_cool_list = list_init(); // verify no data assert(my_cool_list.head == NULL); @@ -662,6 +662,74 @@ void test_list_swap(void) } +// Test the "list_remove_data" function +// - list_init +// - list_is_empty +// - list_get_len +// - list_clear +void test_remove_data(void) +{ + // this value MUST be 5 + const int initial_list_size = 5; + const int midpoint = initial_list_size / 2; + List my_cool_list = list_init(); + + // verify no data + assert(my_cool_list.head == NULL); + assert(my_cool_list.tail == NULL); + assert(list_get_len(&my_cool_list) == 0); + assert(list_is_empty(&my_cool_list)); + + int test_data[initial_list_size]; + + for(int i = 0; i < initial_list_size; i++) + { + // 0 -> 1 -> 2 -> 3 -> 4 + test_data[i] = i; + list_push_back(&my_cool_list, &test_data[i]); + } + + assert(list_get_len(&my_cool_list) == initial_list_size); + + // remove from the front + assert(list_remove_data(&my_cool_list, &test_data[0])); + + assert(my_cool_list.head->data == &test_data[1]); + assert(list_get_len(&my_cool_list) == initial_list_size - 1); + + // remove from the back + assert(list_remove_data(&my_cool_list, &test_data[initial_list_size - 1])); + + assert(my_cool_list.head->data == &test_data[1]); + assert(my_cool_list.tail->data == &test_data[initial_list_size - 2]); + assert(list_get_len(&my_cool_list) == initial_list_size - 2); + + // remove from the middle + assert(list_remove_data(&my_cool_list, &test_data[midpoint])); + + assert(my_cool_list.head->data == &test_data[1]); + assert(my_cool_list.tail->data == &test_data[initial_list_size - 2]); + assert(list_get_len(&my_cool_list) == initial_list_size - 3); + + // fail on removal of pointer not in list + assert(!list_remove_data(&my_cool_list, NULL)); + + assert(my_cool_list.head->data == &test_data[1]); + assert(my_cool_list.tail->data == &test_data[initial_list_size - 2]); + assert(list_get_len(&my_cool_list) == initial_list_size - 3); + + // remove last two elements, make sure list can be emptied + assert(!list_is_empty(&my_cool_list)); + assert(list_remove_data(&my_cool_list, &test_data[1])); + assert(!list_is_empty(&my_cool_list)); + assert(list_remove_data(&my_cool_list, &test_data[3])); + assert(list_is_empty(&my_cool_list)); + + list_clear(&my_cool_list); + + assert(list_is_empty(&my_cool_list)); +} + int main(void) { printf("Testing List Create and Clear.\n"); @@ -682,6 +750,9 @@ int main(void) printf("Testing List Swap.\n"); test_list_swap(); + printf("Testing List Remove Data.\n"); + test_remove_data(); + printf("-------------------------------------------------------------------------------\n"); printf("List Tests Passed :)\n"); printf("-------------------------------------------------------------------------------\n");