Adds Shortcut and Four Fingers (#95)

* adds shortcut joker

* adds Four Fingers joker, and combination support for Four Fingers + Shortcut

---------

Co-authored-by: MeirGavish <meir.gavish@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
theturtlemafia
2025-11-01 16:28:40 +02:00
committed by GitHub
co-authored by MeirGavish Copilot
parent eb815b7820
commit 03273de829
11 changed files with 553 additions and 153 deletions
+259 -13
View File
@@ -2,10 +2,13 @@
#include "card.h"
#include "game.h"
static void get_distribution(CardObject **cards, int top, u8 *ranks_out, u8 *suits_out) {
void get_hand_distribution(u8 *ranks_out, u8 *suits_out) {
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]++;
@@ -14,12 +17,17 @@ static void get_distribution(CardObject **cards, int top, u8 *ranks_out, u8 *sui
}
}
void get_hand_distribution(u8 *ranks_out, u8 *suits_out) {
get_distribution(get_hand_array(), get_hand_top(), ranks_out, suits_out);
}
void get_played_distribution(u8 *ranks_out, u8 *suits_out) {
get_distribution(get_played_array(), get_played_top(), ranks_out, suits_out);
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++) {
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.
@@ -63,15 +71,103 @@ bool hand_contains_full_house(u8* ranks) {
return (count_three >= 2 || (count_three && count_pair));
}
// This is mostly from Google Gemini
bool hand_contains_straight(u8 *ranks) {
for (int i = 0; i < NUM_RANKS - 4; i++)
if (!is_shortcut_joker_active())
{
if (ranks[i] && ranks[i + 1] && ranks[i + 2] && ranks[i + 3] && ranks[i + 4])
return true;
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.
int last_needed = TWO + (straight_size - 2); // -1 for inclusive integer distance and another -1 for the Ace e.g. need=5 -> need 2..5
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;
}
}
}
// Check for ace low straight
if (ranks[ACE] && ranks[TWO] && ranks[THREE] && ranks[FOUR] && ranks[FIVE])
return true;
return false;
}
@@ -79,10 +175,160 @@ bool hand_contains_straight(u8 *ranks) {
bool hand_contains_flush(u8 *suits) {
for (int i = 0; i < NUM_SUITS; i++)
{
if (suits[i] >= MAX_SELECTION_SIZE) // this allows MAX_SELECTION_SIZE - 1 for four fingers joker
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;
}
}
}
}