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>
This commit is contained in:
Rickey
2026-05-22 18:06:13 -07:00
committed by GitHub
co-authored by Copilot Autofix powered by AI
parent 2329c4c681
commit 92cb9f9883
11 changed files with 428 additions and 165 deletions
+3 -6
View File
@@ -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);
+30 -5
View File
@@ -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);
+107
View File
@@ -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
+2 -2
View File
@@ -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;
+24 -53
View File
@@ -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)
+36 -29
View File
@@ -11,17 +11,19 @@
#include "layout.h"
#include "soundbank.h"
#include "sprite.h"
#include "state_machine.h"
#include "timer.h"
#include "util.h"
#include <maxmod.h>
#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)
+31 -30
View File
@@ -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?
}
+24 -26
View File
@@ -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();
}
}
+25 -2
View File
@@ -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;
}
+63
View File
@@ -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;
}
}
+83 -12
View File
@@ -5,13 +5,13 @@
#include <stdio.h>
// 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");