From 4fbba205ad5643c58278d7b90a76dbe18126dd32 Mon Sep 17 00:00:00 2001 From: RocketGod <57732082+RocketGod-git@users.noreply.github.com> Date: Wed, 25 Jun 2025 07:07:56 -0700 Subject: [PATCH] Made the Blackjack game (#2712) * Made the Blackjack game * Format Blackjack main.cpp * Changed spade to diamond for dark mode visibility * Format code --- .../application/external/blackjack/main.cpp | 53 ++ .../external/blackjack/ui_blackjack.cpp | 727 ++++++++++++++++++ .../external/blackjack/ui_blackjack.hpp | 156 ++++ firmware/application/external/external.cmake | 5 + firmware/application/external/external.ld | 7 + 5 files changed, 948 insertions(+) create mode 100644 firmware/application/external/blackjack/main.cpp create mode 100644 firmware/application/external/blackjack/ui_blackjack.cpp create mode 100644 firmware/application/external/blackjack/ui_blackjack.hpp diff --git a/firmware/application/external/blackjack/main.cpp b/firmware/application/external/blackjack/main.cpp new file mode 100644 index 000000000..cb27fc4ed --- /dev/null +++ b/firmware/application/external/blackjack/main.cpp @@ -0,0 +1,53 @@ +/* + * Blackjack Game for Portapack Mayhem + * Ported / Enhanced / Graphically made awesome by RocketGod (https://betaskynet.com) + * Based on BlackJack 83 for TI Calculator by Harper Maddox (was written in Assembly) + */ + +#include "ui.hpp" +#include "ui_blackjack.hpp" +#include "ui_navigation.hpp" +#include "external_app.hpp" + +namespace ui::external_app::blackjack { +void initialize_app(ui::NavigationView& nav) { + nav.push(); +} +} // namespace ui::external_app::blackjack + +extern "C" { + +__attribute__((section(".external_app.app_blackjack.application_information"), used)) application_information_t _application_information_blackjack = { + (uint8_t*)0x00000000, + ui::external_app::blackjack::initialize_app, + CURRENT_HEADER_VERSION, + VERSION_MD5, + + "Blackjack", + { + // Diamond icon 16x16 + 0x00, 0x00, // ................ + 0x80, 0x01, // .......##....... + 0xC0, 0x03, // ......####...... + 0xE0, 0x07, // .....######..... + 0xF0, 0x0F, // ....########.... + 0xF8, 0x1F, // ...##########... + 0xFC, 0x3F, // ..############.. + 0xFE, 0x7F, // .##############. + 0xFE, 0x7F, // .##############. + 0xFC, 0x3F, // ..############.. + 0xF8, 0x1F, // ...##########... + 0xF0, 0x0F, // ....########.... + 0xE0, 0x07, // .....######..... + 0xC0, 0x03, // ......####...... + 0x80, 0x01, // .......##....... + 0x00, 0x00, // ................ + }, + ui::Color::red().v, // Red color for diamonds + app_location_t::GAMES, + -1, + + {0, 0, 0, 0}, + 0x00000000, +}; +} diff --git a/firmware/application/external/blackjack/ui_blackjack.cpp b/firmware/application/external/blackjack/ui_blackjack.cpp new file mode 100644 index 000000000..4d93b98ef --- /dev/null +++ b/firmware/application/external/blackjack/ui_blackjack.cpp @@ -0,0 +1,727 @@ +/* + * Blackjack Game for Portapack Mayhem + * Ported / Enhanced / Graphically made awesome by RocketGod (https://betaskynet.com) + * Based on BlackJack 83 for TI Calculator by Harper Maddox (was written in Assembly) + */ + +#include "ui_blackjack.hpp" + +namespace ui::external_app::blackjack { + +// Global variables +static BlackjackView* current_instance = nullptr; +static Callback game_update_callback = nullptr; +static uint32_t game_update_timeout = 0; +static uint32_t game_update_counter = 0; +static Painter painter; + +void check_game_timer() { + if (game_update_callback) { + if (++game_update_counter >= game_update_timeout) { + game_update_counter = 0; + game_update_callback(); + } + } +} + +void Ticker::attach(Callback func, double delay_sec) { + game_update_callback = func; + game_update_timeout = delay_sec * 60; +} + +void Ticker::detach() { + game_update_callback = nullptr; +} + +void game_timer_check() { + if (current_instance) { + current_instance->update_game(); + } +} + +BlackjackView::BlackjackView(NavigationView& nav) + : nav_{nav}, game_timer{} { + add_children({&dummy}); + current_instance = this; + game_timer.attach(&game_timer_check, 1.0 / 60.0); +} + +BlackjackView::~BlackjackView() { + current_instance = nullptr; +} + +void BlackjackView::on_show() { + draw_menu_static(); +} + +void BlackjackView::paint(Painter& painter) { + (void)painter; + + if (!initialized) { + initialized = true; + std::srand(LPC_RTC->CTIME0); + init_deck(); + } +} + +void BlackjackView::frame_sync() { + check_game_timer(); + set_dirty(); +} + +void BlackjackView::clear_screen() { + painter.fill_rectangle({0, 0, 240, 320}, Color::black()); +} + +void BlackjackView::init_deck() { + // Initialize deck with card values 0-51 + // 0-12 = Spades (A-K), 13-25 = Hearts, 26-38 = Diamonds, 39-51 = Clubs + for (int i = 0; i < 52; i++) { + deck[i] = i; + } + shuffle_deck(); +} + +void BlackjackView::shuffle_deck() { + // Simple shuffle using random swaps + for (int i = 51; i > 0; i--) { + int j = rand() % (i + 1); + uint8_t temp = deck[i]; + deck[i] = deck[j]; + deck[j] = temp; + } + deck_position = 0; +} + +uint8_t BlackjackView::draw_card() { + if (deck_position >= 52) { + shuffle_deck(); + } + return deck[deck_position++]; +} + +int BlackjackView::get_card_value(uint8_t card) { + int value = (card % 13) + 1; // 1-13 + if (value > 10) value = 10; // Face cards worth 10 + return value; +} + +uint8_t BlackjackView::get_card_suit(uint8_t card) { + return card / 13; // 0=Spades, 1=Hearts, 2=Diamonds, 3=Clubs +} + +std::string BlackjackView::get_card_string(uint8_t card) { + int value = (card % 13) + 1; + std::string result; + + if (value == 1) + result = "A"; + else if (value == 11) + result = "J"; + else if (value == 12) + result = "Q"; + else if (value == 13) + result = "K"; + else if (value == 10) + result = "10"; // Special case for 10 + else + result = std::to_string(value); + + return result; +} + +int BlackjackView::calculate_hand_value(uint8_t* cards, uint8_t count) { + int value = 0; + int aces = 0; + + for (uint8_t i = 0; i < count; i++) { + int card_val = get_card_value(cards[i]); + if (card_val == 1) { + aces++; + value += 1; + } else { + value += card_val; + } + } + + // Convert aces from 1 to 11 if beneficial + while (aces > 0 && value + 10 <= 21) { + value += 10; + aces--; + } + + return value; +} + +void BlackjackView::deal_cards() { + // Clear hands + player_card_count = 0; + dealer_card_count = 0; + + // Deal initial cards + player_cards[player_card_count++] = draw_card(); + dealer_cards[dealer_card_count++] = draw_card(); + player_cards[player_card_count++] = draw_card(); + dealer_cards[dealer_card_count++] = draw_card(); + + dealer_hidden = true; + game_state = GameState::PLAYING; +} + +void BlackjackView::player_hit() { + if (player_card_count < MAX_CARDS_IN_HAND) { + player_cards[player_card_count++] = draw_card(); + + int value = calculate_hand_value(player_cards, player_card_count); + if (value > 21) { + // Bust! + cash = (cash >= bet) ? cash - bet : 0; + losses++; + game_state = GameState::GAME_OVER; + } + } +} + +void BlackjackView::player_stay() { + dealer_hidden = false; + game_state = GameState::DEALER_TURN; + dealer_timer = 0; +} + +void BlackjackView::dealer_turn() { + int dealer_value = calculate_hand_value(dealer_cards, dealer_card_count); + + if (dealer_value < 17 && dealer_card_count < MAX_CARDS_IN_HAND) { + dealer_cards[dealer_card_count++] = draw_card(); + } else { + check_game_over(); + } +} + +void BlackjackView::check_game_over() { + int player_value = calculate_hand_value(player_cards, player_card_count); + int dealer_value = calculate_hand_value(dealer_cards, dealer_card_count); + + if (player_value > 21) { + // Player bust (already handled) + } else if (dealer_value > 21) { + // Dealer bust - player wins + cash += bet; + wins++; + } else if (player_value > dealer_value) { + // Player wins + cash += bet; + wins++; + } else if (player_value < dealer_value) { + // Dealer wins + cash = (cash >= bet) ? cash - bet : 0; + losses++; + } + // Else it's a tie - no money changes hands + + if (cash > high_score) { + high_score = cash; + } + + game_state = GameState::GAME_OVER; +} + +void BlackjackView::update_game() { + switch (game_state) { + case GameState::MENU: + draw_menu(); + break; + + case GameState::BETTING: + draw_betting(); + break; + + case GameState::PLAYING: + draw_game(); + break; + + case GameState::DEALER_TURN: + if (++dealer_timer >= 60) { // 1 second delay + dealer_timer = 0; + dealer_turn(); + } + draw_game(); + break; + + case GameState::GAME_OVER: + draw_game(); + break; + + case GameState::STATS: + draw_stats(); + break; + } +} + +void BlackjackView::draw_menu_static() { + clear_screen(); + + auto style_title = *ui::Theme::getInstance()->fg_green; + auto style_text = *ui::Theme::getInstance()->fg_light; + auto style_rules = *ui::Theme::getInstance()->fg_cyan; + + painter.draw_string({84, 20}, style_title, "BLACKJACK"); + + // Draw rules + painter.draw_string({70, 55}, style_rules, "-- RULES --"); + painter.draw_string({61, 75}, style_text, "Get close to 21"); + painter.draw_string({61, 90}, style_text, "without going over"); + painter.draw_string({61, 110}, style_text, "Dealer hits on 16"); + painter.draw_string({61, 125}, style_text, "Dealer stays on 17"); + painter.draw_string({61, 145}, style_text, "Blackjack pays 1:1"); + + // Controls + painter.draw_string({65, 175}, style_rules, "-- CONTROLS --"); + painter.draw_string({61, 195}, style_text, "SELECT: Start/Hit"); + painter.draw_string({61, 210}, style_text, "LEFT: Stats"); + painter.draw_string({61, 225}, style_text, "RIGHT: Exit/Stay"); + + // Draw high score + painter.draw_string({61, 250}, style_text, "High Score: $" + std::to_string(high_score)); +} + +void BlackjackView::draw_menu() { + // Only handle the flashing text animation + if (++blink_counter >= 30) { + blink_counter = 0; + blink_state = !blink_state; + + if (blink_state) { + auto style = *ui::Theme::getInstance()->fg_yellow; + painter.draw_string({55, 280}, style, "* PRESS SELECT *"); + } else { + // Clear just the text area + painter.fill_rectangle({55, 278, 130, 20}, Color::black()); + } + } +} + +void BlackjackView::draw_stats() { + clear_screen(); + + auto style_title = *ui::Theme::getInstance()->fg_green; + auto style_text = *ui::Theme::getInstance()->fg_light; + auto style_value = *ui::Theme::getInstance()->fg_yellow; + + painter.draw_string({75, 30}, style_title, "STATISTICS"); + + painter.draw_string({30, 80}, style_text, "Wins:"); + painter.draw_string({150, 80}, style_value, std::to_string(wins)); + + painter.draw_string({30, 100}, style_text, "Losses:"); + painter.draw_string({150, 100}, style_value, std::to_string(losses)); + + // Win percentage + uint32_t total = wins + losses; + if (total > 0) { + uint32_t win_pct = (wins * 100) / total; + painter.draw_string({30, 120}, style_text, "Win %:"); + painter.draw_string({150, 120}, style_value, std::to_string(win_pct) + "%"); + } + + painter.draw_string({30, 160}, style_text, "High Score:"); + painter.draw_string({150, 160}, style_value, "$" + std::to_string(high_score)); + + painter.draw_string({30, 180}, style_text, "Cash:"); + painter.draw_string({150, 180}, style_value, "$" + std::to_string(cash)); + + painter.draw_string({40, 250}, style_text, "SELECT: Back"); +} + +void BlackjackView::draw_betting() { + static uint32_t last_bet = 0; + static bool betting_drawn = false; + + if (!betting_drawn) { + clear_screen(); + + auto style_title = *ui::Theme::getInstance()->fg_green; + auto style_text = *ui::Theme::getInstance()->fg_light; + + painter.draw_string({70, 40}, style_title, "PLACE BET"); + painter.draw_string({30, 80}, style_text, "Cash: $" + std::to_string(cash)); + + painter.draw_string({30, 140}, style_text, "ENCODER: +/- $10"); + painter.draw_string({30, 160}, style_text, "SELECT: Deal"); + + betting_drawn = true; + last_bet = 0; + } + + // Update bet display + if (bet != last_bet) { + painter.fill_rectangle({30, 110, 150, 20}, Color::black()); + painter.draw_string({30, 110}, *ui::Theme::getInstance()->fg_yellow, "Bet: $" + std::to_string(bet)); + last_bet = bet; + } +} + +void BlackjackView::draw_game() { + static int last_player_count = -1; + static int last_dealer_count = -1; + static GameState last_game_state = GameState::MENU; + static uint32_t last_bet = 0; + + // Clear and redraw if hands changed or game state changed + if (player_card_count != last_player_count || dealer_card_count != last_dealer_count || game_state != last_game_state) { + clear_screen(); + + auto style = *ui::Theme::getInstance()->fg_green; + painter.draw_string({10, 10}, style, "Cash: $" + std::to_string(cash)); + painter.draw_string({140, 10}, style, "Bet: $" + std::to_string(bet)); + + // Draw dealer hand with value next to label + auto style_value = *ui::Theme::getInstance()->fg_yellow; + painter.draw_string({10, 45}, *ui::Theme::getInstance()->fg_light, "Dealer:"); + if (!dealer_hidden || game_state == GameState::GAME_OVER) { + int dealer_value = calculate_hand_value(dealer_cards, dealer_card_count); + painter.draw_string({70, 45}, style_value, "(" + std::to_string(dealer_value) + ")"); + } + draw_hand(10, 65, dealer_cards, dealer_card_count, true); + + // Draw player hand with value next to label + painter.draw_string({10, 165}, *ui::Theme::getInstance()->fg_light, "You:"); + int player_value = calculate_hand_value(player_cards, player_card_count); + painter.draw_string({50, 165}, style_value, "(" + std::to_string(player_value) + ")"); + draw_hand(10, 185, player_cards, player_card_count, false); + + // Draw controls or result + if (game_state == GameState::PLAYING) { + auto style_text = *ui::Theme::getInstance()->fg_light; + painter.draw_string({30, 290}, style_text, "SELECT: Hit"); + painter.draw_string({130, 290}, style_text, "RIGHT: Stay"); + } else if (game_state == GameState::GAME_OVER) { + const Style* style_result = ui::Theme::getInstance()->fg_yellow; + std::string result; + + if (player_value > 21) { + result = "BUST! You Lose"; + style_result = ui::Theme::getInstance()->fg_red; + } else if (calculate_hand_value(dealer_cards, dealer_card_count) > 21) { + result = "Dealer Bust! Win!"; + style_result = ui::Theme::getInstance()->fg_green; + } else if (player_value > calculate_hand_value(dealer_cards, dealer_card_count)) { + result = "You Win!"; + style_result = ui::Theme::getInstance()->fg_green; + } else if (player_value < calculate_hand_value(dealer_cards, dealer_card_count)) { + result = "You Lose"; + style_result = ui::Theme::getInstance()->fg_red; + } else { + result = "Push (Tie)"; + } + + // Draw result + painter.draw_string({60, 270}, *style_result, result); + + // Draw compact bet selector in top right area + auto style_bet = *ui::Theme::getInstance()->fg_cyan; + painter.draw_string({140, 25}, style_bet, "Next: $" + std::to_string(bet)); + + // Show controls + painter.draw_string({10, 290}, *ui::Theme::getInstance()->fg_light, "SELECT: Deal ENC: +/-"); + } + + last_player_count = player_card_count; + last_dealer_count = dealer_card_count; + last_game_state = game_state; + last_bet = bet; + } else if (game_state == GameState::GAME_OVER && bet != last_bet) { + // Only update the bet display when it changes + painter.fill_rectangle({140, 25, 90, 16}, Color::black()); + painter.draw_string({140, 25}, *ui::Theme::getInstance()->fg_cyan, "Next: $" + std::to_string(bet)); + last_bet = bet; + } +} + +void BlackjackView::draw_suit_symbol(int x, int y, uint8_t suit, Color color, bool large) { + if (large) { + // Large suit symbols for center of card + switch (suit) { + case 0: // Spades + // Top curve + painter.fill_rectangle({x + 9, y + 4, 2, 2}, color); + painter.fill_rectangle({x + 8, y + 5, 4, 2}, color); + painter.fill_rectangle({x + 7, y + 6, 6, 2}, color); + painter.fill_rectangle({x + 6, y + 7, 8, 2}, color); + painter.fill_rectangle({x + 5, y + 8, 10, 2}, color); + painter.fill_rectangle({x + 4, y + 9, 12, 2}, color); + painter.fill_rectangle({x + 3, y + 10, 14, 2}, color); + painter.fill_rectangle({x + 2, y + 11, 16, 2}, color); + painter.fill_rectangle({x + 1, y + 12, 18, 2}, color); + painter.fill_rectangle({x + 0, y + 13, 20, 3}, color); + painter.fill_rectangle({x + 1, y + 16, 18, 2}, color); + painter.fill_rectangle({x + 2, y + 17, 16, 1}, color); + painter.fill_rectangle({x + 3, y + 18, 14, 1}, color); + // Stem + painter.fill_rectangle({x + 9, y + 19, 2, 4}, color); + painter.fill_rectangle({x + 8, y + 22, 4, 1}, color); + painter.fill_rectangle({x + 7, y + 23, 6, 1}, color); + painter.fill_rectangle({x + 6, y + 24, 8, 1}, color); + break; + + case 1: // Hearts + // Left bump + painter.fill_rectangle({x + 3, y + 5, 4, 2}, color); + painter.fill_rectangle({x + 2, y + 6, 6, 2}, color); + painter.fill_rectangle({x + 1, y + 7, 8, 3}, color); + painter.fill_rectangle({x + 0, y + 9, 9, 3}, color); + // Right bump + painter.fill_rectangle({x + 13, y + 5, 4, 2}, color); + painter.fill_rectangle({x + 12, y + 6, 6, 2}, color); + painter.fill_rectangle({x + 11, y + 7, 8, 3}, color); + painter.fill_rectangle({x + 11, y + 9, 9, 3}, color); + // Body + painter.fill_rectangle({x + 1, y + 11, 18, 3}, color); + painter.fill_rectangle({x + 2, y + 14, 16, 2}, color); + painter.fill_rectangle({x + 3, y + 16, 14, 2}, color); + painter.fill_rectangle({x + 4, y + 18, 12, 1}, color); + painter.fill_rectangle({x + 5, y + 19, 10, 1}, color); + painter.fill_rectangle({x + 6, y + 20, 8, 1}, color); + painter.fill_rectangle({x + 7, y + 21, 6, 1}, color); + painter.fill_rectangle({x + 8, y + 22, 4, 1}, color); + painter.fill_rectangle({x + 9, y + 23, 2, 1}, color); + break; + + case 2: // Diamonds + painter.fill_rectangle({x + 10, y + 3, 1, 1}, color); + painter.fill_rectangle({x + 9, y + 4, 3, 1}, color); + painter.fill_rectangle({x + 8, y + 5, 5, 1}, color); + painter.fill_rectangle({x + 7, y + 6, 7, 1}, color); + painter.fill_rectangle({x + 6, y + 7, 9, 1}, color); + painter.fill_rectangle({x + 5, y + 8, 11, 1}, color); + painter.fill_rectangle({x + 4, y + 9, 13, 1}, color); + painter.fill_rectangle({x + 3, y + 10, 15, 1}, color); + painter.fill_rectangle({x + 2, y + 11, 17, 1}, color); + painter.fill_rectangle({x + 1, y + 12, 19, 1}, color); + painter.fill_rectangle({x + 0, y + 13, 21, 1}, color); + painter.fill_rectangle({x + 1, y + 14, 19, 1}, color); + painter.fill_rectangle({x + 2, y + 15, 17, 1}, color); + painter.fill_rectangle({x + 3, y + 16, 15, 1}, color); + painter.fill_rectangle({x + 4, y + 17, 13, 1}, color); + painter.fill_rectangle({x + 5, y + 18, 11, 1}, color); + painter.fill_rectangle({x + 6, y + 19, 9, 1}, color); + painter.fill_rectangle({x + 7, y + 20, 7, 1}, color); + painter.fill_rectangle({x + 8, y + 21, 5, 1}, color); + painter.fill_rectangle({x + 9, y + 22, 3, 1}, color); + painter.fill_rectangle({x + 10, y + 23, 1, 1}, color); + break; + + case 3: // Clubs + // Center circle + painter.fill_rectangle({x + 8, y + 8, 5, 1}, color); + painter.fill_rectangle({x + 7, y + 9, 7, 3}, color); + painter.fill_rectangle({x + 8, y + 12, 5, 1}, color); + // Left circle + painter.fill_rectangle({x + 3, y + 11, 4, 1}, color); + painter.fill_rectangle({x + 2, y + 12, 6, 3}, color); + painter.fill_rectangle({x + 3, y + 15, 4, 1}, color); + // Right circle + painter.fill_rectangle({x + 14, y + 11, 4, 1}, color); + painter.fill_rectangle({x + 13, y + 12, 6, 3}, color); + painter.fill_rectangle({x + 14, y + 15, 4, 1}, color); + // Connect circles + painter.fill_rectangle({x + 6, y + 13, 9, 2}, color); + // Stem + painter.fill_rectangle({x + 9, y + 16, 3, 4}, color); + painter.fill_rectangle({x + 8, y + 19, 5, 1}, color); + painter.fill_rectangle({x + 7, y + 20, 7, 1}, color); + painter.fill_rectangle({x + 6, y + 21, 9, 1}, color); + break; + } + } else { + // Small suit symbols + switch (suit) { + case 0: // Spades - small + painter.fill_rectangle({x + 2, y + 1, 3, 3}, color); + painter.fill_rectangle({x + 1, y + 3, 5, 2}, color); + painter.fill_rectangle({x + 3, y + 5, 1, 2}, color); + break; + + case 1: // Hearts - small + painter.fill_rectangle({x + 1, y + 1, 2, 2}, color); + painter.fill_rectangle({x + 4, y + 1, 2, 2}, color); + painter.fill_rectangle({x + 1, y + 2, 5, 2}, color); + painter.fill_rectangle({x + 2, y + 4, 3, 1}, color); + painter.fill_rectangle({x + 3, y + 5, 1, 1}, color); + break; + + case 2: // Diamonds - small + painter.fill_rectangle({x + 3, y + 1, 1, 1}, color); + painter.fill_rectangle({x + 2, y + 2, 3, 1}, color); + painter.fill_rectangle({x + 1, y + 3, 5, 1}, color); + painter.fill_rectangle({x + 2, y + 4, 3, 1}, color); + painter.fill_rectangle({x + 3, y + 5, 1, 1}, color); + break; + + case 3: // Clubs - small + painter.fill_rectangle({x + 3, y + 1, 2, 2}, color); + painter.fill_rectangle({x + 1, y + 3, 2, 2}, color); + painter.fill_rectangle({x + 4, y + 3, 2, 2}, color); + painter.fill_rectangle({x + 3, y + 5, 2, 2}, color); + break; + } + } +} + +void BlackjackView::draw_card(int x, int y, uint8_t card, bool hidden) { + const int width = 60; + const int height = 80; + + // Draw card background + painter.fill_rectangle({x, y, width, height}, Color::white()); + painter.draw_rectangle({x, y, width, height}, Color::black()); + painter.draw_rectangle({x + 1, y + 1, width - 2, height - 2}, Color::grey()); // Inner border + + if (hidden) { + // Draw card back pattern - diagonal lines + for (int i = 4; i < width - 4; i += 6) { + for (int j = 4; j < height - 4; j += 6) { + painter.fill_rectangle({x + i, y + j, 3, 3}, Color::blue()); + painter.fill_rectangle({x + i + 3, y + j + 3, 3, 3}, Color::red()); + } + } + } else { + // Draw card value + uint8_t suit = get_card_suit(card); + Color suit_color = (suit == 1 || suit == 2) ? Color::red() : Color::black(); + + const auto* base_style = ui::Theme::getInstance()->fg_light; + Style card_style{ + .font = base_style->font, + .background = Color::white(), + .foreground = suit_color}; + + std::string value_str = get_card_string(card); + + // Draw value in top-left corner + painter.draw_string({x + 4, y + 4}, card_style, value_str); + + // Draw small suit symbol next to value + int suit_x = (value_str == "10") ? x + 20 : x + 12; + draw_suit_symbol(suit_x, y + 4, suit, suit_color, false); + + // Draw value in bottom-right corner + int bottom_x = (value_str == "10") ? x + width - 24 : x + width - 16; + painter.draw_string({bottom_x, y + height - 18}, card_style, value_str); + + // Draw small suit symbol in bottom-right + draw_suit_symbol(x + width - 10, y + height - 16, suit, suit_color, false); + + // Draw large suit symbol in center + draw_suit_symbol(x + 20, y + 28, suit, suit_color, true); + } +} + +void BlackjackView::draw_hand(int x, int y, uint8_t* cards, uint8_t count, bool is_dealer) { + // Calculate total width needed + const int card_width = 60; + const int overlap = 40; // Amount of overlap when cards need to fit + const int max_width = 230 - x; // Available width on screen + + int spacing; + if (count == 1) { + spacing = 0; + } else if (count == 2) { + spacing = card_width + 5; // Small gap for 2 cards + } else { + // Calculate spacing to fit all cards + int total_overlap_width = card_width + (count - 1) * overlap; + if (total_overlap_width <= max_width) { + spacing = overlap; + } else { + // Need more overlap to fit + spacing = (max_width - card_width) / (count - 1); + } + } + + for (uint8_t i = 0; i < count; i++) { + bool hide = is_dealer && dealer_hidden && i == 1; + int card_x = x + (i * spacing); + draw_card(card_x, y, cards[i], hide); + } +} + +bool BlackjackView::on_key(const KeyEvent key) { + if (key == KeyEvent::Select) { + switch (game_state) { + case GameState::MENU: + if (cash < 10) { + cash = 100; // Reset if broke - maybe should provide https://gamblersanonymous.org/ link if they also lost their wife and house + wins = 0; + losses = 0; + } + game_state = GameState::BETTING; + break; + + case GameState::BETTING: + deal_cards(); + break; + + case GameState::PLAYING: + player_hit(); + break; + + case GameState::GAME_OVER: + // Deal new hand with current bet + if (cash >= bet) { + deal_cards(); + } else if (cash >= 10) { + // Not enough for current bet, reduce to what they can afford + bet = 10; + deal_cards(); + } else { + // Broke, reset game + cash = 100; + wins = 0; + losses = 0; + game_state = GameState::MENU; + draw_menu_static(); + } + break; + + case GameState::STATS: + game_state = GameState::MENU; + draw_menu_static(); + break; + + default: + break; + } + return true; + } else if (key == KeyEvent::Left) { + if (game_state == GameState::MENU) { + game_state = GameState::STATS; + } + return true; + } else if (key == KeyEvent::Right) { + if (game_state == GameState::MENU) { + nav_.pop(); + return true; + } else if (game_state == GameState::PLAYING) { + player_stay(); + return true; + } + } + + return false; +} + +bool BlackjackView::on_encoder(const EncoderEvent delta) { + if (game_state == GameState::BETTING || game_state == GameState::GAME_OVER) { + if (delta > 0 && bet + 10 <= cash) { + bet += 10; + } else if (delta < 0 && bet >= 20) { + bet -= 10; + } + return true; + } + + return false; +} + +} // namespace ui::external_app::blackjack diff --git a/firmware/application/external/blackjack/ui_blackjack.hpp b/firmware/application/external/blackjack/ui_blackjack.hpp new file mode 100644 index 000000000..90d8d8cb6 --- /dev/null +++ b/firmware/application/external/blackjack/ui_blackjack.hpp @@ -0,0 +1,156 @@ +/* + * Blackjack Game for Portapack Mayhem + * Ported / Enhanced / Graphically made awesome by RocketGod (https://betaskynet.com) + * Based on BlackJack 83 for TI Calculator by Harper Maddox (was written in Assembly) + */ + +#ifndef __UI_BLACKJACK_H__ +#define __UI_BLACKJACK_H__ + +#include "ui.hpp" +#include "ui_navigation.hpp" +#include "event_m0.hpp" +#include "message.hpp" +#include "irq_controls.hpp" +#include "random.hpp" +#include "lpc43xx_cpp.hpp" +#include "ui_widget.hpp" +#include "app_settings.hpp" +#include +#include + +namespace ui::external_app::blackjack { + +// Game states +enum class GameState { + MENU, + BETTING, + PLAYING, + DEALER_TURN, + GAME_OVER, + STATS +}; + +// Card structure +struct Card { + uint8_t value; // 1-13 (Ace=1, Jack=11, Queen=12, King=13) + uint8_t suit; // 0=Spades, 1=Hearts, 2=Diamonds, 3=Clubs + + Card() + : value(0), suit(0) {} + Card(uint8_t v, uint8_t s) + : value(v), suit(s) {} +}; + +// Timer class +using Callback = void (*)(void); + +class Ticker { + public: + Ticker() = default; + void attach(Callback func, double delay_sec); + void detach(); +}; + +// Function declarations +void check_game_timer(); +void game_timer_check(); + +class BlackjackView : public View { + public: + BlackjackView(NavigationView& nav); + ~BlackjackView(); + + void on_show() override; + std::string title() const override { return "Blackjack"; } + void focus() override { dummy.focus(); } + void paint(Painter& painter) override; + void frame_sync(); + bool on_encoder(const EncoderEvent event) override; + bool on_key(KeyEvent key) override; + + // Public for timer callback + GameState game_state = GameState::MENU; + void update_game(); + + private: + NavigationView& nav_; + bool initialized = false; + + // Game variables + static constexpr uint8_t MAX_CARDS_IN_HAND = 11; // Maximum possible cards before bust + uint8_t deck[52]; + uint8_t deck_position = 0; + + uint8_t player_cards[MAX_CARDS_IN_HAND]; + uint8_t player_card_count = 0; + + uint8_t dealer_cards[MAX_CARDS_IN_HAND]; + uint8_t dealer_card_count = 0; + + // Game state variables + uint32_t cash = 100; + uint32_t bet = 10; + uint32_t wins = 0; + uint32_t losses = 0; + uint32_t high_score = 100; + bool dealer_hidden = true; + + // UI state + uint8_t menu_selection = 0; + bool blink_state = true; + uint32_t blink_counter = 0; + uint32_t dealer_timer = 0; + + // Timer + Ticker game_timer; + + // Methods + void init_deck(); + void shuffle_deck(); + uint8_t draw_card(); + void deal_cards(); + int calculate_hand_value(uint8_t* cards, uint8_t count); + int get_card_value(uint8_t card); + uint8_t get_card_suit(uint8_t card); + std::string get_card_string(uint8_t card); + void player_hit(); + void player_stay(); + void dealer_turn(); + void check_game_over(); + void reset_game(); + + // Drawing methods + void draw_menu(); + void draw_menu_static(); + void draw_stats(); + void draw_game(); + void draw_betting(); + void draw_card(int x, int y, uint8_t card, bool hidden = false); + void draw_hand(int x, int y, uint8_t* cards, uint8_t count, bool is_dealer = false); + void draw_suit_symbol(int x, int y, uint8_t suit, Color color, bool large); + void clear_screen(); + + // Settings + app_settings::SettingsManager settings_{ + "blackjack", + app_settings::Mode::NO_RF, + {{"cash"sv, &cash}, + {"wins"sv, &wins}, + {"losses"sv, &losses}, + {"highscore"sv, &high_score}}}; + + Button dummy{ + {240, 0, 0, 0}, + ""}; + + MessageHandlerRegistration message_handler_frame_sync{ + Message::ID::DisplayFrameSync, + [this](const Message* const) { + this->frame_sync(); + }}; +}; + +} // namespace ui::external_app::blackjack + +#endif /* __UI_BLACKJACK_H__ */ diff --git a/firmware/application/external/external.cmake b/firmware/application/external/external.cmake index 581bbf3d0..216ebd158 100644 --- a/firmware/application/external/external.cmake +++ b/firmware/application/external/external.cmake @@ -224,6 +224,10 @@ set(EXTCPPSRC #space_invaders external/spaceinv/main.cpp external/spaceinv/ui_spaceinv.cpp + + #blackjack + external/blackjack/main.cpp + external/blackjack/ui_blackjack.cpp ) set(EXTAPPLIST @@ -281,4 +285,5 @@ set(EXTAPPLIST gfxeq detector_rx spaceinv + blackjack ) diff --git a/firmware/application/external/external.ld b/firmware/application/external/external.ld index 9a8963428..5bf63d44a 100644 --- a/firmware/application/external/external.ld +++ b/firmware/application/external/external.ld @@ -77,6 +77,7 @@ MEMORY ram_external_app_detector_rx (rwx) : org = 0xADE40000, len = 32k ram_external_app_dinogame (rwx) : org = 0xADE50000, len = 32k ram_external_app_spaceinv (rwx) : org = 0xADE60000, len = 32k + ram_external_app_blackjack (rwx) : org = 0xADE70000, len = 32k } SECTIONS @@ -405,5 +406,11 @@ SECTIONS *(*ui*external_app*spaceinv*); } > ram_external_app_spaceinv + .external_app_blackjack : ALIGN(4) SUBALIGN(4) + { + KEEP(*(.external_app.app_blackjack.application_information)); + *(*ui*external_app*blackjack*); + } > ram_external_app_blackjack + }