From 2329c4c6817bf83e414b02c94cabab21c1c679d8 Mon Sep 17 00:00:00 2001 From: Geralt <84459734+MathisMartin31@users.noreply.github.com> Date: Sat, 23 May 2026 02:51:39 +0200 Subject: [PATCH] Refactor/hand (#490) * move all hand funcs to their new file. now to clean up * WIP * Cleanup + doc hand.h * clang format * Refactor loose variables in hand.c into a proper Hand struct * Fix some more issues * Apply *some* suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * remove outdated comment * fix rebase issue * fix rng_info missing from g_game_vars init --------- Co-authored-by: MathisMartin31 Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- include/game.h | 66 --- include/game_variables.h | 3 + include/hand.h | 234 ++++++++++ include/hand_analysis.h | 42 -- source/blind.c | 1 + source/game.c | 559 ++++-------------------- source/hand.c | 909 +++++++++++++++++++++++++++++++++++++++ source/hand_analysis.c | 456 -------------------- source/joker_effects.c | 8 +- 9 files changed, 1235 insertions(+), 1043 deletions(-) create mode 100644 include/hand.h delete mode 100644 include/hand_analysis.h create mode 100644 source/hand.c delete mode 100644 source/hand_analysis.c diff --git a/include/game.h b/include/game.h index 4ba9a03..456e29a 100644 --- a/include/game.h +++ b/include/game.h @@ -8,7 +8,6 @@ #include -#define MAX_HAND_SIZE 16 #define MAX_DECK_SIZE 52 #define MAX_JOKERS_HELD_SIZE 5 // This doesn't account for negatives right now. #define MAX_SHOP_JOKERS 2 // TODO: Make this dynamic and allow for other items besides jokers @@ -54,18 +53,6 @@ enum GameState GAME_STATE_UNDEFINED }; -enum HandState -{ - HAND_DRAW, - HAND_SELECT, - // This is actually a misnomer because it's used for the deck - // but it mechanically makes sense to be a state of the hand - HAND_SHUFFLING, - HAND_DISCARD, - HAND_PLAY, - HAND_PLAYING -}; - enum PlayState { PLAY_STARTING, @@ -79,53 +66,6 @@ enum PlayState PLAY_ENDED }; -// Hand types -enum HandType -{ - NONE, - HIGH_CARD, - PAIR, - TWO_PAIR, - THREE_OF_A_KIND, - STRAIGHT, - FLUSH, - FULL_HOUSE, - FOUR_OF_A_KIND, - STRAIGHT_FLUSH, - ROYAL_FLUSH, - FIVE_OF_A_KIND, - FLUSH_HOUSE, - FLUSH_FIVE -}; - -// clang-format off -// Store all contained hands to optimize "whole hand condition" Jokers -typedef struct ContainedHandTypes -{ - union - { - struct - { - u16 HIGH_CARD : 1; - u16 PAIR : 1; - u16 TWO_PAIR : 1; - u16 THREE_OF_A_KIND : 1; - u16 STRAIGHT : 1; - u16 FLUSH : 1; - u16 FULL_HOUSE : 1; - u16 FOUR_OF_A_KIND : 1; - u16 STRAIGHT_FLUSH : 1; - u16 ROYAL_FLUSH : 1; - u16 FIVE_OF_A_KIND : 1; - u16 FLUSH_HOUSE : 1; - u16 FLUSH_FIVE : 1; - u16 : 3; - }; - u16 value; - }; -} ContainedHandTypes; -// clang-format on - // Game functions void game_init(); @@ -142,9 +82,6 @@ void game_reset(); void game_update(); void game_change_state(enum GameState new_game_state); -CardObject** get_hand_array(void); -int get_hand_top(void); -int hand_get_size(void); CardObject** get_played_array(void); int get_played_top(void); int get_scored_card_index(void); @@ -159,9 +96,6 @@ List* get_shop_jokers_list(void); Bitset* get_avail_jokers_bitset(void); void set_shop_joker_avail(int joker_id, bool avail); -ContainedHandTypes* get_contained_hands(void); -enum HandType* get_hand_type(void); - int get_deck_top(void); int get_num_discards_remaining(void); int get_num_hands_remaining(void); diff --git a/include/game_variables.h b/include/game_variables.h index eb5ff76..cd1c92b 100644 --- a/include/game_variables.h +++ b/include/game_variables.h @@ -27,6 +27,8 @@ #define MAX_HANDS 4 #define MAX_DISCARDS 4 +#define DEFAULT_HAND_SIZE 8 + /** * @brief A central location for all game variables. * @@ -45,6 +47,7 @@ typedef struct s32 round; s32 ante; s32 money; + s32 hand_size; // Blind variables diff --git a/include/hand.h b/include/hand.h new file mode 100644 index 0000000..4379880 --- /dev/null +++ b/include/hand.h @@ -0,0 +1,234 @@ +/** + * @file hand.h + * + * @brief Functions relative to manipulating and analyzing the contents of the Hand, + * a.k.a. the Cards we hold and play. + */ +#ifndef HAND_H +#define HAND_H + +#include "card.h" + +#include + +#define MAX_HAND_SIZE 16 + +enum HandState +{ + HAND_DRAW, + HAND_SELECT, + // This is actually a misnomer because it's used for the deck + // but it mechanically makes sense to be a state of the hand + HAND_SHUFFLING, + HAND_DISCARD, + HAND_PLAY, + HAND_PLAYING +}; + +enum HandType +{ + NONE, + HIGH_CARD, + PAIR, + TWO_PAIR, + THREE_OF_A_KIND, + STRAIGHT, + FLUSH, + FULL_HOUSE, + FOUR_OF_A_KIND, + STRAIGHT_FLUSH, + ROYAL_FLUSH, + FIVE_OF_A_KIND, + FLUSH_HOUSE, + FLUSH_FIVE +}; + +// clang-format off +// Store all contained hands to optimize "whole hand condition" Jokers +typedef struct ContainedHandTypes +{ + union + { + struct + { + u16 HIGH_CARD : 1; + u16 PAIR : 1; + u16 TWO_PAIR : 1; + u16 THREE_OF_A_KIND : 1; + u16 STRAIGHT : 1; + u16 FLUSH : 1; + u16 FULL_HOUSE : 1; + u16 FOUR_OF_A_KIND : 1; + u16 STRAIGHT_FLUSH : 1; + u16 ROYAL_FLUSH : 1; + u16 FIVE_OF_A_KIND : 1; + u16 FLUSH_HOUSE : 1; + u16 FLUSH_FIVE : 1; + u16 : 3; + }; + u16 value; + }; +} ContainedHandTypes; +// clang-format on + +// Hand Structure Manipulation + +/** + * @brief Set the hand state. Primarily used by the GAME_PLAYING game state. + * + * @sa HandState + */ +void set_hand_state(enum HandState); + +/** + * @brief Get the hand state + * + * @return enum HandState + * + * @sa set_hand_state + */ +enum HandState get_hand_state(void); + +/** + * @brief Determine the HandType and ContainedHandTypes of the currently selected Cards, + * then print the Hand's name, chips, and mult on screen. + */ +void compute_hand_value_info(void); + +/** + * @brief Get the current hand type + * + * @return enum HandType + * + * @sa compute_hand_value_info + */ +enum HandType get_hand_type(void); + +/** + * @brief Get the contained hands within the selected hand + * + * @return ContainedHandTypes* + * + * @sa compute_hand_value_info + */ +ContainedHandTypes* get_contained_hands(void); + +/** + * @brief Get the hand array of Cards currently held in hand + * + * @return CardObject** + */ +CardObject** get_hand_array(void); + +/** + * @brief Get the position in hand array of the last card obtained + * + * @return int + * + * @sa get_hand_array + */ +int get_hand_top(void); + +/** + * @brief Set the position in hand array of the last card obtained + * + * @param new_hand_top + * + * @sa get_hand_top + */ +void set_hand_top(int new_hand_top); + +/** + * @brief Get the current number of Cards in hand. + * + * @return `hand_top + 1` + * + * @sa get_hand_top + */ +int hand_nb_held_cards(void); + +/** + * @brief Get the current number of selected Cards. + * + * @return `card_selections` + */ +int hand_get_nb_selected_cards(void); + +/** + * @brief Set the current number of selected Cards. + * + * @param new_selections + * + * @sa hand_get_nb_selected_cards + */ +void hand_set_nb_selected_cards(int new_selections); + +/** + * @brief Set the card at a given index in Hand as selected + * + * @param index Index of card to select in hand + */ +void hand_select_card(int index); + +/** + * @brief Switch to the given sort method. Can sort playing cards in two ways: by rank and suit. + * The order of suits is as follows: + * ``` + * SPADES > HEARTS > CLUBS > DIAMONDS + * ``` + * + * @param to_sort_by_suit + */ +void hand_change_sort(bool to_sort_by_suit); + +/** + * @brief Deselect all cards in hand. + */ +void hand_deselect_all_cards(void); + +/** + * @brief Swaps the order of two cards in hand. + * + * @param idx_a index of the first card + * @param idx_b index of the second card + */ +void swap_cards_in_hand(int idx_a, int idx_b); + +/** + * @brief Destroy the sprites of the Cards held in hand and recreate then in the same + * order as the Cards in the hand array. This allows Cards to render properly + * when help in hand during the round or a Tarot/Spectral booster pack. + */ +void reorder_card_sprites_layers(void); + +/** + * @brief Sort the hand array according to the selected method to do that. + * + * @sa hand_change_sort + */ +void sort_cards(void); + +// Hand Contents Analysis + +/** + * Finds the largest flush (set of cards with the same suit) in the given array of played cards. + * Marks the cards belonging to the best flush in the out_selection array. + * + * @param played Array of pointers to CardObject representing played cards. + * @param top Index of the top of the played stack. + * @param min_len Minimum number of cards required for a flush. + * @param out_selection Output array of bools; set to true for cards in the best flush, false + * otherwise. + * @return The number of cards in the best flush found, or 0 if no flush meets min_len. + */ +int find_flush_in_played_cards(CardObject** played, int top, int min_len, bool* out_selection); +int find_straight_in_played_cards( + CardObject** played, + int top, + bool shortcut_active, + int min_len, + bool* out_selection +); +void select_paired_cards_in_hand(CardObject** played, int top, bool* selection); + +#endif diff --git a/include/hand_analysis.h b/include/hand_analysis.h deleted file mode 100644 index 78e3756..0000000 --- a/include/hand_analysis.h +++ /dev/null @@ -1,42 +0,0 @@ -#ifndef HAND_ANALYSIS_H -#define HAND_ANALYSIS_H - -#include "card.h" - -#include - -/** - * @brief Outputs the distribution of ranks and suits in the hand - * @param ranks_out output - updated such as ranks_out[rank] is the number of cards of rank in the - * hand. Must be of size NUM_RANKS. - * @param suits_out output - updated such as suits_out[suit] is the number of cards if suit in the - * hand Must be of size NUM_SUITS - */ -void get_hand_distribution(u8 ranks_out[NUM_RANKS], u8 suits_out[NUM_SUITS]); - -/** - * @brief Outputs the distribution of ranks and suits in the played stack - * @param ranks_out output - updated such as ranks_out[rank] is the number of cards of rank in the - * played stack. Must be of size NUM_RANKS. - * @param suits_out output - updated such as suits_out[suit] is the number of cards if suit in the - * played stack. Must be of size NUM_SUITS - */ -void get_played_distribution(u8 ranks_out[NUM_RANKS], u8 suits_out[NUM_SUITS]); - -u8 hand_contains_n_of_a_kind(u8* ranks); -bool hand_contains_two_pair(u8* ranks); -bool hand_contains_full_house(u8* ranks); -bool hand_contains_straight(u8* ranks); -bool hand_contains_flush(u8* suits); - -int find_flush_in_played_cards(CardObject** played, int top, int min_len, bool* out_selection); -int find_straight_in_played_cards( - CardObject** played, - int top, - bool shortcut_active, - int min_len, - bool* out_selection -); -void select_paired_cards_in_hand(CardObject** played, int top, bool* selection); - -#endif diff --git a/source/blind.c b/source/blind.c index 6451bcb..0a6410a 100644 --- a/source/blind.c +++ b/source/blind.c @@ -3,6 +3,7 @@ #include "blind_gfx.h" #include "game.h" #include "graphic_utils.h" +#include "hand.h" #include "list.h" #include "random.h" #include "stdbool.h" diff --git a/source/game.c b/source/game.c index 5a65a68..210ed17 100644 --- a/source/game.c +++ b/source/game.c @@ -17,7 +17,7 @@ #include "game/shop.h" #include "game_variables.h" #include "graphic_utils.h" -#include "hand_analysis.h" +#include "hand.h" #include "joker.h" #include "layout.h" #include "list.h" @@ -86,13 +86,6 @@ #define EXPIRE_ANIMATION_FRAME_COUNT 3 -typedef struct -{ - u32 chips; - u32 mult; - char* display_name; -} HandValues; - // 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. @@ -111,10 +104,8 @@ static void noop(void) static void game_round_on_init(void); static void game_playing_on_update(void); -static void sort_cards(void); static void display_temp_score(u32 value); static void check_flaming_score(void); -static void set_hand(void); static int deck_get_size(void); static int deck_get_max_size(void); static bool check_and_score_joker_for_event( @@ -154,12 +145,8 @@ static bool game_playing_hand_row_on_selection_changed( static int game_playing_hand_row_get_size(void); static int hand_sel_idx_to_card_idx(int selection_index); -static void hand_select_card(int index); -static void hand_toggle_sort(void); -static void hand_change_sort(bool to_sort_by_suit); -static void hand_deselect_all_cards(void); -static bool can_play_hand(void); static bool can_discard_hand(void); +static bool can_play_hand(void); // Consts @@ -195,9 +182,8 @@ static const BG_POINT SCORE_FLAME_CHIPS_POS = {1, 9}; static const BG_POINT SCORE_FLAME_MULT_POS = {5, 9}; // Rects for TTE (in pixels) -static const Rect HAND_SIZE_RECT_SELECT = {128, 128, 152, 136 }; -static const Rect HAND_SIZE_RECT_PLAYING = {128, 152, 152, 160 }; -static const Rect HAND_TYPE_RECT = {8, 64, 64, 72 }; +static const Rect HAND_SIZE_RECT_SELECT = {120, 128, 160, 136 }; +static const Rect HAND_SIZE_RECT_PLAYING = {120, 152, 160, 160 }; // Score displayed in the same place as the hand type static const Rect TEMP_SCORE_RECT = {8, 64, 64, 72 }; static const Rect SCORE_RECT = {24, 48, 64, 56 }; @@ -272,38 +258,17 @@ 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}; -static const HandValues hand_base_values[] = { - {.chips = 0, .mult = 0, .display_name = NULL }, // NONE - {.chips = 5, .mult = 1, .display_name = "Hi-Card"}, // HIGH_CARD - {.chips = 10, .mult = 2, .display_name = "Pair" }, // PAIR - {.chips = 20, .mult = 2, .display_name = "2 Pair" }, // TWO_PAIR - {.chips = 30, .mult = 3, .display_name = "3 OAK" }, // THREE_OF_A_KIND - {.chips = 30, .mult = 4, .display_name = "Strt" }, // STRAIGHT - {.chips = 35, .mult = 4, .display_name = "Flush" }, // FLUSH - {.chips = 40, .mult = 4, .display_name = "Full H" }, // FULL_HOUSE - {.chips = 60, .mult = 7, .display_name = "4 OAK" }, // FOUR_OF_A_KIND - {.chips = 100, .mult = 8, .display_name = "Strt F" }, // STRAIGHT_FLUSH - {.chips = 100, .mult = 8, .display_name = "Royal F"}, // ROYAL_FLUSH - {.chips = 120, .mult = 12, .display_name = "5 OAK" }, // FIVE_OF_A_KIND - {.chips = 140, .mult = 14, .display_name = "Flush H"}, // FLUSH_HOUSE - {.chips = 160, .mult = 16, .display_name = "Flush 5"} // FLUSH_FIVE -}; - // 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 HandState hand_state = HAND_DRAW; static enum PlayState play_state = PLAY_STARTING; -static enum HandType hand_type = NONE; -static ContainedHandTypes _contained_hands = {0}; - // Initialization of the global vars // clang-format off GameVariables g_game_vars = { - .timer = 0, - .rng_info = {0, 0}, + .timer = 0, .rng_info = {0, 0}, .round = 0, .ante = 0, .money = 0, + .hand_size = DEFAULT_HAND_SIZE, .current_blind = BLIND_TYPE_SMALL, .next_boss_blind = BLIND_TYPE_BIG, @@ -359,9 +324,7 @@ static u32 chips = 0; static u32 mult = 0; static bool retrigger = false; -static int hand_size = 8; // Default hand size is 8 static int cards_drawn = 0; -static int hand_selections = 0; // Keeping track of cards scored static int scored_card_index = 0; @@ -375,8 +338,6 @@ static ListItr _joker_scored_itr; static ListItr _joker_card_scored_end_itr; static ListItr _joker_round_end_itr; -static bool sort_by_suit = false; - static List _owned_jokers_list; static List _discarded_jokers_list; static List _expired_jokers_list; @@ -388,9 +349,6 @@ static List _shop_jokers_list; static CardObject* played[MAX_SELECTION_SIZE] = {NULL}; static int played_top = -1; -static CardObject* hand[MAX_HAND_SIZE] = {NULL}; -static int hand_top = -1; - static Card* deck[MAX_DECK_SIZE] = {NULL}; static int deck_top = -1; @@ -658,21 +616,6 @@ void game_change_state(enum GameState new_game_state) } } -CardObject** get_hand_array(void) -{ - return hand; -} - -int get_hand_top(void) -{ - return hand_top; -} - -int hand_get_size(void) -{ - return hand_top + 1; -} - CardObject** get_played_array(void) { return played; @@ -902,262 +845,6 @@ static inline void display_ante(int value) ); } -// idx_a and idx_b are assumed to be valid indexes within the hand array -// no checks will be performed here for performance's sake -static inline void swap_cards_in_hand(int idx_a, int idx_b) -{ - CardObject* temp = hand[idx_a]; - hand[idx_a] = hand[idx_b]; - hand[idx_b] = temp; -} - -static inline void sort_hand_by_suit(void) -{ - for (int idx_a = 0; idx_a < hand_top; idx_a++) - { - for (int idx_b = idx_a + 1; idx_b <= hand_top; idx_b++) - { - if (hand[idx_a] == NULL || - (hand[idx_b] != NULL && (hand[idx_a]->card->suit > hand[idx_b]->card->suit || - (hand[idx_a]->card->suit == hand[idx_b]->card->suit && - hand[idx_a]->card->rank > hand[idx_b]->card->rank)))) - { - swap_cards_in_hand(idx_a, idx_b); - } - } - } -} - -static inline void sort_hand_by_rank(void) -{ - for (int idx_a = 0; idx_a < hand_top; idx_a++) - { - for (int idx_b = idx_a + 1; idx_b <= hand_top; idx_b++) - { - if (hand[idx_a] == NULL || - (hand[idx_b] != NULL && hand[idx_a]->card->rank > hand[idx_b]->card->rank)) - { - swap_cards_in_hand(idx_a, idx_b); - } - } - } -} - -static inline bool shift_null_card_to_end(int null_card_idx) -{ - // Start by searching any non NULL cards after the NULL one - // don't start at null_card_idx+1 to avoid potential illegal array access - int non_null_card_idx = null_card_idx; - for (; non_null_card_idx <= hand_top; non_null_card_idx++) - { - if (hand[non_null_card_idx] != NULL) - { - break; - } - } - - // return false if there are no non-NULL cards left/there are no more sprites to destroy - if (non_null_card_idx > hand_top) - { - return false; - } - - // If there is one, shift it and all the cards that follow forward - // This way we close the gap and ensure the next card is not NULL - - // Iterating up to `hand_top - non_null_card_idx + 1` should end up out of bounds - // but for some reason it doesn't pose any issue, and taking out the +1 breaks - // the code, so I'll be elaving it here until someone figures it out ^^' - for (int j = 0; j <= hand_top - non_null_card_idx + 1; j++) - { - hand[null_card_idx + j] = hand[non_null_card_idx + j]; - } - - return true; -} - -static void reorder_card_sprites_layers(void) -{ - // Update the sprites in the hand by destroying them and creating new ones in the correct order - // (This feels like a diabolical solution but like literally how else would you do this) - for (int i = 0; i <= hand_top; i++) - { - // a NULL card will only happen if we rearrange the sprites without having sorted them - // before. Any NULL CardObject will be sent to the end by shifting all elements forward - if (hand[i] == NULL) - { - if (!shift_null_card_to_end(i)) - { - break; - } - } - - // card_object_get_sprite() will not work here since we need the address - sprite_destroy(&(hand[i]->sprite_object->sprite)); - } - - // Recreate the sprites for the remaining non NULL cards, in order - for (int i = 0; i <= hand_top; i++) - { - if (hand[i] != NULL) - { - // Set the sprite for the card object - card_object_set_sprite(hand[i], i); - sprite_position( - card_object_get_sprite(hand[i]), - fx2int(hand[i]->sprite_object->x), - fx2int(hand[i]->sprite_object->y) - ); - } - } -} - -static void sort_cards(void) -{ - if (sort_by_suit) - { - sort_hand_by_suit(); - } - else - { - sort_hand_by_rank(); - } - - reorder_card_sprites_layers(); -} - -static ContainedHandTypes compute_contained_hand_types(void) -{ - ContainedHandTypes hand_types = {0}; - - // Idk if this is how Balatro does it but this is how I'm doing it - if (hand_selections == 0 || hand_state == HAND_DISCARD) - { - return hand_types; - } - - hand_types.HIGH_CARD = 1; - - u8 suits[NUM_SUITS]; - u8 ranks[NUM_RANKS]; - get_hand_distribution(ranks, suits); - - // The following can be optimized better but not sure how much it matters - u8 n_of_a_kind = hand_contains_n_of_a_kind(ranks); - - // Pair and 2 Pair - if (n_of_a_kind >= 2) - { - hand_types.PAIR = 1; - - if (hand_contains_two_pair(ranks)) - { - hand_types.TWO_PAIR = 1; - } - } - - // 3 OAK - if (n_of_a_kind >= 3) - { - hand_types.THREE_OF_A_KIND = 1; - } - - // Straight - if (hand_contains_straight(ranks)) - { - hand_types.STRAIGHT = 1; - } - - // Flush - if (hand_contains_flush(suits)) - { - hand_types.FLUSH = 1; - } - - // Full House - if (n_of_a_kind >= 3 && hand_contains_full_house(ranks)) - { - hand_types.FULL_HOUSE = 1; - } - - // 4 OAK - if (n_of_a_kind >= 4) - { - hand_types.FOUR_OF_A_KIND = 1; - } - - // Straight Flush - if (hand_types.STRAIGHT && hand_types.FLUSH) - { - hand_types.STRAIGHT_FLUSH = 1; - } - - // Royal Flush - if (hand_types.STRAIGHT_FLUSH) - { - if (ranks[TEN] && ranks[JACK] && ranks[QUEEN] && ranks[KING] && ranks[ACE]) - { - hand_types.ROYAL_FLUSH = 1; - } - } - - // 5 OAK - if (n_of_a_kind >= 5) - { - hand_types.FIVE_OF_A_KIND = 1; - } - - // Flush House and Five - if (hand_types.FLUSH) - { - if (hand_types.FULL_HOUSE) - { - hand_types.FLUSH_HOUSE = 1; - } - - if (hand_types.FIVE_OF_A_KIND) - { - hand_types.FLUSH_FIVE = 1; - } - } - - return hand_types; -} - -ContainedHandTypes* get_contained_hands(void) -{ - return &_contained_hands; -} - -enum HandType compute_hand_type(struct ContainedHandTypes contained_types) -{ - enum HandType ret; - - // test each pit see if it's set to 1, and return the first one - for (ret = FLUSH_FIVE; ret > NONE; ret--) - { - // Shift the bit we want to check to the front and mask it with 1 to keep only that - // Since the ContainedHandTypes is ordered the same way as the HandType enum, we - // can shift right by ret-1 to have the bit we want at the front - if ((contained_types.value >> (ret - 1)) & 0x1) - { - break; - } - } - - // If we broke early, ret contains the value of the HandType enum corresponding to - // the position of the highest bit set to 1 in contained_types.value, which is the - // most powerful poker hand contained in the current Hand - // If not, then it contains NONE, which is what we're supposed to return when there - // are no Hands contained in what we played - return ret; -} - -enum HandType* get_hand_type(void) -{ - return &hand_type; -} - // Returns true if the card is *considered* a face card bool card_is_face(Card* card) { @@ -1375,43 +1062,6 @@ void display_discards(void) ); } -static void print_hand_type(const char* hand_type_str) -{ - if (hand_type_str == NULL) - return; // NULL-checking paranoia - - Rect hand_type_rect = HAND_TYPE_RECT; - update_text_rect_to_center_str(&hand_type_rect, hand_type_str, SCREEN_LEFT); - tte_printf( - "#{P:%d,%d; cx:0x%X000}%s", - hand_type_rect.left, - hand_type_rect.top, - TTE_WHITE_PB, - hand_type_str - ); -} - -static void set_hand(void) -{ - tte_erase_rect_wrapper(HAND_TYPE_RECT); - _contained_hands = compute_contained_hand_types(); - hand_type = compute_hand_type(_contained_hands); - - HandValues hand = hand_base_values[hand_type]; - - chips = hand.chips; - mult = hand.mult; - - print_hand_type(hand.display_name); - display_chips(); - display_mult(); -} - -static bool can_discard_hand(void) -{ - return (g_game_vars.discards > 0 && hand_state == HAND_SELECT && hand_selections > 0); -} - static int deck_get_size(void) { return deck_top + 1; @@ -1420,7 +1070,7 @@ static int deck_get_size(void) static int deck_get_max_size(void) { // This is the max amount of cards that the player currently has in their possession - return hand_top + played_top + deck_top + discard_top + 4; + return get_hand_top() + played_top + deck_top + discard_top + 4; } static inline void deck_shuffle(void) @@ -1436,9 +1086,9 @@ static inline void deck_shuffle(void) static void game_round_on_init(void) { - hand_state = HAND_DRAW; + set_hand_state(HAND_DRAW); + hand_set_nb_selected_cards(0); cards_drawn = 0; - hand_selections = 0; sprite_destroy(&g_game_vars.playing_blind_token); g_game_vars.playing_blind_token = blind_token_new( @@ -1504,6 +1154,14 @@ static void game_round_on_init(void) } // Playing state functions +static bool can_discard_hand(void) +{ + return ( + g_game_vars.discards > 0 && get_hand_state() == HAND_SELECT && + hand_get_nb_selected_cards() > 0 + ); +} + static void game_playing_discard_on_pressed(void) { if (!can_discard_hand()) @@ -1520,10 +1178,10 @@ static void game_playing_execute_discard(void) if (!can_discard_hand()) return; - hand_state = HAND_DISCARD; + set_hand_state(HAND_DISCARD); --g_game_vars.discards; display_discards(); - set_hand(); + compute_hand_value_info(); } static void game_playing_sort_by_rank_on_pressed(void) @@ -1536,40 +1194,6 @@ static void game_playing_sort_by_suit_on_pressed(void) hand_change_sort(true); } -static void hand_deselect_all_cards(void) -{ - bool any_cards_deselected = false; - for (int i = 0; i <= get_hand_top(); i++) - { - if (card_object_is_selected(hand[i])) - { - card_object_set_selected(hand[i], false); - hand_selections--; - any_cards_deselected = true; - } - } - - if (any_cards_deselected) - { - play_sfx(SFX_CARD_DESELECT, MM_BASE_PITCH_RATE, SFX_DEFAULT_VOLUME); - } -} - -static inline void hand_toggle_sort(void) -{ - sort_by_suit = !sort_by_suit; - sort_cards(); -} - -static inline void hand_change_sort(bool to_sort_by_suit) -{ - if (to_sort_by_suit != sort_by_suit) - { - sort_by_suit = to_sort_by_suit; - sort_cards(); - } -} - static void game_playing_play_hand_on_pressed(void) { if (!can_play_hand()) @@ -1586,14 +1210,14 @@ static void game_playing_execute_play_hand(void) if (!can_play_hand()) return; - hand_state = HAND_PLAY; + set_hand_state(HAND_PLAY); --g_game_vars.hands; display_hands(); } static int game_playing_hand_row_get_size(void) { - return hand_get_size(); + return hand_nb_held_cards(); } // card moving logic @@ -1712,7 +1336,7 @@ static void game_playing_hand_row_on_key_transit( else if (key_hit(DESELECT_CARDS)) { hand_deselect_all_cards(); - set_hand(); + compute_hand_value_info(); } else if (key_hit(PLAY_HAND_KEY)) { @@ -1769,7 +1393,7 @@ static void game_playing_button_row_on_key_hit(SelectionGrid* selection_grid, Se static bool can_play_hand(void) { - if (hand_state != HAND_SELECT || hand_selections == 0) + if (get_hand_state() != HAND_SELECT || hand_get_nb_selected_cards() == 0) return false; return true; } @@ -1785,27 +1409,7 @@ static inline int hand_sel_idx_to_card_idx(int selection_index) // This is because the hand is drawn from right to left. // There is no particular reason for why that was done, it's just how it was done. // Maybe one day it can be reverted and made consistent so this conversion is not needed. - return hand_get_size() - selection_index - 1; -} - -static void hand_select_card(int index) -{ - if (index < 0 || index >= hand_get_size() || hand_state != HAND_SELECT || hand[index] == NULL) - return; - - if (card_object_is_selected(hand[index])) - { - card_object_set_selected(hand[index], false); - hand_selections--; - play_sfx(SFX_CARD_DESELECT, MM_BASE_PITCH_RATE, SFX_DEFAULT_VOLUME); - } - else if (hand_selections < MAX_SELECTION_SIZE) - { - card_object_set_selected(hand[index], true); - hand_selections++; - play_sfx(SFX_CARD_SELECT, MM_BASE_PITCH_RATE, SFX_DEFAULT_VOLUME); - } - set_hand(); + return hand_nb_held_cards() - selection_index - 1; } static inline void game_playing_process_hand_select_input(void) @@ -1815,7 +1419,8 @@ static inline void game_playing_process_hand_select_input(void) static inline void card_draw(void) { - if (deck_top < 0 || hand_top >= hand_size - 1 || hand_top >= MAX_HAND_SIZE - 1) + if (deck_top < 0 || get_hand_top() >= g_game_vars.hand_size - 1 || + get_hand_top() >= MAX_HAND_SIZE - 1) return; CardObject* card_object = card_object_new(deck_pop()); @@ -1827,7 +1432,8 @@ static inline void card_draw(void) card_object->sprite_object->y = deck_y; sprite_position(card_object->sprite_object->sprite, fx2int(deck_x), fx2int(deck_y)); - hand[++hand_top] = card_object; + set_hand_top(get_hand_top() + 1); + get_hand_array()[get_hand_top()] = card_object; // Sort the hand after drawing a card sort_cards(); @@ -1875,14 +1481,16 @@ static inline void card_in_hand_loop_handle_discard_and_shuffling( bool* break_loop ) { - if (hand_state != HAND_DISCARD && hand_state != HAND_SHUFFLING) + if (get_hand_state() != HAND_DISCARD && get_hand_state() != HAND_SHUFFLING) { // Assumes hand_state is one of these return; } + CardObject** hand = get_hand_array(); + *break_loop = false; - if (card_object_is_selected(hand[card_idx]) || hand_state == HAND_SHUFFLING) + if (card_object_is_selected(hand[card_idx]) || get_hand_state() == HAND_SHUFFLING) { if (!discarded_card) { @@ -1905,7 +1513,7 @@ static inline void card_in_hand_loop_handle_discard_and_shuffling( card_object_destroy(&hand[card_idx]); reorder_card_sprites_layers(); - hand_top--; + set_hand_top(get_hand_top() - 1); // This technically isn't drawing cards, I'm just reusing the variable cards_drawn++; sound_played = false; @@ -1919,7 +1527,7 @@ static inline void card_in_hand_loop_handle_discard_and_shuffling( } else { - if (hand_state == HAND_DISCARD) + if (get_hand_state() == HAND_DISCARD) { // Don't raise the card if we're mass discarding, it looks stupid. *hand_y -= int2fx(15); @@ -1928,23 +1536,24 @@ static inline void card_in_hand_loop_handle_discard_and_shuffling( { *hand_y += int2fx(24); } - *hand_x = - *hand_x + (int2fx(card_idx) - int2fx(hand_top) / 2) * -HAND_SPACING_LUT[hand_top]; + *hand_x = *hand_x + (int2fx(card_idx) - int2fx(get_hand_top()) / 2) * + -HAND_SPACING_LUT[get_hand_top()]; } } else { - *hand_x = *hand_x + (int2fx(card_idx) - int2fx(hand_top) / 2) * -HAND_SPACING_LUT[hand_top]; + *hand_x = *hand_x + (int2fx(card_idx) - int2fx(get_hand_top()) / 2) * + -HAND_SPACING_LUT[get_hand_top()]; } if (card_idx == 0 && discarded_card == false && g_game_vars.timer % FRAMES(10) == 0) { // This is never reached in the case of HAND_SHUFFLING. Not sure why but that's how it's // supposed to be. - hand_state = HAND_DRAW; + set_hand_state(HAND_DRAW); sound_played = false; cards_drawn = 0; - hand_selections = 0; + hand_set_nb_selected_cards(0); g_game_vars.timer = TM_ZERO; *break_loop = true; return; @@ -1960,7 +1569,8 @@ static inline void select_flush_and_straight_cards_in_played_hand(void) int min_len = get_straight_and_flush_size(); // if we have a flush in our hand - if (hand_type == FLUSH || hand_type == STRAIGHT_FLUSH || hand_type == ROYAL_FLUSH) + if (get_hand_type() == FLUSH || get_hand_type() == STRAIGHT_FLUSH || + get_hand_type() == ROYAL_FLUSH) { bool flush_selection[MAX_HAND_SIZE] = {false}; find_flush_in_played_cards(played, played_top, min_len, flush_selection); @@ -1972,7 +1582,8 @@ static inline void select_flush_and_straight_cards_in_played_hand(void) } // If we have a straight in our hand - if (hand_type == STRAIGHT || hand_type == STRAIGHT_FLUSH || hand_type == ROYAL_FLUSH) + if (get_hand_type() == STRAIGHT || get_hand_type() == STRAIGHT_FLUSH || + get_hand_type() == ROYAL_FLUSH) { bool straight_selection[MAX_HAND_SIZE] = {false}; find_straight_in_played_cards( @@ -2207,16 +1818,16 @@ static bool play_ended_played_cards_update(int played_idx) { if (game_round_is_over()) { - hand_state = HAND_SHUFFLING; + set_hand_state(HAND_SHUFFLING); } else { - hand_state = HAND_DRAW; + set_hand_state(HAND_DRAW); } play_state = PLAY_STARTING; cards_drawn = 0; - hand_selections = 0; + hand_set_nb_selected_cards(0); played_top = -1; // Reset the played stack scored_card_index = 0; _joker_scored_itr = list_itr_create(&_owned_jokers_list); @@ -2293,7 +1904,7 @@ static inline bool play_scoring_cards_update(void) { // reuse these variables for held cards _joker_scored_itr = list_itr_create(&_owned_jokers_list); - scored_card_index = hand_top; + scored_card_index = get_hand_top(); play_state = PLAY_SCORING_HELD_CARDS; @@ -2393,6 +2004,8 @@ static inline bool play_scoring_held_cards_update(int played_idx) { tte_erase_rect_wrapper(HELD_CARDS_SCORES_RECT); + CardObject** hand = get_hand_array(); + // Go through all held cards and see if they activate Jokers for (; scored_card_index >= 0; scored_card_index--) { @@ -2594,14 +2207,9 @@ static inline void played_cards_update_loop(void) } } -static inline int hand_get_max_size(void) -{ - return hand_size; -} - static inline void game_playing_process_input_and_state(void) { - if (hand_state == HAND_SELECT) + if (get_hand_state() == HAND_SELECT) { game_playing_process_hand_select_input(); } @@ -2666,7 +2274,7 @@ static inline void game_playing_process_input_and_state(void) static inline void game_playing_process_card_draw() { - if (hand_state == HAND_DRAW && cards_drawn < hand_size) + if (get_hand_state() == HAND_DRAW && cards_drawn < g_game_vars.hand_size) { if (g_game_vars.timer % FRAMES(10) == 0) // Draw a card every 10 frames { @@ -2674,9 +2282,9 @@ static inline void game_playing_process_card_draw() card_draw(); } } - else if (hand_state == HAND_DRAW) + else if (get_hand_state() == HAND_DRAW) { - hand_state = HAND_SELECT; // Change the hand state to select after drawing all the cards + set_hand_state(HAND_SELECT); // Change the hand state to select after drawing all the cards cards_drawn = 0; g_game_vars.timer = TM_ZERO; } @@ -2685,7 +2293,7 @@ static inline void game_playing_process_card_draw() static inline void game_playing_discarded_cards_loop(void) { // Discarded cards loop (mainly for shuffling) - if (hand_get_size() == 0 && hand_state == HAND_SHUFFLING && discard_top >= -1 && + if (hand_nb_held_cards() == 0 && get_hand_state() == HAND_SHUFFLING && discard_top >= -1 && g_game_vars.timer > FRAMES(10)) { // Change the background to the round end background. This is how it works in Balatro, so @@ -2740,7 +2348,7 @@ static inline void game_playing_discarded_cards_loop(void) static inline void select_cards_in_played_hand() { - switch (hand_type) // select the cards that apply to the hand type + switch (get_hand_type()) // select the cards that apply to the hand type { case NONE: break; @@ -2786,18 +2394,20 @@ static inline void cards_in_hand_update_loop(void) // TODO: Break this function up into smaller ones, Gods be good // Start from the end of the hand and work backwards because that's how Balatro does it - for (int i = hand_top + 1; i >= 0; i--) + CardObject** hand = get_hand_array(); + + for (int i = get_hand_top(); i >= 0; i--) { if (hand[i] != NULL) { FIXED hand_x = int2fx(HAND_START_POS.x); FIXED hand_y = int2fx(HAND_START_POS.y); - switch (hand_state) + switch (get_hand_state()) { case HAND_DRAW: - hand_x = - hand_x + (int2fx(i) - int2fx(hand_top) / 2) * -HAND_SPACING_LUT[hand_top]; + hand_x = hand_x + (int2fx(i) - int2fx(get_hand_top()) / 2) * + -HAND_SPACING_LUT[get_hand_top()]; break; case HAND_SELECT: bool is_focused = @@ -2830,10 +2440,10 @@ static inline void cards_in_hand_update_loop(void) hand[i]->sprite_object->vy = 0; } - hand_x = - hand_x + (int2fx(i) - int2fx(hand_top) / 2) * - -HAND_SPACING_LUT[hand_top]; // TODO: Change this later to - // reference a 2D LUT of positions + hand_x = hand_x + (int2fx(i) - int2fx(get_hand_top()) / 2) * + -HAND_SPACING_LUT[get_hand_top()]; // TODO: Change this + // later to reference a + // 2D LUT of positions break; case HAND_SHUFFLING: /* FALL THROUGH */ @@ -2850,8 +2460,8 @@ static inline void cards_in_hand_update_loop(void) break; case HAND_PLAY: - hand_x = - hand_x + (int2fx(i) - int2fx(hand_top) / 2) * -HAND_SPACING_LUT[hand_top]; + hand_x = hand_x + (int2fx(i) - int2fx(get_hand_top()) / 2) * + -HAND_SPACING_LUT[get_hand_top()]; hand_y += int2fx(24); if (card_object_is_selected(hand[i]) && discarded_card == false && @@ -2869,8 +2479,8 @@ static inline void cards_in_hand_update_loop(void) SFX_DEFAULT_VOLUME ); - hand_top--; - hand_selections--; + set_hand_top(get_hand_top() - 1); + hand_set_nb_selected_cards(hand_get_nb_selected_cards() - 1); cards_drawn++; discarded_card = true; @@ -2878,9 +2488,9 @@ static inline void cards_in_hand_update_loop(void) if (i == 0 && discarded_card == false && g_game_vars.timer % FRAMES(10) == 0) { - hand_state = HAND_PLAYING; + set_hand_state(HAND_PLAYING); cards_drawn = 0; - hand_selections = 0; + hand_set_nb_selected_cards(0); g_game_vars.timer = TM_ZERO; scored_card_index = played_top + 1; @@ -2890,8 +2500,8 @@ static inline void cards_in_hand_update_loop(void) break; // Don't need to do anything here, just wait for the player to select cards case HAND_PLAYING: - hand_x = - hand_x + (int2fx(i) - int2fx(hand_top) / 2) * -HAND_SPACING_LUT[hand_top]; + hand_x = hand_x + (int2fx(i) - int2fx(get_hand_top()) / 2) * + -HAND_SPACING_LUT[get_hand_top()]; hand_y += int2fx(24); break; } @@ -2908,30 +2518,30 @@ static inline void game_playing_ui_text_update(void) static int last_hand_size = 0; static int last_deck_size = 0; - if (last_hand_size != hand_get_size() || last_deck_size != deck_get_size()) + if (last_hand_size != hand_nb_held_cards() || last_deck_size != deck_get_size()) { if (background_legacy == BG_CARD_SELECTING) { // Hand size/max size tte_printf( - "#{P:%d,%d; cx:0x%X000}%d/%d", + "#{P:%d,%d; cx:0x%X000}%2d/%-2ld", HAND_SIZE_RECT_SELECT.left, HAND_SIZE_RECT_SELECT.top, TTE_WHITE_PB, - hand_get_size(), - hand_get_max_size() + hand_nb_held_cards(), + g_game_vars.hand_size ); } else if (background_legacy == BG_CARD_PLAYING) { // Hand size/max size tte_printf( - "#{P:%d,%d; cx:0x%X000}%d/%d", + "#{P:%d,%d; cx:0x%X000}%2d/%-2ld", HAND_SIZE_RECT_PLAYING.left, HAND_SIZE_RECT_PLAYING.top, TTE_WHITE_PB, - hand_get_size(), - hand_get_max_size() + hand_nb_held_cards(), + g_game_vars.hand_size ); } @@ -2948,7 +2558,7 @@ static inline void game_playing_ui_text_update(void) deck_get_max_size() ); - last_hand_size = hand_get_size(); + last_hand_size = hand_nb_held_cards(); last_deck_size = deck_get_size(); } } @@ -2980,11 +2590,12 @@ static inline void game_playing_process_flaming_score(void) static void game_playing_on_update(void) { // Background logic (thissss might be moved to the card'ssss logic later. I'm a sssssnake) - if (hand_state == HAND_DRAW || hand_state == HAND_DISCARD || hand_state == HAND_SELECT) + if (get_hand_state() == HAND_DRAW || get_hand_state() == HAND_DISCARD || + get_hand_state() == HAND_SELECT) { change_background(BG_CARD_SELECTING, false); } - else if (hand_state != HAND_SHUFFLING) + else if (get_hand_state() != HAND_SHUFFLING) { change_background(BG_CARD_PLAYING, false); } diff --git a/source/hand.c b/source/hand.c new file mode 100644 index 0000000..54538df --- /dev/null +++ b/source/hand.c @@ -0,0 +1,909 @@ +/** + * @file hand.c + * + * @brief Implementation of functions relative to manipulating and analyzing the + * contents of the Hand. + */ +#include "hand.h" + +#include "audio_utils.h" +#include "card.h" +#include "game.h" +#include "game_variables.h" +#include "graphic_utils.h" +#include "soundbank.h" +#include "util.h" + +#include + +typedef struct +{ + u32 chips; + u32 mult; + char* display_name; +} HandValues; + +static const HandValues hand_base_values[] = { + {.chips = 0, .mult = 0, .display_name = NULL }, // NONE + {.chips = 5, .mult = 1, .display_name = "Hi-Card"}, // HIGH_CARD + {.chips = 10, .mult = 2, .display_name = "Pair" }, // PAIR + {.chips = 20, .mult = 2, .display_name = "2 Pair" }, // TWO_PAIR + {.chips = 30, .mult = 3, .display_name = "3 OAK" }, // THREE_OF_A_KIND + {.chips = 30, .mult = 4, .display_name = "Strt" }, // STRAIGHT + {.chips = 35, .mult = 4, .display_name = "Flush" }, // FLUSH + {.chips = 40, .mult = 4, .display_name = "Full H" }, // FULL_HOUSE + {.chips = 60, .mult = 7, .display_name = "4 OAK" }, // FOUR_OF_A_KIND + {.chips = 100, .mult = 8, .display_name = "Strt F" }, // STRAIGHT_FLUSH + {.chips = 100, .mult = 8, .display_name = "Royal F"}, // ROYAL_FLUSH + {.chips = 120, .mult = 12, .display_name = "5 OAK" }, // FIVE_OF_A_KIND + {.chips = 140, .mult = 14, .display_name = "Flush H"}, // FLUSH_HOUSE + {.chips = 160, .mult = 16, .display_name = "Flush 5"} // FLUSH_FIVE +}; + +// clang-format off +// Rects for TTE (in pixels) left top right bottom +static const Rect HAND_TYPE_RECT = {8, 64, 64, 72}; +// clang-format on + +typedef struct Hand +{ + // Hand stack + CardObject* cards[MAX_HAND_SIZE]; + s32 hand_top; // Position of the last card in hand array, -1 when no card in hand + s32 hand_selections; // Number of selected Cards. + + // Hand Type + enum HandType hand_type; + ContainedHandTypes contained_hands; + + enum HandState state; + bool sort_by_suit; +} Hand; + +static Hand hand = { + .cards = {NULL}, + .hand_top = -1, + .hand_selections = 0, + .hand_type = NONE, + .contained_hands = {{{0}}}, + .state = HAND_DRAW, + .sort_by_suit = false +}; + +// Forward declarations +static ContainedHandTypes compute_contained_hand_types(void); +static enum HandType compute_hand_type(struct ContainedHandTypes contained_types); + +// Hand Struct Manipulation + +enum HandState get_hand_state(void) +{ + return hand.state; +} + +void set_hand_state(enum HandState new_hand_state) +{ + hand.state = new_hand_state; +} + +CardObject** get_hand_array(void) +{ + return hand.cards; +} + +int get_hand_top(void) +{ + return hand.hand_top; +} + +void set_hand_top(int new_hand_top) +{ + hand.hand_top = new_hand_top; +} + +int hand_nb_held_cards(void) +{ + return hand.hand_top + 1; +} + +int hand_get_nb_selected_cards(void) +{ + return hand.hand_selections; +} + +void hand_set_nb_selected_cards(int new_selections) +{ + hand.hand_selections = new_selections; +} + +enum HandType get_hand_type(void) +{ + return hand.hand_type; +} + +ContainedHandTypes* get_contained_hands(void) +{ + return &hand.contained_hands; +} + +static void print_hand_type(const char* hand_type_str) +{ + if (hand_type_str == NULL) + return; // NULL-checking paranoia + + Rect hand_type_rect = HAND_TYPE_RECT; + update_text_rect_to_center_str(&hand_type_rect, hand_type_str, SCREEN_LEFT); + tte_printf( + "#{P:%d,%d; cx:0x%X000}%s", + hand_type_rect.left, + hand_type_rect.top, + TTE_WHITE_PB, + hand_type_str + ); +} + +void compute_hand_value_info(void) +{ + tte_erase_rect_wrapper(HAND_TYPE_RECT); + hand.contained_hands = compute_contained_hand_types(); + hand.hand_type = compute_hand_type(hand.contained_hands); + + HandValues hand_values = hand_base_values[hand.hand_type]; + + set_chips(hand_values.chips); + set_mult(hand_values.mult); + + print_hand_type(hand_values.display_name); + display_chips(); + display_mult(); +} + +// idx_a and idx_b are assumed to be valid indexes within the hand array +// no checks will be performed here for performance's sake +void swap_cards_in_hand(int idx_a, int idx_b) +{ + CardObject* temp = hand.cards[idx_a]; + hand.cards[idx_a] = hand.cards[idx_b]; + hand.cards[idx_b] = temp; +} + +static inline void sort_hand_by_suit(void) +{ + for (int idx_a = 0; idx_a < hand.hand_top; idx_a++) + { + for (int idx_b = idx_a + 1; idx_b <= hand.hand_top; idx_b++) + { + if (hand.cards[idx_a] == NULL || + (hand.cards[idx_b] != NULL && + (hand.cards[idx_a]->card->suit > hand.cards[idx_b]->card->suit || + (hand.cards[idx_a]->card->suit == hand.cards[idx_b]->card->suit && + hand.cards[idx_a]->card->rank > hand.cards[idx_b]->card->rank)))) + { + swap_cards_in_hand(idx_a, idx_b); + } + } + } +} + +static inline void sort_hand_by_rank(void) +{ + for (int idx_a = 0; idx_a < hand.hand_top; idx_a++) + { + for (int idx_b = idx_a + 1; idx_b <= hand.hand_top; idx_b++) + { + if (hand.cards[idx_a] == NULL || + (hand.cards[idx_b] != NULL && + hand.cards[idx_a]->card->rank > hand.cards[idx_b]->card->rank)) + { + swap_cards_in_hand(idx_a, idx_b); + } + } + } +} + +static inline bool shift_null_card_to_end(int null_card_idx) +{ + // Start by searching any non NULL cards after the NULL one + // don't start at null_card_idx+1 to avoid potential illegal array access + int non_null_card_idx = null_card_idx; + for (; non_null_card_idx <= hand.hand_top; non_null_card_idx++) + { + if (hand.cards[non_null_card_idx] != NULL) + { + break; + } + } + + // return false if there are no non-NULL cards left/there are no more sprites to destroy + if (non_null_card_idx > hand.hand_top) + { + return false; + } + + // If there is one, shift it and all the cards that follow forward + // This way we close the gap and ensure the next card is not NULL + for (int j = 0; j <= hand.hand_top - non_null_card_idx; j++) + { + hand.cards[null_card_idx + j] = hand.cards[non_null_card_idx + j]; + } + + return true; +} + +void reorder_card_sprites_layers(void) +{ + // Update the sprites in the hand by destroying them and creating new ones in the correct order + // (This feels like a diabolical solution but like literally how else would you do this) + for (int i = 0; i <= hand.hand_top; i++) + { + // a NULL card will only happen if we rearrange the sprites without having sorted them + // before. Any NULL CardObject will be sent to the end by shifting all elements forward + if (hand.cards[i] == NULL) + { + if (!shift_null_card_to_end(i)) + { + break; + } + } + + // card_object_get_sprite() will not work here since we need the address + sprite_destroy(&(hand.cards[i]->sprite_object->sprite)); + } + + // Recreate the sprites for the remaining non NULL cards, in order + for (int i = 0; i <= hand.hand_top; i++) + { + if (hand.cards[i] != NULL) + { + // Set the sprite for the card object + card_object_set_sprite(hand.cards[i], i); + sprite_position( + card_object_get_sprite(hand.cards[i]), + fx2int(hand.cards[i]->sprite_object->x), + fx2int(hand.cards[i]->sprite_object->y) + ); + } + } +} + +void sort_cards(void) +{ + if (hand.sort_by_suit) + { + sort_hand_by_suit(); + } + else + { + sort_hand_by_rank(); + } + + reorder_card_sprites_layers(); +} + +void hand_change_sort(bool to_sort_by_suit) +{ + if (to_sort_by_suit != hand.sort_by_suit) + { + hand.sort_by_suit = to_sort_by_suit; + sort_cards(); + } +} + +void hand_select_card(int index) +{ + if (index < 0 || index >= hand_nb_held_cards() || hand.state != HAND_SELECT || + hand.cards[index] == NULL) + return; + + if (card_object_is_selected(hand.cards[index])) + { + card_object_set_selected(hand.cards[index], false); + hand.hand_selections--; + play_sfx(SFX_CARD_DESELECT, MM_BASE_PITCH_RATE, SFX_DEFAULT_VOLUME); + } + else if (hand.hand_selections < MAX_SELECTION_SIZE) + { + card_object_set_selected(hand.cards[index], true); + hand.hand_selections++; + play_sfx(SFX_CARD_SELECT, MM_BASE_PITCH_RATE, SFX_DEFAULT_VOLUME); + } + compute_hand_value_info(); +} + +void hand_deselect_all_cards(void) +{ + bool any_cards_deselected = false; + for (int i = 0; i <= hand.hand_top; i++) + { + if (card_object_is_selected(hand.cards[i])) + { + card_object_set_selected(hand.cards[i], false); + hand.hand_selections--; + any_cards_deselected = true; + } + } + + if (any_cards_deselected) + { + play_sfx(SFX_CARD_DESELECT, MM_BASE_PITCH_RATE, SFX_DEFAULT_VOLUME); + } +} + +// Hand Analysis + +/** + * @brief Outputs the distribution of ranks and suits in the hand + * @param ranks_out output - updated such as ranks_out[rank] is the number of cards of rank in the + * hand. Must be of size NUM_RANKS. + * @param suits_out output - updated such as suits_out[suit] is the number of cards if suit in the + * hand Must be of size NUM_SUITS + */ +static void get_hand_distribution(u8 ranks_out[NUM_RANKS], u8 suits_out[NUM_SUITS]) +{ + for (int i = 0; i < NUM_RANKS; i++) + ranks_out[i] = 0; + for (int i = 0; i < NUM_SUITS; i++) + suits_out[i] = 0; + + int top = hand.hand_top; + for (int i = 0; i <= top; i++) + { + if (hand.cards[i] && card_object_is_selected(hand.cards[i])) + { + ranks_out[hand.cards[i]->card->rank]++; + suits_out[hand.cards[i]->card->suit]++; + } + } +} + +/** + * @brief Outputs the distribution of ranks and suits in the played stack + * @param ranks_out output - updated such as ranks_out[rank] is the number of cards of rank in the + * played stack. Must be of size NUM_RANKS. + * @param suits_out output - updated such as suits_out[suit] is the number of cards if suit in the + * played stack. Must be of size NUM_SUITS + */ +GBAL_UNUSED +static void get_played_distribution(u8 ranks_out[NUM_RANKS], u8 suits_out[NUM_SUITS]) +{ + for (int i = 0; i < NUM_RANKS; i++) + ranks_out[i] = 0; + for (int i = 0; i < NUM_SUITS; i++) + suits_out[i] = 0; + + CardObject** played = get_played_array(); + int top = get_played_top(); + for (int i = 0; i <= top; i++) + { + /* The difference from get_hand_distribution() (not checking if card is selected) + * is in line Balatro behavior, + * see https://github.com/GBALATRO/balatro-gba/issues/341#issuecomment-3691363488 + */ + if (!played[i]) + continue; + ranks_out[played[i]->card->rank]++; + suits_out[played[i]->card->suit]++; + } +} + +// Returns the highest N of a kind. So a full-house would return 3. +static u8 hand_contains_n_of_a_kind(u8* ranks) +{ + u8 highest_n = 0; + for (int i = 0; i < NUM_RANKS; i++) + { + if (ranks[i] > highest_n) + highest_n = ranks[i]; + } + return highest_n; +} + +static bool hand_contains_two_pair(u8* ranks) +{ + bool contains_other_pair = false; + for (int i = 0; i < NUM_RANKS; i++) + { + if (ranks[i] >= 2) + { + if (contains_other_pair) + return true; + contains_other_pair = true; + } + } + return false; +} + +static bool hand_contains_full_house(u8* ranks) +{ + int count_three = 0; + int count_pair = 0; + for (int i = 0; i < NUM_RANKS; i++) + { + if (ranks[i] >= 3) + { + count_three++; + } + else if (ranks[i] >= 2) + { + count_pair++; + } + } + // Full house if there is: + // - at least one three-of-a-kind and at least one other pair, + // - OR at least two three-of-a-kinds (second "three" acts as pair). + // This accounts for hands with 6 or more cards even though + // they are currently not possible and probably never will be. + return (count_three >= 2 || (count_three && count_pair)); +} + +// This is mostly from Google Gemini +static bool hand_contains_straight(u8* ranks) +{ + if (!is_shortcut_joker_active()) + { + int straight_size = get_straight_and_flush_size(); + // This is the regular case of detecting straights + int run = 0; + for (int i = 0; i < NUM_RANKS; ++i) + { + if (ranks[i]) + { + if (++run >= straight_size) + return true; + } + else + { + run = 0; + } + } + + // Check for ace low straight + if (straight_size >= 2 && ranks[ACE]) + { + // With A as low, the highest rank you can use is FIVE. + // -1 for inclusive integer distance and another -1 for the Ace e.g. need=5 -> need 2..5 + int last_needed = TWO + (straight_size - 2); + if (last_needed <= FIVE) + { + bool ok = true; + for (int r = TWO; r <= last_needed; ++r) + { + if (!ranks[r]) + { + ok = false; + break; + } + } + if (ok) + return true; + } + } + + return false; + } + else + { + // Shortcut Joker is active, we have to detect straights where any card may "skip" 1 rank + // We do this with a dynamic programming algorithm that calculates + // the longest possible straight that can end on each rank + // and stopping when we find one that is {straight-size} cards long + u8 longest_short_cut_at[NUM_RANKS] = {0}; + + // A low ace can start a sequence. 'ace_low_len' is 1 if an ace is present, + // acting as a potential predecessor for TWO and THREE. + int ace_low_len = ranks[ACE] ? 1 : 0; + + // Iterate through all ranks from TWO up to ACE. + for (int i = 0; i < NUM_RANKS; i++) + { + // No cards in this rank, no straight can end here, continue + if (ranks[i] == 0) + { + longest_short_cut_at[i] = 0; + continue; + } + + int prev_len1 = 0; + int prev_len2 = 0; + + // This logic handles the special connections for ace-low straights. + if (i == TWO) + { + // A TWO can be preceded by a low ACE (no skip). + prev_len1 = ace_low_len; + } + else if (i == THREE) + { + // A THREE can be preceded by a TWO (no skip) or a low ACE (skip). + prev_len1 = longest_short_cut_at[TWO]; + prev_len2 = ace_low_len; + } + else if (i == ACE) + { + // An ACE (as the highest card) can be preceded by a KING or a QUEEN. + prev_len1 = longest_short_cut_at[KING]; + prev_len2 = longest_short_cut_at[QUEEN]; + } + else // For all other cards (FOUR through KING). + { + // A card can be preceded by the rank directly below or two ranks below. + prev_len1 = longest_short_cut_at[i - 1]; + prev_len2 = longest_short_cut_at[i - 2]; + } + + // The length of the straight ending at rank 'i' is 1 (for the card itself) + // plus the length of the longest valid preceding straight. + longest_short_cut_at[i] = 1 + max(prev_len1, prev_len2); + + // If we've formed a sequence of {straight-size} or more cards, we have a straight. + if (longest_short_cut_at[i] >= get_straight_and_flush_size()) + { + return true; + } + } + } + + return false; +} + +static bool hand_contains_flush(u8* suits) +{ + for (int i = 0; i < NUM_SUITS; i++) + { + if (suits[i] >= get_straight_and_flush_size()) + { + return true; + } + } + return false; +} + +// Returns the number of cards in the best flush found +// or 0 if no flush of min_len is found, and marks them in out_selection. +int find_flush_in_played_cards(CardObject** played, int top, int min_len, bool* out_selection) +{ + if (top < 0) + return 0; + for (int i = 0; i <= top; i++) + out_selection[i] = false; + + int suit_counts[NUM_SUITS] = {0}; + for (int i = 0; i <= top; i++) + { + if (played[i] && played[i]->card) + { + suit_counts[played[i]->card->suit]++; + } + } + + int best_suit = -1; + int best_count = 0; + for (int i = 0; i < NUM_SUITS; i++) + { + if (suit_counts[i] > best_count) + { + best_count = suit_counts[i]; + best_suit = i; + } + } + + if (best_count >= min_len) + { + for (int i = 0; i <= top; i++) + { + if (played[i] && played[i]->card && played[i]->card->suit == best_suit) + { + out_selection[i] = true; + } + } + return best_count; + } + return 0; +} + +// Returns the number of cards in the best straight or 0 if no straight of min_len is found, marks +// as true them in out_selection[]. This is mostly from Google Gemini +int find_straight_in_played_cards( + CardObject** played, + int top, + bool shortcut_active, + int min_len, + bool* out_selection +) +{ + if (top < 0) + return 0; + for (int i = 0; i <= top; i++) + out_selection[i] = false; + + // --- Setup for Backtracking DP --- + u8 longest_straight_at[NUM_RANKS] = {0}; + int parent[NUM_RANKS]; + for (int i = 0; i < NUM_RANKS; i++) + parent[i] = -1; + + u8 ranks[NUM_RANKS] = {0}; + for (int i = 0; i <= top; i++) + { + if (played[i] && played[i]->card) + { + ranks[played[i]->card->rank]++; + } + } + + // --- Run DP to find longest straight --- + // This is nearly identical to hand_contains_straight() logic + // TODO: Consolidate functions to avoid code duplication? + // Might cost performance because this does a little more + int ace_low_len = ranks[ACE] ? 1 : 0; + for (int i = 0; i < NUM_RANKS; i++) + { + if (ranks[i] > 0) + { + int prev1 = 0, prev2 = 0; + int parent1 = -1, parent2 = -1; + + if (shortcut_active) + { + if (i == TWO) + { + prev1 = ace_low_len; + parent1 = ACE; + } + else if (i == THREE) + { + prev1 = longest_straight_at[TWO]; + parent1 = TWO; + prev2 = ace_low_len; + parent2 = ACE; + } + else if (i == ACE) + { + prev1 = longest_straight_at[KING]; + parent1 = KING; + prev2 = longest_straight_at[QUEEN]; + parent2 = QUEEN; + } + else + { + prev1 = longest_straight_at[i - 1]; + parent1 = i - 1; + if (i > 1) + { + prev2 = longest_straight_at[i - 2]; + parent2 = i - 2; + } + } + } + else + { + if (i == TWO) + { + prev1 = ace_low_len; + parent1 = ACE; + } + else if (i == ACE) + { + prev1 = longest_straight_at[KING]; + parent1 = KING; + } + else + { + prev1 = longest_straight_at[i - 1]; + parent1 = i - 1; + } + } + + // Parallels longest_short_cut_at[i] = 1 + max(prev_len1, prev_len2); + // in hand_contains_straight() + if (prev1 >= prev2) + { + longest_straight_at[i] = 1 + prev1; + parent[i] = parent1; + } + else + { + longest_straight_at[i] = 1 + prev2; + parent[i] = parent2; + } + } + } + + // --- Find best straight and backtrack --- + int best_len = 0; + int end_rank = -1; + for (int i = 0; i < NUM_RANKS; i++) + { + if (longest_straight_at[i] >= best_len) + { + best_len = longest_straight_at[i]; + end_rank = i; + } + } + + if (best_len >= min_len) + { + u8 needed_ranks[NUM_RANKS] = {0}; + int current_rank = end_rank; + while (current_rank != -1 && best_len > 0) + { + needed_ranks[current_rank]++; + current_rank = parent[current_rank]; + best_len--; + } + + for (int i = 0; i <= top; i++) + { + if (played[i] && played[i]->card && needed_ranks[played[i]->card->rank] > 0) + { + out_selection[i] = true; + needed_ranks[played[i]->card->rank]--; + } + } + + int final_card_count = 0; + for (int i = 0; i <= top; i++) + { + if (out_selection[i]) + final_card_count++; + } + return final_card_count; + } + return 0; +} + +// This is used for the special case in "Four Fingers" where you can add a pair into a straight +// (e.g. AA234 should score all 5 cards) +void select_paired_cards_in_hand(CardObject** played, int played_top, bool* selection) +{ + // Build a set of ranks that are already selected + bool rank_selected[NUM_RANKS] = {0}; + bool any_selected_rank = false; + + for (int i = 0; i <= played_top; i++) + { + if (selection[i] && played[i] && played[i]->card) + { + rank_selected[played[i]->card->rank] = true; + any_selected_rank = true; + } + } + + // If no ranks were selected initially, nothing to do + if (!any_selected_rank) + return; + + // Add any unselected card to the selection if if shares a rank with the selected ranks + for (int i = 0; i <= played_top; i++) + { + if (played[i] && played[i]->card && !selection[i]) + { + if (rank_selected[played[i]->card->rank]) + { + selection[i] = true; + } + } + } +} + +static ContainedHandTypes compute_contained_hand_types(void) +{ + ContainedHandTypes hand_types = {0}; + + // Idk if this is how Balatro does it but this is how I'm doing it + if (hand.hand_selections == 0 || hand.state == HAND_DISCARD) + { + return hand_types; + } + + hand_types.HIGH_CARD = 1; + + u8 suits[NUM_SUITS]; + u8 ranks[NUM_RANKS]; + get_hand_distribution(ranks, suits); + + // The following can be optimized better but not sure how much it matters + u8 n_of_a_kind = hand_contains_n_of_a_kind(ranks); + + // Pair and 2 Pair + if (n_of_a_kind >= 2) + { + hand_types.PAIR = 1; + + if (hand_contains_two_pair(ranks)) + { + hand_types.TWO_PAIR = 1; + } + } + + // 3 OAK + if (n_of_a_kind >= 3) + { + hand_types.THREE_OF_A_KIND = 1; + } + + // Straight + if (hand_contains_straight(ranks)) + { + hand_types.STRAIGHT = 1; + } + + // Flush + if (hand_contains_flush(suits)) + { + hand_types.FLUSH = 1; + } + + // Full House + if (n_of_a_kind >= 3 && hand_contains_full_house(ranks)) + { + hand_types.FULL_HOUSE = 1; + } + + // 4 OAK + if (n_of_a_kind >= 4) + { + hand_types.FOUR_OF_A_KIND = 1; + } + + // Straight Flush + if (hand_types.STRAIGHT && hand_types.FLUSH) + { + hand_types.STRAIGHT_FLUSH = 1; + } + + // Royal Flush + if (hand_types.STRAIGHT_FLUSH) + { + if (ranks[TEN] && ranks[JACK] && ranks[QUEEN] && ranks[KING] && ranks[ACE]) + { + hand_types.ROYAL_FLUSH = 1; + } + } + + // 5 OAK + if (n_of_a_kind >= 5) + { + hand_types.FIVE_OF_A_KIND = 1; + } + + // Flush House and Five + if (hand_types.FLUSH) + { + if (hand_types.FULL_HOUSE) + { + hand_types.FLUSH_HOUSE = 1; + } + + if (hand_types.FIVE_OF_A_KIND) + { + hand_types.FLUSH_FIVE = 1; + } + } + + return hand_types; +} + +static enum HandType compute_hand_type(struct ContainedHandTypes contained_types) +{ + enum HandType ret; + + // test each pit see if it's set to 1, and return the first one + for (ret = FLUSH_FIVE; ret > NONE; ret--) + { + // Shift the bit we want to check to the front and mask it with 1 to keep only that + // Since the ContainedHandTypes is ordered the same way as the HandType enum, we + // can shift right by ret-1 to have the bit we want at the front + if ((contained_types.value >> (ret - 1)) & 0x1) + { + break; + } + } + + // If we broke early, ret contains the value of the HandType enum corresponding to + // the position of the highest bit set to 1 in contained_types.value, which is the + // most powerful poker hand contained in the current Hand + // If not, then it contains NONE, which is what we're supposed to return when there + // are no Hands contained in what we played + return ret; +} diff --git a/source/hand_analysis.c b/source/hand_analysis.c deleted file mode 100644 index 250da47..0000000 --- a/source/hand_analysis.c +++ /dev/null @@ -1,456 +0,0 @@ -#include "hand_analysis.h" - -#include "card.h" -#include "game.h" - -void get_hand_distribution(u8 ranks_out[NUM_RANKS], u8 suits_out[NUM_SUITS]) -{ - for (int i = 0; i < NUM_RANKS; i++) - ranks_out[i] = 0; - for (int i = 0; i < NUM_SUITS; i++) - suits_out[i] = 0; - - CardObject** cards = get_hand_array(); - int top = get_hand_top(); - for (int i = 0; i <= top; i++) - { - if (cards[i] && card_object_is_selected(cards[i])) - { - ranks_out[cards[i]->card->rank]++; - suits_out[cards[i]->card->suit]++; - } - } -} - -void get_played_distribution(u8 ranks_out[NUM_RANKS], u8 suits_out[NUM_SUITS]) -{ - for (int i = 0; i < NUM_RANKS; i++) - ranks_out[i] = 0; - for (int i = 0; i < NUM_SUITS; i++) - suits_out[i] = 0; - - CardObject** played = get_played_array(); - int top = get_played_top(); - for (int i = 0; i <= top; i++) - { - /* The difference from get_hand_distribution() (not checking if card is selected) - * is in line Balatro behavior, - * see https://github.com/GBALATRO/balatro-gba/issues/341#issuecomment-3691363488 - */ - if (!played[i]) - continue; - ranks_out[played[i]->card->rank]++; - suits_out[played[i]->card->suit]++; - } -} - -// Returns the highest N of a kind. So a full-house would return 3. -u8 hand_contains_n_of_a_kind(u8* ranks) -{ - u8 highest_n = 0; - for (int i = 0; i < NUM_RANKS; i++) - { - if (ranks[i] > highest_n) - highest_n = ranks[i]; - } - return highest_n; -} - -bool hand_contains_two_pair(u8* ranks) -{ - bool contains_other_pair = false; - for (int i = 0; i < NUM_RANKS; i++) - { - if (ranks[i] >= 2) - { - if (contains_other_pair) - return true; - contains_other_pair = true; - } - } - return false; -} - -bool hand_contains_full_house(u8* ranks) -{ - int count_three = 0; - int count_pair = 0; - for (int i = 0; i < NUM_RANKS; i++) - { - if (ranks[i] >= 3) - { - count_three++; - } - else if (ranks[i] >= 2) - { - count_pair++; - } - } - // Full house if there is: - // - at least one three-of-a-kind and at least one other pair, - // - OR at least two three-of-a-kinds (second "three" acts as pair). - // This accounts for hands with 6 or more cards even though - // they are currently not possible and probably never will be. - return (count_three >= 2 || (count_three && count_pair)); -} - -// This is mostly from Google Gemini -bool hand_contains_straight(u8* ranks) -{ - if (!is_shortcut_joker_active()) - { - int straight_size = get_straight_and_flush_size(); - // This is the regular case of detecting straights - int run = 0; - for (int i = 0; i < NUM_RANKS; ++i) - { - if (ranks[i]) - { - if (++run >= straight_size) - return true; - } - else - { - run = 0; - } - } - - // Check for ace low straight - if (straight_size >= 2 && ranks[ACE]) - { - // With A as low, the highest rank you can use is FIVE. - // -1 for inclusive integer distance and another -1 for the Ace e.g. need=5 -> need 2..5 - int last_needed = TWO + (straight_size - 2); - if (last_needed <= FIVE) - { - bool ok = true; - for (int r = TWO; r <= last_needed; ++r) - { - if (!ranks[r]) - { - ok = false; - break; - } - } - if (ok) - return true; - } - } - - return false; - } - else - { - // Shortcut Joker is active, we have to detect straights where any card may "skip" 1 rank - // We do this with a dynamic programming algorithm that calculates - // the longest possible straight that can end on each rank - // and stopping when we find one that is {straight-size} cards long - u8 longest_short_cut_at[NUM_RANKS] = {0}; - - // A low ace can start a sequence. 'ace_low_len' is 1 if an ace is present, - // acting as a potential predecessor for TWO and THREE. - int ace_low_len = ranks[ACE] ? 1 : 0; - - // Iterate through all ranks from TWO up to ACE. - for (int i = 0; i < NUM_RANKS; i++) - { - // No cards in this rank, no straight can end here, continue - if (ranks[i] == 0) - { - longest_short_cut_at[i] = 0; - continue; - } - - int prev_len1 = 0; - int prev_len2 = 0; - - // This logic handles the special connections for ace-low straights. - if (i == TWO) - { - // A TWO can be preceded by a low ACE (no skip). - prev_len1 = ace_low_len; - } - else if (i == THREE) - { - // A THREE can be preceded by a TWO (no skip) or a low ACE (skip). - prev_len1 = longest_short_cut_at[TWO]; - prev_len2 = ace_low_len; - } - else if (i == ACE) - { - // An ACE (as the highest card) can be preceded by a KING or a QUEEN. - prev_len1 = longest_short_cut_at[KING]; - prev_len2 = longest_short_cut_at[QUEEN]; - } - else // For all other cards (FOUR through KING). - { - // A card can be preceded by the rank directly below or two ranks below. - prev_len1 = longest_short_cut_at[i - 1]; - prev_len2 = longest_short_cut_at[i - 2]; - } - - // The length of the straight ending at rank 'i' is 1 (for the card itself) - // plus the length of the longest valid preceding straight. - longest_short_cut_at[i] = 1 + max(prev_len1, prev_len2); - - // If we've formed a sequence of {straight-size} or more cards, we have a straight. - if (longest_short_cut_at[i] >= get_straight_and_flush_size()) - { - return true; - } - } - } - - return false; -} - -bool hand_contains_flush(u8* suits) -{ - for (int i = 0; i < NUM_SUITS; i++) - { - if (suits[i] >= get_straight_and_flush_size()) - { - return true; - } - } - return false; -} - -// Returns the number of cards in the best flush found -// or 0 if no flush of min_len is found, and marks them in out_selection. -/** - * Finds the largest flush (set of cards with the same suit) in the given array of played cards. - * Marks the cards belonging to the best flush in the out_selection array. - * - * @param played Array of pointers to CardObject representing played cards. - * @param top Index of the top of the played stack. - * @param min_len Minimum number of cards required for a flush. - * @param out_selection Output array of bools; set to true for cards in the best flush, false - * otherwise. - * @return The number of cards in the best flush found, or 0 if no flush meets min_len. - */ -int find_flush_in_played_cards(CardObject** played, int top, int min_len, bool* out_selection) -{ - if (top < 0) - return 0; - for (int i = 0; i <= top; i++) - out_selection[i] = false; - - int suit_counts[NUM_SUITS] = {0}; - for (int i = 0; i <= top; i++) - { - if (played[i] && played[i]->card) - { - suit_counts[played[i]->card->suit]++; - } - } - - int best_suit = -1; - int best_count = 0; - for (int i = 0; i < NUM_SUITS; i++) - { - if (suit_counts[i] > best_count) - { - best_count = suit_counts[i]; - best_suit = i; - } - } - - if (best_count >= min_len) - { - for (int i = 0; i <= top; i++) - { - if (played[i] && played[i]->card && played[i]->card->suit == best_suit) - { - out_selection[i] = true; - } - } - return best_count; - } - return 0; -} - -// Returns the number of cards in the best straight or 0 if no straight of min_len is found, marks -// as true them in out_selection[]. This is mostly from Google Gemini -int find_straight_in_played_cards( - CardObject** played, - int top, - bool shortcut_active, - int min_len, - bool* out_selection -) -{ - if (top < 0) - return 0; - for (int i = 0; i <= top; i++) - out_selection[i] = false; - - // --- Setup for Backtracking DP --- - u8 longest_straight_at[NUM_RANKS] = {0}; - int parent[NUM_RANKS]; - for (int i = 0; i < NUM_RANKS; i++) - parent[i] = -1; - - u8 ranks[NUM_RANKS] = {0}; - for (int i = 0; i <= top; i++) - { - if (played[i] && played[i]->card) - { - ranks[played[i]->card->rank]++; - } - } - - // --- Run DP to find longest straight --- - // This is nearly identical to hand_contains_straight() logic - // TODO: Consolidate functions to avoid code duplication? - // Might cost performance because this does a little more - int ace_low_len = ranks[ACE] ? 1 : 0; - for (int i = 0; i < NUM_RANKS; i++) - { - if (ranks[i] > 0) - { - int prev1 = 0, prev2 = 0; - int parent1 = -1, parent2 = -1; - - if (shortcut_active) - { - if (i == TWO) - { - prev1 = ace_low_len; - parent1 = ACE; - } - else if (i == THREE) - { - prev1 = longest_straight_at[TWO]; - parent1 = TWO; - prev2 = ace_low_len; - parent2 = ACE; - } - else if (i == ACE) - { - prev1 = longest_straight_at[KING]; - parent1 = KING; - prev2 = longest_straight_at[QUEEN]; - parent2 = QUEEN; - } - else - { - prev1 = longest_straight_at[i - 1]; - parent1 = i - 1; - if (i > 1) - { - prev2 = longest_straight_at[i - 2]; - parent2 = i - 2; - } - } - } - else - { - if (i == TWO) - { - prev1 = ace_low_len; - parent1 = ACE; - } - else if (i == ACE) - { - prev1 = longest_straight_at[KING]; - parent1 = KING; - } - else - { - prev1 = longest_straight_at[i - 1]; - parent1 = i - 1; - } - } - - // Parallels longest_short_cut_at[i] = 1 + max(prev_len1, prev_len2); - // in hand_contains_straight() - if (prev1 >= prev2) - { - longest_straight_at[i] = 1 + prev1; - parent[i] = parent1; - } - else - { - longest_straight_at[i] = 1 + prev2; - parent[i] = parent2; - } - } - } - - // --- Find best straight and backtrack --- - int best_len = 0; - int end_rank = -1; - for (int i = 0; i < NUM_RANKS; i++) - { - if (longest_straight_at[i] >= best_len) - { - best_len = longest_straight_at[i]; - end_rank = i; - } - } - - if (best_len >= min_len) - { - u8 needed_ranks[NUM_RANKS] = {0}; - int current_rank = end_rank; - while (current_rank != -1 && best_len > 0) - { - needed_ranks[current_rank]++; - current_rank = parent[current_rank]; - best_len--; - } - - for (int i = 0; i <= top; i++) - { - if (played[i] && played[i]->card && needed_ranks[played[i]->card->rank] > 0) - { - out_selection[i] = true; - needed_ranks[played[i]->card->rank]--; - } - } - - int final_card_count = 0; - for (int i = 0; i <= top; i++) - { - if (out_selection[i]) - final_card_count++; - } - return final_card_count; - } - return 0; -} - -// This is used for the special case in "Four Fingers" where you can add a pair into a straight -// (e.g. AA234 should score all 5 cards) -void select_paired_cards_in_hand(CardObject** played, int played_top, bool* selection) -{ - // Build a set of ranks that are already selected - bool rank_selected[NUM_RANKS] = {0}; - bool any_selected_rank = false; - - for (int i = 0; i <= played_top; i++) - { - if (selection[i] && played[i] && played[i]->card) - { - rank_selected[played[i]->card->rank] = true; - any_selected_rank = true; - } - } - - // If no ranks were selected initially, nothing to do - if (!any_selected_rank) - return; - - // Add any unselected card to the selection if if shares a rank with the selected ranks - for (int i = 0; i <= played_top; i++) - { - if (played[i] && played[i]->card && !selection[i]) - { - if (rank_selected[played[i]->card->rank]) - { - selection[i] = true; - } - } - } -} diff --git a/source/joker_effects.c b/source/joker_effects.c index f172c7b..2d83f68 100644 --- a/source/joker_effects.c +++ b/source/joker_effects.c @@ -1,6 +1,6 @@ #include "game.h" #include "game_variables.h" -#include "hand_analysis.h" +#include "hand.h" #include "joker.h" #include "list.h" #include "pool.h" @@ -678,8 +678,7 @@ static u32 blackboard_joker_effect( bool all_cards_are_spades_or_clubs = true; CardObject** hand = get_hand_array(); - int hand_size = hand_get_size(); - for (int i = 0; i < hand_size; i++) + for (int i = 0; i < g_game_vars.hand_size; i++) { u8 suit = hand[i]->card->suit; if (suit == HEARTS || suit == DIAMONDS) @@ -737,8 +736,7 @@ static u32 raised_fist_joker_effect( *p_lowest_value_index = 0; u8 lowest_value = IMPOSSIBLY_HIGH_CARD_VALUE; CardObject** hand = get_hand_array(); - int hand_size = hand_get_size(); - for (int i = 0; i < hand_size; i++) + for (int i = 0; i < g_game_vars.hand_size; i++) { u8 value = card_get_value(hand[i]->card); if (lowest_value > value)