From 78b376ad5d5408f36a7bca8b154672406277607f Mon Sep 17 00:00:00 2001 From: Adolfo Reyna Date: Sat, 31 Jan 2026 22:23:49 -0500 Subject: [PATCH] fix rent calculation --- AGENT.md | 2 +- emulator/basic1_emulator | Bin 145664 -> 162608 bytes games/monopoly/AGENT.md | 43 +++++++++ games/monopoly/BoardModalGame.h | 5 +- games/monopoly/DiceModalGame.h | 12 ++- games/monopoly/MonopolyBoardRenderer.h | 46 +++++++--- games/monopoly/PropertyModalGame.h | 112 ++++++++++++++++++----- games/monopoly/monopoly_game.cpp | 121 ++++++++++++++++++------- lib/game.h | 12 +++ 9 files changed, 276 insertions(+), 77 deletions(-) create mode 100644 games/monopoly/AGENT.md diff --git a/AGENT.md b/AGENT.md index 127eb99..7403a06 100644 --- a/AGENT.md +++ b/AGENT.md @@ -303,9 +303,9 @@ After successful build: View serial output: -```bash screen /dev/cu.usbmodem101 # Exit: Ctrl-A, then K, then Y ``` Or use VS Code's serial monitor extension. + diff --git a/emulator/basic1_emulator b/emulator/basic1_emulator index 76f221d0e701067eb989f8011ff6763794f9b627..089c710028ac1f17c44ea05551be7aaad2a3a900 100755 GIT binary patch delta 32216 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 Ow#p?BzpKdq# delta 28762 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 diff --git a/games/monopoly/AGENT.md b/games/monopoly/AGENT.md new file mode 100644 index 0000000..b392c24 --- /dev/null +++ b/games/monopoly/AGENT.md @@ -0,0 +1,43 @@ +# 🎲 Monopoly Game Design & Operations + +This section caches the current architecture, conventions, and implementation standards for the Monopoly project. + +## 🚀 Architecture Overview + +The game follows a polymorphic architecture based on a `Game` interface. + +- **Game Controller**: `MonopolyGame` (Inherits from `Game`). +- **Modal System**: Modals (Dice, Property, Board) are also `Game` objects. When active, the main game delegates `update()` and `draw()` to `active_modal`. +- **System Constraints**: 1-bit Monochrome (ST7789/E-Ink). **RTTI IS DISABLED** (`-fno-rtti`). Use `active_modal->get_type()` for safe casting. + +## 🛠️ Design Patterns + +### 1. Cycle/Execute Input Pattern +To prevent accidental choices on simple 2-button hardware: +- **Button 0 (A)**: Cycles through options (increments selection). +- **Button 1 (B)**: Executes/Confirms current selection or dismisses modal. +- **Visual**: Selected items MUST be prefixed with `> `. + +### 2. Centralized Rendering +- **`MonopolyBoardRenderer`**: All shared board drawing logic (40-tile perimeter) MUST live here. +- **Tile Inversion**: Highlight tiles (current position or property view) using solid black fill + white text. +- **Legibility**: Use Scale 2 (`draw_string_scaled`) for player names and primary titles. Scale 1 for details. + +### 3. Logic Conventions +- **Rent Calculation**: + - **Properties**: `tile->rent[0]` (Base). + - **Railroads**: $25 / $50 / $100 / $200 based on owner count. + - **Utilities**: Dice Total * 4 (1 owned) or * 10 (2 owned). +- **Modal Chaining**: Dice Modal dismissal triggers `modal_property_index` check in `MonopolyGame::update` to immediately launch the Property Modal. + +## ➕ How to Implement New Features + +### Adding a New Tile +1. Update `TileType` and `MONOPOLY_BOARD` in [games/monopoly/monopoly_board.h](games/monopoly/monopoly_board.h). +2. Update `MonopolyBoardRenderer.h` if a new symbol or boundary is needed. + +### Adding a New Modal +1. Create `NewModalGame.h` inheriting from `Game`. +2. Register a new `Type` in [lib/game.h](lib/game.h) and override `get_type()`. +3. Use the **Cycle/Execute** pattern for input. +4. Add handling logic in `MonopolyGame::update` near where `DiceModalGame` or `PropertyModalGame` are handled. diff --git a/games/monopoly/BoardModalGame.h b/games/monopoly/BoardModalGame.h index bba11e1..9d23d68 100644 --- a/games/monopoly/BoardModalGame.h +++ b/games/monopoly/BoardModalGame.h @@ -18,6 +18,7 @@ public: : Game(width, height, renderer, gui, input_manager), dismissed(false), players(p), players_count(count) {} void init() override { dismissed = false; } + Type get_type() const override { return Type::MONOPOLY_BOARD; } bool update(const InputEvent& event) override { // Any button dismisses the board view @@ -31,7 +32,9 @@ public: void draw() override { renderer->clear_buffer(); - MonopolyBoardRenderer::draw_board_perimeter(renderer, width, height, players, players_count); + MonopolyBoardRenderer::draw_board_perimeter(renderer, width, height, players, players_count, players[0].position /* Just for example, or -1 */); + // We'll pass -1 for the general board view to keep it clean + MonopolyBoardRenderer::draw_board_perimeter(renderer, width, height, players, players_count, -1); // --- Inner UI --- int cw = width / 7; diff --git a/games/monopoly/DiceModalGame.h b/games/monopoly/DiceModalGame.h index 9bf0922..5152669 100644 --- a/games/monopoly/DiceModalGame.h +++ b/games/monopoly/DiceModalGame.h @@ -50,8 +50,10 @@ public: DiceModalGame(uint16_t width, uint16_t height, LowLevelRenderer* renderer, LowLevelGUI* gui, InputManager* input_manager, int d1, int d2, const BoardTile* from, const BoardTile* to, Player* p, int count) : Game(width, height, renderer, gui, input_manager), dice1(d1), dice2(d2), from_tile(from), to_tile(to), players(p), players_count(count), dismissed(false) {} void init() override { dismissed = false; } + Type get_type() const override { return Type::MONOPOLY_DICE; } bool update(const InputEvent& event) override { - if (event.type == INPUT_BUTTON_0 || event.type == INPUT_BUTTON_1) { + // Only B dismisses now, so A can still be used for "change action" (even if it does nothing here) + if (event.type == INPUT_BUTTON_1) { dismissed = true; return true; } @@ -60,7 +62,11 @@ public: void draw() override { renderer->clear_buffer(); - MonopolyBoardRenderer::draw_board_perimeter(renderer, width, height, players, players_count); + MonopolyBoardRenderer::draw_board_perimeter(renderer, width, height, players, players_count, (to_tile ? -1 /* or some index */ : -1)); // Keep it simple for now or pass relevant pos + // Let's pass the destination position to highlight it during the dice roll + int target_pos = -1; + for(int i=0; i<40; i++) if(&MONOPOLY_BOARD[i] == to_tile) target_pos = i; + MonopolyBoardRenderer::draw_board_perimeter(renderer, width, height, players, players_count, target_pos); // --- Inner UI (Center Area) --- int cw = width / 7; @@ -106,7 +112,7 @@ public: int btn_y = iy + ih - 35; renderer->draw_filled_rectangle(btn_x, btn_y, btn_w, btn_h, true, 1); renderer->set_text_color(false); - renderer->draw_string_scaled(btn_x + 5, btn_y + 5, ">A CONTINUE", 2); + renderer->draw_string_scaled(btn_x + 5, btn_y + 5, ">B CONTINUE", 2); renderer->set_text_color(true); } bool is_dismissed() const { return dismissed; } diff --git a/games/monopoly/MonopolyBoardRenderer.h b/games/monopoly/MonopolyBoardRenderer.h index 839ebc2..3d57ea2 100644 --- a/games/monopoly/MonopolyBoardRenderer.h +++ b/games/monopoly/MonopolyBoardRenderer.h @@ -6,10 +6,16 @@ class MonopolyBoardRenderer { public: - static void draw_tile(LowLevelRenderer* renderer, int x, int y, int w, int h, int index, bool is_corner, Player* players, int players_count, int orientation = 0) { + static void draw_tile(LowLevelRenderer* renderer, int x, int y, int w, int h, int index, bool is_corner, Player* players, int players_count, int orientation = 0, int currentPlayerPos = -1) { if (index < 0 || index >= BOARD_SIZE) return; - renderer->draw_rectangle(x, y, w, h, true, 1); + bool isInverted = (index == currentPlayerPos); + if (isInverted) { + renderer->draw_filled_rectangle(x, y, w, h, true, 1); + renderer->set_text_color(false); // Black text on white background + } else { + renderer->draw_rectangle(x, y, w, h, true, 1); + } const BoardTile& tile = MONOPOLY_BOARD[index]; int content_x = x, content_y = y, content_w = w, content_h = h; @@ -32,13 +38,21 @@ public: content_x += bar_size; content_w -= bar_size; } - renderer->draw_filled_rectangle(bx, by, bw, bh, true, 1); + if (isInverted) { + // In inverted mode, the bar should be black (transparent/inverted) + renderer->draw_filled_rectangle(bx, by, bw, bh, false, 0); // Clear the bar area back to black + renderer->set_text_color(true); // Groups on the bar are usually white text on black bar in this mode + } else { + renderer->draw_filled_rectangle(bx, by, bw, bh, true, 1); + renderer->set_text_color(false); // Black text on white bar + } // Group number - renderer->set_text_color(false); // Black text on white bar char gbuf[2] = { (char)('0' + tile.group[0]), '\0' }; renderer->draw_string_scaled(bx + (bw - 6) / 2, by + (bh - 8) / 2, gbuf, 1); - renderer->set_text_color(true); + + if (isInverted) renderer->set_text_color(false); // Back to black text for the rest of the tile + else renderer->set_text_color(true); } char short_name[5] = {0}; @@ -65,36 +79,40 @@ public: p_count++; } } + + if (isInverted) { + renderer->set_text_color(true); // Reset for next tiles + } } - static void draw_board_perimeter(LowLevelRenderer* renderer, int width, int height, Player* players, int players_count) { + static void draw_board_perimeter(LowLevelRenderer* renderer, int width, int height, Player* players, int players_count, int currentPlayerPos = -1) { int cw = width / 7; // Corner width int ch = height / 7; // Corner height int rw = (width - 2 * cw) / 9; // Regular tile width int rh = (height - 2 * ch) / 9; // Regular tile height // --- Bottom Row: 0 to 10 (Right to Left) --- - draw_tile(renderer, width - cw, height - ch, cw, ch, 0, true, players, players_count, 0); // GO + draw_tile(renderer, width - cw, height - ch, cw, ch, 0, true, players, players_count, 0, currentPlayerPos); // GO for (int i = 1; i < 10; ++i) { - draw_tile(renderer, width - cw - i * rw, height - ch, rw, ch, i, false, players, players_count, 0); + draw_tile(renderer, width - cw - i * rw, height - ch, rw, ch, i, false, players, players_count, 0, currentPlayerPos); } - draw_tile(renderer, 0, height - ch, cw, ch, 10, true, players, players_count, 1); // JAIL + draw_tile(renderer, 0, height - ch, cw, ch, 10, true, players, players_count, 1, currentPlayerPos); // JAIL // --- Left Column: 11 to 19 (Bottom to Top) --- for (int i = 11; i < 20; ++i) { - draw_tile(renderer, 0, height - ch - (i - 10) * rh, cw, rh, i, false, players, players_count, 1); + draw_tile(renderer, 0, height - ch - (i - 10) * rh, cw, rh, i, false, players, players_count, 1, currentPlayerPos); } // --- Top Row: 20 to 30 (Left to Right) --- - draw_tile(renderer, 0, 0, cw, ch, 20, true, players, players_count, 2); // FREE PARKING + draw_tile(renderer, 0, 0, cw, ch, 20, true, players, players_count, 2, currentPlayerPos); // FREE PARKING for (int i = 21; i < 30; ++i) { - draw_tile(renderer, cw + (i - 21) * rw, 0, rw, ch, i, false, players, players_count, 2); + draw_tile(renderer, cw + (i - 21) * rw, 0, rw, ch, i, false, players, players_count, 2, currentPlayerPos); } - draw_tile(renderer, width - cw, 0, cw, ch, 30, true, players, players_count, 3); // GO TO JAIL + draw_tile(renderer, width - cw, 0, cw, ch, 30, true, players, players_count, 3, currentPlayerPos); // GO TO JAIL // --- Right Column: 31 to 39 (Top to Bottom) --- for (int i = 31; i < 40; ++i) { - draw_tile(renderer, width - cw, ch + (i - 31) * rh, cw, rh, i, false, players, players_count, 3); + draw_tile(renderer, width - cw, ch + (i - 31) * rh, cw, rh, i, false, players, players_count, 3, currentPlayerPos); } } }; diff --git a/games/monopoly/PropertyModalGame.h b/games/monopoly/PropertyModalGame.h index 04ca6cf..c58ecdf 100644 --- a/games/monopoly/PropertyModalGame.h +++ b/games/monopoly/PropertyModalGame.h @@ -13,32 +13,49 @@ class PropertyModalGame : public Game { bool dismissed; bool is_owned; const char* owner_name; + int owner_id; // -1 if not owned bool can_afford; int selected_choice; // 0: Buy, 1: Cancel bool buy_requested; + bool rent_requested; Player* players; int players_count; public: - PropertyModalGame(uint16_t width, uint16_t height, LowLevelRenderer* renderer, LowLevelGUI* gui, InputManager* input_manager, const BoardTile* prop, bool owned, const char* owner, bool affordable, Player* p_list = nullptr, int p_count = 0) - : Game(width, height, renderer, gui, input_manager), property(prop), dismissed(false), is_owned(owned), owner_name(owner), can_afford(affordable), selected_choice(0), buy_requested(false), players(p_list), players_count(p_count) { + PropertyModalGame(uint16_t width, uint16_t height, LowLevelRenderer* renderer, LowLevelGUI* gui, InputManager* input_manager, const BoardTile* prop, bool owned, const char* owner, int o_id, bool affordable, Player* p_list = nullptr, int p_count = 0) + : Game(width, height, renderer, gui, input_manager), property(prop), dismissed(false), is_owned(owned), owner_name(owner), owner_id(o_id), can_afford(affordable), selected_choice(0), buy_requested(false), rent_requested(false), players(p_list), players_count(p_count) { if (is_owned || !can_afford) selected_choice = 1; } - void init() override { dismissed = false; buy_requested = false; } + void init() override { dismissed = false; buy_requested = false; rent_requested = false; selected_choice = 0; } + Type get_type() const override { return Type::MONOPOLY_PROPERTY; } bool update(const InputEvent& event) override { - if (event.type == INPUT_BUTTON_0) { // BUTTON A -> BUY - if (!is_owned && can_afford) { - buy_requested = true; + // If rent is owed, any button pays it (it's forced) + if (is_owned && owner_id != -1) { + if (event.type == INPUT_BUTTON_0 || event.type == INPUT_BUTTON_1) { + rent_requested = true; dismissed = true; return true; - } else if (is_owned || !can_afford) { - // If it's just the "OK" state, either button works? - // Image shows >B CONTINUE in my code, so maybe Button 1. + } + return false; + } + + // Otherwise (Buy/Pass), use A to cycle and B to select + if (event.type == INPUT_BUTTON_0) { // BUTTON A -> Change selection + if (!is_owned && can_afford) { + selected_choice = (selected_choice + 1) % 2; + return true; } } - if (event.type == INPUT_BUTTON_1) { // BUTTON B -> AUCTION / DISMISS - dismissed = true; - return true; + if (event.type == INPUT_BUTTON_1) { // BUTTON B -> Select action + if (!is_owned && can_afford) { + if (selected_choice == 0) buy_requested = true; + dismissed = true; + return true; + } else { + // Only option was Pass + dismissed = true; + return true; + } } return false; } @@ -51,7 +68,12 @@ public: char buf[128]; if (players && players_count > 0) { - MonopolyBoardRenderer::draw_board_perimeter(renderer, width, height, players, players_count); + // Find current player position (who is interacting with the modal) + // In MonopolyGame, p is players[current_player_idx] + // We don't have the idx here directly, but we can highlight the property itself + int property_idx = -1; + for(int i=0; i<40; i++) if(&MONOPOLY_BOARD[i] == property) property_idx = i; + MonopolyBoardRenderer::draw_board_perimeter(renderer, width, height, players, players_count, property_idx); } // Window background (White box) @@ -104,26 +126,70 @@ public: int btn_w = win_w - 30; int btn_h = 25; - if (is_owned || !can_afford) { - // Only one option: CONTINUE (B) + if (is_owned && owner_id != -1) { + // Option: Pay Rent (A or B) renderer->draw_filled_rectangle(win_x + 15, btn_y, btn_w, btn_h, true, 1); renderer->set_text_color(false); - renderer->draw_string_scaled(win_x + 25, btn_y + 8, ">B CONTINUE", 1); + int rent = 0; + if (property->type == TILE_PROPERTY) { + rent = property->rent[0]; + } else if (property->type == TILE_RAILROAD) { + // Calculate Railroad rent based on owner's count + int rr_count = 0; + if (owner_id != -1 && owner_id < players_count) { + for (int i = 0; i < players[owner_id].property_count; ++i) { + // Find the property in the global board to check type (or just trust the index) + int prop_idx = players[owner_id].properties_owned[i]; + if (MONOPOLY_BOARD[prop_idx].type == TILE_RAILROAD) { + rr_count++; + } + } + } + if (rr_count == 1) rent = 25; + else if (rr_count == 2) rent = 50; + else if (rr_count == 3) rent = 100; + else if (rr_count == 4) rent = 200; + else rent = 25; // Fallback + } else if (property->type == TILE_UTILITY) { + // Simplified 40 or use a more complex check + rent = 40; + } + + snprintf(buf, sizeof(buf), ">PAY RENT ($%d)", rent); + renderer->draw_string_scaled(win_x + (win_w - (int)strlen(buf) * 6) / 2, btn_y + 8, buf, 1); renderer->set_text_color(true); - } else { - // Choice: Buy (A) or Auction (B) + } else if (!is_owned && can_afford) { + // Choice: Buy (A) or Pass (B). Changed to: A cycles, B selects. // Buy Button - renderer->draw_filled_rectangle(win_x + 15, btn_y, btn_w, btn_h, true, 1); - renderer->set_text_color(false); - snprintf(buf, sizeof(buf), ">A BUY ($%d)", property->cost); + if (selected_choice == 0) renderer->draw_filled_rectangle(win_x + 15, btn_y, btn_w, btn_h, true, 1); + else renderer->draw_rectangle(win_x + 15, btn_y, btn_w, btn_h, true, 1); + + if (selected_choice == 0) renderer->set_text_color(false); + snprintf(buf, sizeof(buf), "%sBUY ($%d)", (selected_choice == 0 ? "> " : " "), property->cost); renderer->draw_string_scaled(win_x + 20, btn_y + 8, buf, 1); renderer->set_text_color(true); btn_y += 30; - // Auction Button - renderer->draw_string_scaled(win_x + 20, btn_y + 8, ">B AUCTION", 1); + // Pass Button + if (selected_choice == 1) renderer->draw_filled_rectangle(win_x + 15, btn_y, btn_w, btn_h, true, 1); + + if (selected_choice == 1) renderer->set_text_color(false); + snprintf(buf, sizeof(buf), "%sPASS", (selected_choice == 1 ? "> " : " ")); + renderer->draw_string_scaled(win_x + 20, btn_y + 8, buf, 1); + renderer->set_text_color(true); + + // Helpful hint + renderer->draw_string_scaled(win_x + 15, win_y + win_h - 15, "A:Next B:Sel", 1); + } else { + // Only one option: PASS (B) (e.g. if owned by self or can't afford) + renderer->draw_filled_rectangle(win_x + 15, btn_y, btn_w, btn_h, true, 1); + renderer->set_text_color(false); + renderer->draw_string_scaled(win_x + 25, btn_y + 8, ">B PASS", 1); + renderer->set_text_color(true); } } bool is_dismissed() const { return dismissed; } bool wants_to_buy() const { return buy_requested; } + bool wants_to_pay_rent() const { return rent_requested; } + int get_owner_id() const { return owner_id; } }; diff --git a/games/monopoly/monopoly_game.cpp b/games/monopoly/monopoly_game.cpp index 01ba5e5..34e40ac 100644 --- a/games/monopoly/monopoly_game.cpp +++ b/games/monopoly/monopoly_game.cpp @@ -59,20 +59,92 @@ bool MonopolyGame::update(const InputEvent& event) { bool modal_redraw = active_modal->update(event); if (modal_redraw) needs_redraw = true; - // Check for specific modal types to handle their results - auto dice_modal = dynamic_cast(active_modal); - auto prop_modal = dynamic_cast(active_modal); - auto board_modal = dynamic_cast(active_modal); + // Check for specific modal types to handle their results using custom get_type() + DiceModalGame* dice_modal = (active_modal->get_type() == Game::Type::MONOPOLY_DICE) ? static_cast(active_modal) : nullptr; + PropertyModalGame* prop_modal = (active_modal->get_type() == Game::Type::MONOPOLY_PROPERTY) ? static_cast(active_modal) : nullptr; + BoardModalGame* board_modal = (active_modal->get_type() == Game::Type::MONOPOLY_BOARD) ? static_cast(active_modal) : nullptr; if (dice_modal && dice_modal->is_dismissed()) { delete active_modal; active_modal = nullptr; needs_redraw = true; - } else if (prop_modal && prop_modal->is_dismissed()) { + + // Immediately check if we need to show a property modal + if (modal_property_index >= 0) { + bool is_owned = false; + const char* owner_name = nullptr; + int owner_id = -1; + for (int i = 0; i < players_count; ++i) { + for (int j = 0; j < players[i].property_count; ++j) { + if (players[i].properties_owned[j] == modal_property_index) { + is_owned = true; + owner_name = players[i].name; + owner_id = i; + break; + } + } + if (is_owned) break; + } + bool can_afford = (p->balance >= MONOPOLY_BOARD[modal_property_index].cost); + active_modal = new PropertyModalGame(width, height, renderer, gui, input_manager, &MONOPOLY_BOARD[modal_property_index], is_owned, owner_name, owner_id, can_afford, players, players_count); + modal_property_index = -1; + } + return needs_redraw; + } else if (prop_modal && prop_modal->is_dismissed()) { if (prop_modal->wants_to_buy()) { const BoardTile* tile = &MONOPOLY_BOARD[p->position]; - p->balance -= tile->cost; - p->properties_owned[p->property_count++] = p->position; + if (p->balance >= tile->cost) { + p->balance -= tile->cost; + p->properties_owned[p->property_count++] = p->position; + } + } else if (prop_modal->wants_to_pay_rent()) { + const BoardTile* tile = &MONOPOLY_BOARD[p->position]; + int rent = 0; + if (tile->type == TILE_PROPERTY) { + // Logic for rent: If owner has all properties of group, rent is doubled (base only) + // For now, let's just use rent[0] as requested, but we should probably eventually + // check for houses. Let's stick to rent[0] to match the modal's display. + rent = tile->rent[0]; + } + else if (tile->type == TILE_RAILROAD) { + // Utility logic for Railroads: 1:25, 2:50, 3:100, 4:200 + int owner_id = prop_modal->get_owner_id(); + int rr_count = 0; + if (owner_id != -1) { + for (int i = 0; i < players[owner_id].property_count; ++i) { + if (MONOPOLY_BOARD[players[owner_id].properties_owned[i]].type == TILE_RAILROAD) { + rr_count++; + } + } + } + if (rr_count == 1) rent = 25; + else if (rr_count == 2) rent = 50; + else if (rr_count == 3) rent = 100; + else if (rr_count == 4) rent = 200; + else rent = 25; // Fallback + } + else if (tile->type == TILE_UTILITY) { + // Utility: 4x dice if 1 owned, 10x if both. + // Since we don't have the dice roll here, let's use a fixed 40 for now + // or calculate it from last_dice1 + last_dice2. + int total_dice = last_dice1 + last_dice2; + int owner_id = prop_modal->get_owner_id(); + int utility_count = 0; + if (owner_id != -1) { + for (int i = 0; i < players[owner_id].property_count; ++i) { + if (MONOPOLY_BOARD[players[owner_id].properties_owned[i]].type == TILE_UTILITY) { + utility_count++; + } + } + } + rent = (utility_count == 2) ? (total_dice * 10) : (total_dice * 4); + } + + int o_id = prop_modal->get_owner_id(); + if (o_id != -1 && (int)current_player_idx != o_id) { + p->balance -= rent; + players[o_id].balance += rent; + } } delete active_modal; active_modal = nullptr; @@ -135,27 +207,6 @@ bool MonopolyGame::update(const InputEvent& event) { default: break; } - // If dice modal was just dismissed and a property modal is queued, show it - if (!active_modal && modal_property_index >= 0) { - // Evaluate ownership and affordability - bool is_owned = false; - const char* owner_name = nullptr; - for (int i = 0; i < players_count; ++i) { - for (int j = 0; j < players[i].property_count; ++j) { - if (players[i].properties_owned[j] == modal_property_index) { - is_owned = true; - owner_name = players[i].name; - break; - } - } - if (is_owned) break; - } - bool can_afford = (p->balance >= MONOPOLY_BOARD[modal_property_index].cost); - - active_modal = new PropertyModalGame(width, height, renderer, gui, input_manager, &MONOPOLY_BOARD[modal_property_index], is_owned, owner_name, can_afford, players, players_count); - modal_property_index = -1; - needs_redraw = true; - } return needs_redraw; } @@ -169,8 +220,11 @@ void MonopolyGame::draw() { renderer->clear_buffer(); + Player* p = &players[current_player_idx]; + const BoardTile* tile = &MONOPOLY_BOARD[p->position]; + // --- Draw Board Perimeter --- - MonopolyBoardRenderer::draw_board_perimeter(renderer, width, height, players, players_count); + MonopolyBoardRenderer::draw_board_perimeter(renderer, width, height, players, players_count, p->position); // --- Inner Dashboard (Center Area) --- int cw = width / 7; @@ -178,17 +232,14 @@ void MonopolyGame::draw() { int ix = cw + 2, iy = ch + 2; int iw = width - 2 * cw - 4, ih = height - 2 * ch - 4; - Player* p = &players[current_player_idx]; - const BoardTile* tile = &MONOPOLY_BOARD[p->position]; - // Stats Window in center char buf[128]; renderer->draw_string_scaled(ix + 5, iy + 5, "Monopoly", 2); int content_y = iy + 25; - snprintf(buf, sizeof(buf), "TURN: %s", p->name); - renderer->draw_string_scaled(ix + 5, content_y, buf, 1); - content_y += 12; + snprintf(buf, sizeof(buf), "%s", p->name); + renderer->draw_string_scaled(ix + 5, content_y, buf, 2); + content_y += 20; snprintf(buf, sizeof(buf), "BAL: $%d", p->balance); renderer->draw_string_scaled(ix + 5, content_y, buf, 1); diff --git a/lib/game.h b/lib/game.h index 75884e5..27d179c 100644 --- a/lib/game.h +++ b/lib/game.h @@ -28,6 +28,13 @@ class InputManager; */ class Game { public: + enum class Type { + BASE, + MONOPOLY_DICE, + MONOPOLY_PROPERTY, + MONOPOLY_BOARD + }; + /** * @brief Construct a new Game * @param width Display width in pixels @@ -83,6 +90,11 @@ public: */ virtual bool wants_to_exit() const { return false; } + /** + * @brief Get the type of game for safe downcasting without RTTI + */ + virtual Type get_type() const { return Type::BASE; } + protected: // Display dimensions uint16_t width;