#include "card.h" #include #include // Audio #include "soundbank.h" #define MAX_HAND_SIZE 16 #define MAX_DECK_SIZE 52 #define MAX_SELECTION_SIZE 5 // Card sprites lookup table. First index is the suit, second index is the rank. The value is the tile index. const static u16 card_sprite_lut[4][13] = { {0, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192}, {208, 224, 240, 256, 272, 288, 304, 320, 336, 352, 368, 384, 400}, {416, 432, 448, 464, 480, 496, 512, 528, 544, 560, 576, 592, 608}, {624, 640, 656, 672, 688, 704, 720, 736, 752, 768, 784, 800, 816} }; static enum HandState hand_state = HAND_DRAW; static enum HandType hand_type = NONE; static CardObject *played[MAX_SELECTION_SIZE] = {NULL}; static CardObject *hand[MAX_HAND_SIZE] = {NULL}; static Card *deck[MAX_DECK_SIZE] = {NULL}; static int played_top = -1; static int hand_top = -1; static int deck_top = -1; static int hand_size = 8; // Default hand size is 8 static int cards_drawn = 0; static int hand_selections = 0; static int card_focused = 0; static bool sort_by_suit = false; // General functions static inline void sort_cards() { if (sort_by_suit) { for (int a = 0; a < hand_top; a++) { for (int b = a + 1; b <= hand_top; b++) { if (hand[a] == NULL || (hand[b] != NULL && (hand[a]->card->suit > hand[b]->card->suit || (hand[a]->card->suit == hand[b]->card->suit && hand[a]->card->rank > hand[b]->card->rank)))) { CardObject *temp = hand[a]; hand[a] = hand[b]; hand[b] = temp; } } } } else { for (int a = 0; a < hand_top; a++) { for (int b = a + 1; b <= hand_top; b++) { if (hand[a] == NULL || (hand[b] != NULL && hand[a]->card->rank > hand[b]->card->rank)) { CardObject *temp = hand[a]; hand[a] = hand[b]; hand[b] = temp; } } } } // Update the sprites in the hand by destroying them and creating new ones in the correct order // (This is feels like a diabolical solution but like literally how else would you do this) for (int i = 0; i <= hand_top; i++) { if (hand[i] != NULL) { sprite_destroy(&hand[i]->sprite); } } for (int i = 0; i <= hand_top; i++) { if (hand[i] != NULL) { hand[i]->sprite = sprite_new(ATTR0_SQUARE | ATTR0_4BPP | ATTR0_AFF, ATTR1_SIZE_32, card_sprite_lut[hand[i]->card->suit][hand[i]->card->rank], 0, i); sprite_position(hand[i]->sprite, fx2int(hand[i]->x), fx2int(hand[i]->y)); } } } // Played stack static inline void played_push(CardObject *card_object) { if (played_top >= MAX_SELECTION_SIZE - 1) return; played[++played_top] = card_object; } static inline CardObject *played_pop() { if (played_top < 0) return NULL; return played[played_top--]; } // Deck stack static inline void deck_push(Card *card) { if (deck_top >= MAX_DECK_SIZE - 1) return; deck[++deck_top] = card; } static inline Card *deck_pop() { if (deck_top < 0) return NULL; return deck[deck_top--]; } // Card methods Card *card_new(u8 suit, u8 rank) { Card *card = malloc(sizeof(Card)); card->suit = suit; card->rank = rank; return card; } void card_destroy(Card **card) { if (*card == NULL) return; free(*card); *card = NULL; } // CardObject methods CardObject *card_object_new(Card *card) { CardObject *card_object = malloc(sizeof(CardObject)); card_object->card = card; card_object->sprite = NULL; card_object->x = 0; card_object->y = 0; card_object->vx = 0; card_object->vy = 0; card_object->selected = false; return card_object; } void card_object_destroy(CardObject **card_object) { if (*card_object == NULL) return; sprite_destroy(&(*card_object)->sprite); card_destroy(&(*card_object)->card); free(*card_object); *card_object = NULL; } void card_draw() { if (deck_top < 0 || hand_top >= hand_size - 1 || hand_top >= MAX_HAND_SIZE - 1) return; CardObject *card_object = card_object_new(deck_pop()); const FIXED deck_x = int2fx(208); const FIXED deck_y = int2fx(110); card_object->x = deck_x; card_object->y = deck_y; hand[++hand_top] = card_object; // Sort the hand after drawing a card sort_cards(); const int pitch_lut[MAX_HAND_SIZE] = {1024, 1048, 1072, 1096, 1120, 1144, 1168, 1192, 1216, 1240, 1264, 1288, 1312, 1336, 1360, 1384}; mm_sound_effect sfx_draw = {{SFX_CARD_DRAW}, pitch_lut[cards_drawn], 0, 255, 128,}; mmEffectEx(&sfx_draw); } // Card functions void card_init() { // Fill the deck with all the cards for (int suit = 0; suit < 4; suit++) { for (int rank = 0; rank < 13; rank++) { Card *card = card_new(suit, rank); deck_push(card); } } // Shuffle the deck for (int i = 0; i < MAX_DECK_SIZE; i++) { int j = rand() % MAX_DECK_SIZE; Card *temp = deck[i]; deck[i] = deck[j]; deck[j] = temp; } } void card_update() // This whole function is currently pretty unoptimized due to the fixed point math, but that can be fixed later with a LUT { const int update_frame = 10; // This is the number of frames between each card event static int timer = 1; timer++; if (hand_state == HAND_DRAW && cards_drawn < hand_size) { if (timer % update_frame == 0) // Draw a card every 10 frames { cards_drawn++; card_draw(); } } else if (hand_state == HAND_DRAW) { hand_state = HAND_SELECT; // Change the hand state to select after drawing all the cards cards_drawn = 0; timer = 1; } static int played_selections = 0; static bool sound_played = false; bool discarded_card = false; for (int i = 0; i < MAX_HAND_SIZE; i++) { if (hand[i] != NULL) { const int spacing_lut[MAX_HAND_SIZE] = {28, 28, 28, 28, 27, 21, 18, 15, 13, 12, 10, 9, 9, 8, 8, 7}; // This is a stupid way to do this but I don't care FIXED hand_x = int2fx(120); FIXED hand_y = int2fx(90); switch (hand_state) { case HAND_DRAW: hand_x = hand_x + (int2fx(i) - int2fx(hand_top) / 2) * -spacing_lut[hand_top]; break; case HAND_SELECT: if (i == card_focused && !hand[i]->selected) { hand_y -= (10 << FIX_SHIFT); } else if (i != card_focused && hand[i]->selected) { hand_y -= (15 << FIX_SHIFT); } else if (i == card_focused && hand[i]->selected) { hand_y -= (20 << FIX_SHIFT); } if (i != card_focused && hand[i]->y > hand_y) { hand[i]->y = hand_y; hand[i]->vy = 0; } hand_x = hand_x + (int2fx(i) - int2fx(hand_top) / 2) * -spacing_lut[hand_top]; // TODO: Change this later to reference a 2D LUT of positions break; case HAND_DISCARD: // TODO: Add sound if (hand[i]->selected) { if (!discarded_card) { hand_x = int2fx(240); hand_y = int2fx(70); if (!sound_played) { const int pitch_lut[MAX_SELECTION_SIZE] = {1024, 960, 896, 832, 768}; mm_sound_effect sfx_draw = {{SFX_CARD_DRAW}, pitch_lut[cards_drawn], 0, 255, 128,}; mmEffectEx(&sfx_draw); sound_played = true; } if (hand[i]->x >= hand_x) { card_object_destroy(&hand[i]); sort_cards(); hand_top--; cards_drawn++; // This technically isn't drawing cards, I'm just reusing the variable sound_played = false; timer = 1; } discarded_card = true; } else { hand_y -= (15 << FIX_SHIFT); hand_x = hand_x + (int2fx(i) - int2fx(hand_top) / 2) * -spacing_lut[hand_top]; } } else { hand_x = hand_x + (int2fx(i) - int2fx(hand_top) / 2) * -spacing_lut[hand_top]; } if (i == hand_top && discarded_card == false && timer % update_frame == 0) { hand_state = HAND_DRAW; sound_played = false; cards_drawn = 0; hand_selections = 0; timer = 1; break; } break; case HAND_PLAY: hand_x = hand_x + (int2fx(i) - int2fx(hand_top) / 2) * -spacing_lut[hand_top]; hand_y += (24 << FIX_SHIFT); if (hand[i]->selected && timer % update_frame == 0) { hand[i]->selected = false; played_push(hand[i]); sprite_destroy(&hand[i]->sprite); hand[i] = NULL; sort_cards(); const int pitch_lut[MAX_SELECTION_SIZE] = {1024, 960, 896, 832, 768}; mm_sound_effect sfx_draw = {{SFX_CARD_DRAW}, pitch_lut[cards_drawn], 0, 255, 128,}; mmEffectEx(&sfx_draw); hand_top--; hand_selections--; cards_drawn++; timer = 1; } if (i == hand_top && timer % update_frame == 0) { hand_state = HAND_PLAYING; cards_drawn = 0; hand_selections = 0; timer = 1; played_selections = played_top + 1; switch (hand_type) // select the cards that apply to the hand type { case NONE: break; case HIGH_CARD: int highest_rank_index = 0; for (int i = 0; i <= played_top; i++) // find the card with the highest rank in the hand { if (played[i]->card->rank > played[highest_rank_index]->card->rank) { highest_rank_index = i; } } played[highest_rank_index]->selected = true; break; case PAIR: for (int i = 0; i <= played_top - 1; i++) // find two cards with the same rank { if (played[i]->card->rank == played[i + 1]->card->rank) { played[i]->selected = true; played[i + 1]->selected = true; break; } } break; case TWO_PAIR: for (int i = 0; i <= played_top - 1; i++) // find two pairs of cards with the same rank { if (played[i]->card->rank == played[i + 1]->card->rank) { played[i]->selected = true; played[i + 1]->selected = true; } } break; case THREE_OF_A_KIND: for (int i = 0; i <= played_top - 2; i++) // find three cards with the same rank { if (played[i]->card->rank == played[i + 1]->card->rank && played[i]->card->rank == played[i + 2]->card->rank) { played[i]->selected = true; played[i + 1]->selected = true; played[i + 2]->selected = true; break; } } break; case STRAIGHT: for (int i = 0; i <= played_top; i++) // find five cards in a row { played[i]->selected = true; // we don't have to check if cards are being played because a straight needs 5 cards which is the max selection size } break; case FLUSH: for (int i = 0; i <= played_top; i++) // find five cards with the same suit { played[i]->selected = true; // flush functions the same as straight } break; case FULL_HOUSE: for (int i = 0; i <= played_top; i++) // find three cards with the same rank and two cards with the same rank { played[i]->selected = true; // full house functions the same as straight } break; case FOUR_OF_A_KIND: for (int i = 0; i <= played_top - 3; i++) // find four cards with the same rank { if (played[i]->card->rank == played[i + 1]->card->rank && played[i]->card->rank == played[i + 2]->card->rank && played[i]->card->rank == played[i + 3]->card->rank) { played[i]->selected = true; played[i + 1]->selected = true; played[i + 2]->selected = true; played[i + 3]->selected = true; break; } } break; case STRAIGHT_FLUSH: for (int i = 0; i <= played_top; i++) // find five cards in a row with the same suit { played[i]->selected = true; // straight flush functions the same as straight } break; case ROYAL_FLUSH: for (int i = 0; i <= played_top; i++) // find five cards in a row with the same suit starting with a 10 { played[i]->selected = true; // royal flush functions the same as straight } break; case FIVE_OF_A_KIND: for (int i = 0; i <= played_top; i++) // find five cards with the same rank { played[i]->selected = true; // five of a kind functions the same as straight } break; case FLUSH_HOUSE: for (int i = 0; i <= played_top; i++) // find three cards with the same rank and two cards with the same suit { played[i]->selected = true; // flush house functions the same as straight } break; case FLUSH_FIVE: for (int i = 0; i <= played_top; i++) // find five cards with the same suit { played[i]->selected = true; // flush five functions the same as straight } break; } } break; case HAND_PLAYING: hand_x = hand_x + (int2fx(i) - int2fx(hand_top) / 2) * -spacing_lut[hand_top]; hand_y += (24 << FIX_SHIFT); // Method for switching back to drawn not thought of yet break; } hand[i]->vx += (hand_x - hand[i]->x) / 8; hand[i]->vy += (hand_y - hand[i]->y) / 8; // set velocity to 0 if it's close enough to the target const float epsilon = float2fx(0.01f); if (hand[i]->vx < epsilon && hand[i]->vx > -epsilon && hand[i]->vy < epsilon && hand[i]->vy > -epsilon) { hand[i]->vx = 0; hand[i]->vy = 0; hand[i]->x = hand_x; hand[i]->y = hand_y; } else { hand[i]->vx = (hand[i]->vx * 7) / 10; hand[i]->vy = (hand[i]->vy * 7) / 10; hand[i]->x += hand[i]->vx; hand[i]->y += hand[i]->vy; } obj_aff_rotate(hand[i]->sprite->aff, -hand[i]->vx); sprite_position(hand[i]->sprite, fx2int(hand[i]->x), fx2int(hand[i]->y)); } } if (played_selections > 0 && (timer % update_frame == 0 || played[played_selections - 1]->selected == false)) { played_selections--; } for (int i = 0; i < played_top + 1; i++) { if (played[i] != NULL) { if (played[i]->sprite == NULL) { played[i]->sprite = sprite_new(ATTR0_SQUARE | ATTR0_4BPP | ATTR0_AFF, ATTR1_SIZE_32, card_sprite_lut[played[i]->card->suit][played[i]->card->rank], 0, i + MAX_HAND_SIZE); } FIXED played_x = int2fx(120); FIXED played_y = int2fx(70); played_x = played_x + (int2fx(i) - int2fx(played_top) / 2) * -27; if (played[i]->selected && played_selections <= i) { played_y -= (10 << FIX_SHIFT); } played[i]->vx += (played_x - played[i]->x) / 8; played[i]->vy += (played_y - played[i]->y) / 8; // set velocity to 0 if it's close enough to the target const float epsilon = float2fx(0.01f); if (played[i]->vx < epsilon && played[i]->vx > -epsilon && played[i]->vy < epsilon && played[i]->vy > -epsilon) { played[i]->vx = 0; played[i]->vy = 0; played[i]->x = played_x; played[i]->y = played_y; } else { played[i]->vx = (played[i]->vx * 7) / 10; played[i]->vy = (played[i]->vy * 7) / 10; played[i]->x += played[i]->vx; played[i]->y += played[i]->vy; } obj_aff_rotate(played[i]->sprite->aff, -played[i]->vx); sprite_position(played[i]->sprite, fx2int(played[i]->x), fx2int(played[i]->y)); } } } // Hand functions void hand_set_focus(int index) { if (index < 0 || index > hand_top || hand_state != HAND_SELECT) return; card_focused = index; mm_sound_effect sfx_focus = {{SFX_CARD_FOCUS}, 1024 + rand() % 512, 0, 255, 128,}; mmEffectEx(&sfx_focus); } int hand_get_focus() { return card_focused; } void hand_select() { if (hand_state != HAND_SELECT || hand[card_focused] == NULL) return; if (hand[card_focused]->selected) { hand[card_focused]->selected = false; hand_selections--; mm_sound_effect sfx_select = {{SFX_CARD_SELECT}, 1024, 0, 255, 128,}; mmEffectEx(&sfx_select); } else if (hand_selections < MAX_SELECTION_SIZE) { hand[card_focused]->selected = true; hand_selections++; mm_sound_effect sfx_deselect = {{SFX_CARD_DESELECT}, 1024, 0, 255, 128,}; mmEffectEx(&sfx_deselect); } } void hand_change_sort() { sort_by_suit = !sort_by_suit; sort_cards(); } int hand_get_size() { return hand_top + 1; } int hand_get_max_size() { return hand_size; } enum HandType hand_get_type() { // Idk if this is how Balatro does it but this is how I'm doing it if (hand_selections == 0) { hand_type = NONE; return hand_type; } hand_type = HIGH_CARD; u8 suits[4] = {0}; u8 ranks[13] = {0}; for (int i = 0; i <= hand_top; i++) { if (hand[i] != NULL && hand[i]->selected) { suits[hand[i]->card->suit]++; ranks[hand[i]->card->rank]++; } } // Check for flush for (int i = 0; i < 4; i++) { if (suits[i] >= MAX_SELECTION_SIZE) // if i add jokers just MAX_SELECTION_SIZE - 1 for four fingers { hand_type = FLUSH; break; } } // Check for straight for (int i = 0; i < 9; i++) { if (ranks[i] && ranks[i + 1] && ranks[i + 2] && ranks[i + 3] && ranks[i + 4]) { if (hand_type == FLUSH) { hand_type = STRAIGHT_FLUSH; } else { hand_type = STRAIGHT; } break; } } // Check for ace low straight if (ranks[ACE] && ranks[TWO] && ranks[THREE] && ranks[FOUR] && ranks[FIVE]) { hand_type = STRAIGHT; } // Check for royal flush if (hand_type == STRAIGHT_FLUSH && ranks[TEN] && ranks[JACK] && ranks[QUEEN] && ranks[KING] && ranks[ACE]) { hand_type = ROYAL_FLUSH; return hand_type; } // Check for straight flush if (hand_type == STRAIGHT_FLUSH) { hand_type = STRAIGHT_FLUSH; return hand_type; } // Check for flush five and five of a kind if (hand_type == FLUSH && ranks[FIVE] >= 5) { hand_type = FLUSH_FIVE; return hand_type; } else if (ranks[FIVE] >= 5) { return FIVE_OF_A_KIND; } // Check for for of a kind for (int i = 0; i < 13; i++) { if (ranks[i] >= 4) { hand_type = FOUR_OF_A_KIND; return hand_type; } else if (ranks[i] == 3) { if (hand_type == PAIR) { hand_type = FULL_HOUSE; return hand_type; } else { hand_type = THREE_OF_A_KIND; } } else if (ranks[i] == 2) { if (hand_type == THREE_OF_A_KIND) { hand_type = FULL_HOUSE; return hand_type; } else if (hand_type == PAIR) { hand_type = TWO_PAIR; return hand_type; } else { hand_type = PAIR; } } } return hand_type; } bool hand_discard() { if (hand_state != HAND_SELECT || hand_selections == 0) return false; hand_state = HAND_DISCARD; card_focused = 0; return true; } bool hand_play() { if (hand_state != HAND_SELECT || hand_selections == 0) return false; hand_state = HAND_PLAY; card_focused = 0; return true; } // Deck functions int deck_get_size() { return deck_top + 1; } int deck_get_max_size() { return MAX_DECK_SIZE; // This shouldn't be the array max size, it should be the total amount of cards that you have but I can't do that until I implement a way for discarded cards to still be stored in ram }