fix rent calculation

This commit is contained in:
Adolfo Reyna
2026-01-31 22:23:49 -05:00
parent fa4c6f00ca
commit 78b376ad5d
9 changed files with 276 additions and 77 deletions

View File

@@ -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.

Binary file not shown.

43
games/monopoly/AGENT.md Normal file
View File

@@ -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.

View File

@@ -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;

View File

@@ -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; }

View File

@@ -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);
}
}
};

View File

@@ -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; }
};

View File

@@ -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<DiceModalGame*>(active_modal);
auto prop_modal = dynamic_cast<PropertyModalGame*>(active_modal);
auto board_modal = dynamic_cast<BoardModalGame*>(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<DiceModalGame*>(active_modal) : nullptr;
PropertyModalGame* prop_modal = (active_modal->get_type() == Game::Type::MONOPOLY_PROPERTY) ? static_cast<PropertyModalGame*>(active_modal) : nullptr;
BoardModalGame* board_modal = (active_modal->get_type() == Game::Type::MONOPOLY_BOARD) ? static_cast<BoardModalGame*>(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);

View File

@@ -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;