#include "game.h" #include "affine_background.h" #include "affine_background_gfx.h" #include "audio_utils.h" #include "background_main_menu_gfx.h" #include "button.h" #include "card.h" #include "game/blind_select.h" #include "game/common_ui.h" #include "game/game_over.h" #include "game/joker_row.h" #include "game/main_menu.h" #include "game/options_menu.h" #include "game/round.h" #include "game/round_end.h" #include "game/run_setup.h" #include "game/shop.h" #include "game_variables.h" #include "graphic_utils.h" #include "hand.h" #include "joker.h" #include "layout.h" #include "list.h" #include "random.h" #include "save.h" #include "selection_grid.h" #include "soundbank.h" #include "splash_screen.h" #include "sprite.h" #include "state_machine.h" #include "timer.h" #include "tonc_memdef.h" #include "util.h" #include #include #include #include #define STRAIGHT_AND_FLUSH_SIZE_FOUR_FINGERS 4 #define STRAIGHT_AND_FLUSH_SIZE_DEFAULT 5 // SE sizes #define STARTING_ROUND 0 #define STARTING_ANTE 1 #define STARTING_MONEY 4 #define STARTING_SCORE 0 #define EXPIRE_ANIMATION_FRAME_COUNT 3 // These functions need to be forward declared // so they're visible to the state_info array, // and the sub-state function tables. // This could be done, and maybe should be done, // with an X macro, but I'll leave that to the // reviewer(s). int deck_get_size(void); static int deck_get_max_size(void); // Consts // clang-format off // Rects left top right bottom // Rects for TTE (in pixels) // 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 }; static const Rect MONEY_TEXT_RECT = {8, 120, 64, 128 }; static const Rect CHIPS_TEXT_RECT = {8, 80, 32, 88 }; static const Rect MULT_TEXT_RECT = {40, 80, 64, 88 }; // Rects with UNDEFINED are only used in tte_printf, they need to be fully defined // to be used with tte_erase_rect_wrapper() static const Rect HANDS_TEXT_RECT = {16, 104, UNDEFINED, UNDEFINED }; static const Rect DISCARDS_TEXT_RECT = {48, 104, UNDEFINED, UNDEFINED }; static const Rect DECK_SIZE_RECT = {200, 152, 240, 160 }; static const Rect ROUND_TEXT_RECT = {48, 144, UNDEFINED, UNDEFINED }; static const Rect ANTE_TEXT_RECT = {8, 144, UNDEFINED, UNDEFINED }; // clang-format on 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_MACHINE_DEFINE(state_info, GAME_STATE_MAX); // Initialization of the global vars // clang-format off GameVariables g_game_vars = { .timer = 0, // Setting the seed to an invalid value so that the Run Setup screen knows we're not reusing a previous Run's seed .rng_info = {UNDEFINED, {0}}, .round = 0, .ante = 0, .money = 0, .hand_size = DEFAULT_HAND_SIZE, .deck = DECK_TYPE_RED, .best_hand_score = 0, .nb_played_hands = {0}, .current_blind = BLIND_TYPE_SMALL, .next_boss_blind = BLIND_TYPE_BIG, .blinds_states = { BLIND_STATE_CURRENT, BLIND_STATE_UPCOMING, BLIND_STATE_UPCOMING }, .hands = 0, .discards = 0, .score = 0, .chips = 0, .mult = 0, .playing_blind_token = NULL, .round_end_blind_token = NULL, .game_speed = DEFAULT_GAME_SPEED, .music_volume = DEFAULT_MUSIC_VOLUME, .sound_volume = DEFAULT_SOUND_VOLUME, }; // clang-format on static List s_owned_jokers_list; static List s_discarded_jokers_list; static List s_expired_jokers_list; // Stacks static Card* s_deck[MAX_DECK_SIZE] = {NULL}; static int s_deck_top = -1; // Joker Special Variables static int s_shortcut_joker_count = 0; static int s_four_fingers_joker_count = 0; void deck_push(Card* card) { if (s_deck_top >= MAX_DECK_SIZE - 1) return; s_deck[++s_deck_top] = card; } Card* deck_pop(void) { if (s_deck_top < 0) return NULL; return s_deck[s_deck_top--]; } void display_ante(void) { tte_printf( "#{P:%d,%d; cx:0x%X000}%ld#{cx:0x%X000}/%d", ANTE_TEXT_RECT.left, ANTE_TEXT_RECT.top, TTE_YELLOW_PB, g_game_vars.ante, TTE_WHITE_PB, MAX_ANTE ); } void game_init() { state_machine_remove(&game_sm); state_machine_register(&game_sm); // Initialize all jokers list once s_owned_jokers_list = list_init(); s_discarded_jokers_list = list_init(); s_expired_jokers_list = list_init(); // TODO: Move this to an initialization of the play scoring states shop_reset(); g_game_vars.hands = MAX_HANDS; g_game_vars.discards = MAX_DISCARDS; g_game_vars.timer = TM_ZERO; g_game_vars.current_blind = BLIND_TYPE_SMALL; g_game_vars.blinds_states[0] = BLIND_STATE_CURRENT; g_game_vars.blinds_states[1] = BLIND_STATE_UPCOMING; g_game_vars.blinds_states[2] = BLIND_STATE_UPCOMING; g_game_vars.ante = STARTING_ANTE; g_game_vars.money = STARTING_MONEY; g_game_vars.score = STARTING_SCORE; g_game_vars.round = 0; g_game_vars.chips = 0; g_game_vars.mult = 0; g_game_vars.round = STARTING_ROUND; g_game_vars.best_hand_score = 0; for (int i = 0; i < HAND_TYPE_MAX; i++) g_game_vars.nb_played_hands[i] = 0; } void game_reset() { while (list_get_len(&s_owned_jokers_list) > 0) { JokerObject* joker_object = list_get_at_idx(&s_owned_jokers_list, 0); remove_owned_joker(0); joker_object_destroy(&joker_object); } tte_erase_screen(); // For some reason that I haven't figured out yet, // if I don't destroy the blind tokens they won't // show up on the next run. sprite_destroy(&g_game_vars.playing_blind_token); sprite_destroy(&g_game_vars.round_end_blind_token); list_clear(&s_owned_jokers_list); list_clear(&s_discarded_jokers_list); list_clear(&s_expired_jokers_list); game_init(); init_unbeaten_blinds_lists(); display_round(); display_score(g_game_vars.score); display_chips(); display_mult(); display_hands(); display_discards(); display_money(); // Ante display_ante(); affine_background_load_palette(affine_background_gfxPal); } static inline void discarded_jokers_update_loop(void) { if (list_is_empty(&s_discarded_jokers_list)) { return; } ListItr itr = list_itr_create(&s_discarded_jokers_list); JokerObject* joker_object; while ((joker_object = list_itr_next(&itr))) { if (joker_object->x == joker_object->tx && joker_object->y == joker_object->ty) { list_itr_remove_current_node(&itr); // TODO: joker_object_dispose() instead? joker_object_destroy(&joker_object); } } } static inline void held_jokers_update_loop(void) { static const int SPACING_LUT[MAX_JOKERS_HELD_SIZE][MAX_JOKERS_HELD_SIZE] = { {0, 0, 0, 0, 0 }, {13, -13, 0, 0, 0 }, {26, 0, -26, 0, 0 }, {39, 13, -13, -39, 0 }, {40, 20, 0, -20, -40} }; FIXED hand_x = int2fx(HELD_JOKERS_POS.x); ListItr itr = list_itr_create(&s_owned_jokers_list); JokerObject* joker; int jokers_top = list_get_len(&s_owned_jokers_list) - 1; int i = 0; while ((joker = list_itr_next(&itr))) { // Let the Shop handle the position of this Joker if (joker != shop_get_description_card()) joker->tx = hand_x - int2fx(SPACING_LUT[jokers_top][i]); i++; } } bool joker_object_can_acquire(Item* joker_object) { GBAL_RETURN_IF_NULL(joker_object, false); return (list_get_len(get_jokers_list()) < MAX_JOKERS_HELD_SIZE); } static inline void expired_jokers_update_loop(void) { if (list_is_empty(&s_expired_jokers_list)) { return; } ListItr itr = list_itr_create(&s_expired_jokers_list); JokerObject* joker_object; while ((joker_object = list_itr_next(&itr))) { // let just enough frames pass that we see it rotating and shrinking if (g_game_vars.timer % FRAMES(EXPIRE_ANIMATION_FRAME_COUNT) == 0) { // get joker idx int expired_joker_idx = 0; ListItr joker_itr = list_itr_create(&s_owned_jokers_list); JokerObject* expired_joker; while ((expired_joker = list_itr_next(&joker_itr)) && expired_joker != joker_object) { expired_joker_idx++; } // Removing expired Jokers here, instead of immediately like ones we // sell or discard allow us to have a small shrink animation without // the other owned Jokers rearranging themselves to fill the newly // freed space, therefore obscuring the animation remove_owned_joker(expired_joker_idx); list_itr_remove_current_node(&itr); joker_object_destroy(&joker_object); } } } static inline void jokers_update_loop(void) { held_jokers_update_loop(); discarded_jokers_update_loop(); expired_jokers_update_loop(); } void game_update() { rng_update(); g_game_vars.timer++; jokers_update_loop(); state_machine_update(); sprite_object_update_all(); } void game_change_state(enum GameState new_game_state) { g_game_vars.timer = TM_ZERO; // Reset the timer state_machine_change_state(&game_sm, new_game_state); } enum GameState game_get_state(void) { return game_sm.state; } bool is_joker_owned(int joker_id) { ListItr itr = list_itr_create(&s_owned_jokers_list); JokerObject* joker; while ((joker = list_itr_next(&itr))) { if (joker->joker->id == joker_id) { return true; } } return false; } List* get_jokers_list(void) { return &s_owned_jokers_list; } List* get_expired_jokers_list(void) { return &s_expired_jokers_list; } List* get_discarded_jokers_list(void) { return &s_discarded_jokers_list; } bool is_shortcut_joker_active(void) { return s_shortcut_joker_count > 0; } int get_straight_and_flush_size(void) { return s_four_fingers_joker_count > 0 ? STRAIGHT_AND_FLUSH_SIZE_FOUR_FINGERS : STRAIGHT_AND_FLUSH_SIZE_DEFAULT; } void add_joker(JokerObject* joker_object) { list_push_back(&s_owned_jokers_list, joker_object); // TODO: Extract to on_joker_added() callback // In case the player gets multiple Four Fingers Jokers, // only change size when the first one is added if (joker_object->joker->id == FOUR_FINGERS_JOKER_ID) { s_four_fingers_joker_count++; } if (joker_object->joker->id == SHORTCUT_JOKER_ID) { s_shortcut_joker_count++; } } void remove_owned_joker(int owned_joker_idx) { // TODO: Extract to on_joker_removed() callback JokerObject* joker_object = list_get_at_idx(&s_owned_jokers_list, owned_joker_idx); // In case the player gets multiple Four Fingers Jokers, // and only reset the size when all of them have been removed if (joker_object->joker->id == FOUR_FINGERS_JOKER_ID) { s_four_fingers_joker_count--; } if (joker_object->joker->id == SHORTCUT_JOKER_ID) { s_shortcut_joker_count--; } // TODO: Move to site of joker_destroy()? joker_set_rollable(joker_object->joker->id, true); list_remove_at_idx(&s_owned_jokers_list, owned_joker_idx); } int get_deck_top(void) { return s_deck_top; } int get_num_discards_remaining(void) { return g_game_vars.discards; } int get_num_hands_remaining(void) { return g_game_vars.hands; } void display_money() { Rect money_text_rect = MONEY_TEXT_RECT; tte_erase_rect_wrapper(MONEY_TEXT_RECT); char money_str_buff[INT_MAX_DIGITS + 2]; // + 2 for null terminator and "$" sign snprintf(money_str_buff, sizeof(money_str_buff), "$%ld", g_game_vars.money); // Bias left so the number is centered and the "$" sign is on the left update_text_rect_to_center_str(&money_text_rect, money_str_buff, SCREEN_LEFT); tte_printf( "#{P:%d,%d; cx:0x%X000}%s", money_text_rect.left, money_text_rect.top, TTE_YELLOW_PB, money_str_buff ); } void display_chips(void) { Rect chips_text_rect = CHIPS_TEXT_RECT; // In case of overflow, the rect overflow left by 1 char Rect chips_text_overflow_rect = chips_text_rect; chips_text_overflow_rect.left -= TTE_CHAR_SIZE; tte_erase_rect_wrapper(chips_text_overflow_rect); char chips_str_buff[UINT_MAX_DIGITS + 1]; truncate_uint_to_suffixed_str( g_game_vars.chips, rect_width(&chips_text_rect) / TTE_CHAR_SIZE, chips_str_buff ); update_text_rect_to_right_align_str(&chips_text_rect, chips_str_buff, OVERFLOW_LEFT); tte_printf( "#{P:%d,%d; cx:0x%X000;}%s", chips_text_rect.left, chips_text_rect.top, TTE_WHITE_PB, chips_str_buff ); toggle_flaming_score(); } void display_mult(void) { Rect mult_text_overflow_rect = MULT_TEXT_RECT; // In case of overflow the rect will overflow right by 1 char mult_text_overflow_rect.right += TTE_CHAR_SIZE; tte_erase_rect_wrapper(mult_text_overflow_rect); char mult_str_buff[UINT_MAX_DIGITS + 1]; truncate_uint_to_suffixed_str( g_game_vars.mult, rect_width(&MULT_TEXT_RECT) / TTE_CHAR_SIZE, mult_str_buff ); tte_printf( "#{P:%d,%d; cx:0x%X000;}%s", MULT_TEXT_RECT.left, MULT_TEXT_RECT.top, TTE_WHITE_PB, mult_str_buff ); toggle_flaming_score(); } void display_deck_size_max(void) { // TODO: the text will overflow if deck max size exceeds 99, // we will need a fix at some point for this tte_erase_rect_wrapper(DECK_SIZE_RECT); tte_printf( "#{P:%d,%d; cx:0x%X000}%d/%d", DECK_SIZE_RECT.left, DECK_SIZE_RECT.top, TTE_WHITE_PB, deck_get_size(), deck_get_max_size() ); } // Returns true if the card is *considered* a face card bool card_is_face(Card* card) { // Card is a face card, or Pareidolia is present return ( card->rank == JACK || card->rank == QUEEN || card->rank == KING || is_joker_owned(PAREIDOLIA_JOKER_ID) ); } void display_temp_score(u32 value) { char temp_score_str_buff[UINT_MAX_DIGITS + 1]; Rect temp_score_rect = TEMP_SCORE_RECT; truncate_uint_to_suffixed_str( value, rect_width(&temp_score_rect) / TTE_CHAR_SIZE, temp_score_str_buff ); update_text_rect_to_center_str(&temp_score_rect, temp_score_str_buff, SCREEN_RIGHT); tte_erase_rect_wrapper(TEMP_SCORE_RECT); tte_printf( "#{P:%d,%d; cx:0x%X000}%s", temp_score_rect.left, temp_score_rect.top, TTE_WHITE_PB, temp_score_str_buff ); } void erase_temp_score(void) { tte_erase_rect_wrapper(TEMP_SCORE_RECT); } void display_score(u32 value) { Rect score_rect = SCORE_RECT; // Clear the existing text before redrawing tte_erase_rect_wrapper(SCORE_RECT); char score_str_buff[UINT_MAX_DIGITS + 1]; truncate_uint_to_suffixed_str(value, rect_width(&score_rect) / TTE_CHAR_SIZE, score_str_buff); update_text_rect_to_center_str(&score_rect, score_str_buff, SCREEN_RIGHT); tte_printf( "#{P:%d,%d; cx:0x%X000}%s", score_rect.left, score_rect.top, TTE_WHITE_PB, score_str_buff ); } void display_round(void) { // tte_erase_rect_wrapper(ROUND_TEXT_RECT); tte_printf( "#{P:%d,%d; cx:0x%X000}%ld", ROUND_TEXT_RECT.left, ROUND_TEXT_RECT.top, TTE_YELLOW_PB, g_game_vars.round ); } void display_hands(void) { tte_printf( "#{P:%d,%d; cx:0x%X000}%ld", HANDS_TEXT_RECT.left, HANDS_TEXT_RECT.top, TTE_BLUE_PB, g_game_vars.hands ); } void display_discards(void) { tte_printf( "#{P:%d,%d; cx:0x%X000}%ld", DISCARDS_TEXT_RECT.left, DISCARDS_TEXT_RECT.top, TTE_RED_PB, g_game_vars.discards ); } int deck_get_size(void) { return s_deck_top + 1; } static int deck_get_max_size(void) { // This is the max amount of cards that the player currently has in their possession return get_hand_top() + get_played_top() + s_deck_top + get_discard_top() + 4; } void deck_shuffle(void) { for (int i = s_deck_top; i > 0; i--) { int j = rng_get_u32(RNG_SEQ_CARD_SHUFFLE) % (i + 1); Card* temp = s_deck[i]; s_deck[i] = s_deck[j]; s_deck[j] = temp; } } void game_start(void) { affine_background_change_background(AFFINE_BG_GAME); tte_colors_setup(); g_game_vars.hands = MAX_HANDS; g_game_vars.discards = MAX_DISCARDS; // Fill the deck with all the cards. Later on this can be replaced with a more dynamic system // that allows for different decks and card types. for (int suit = 0; suit < NUM_SUITS; suit++) { for (int rank = 0; rank < NUM_RANKS; rank++) { Card* card = card_new(suit, rank); deck_push(card); } } change_background(BG_BLIND_SELECT, false); // Deck size/max size tte_erase_rect_wrapper(DECK_SIZE_RECT); tte_printf( "#{P:%d,%d; cx:0x%X000}%d/%d", DECK_SIZE_RECT.left, DECK_SIZE_RECT.top, TTE_WHITE_PB, deck_get_size(), deck_get_max_size() ); display_round(); // Set the round display display_score(g_game_vars.score); // Set the score display display_chips(); // Set the chips display display_mult(); // Set the multiplier display display_hands(); // Hand display_discards(); // Discard display_money(); // Set the money display display_ante(); game_change_state(GAME_STATE_BLIND_SELECT); }