34889988a9
* Added documentation to get_played/hand_distribution * Documented get_played_distribution() behavior
457 lines
13 KiB
C
457 lines
13 KiB
C
#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;
|
|
}
|
|
}
|
|
}
|
|
}
|