fix rent calculation
This commit is contained in:
2
AGENT.md
2
AGENT.md
@@ -303,9 +303,9 @@ After successful build:
|
|||||||
|
|
||||||
View serial output:
|
View serial output:
|
||||||
|
|
||||||
```bash
|
|
||||||
screen /dev/cu.usbmodem101
|
screen /dev/cu.usbmodem101
|
||||||
# Exit: Ctrl-A, then K, then Y
|
# Exit: Ctrl-A, then K, then Y
|
||||||
```
|
```
|
||||||
|
|
||||||
Or use VS Code's serial monitor extension.
|
Or use VS Code's serial monitor extension.
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
43
games/monopoly/AGENT.md
Normal file
43
games/monopoly/AGENT.md
Normal 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.
|
||||||
@@ -18,6 +18,7 @@ public:
|
|||||||
: Game(width, height, renderer, gui, input_manager), dismissed(false), players(p), players_count(count) {}
|
: Game(width, height, renderer, gui, input_manager), dismissed(false), players(p), players_count(count) {}
|
||||||
|
|
||||||
void init() override { dismissed = false; }
|
void init() override { dismissed = false; }
|
||||||
|
Type get_type() const override { return Type::MONOPOLY_BOARD; }
|
||||||
|
|
||||||
bool update(const InputEvent& event) override {
|
bool update(const InputEvent& event) override {
|
||||||
// Any button dismisses the board view
|
// Any button dismisses the board view
|
||||||
@@ -31,7 +32,9 @@ public:
|
|||||||
void draw() override {
|
void draw() override {
|
||||||
renderer->clear_buffer();
|
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 ---
|
// --- Inner UI ---
|
||||||
int cw = width / 7;
|
int cw = width / 7;
|
||||||
|
|||||||
@@ -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)
|
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) {}
|
: 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; }
|
void init() override { dismissed = false; }
|
||||||
|
Type get_type() const override { return Type::MONOPOLY_DICE; }
|
||||||
bool update(const InputEvent& event) override {
|
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;
|
dismissed = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -60,7 +62,11 @@ public:
|
|||||||
void draw() override {
|
void draw() override {
|
||||||
renderer->clear_buffer();
|
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) ---
|
// --- Inner UI (Center Area) ---
|
||||||
int cw = width / 7;
|
int cw = width / 7;
|
||||||
@@ -106,7 +112,7 @@ public:
|
|||||||
int btn_y = iy + ih - 35;
|
int btn_y = iy + ih - 35;
|
||||||
renderer->draw_filled_rectangle(btn_x, btn_y, btn_w, btn_h, true, 1);
|
renderer->draw_filled_rectangle(btn_x, btn_y, btn_w, btn_h, true, 1);
|
||||||
renderer->set_text_color(false);
|
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);
|
renderer->set_text_color(true);
|
||||||
}
|
}
|
||||||
bool is_dismissed() const { return dismissed; }
|
bool is_dismissed() const { return dismissed; }
|
||||||
|
|||||||
@@ -6,10 +6,16 @@
|
|||||||
|
|
||||||
class MonopolyBoardRenderer {
|
class MonopolyBoardRenderer {
|
||||||
public:
|
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;
|
if (index < 0 || index >= BOARD_SIZE) return;
|
||||||
|
|
||||||
|
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);
|
renderer->draw_rectangle(x, y, w, h, true, 1);
|
||||||
|
}
|
||||||
|
|
||||||
const BoardTile& tile = MONOPOLY_BOARD[index];
|
const BoardTile& tile = MONOPOLY_BOARD[index];
|
||||||
int content_x = x, content_y = y, content_w = w, content_h = h;
|
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;
|
content_x += bar_size; content_w -= bar_size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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->draw_filled_rectangle(bx, by, bw, bh, true, 1);
|
||||||
|
renderer->set_text_color(false); // Black text on white bar
|
||||||
|
}
|
||||||
|
|
||||||
// Group number
|
// Group number
|
||||||
renderer->set_text_color(false); // Black text on white bar
|
|
||||||
char gbuf[2] = { (char)('0' + tile.group[0]), '\0' };
|
char gbuf[2] = { (char)('0' + tile.group[0]), '\0' };
|
||||||
renderer->draw_string_scaled(bx + (bw - 6) / 2, by + (bh - 8) / 2, gbuf, 1);
|
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};
|
char short_name[5] = {0};
|
||||||
@@ -65,36 +79,40 @@ public:
|
|||||||
p_count++;
|
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 cw = width / 7; // Corner width
|
||||||
int ch = height / 7; // Corner height
|
int ch = height / 7; // Corner height
|
||||||
int rw = (width - 2 * cw) / 9; // Regular tile width
|
int rw = (width - 2 * cw) / 9; // Regular tile width
|
||||||
int rh = (height - 2 * ch) / 9; // Regular tile height
|
int rh = (height - 2 * ch) / 9; // Regular tile height
|
||||||
|
|
||||||
// --- Bottom Row: 0 to 10 (Right to Left) ---
|
// --- 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) {
|
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) ---
|
// --- Left Column: 11 to 19 (Bottom to Top) ---
|
||||||
for (int i = 11; i < 20; ++i) {
|
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) ---
|
// --- 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) {
|
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) ---
|
// --- Right Column: 31 to 39 (Top to Bottom) ---
|
||||||
for (int i = 31; i < 40; ++i) {
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,32 +13,49 @@ class PropertyModalGame : public Game {
|
|||||||
bool dismissed;
|
bool dismissed;
|
||||||
bool is_owned;
|
bool is_owned;
|
||||||
const char* owner_name;
|
const char* owner_name;
|
||||||
|
int owner_id; // -1 if not owned
|
||||||
bool can_afford;
|
bool can_afford;
|
||||||
int selected_choice; // 0: Buy, 1: Cancel
|
int selected_choice; // 0: Buy, 1: Cancel
|
||||||
bool buy_requested;
|
bool buy_requested;
|
||||||
|
bool rent_requested;
|
||||||
Player* players;
|
Player* players;
|
||||||
int players_count;
|
int players_count;
|
||||||
|
|
||||||
public:
|
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)
|
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), can_afford(affordable), selected_choice(0), buy_requested(false), players(p_list), players_count(p_count) {
|
: 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;
|
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 {
|
bool update(const InputEvent& event) override {
|
||||||
if (event.type == INPUT_BUTTON_0) { // BUTTON A -> BUY
|
// 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;
|
||||||
|
}
|
||||||
|
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) {
|
if (!is_owned && can_afford) {
|
||||||
buy_requested = true;
|
selected_choice = (selected_choice + 1) % 2;
|
||||||
dismissed = true;
|
|
||||||
return 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.
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (event.type == INPUT_BUTTON_1) { // BUTTON B -> AUCTION / DISMISS
|
if (event.type == INPUT_BUTTON_1) { // BUTTON B -> Select action
|
||||||
|
if (!is_owned && can_afford) {
|
||||||
|
if (selected_choice == 0) buy_requested = true;
|
||||||
dismissed = true;
|
dismissed = true;
|
||||||
return true;
|
return true;
|
||||||
|
} else {
|
||||||
|
// Only option was Pass
|
||||||
|
dismissed = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -51,7 +68,12 @@ public:
|
|||||||
char buf[128];
|
char buf[128];
|
||||||
|
|
||||||
if (players && players_count > 0) {
|
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)
|
// Window background (White box)
|
||||||
@@ -104,26 +126,70 @@ public:
|
|||||||
int btn_w = win_w - 30;
|
int btn_w = win_w - 30;
|
||||||
int btn_h = 25;
|
int btn_h = 25;
|
||||||
|
|
||||||
if (is_owned || !can_afford) {
|
if (is_owned && owner_id != -1) {
|
||||||
// Only one option: CONTINUE (B)
|
// Option: Pay Rent (A or B)
|
||||||
renderer->draw_filled_rectangle(win_x + 15, btn_y, btn_w, btn_h, true, 1);
|
renderer->draw_filled_rectangle(win_x + 15, btn_y, btn_w, btn_h, true, 1);
|
||||||
renderer->set_text_color(false);
|
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);
|
renderer->set_text_color(true);
|
||||||
} else {
|
} else if (!is_owned && can_afford) {
|
||||||
// Choice: Buy (A) or Auction (B)
|
// Choice: Buy (A) or Pass (B). Changed to: A cycles, B selects.
|
||||||
// Buy Button
|
// Buy Button
|
||||||
renderer->draw_filled_rectangle(win_x + 15, btn_y, btn_w, btn_h, true, 1);
|
if (selected_choice == 0) renderer->draw_filled_rectangle(win_x + 15, btn_y, btn_w, btn_h, true, 1);
|
||||||
renderer->set_text_color(false);
|
else renderer->draw_rectangle(win_x + 15, btn_y, btn_w, btn_h, true, 1);
|
||||||
snprintf(buf, sizeof(buf), ">A BUY ($%d)", property->cost);
|
|
||||||
|
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->draw_string_scaled(win_x + 20, btn_y + 8, buf, 1);
|
||||||
renderer->set_text_color(true);
|
renderer->set_text_color(true);
|
||||||
|
|
||||||
btn_y += 30;
|
btn_y += 30;
|
||||||
// Auction Button
|
// Pass Button
|
||||||
renderer->draw_string_scaled(win_x + 20, btn_y + 8, ">B AUCTION", 1);
|
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 is_dismissed() const { return dismissed; }
|
||||||
bool wants_to_buy() const { return buy_requested; }
|
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; }
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -59,21 +59,93 @@ bool MonopolyGame::update(const InputEvent& event) {
|
|||||||
bool modal_redraw = active_modal->update(event);
|
bool modal_redraw = active_modal->update(event);
|
||||||
if (modal_redraw) needs_redraw = true;
|
if (modal_redraw) needs_redraw = true;
|
||||||
|
|
||||||
// Check for specific modal types to handle their results
|
// Check for specific modal types to handle their results using custom get_type()
|
||||||
auto dice_modal = dynamic_cast<DiceModalGame*>(active_modal);
|
DiceModalGame* dice_modal = (active_modal->get_type() == Game::Type::MONOPOLY_DICE) ? static_cast<DiceModalGame*>(active_modal) : nullptr;
|
||||||
auto prop_modal = dynamic_cast<PropertyModalGame*>(active_modal);
|
PropertyModalGame* prop_modal = (active_modal->get_type() == Game::Type::MONOPOLY_PROPERTY) ? static_cast<PropertyModalGame*>(active_modal) : nullptr;
|
||||||
auto board_modal = dynamic_cast<BoardModalGame*>(active_modal);
|
BoardModalGame* board_modal = (active_modal->get_type() == Game::Type::MONOPOLY_BOARD) ? static_cast<BoardModalGame*>(active_modal) : nullptr;
|
||||||
|
|
||||||
if (dice_modal && dice_modal->is_dismissed()) {
|
if (dice_modal && dice_modal->is_dismissed()) {
|
||||||
delete active_modal;
|
delete active_modal;
|
||||||
active_modal = nullptr;
|
active_modal = nullptr;
|
||||||
needs_redraw = true;
|
needs_redraw = true;
|
||||||
|
|
||||||
|
// 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()) {
|
} else if (prop_modal && prop_modal->is_dismissed()) {
|
||||||
if (prop_modal->wants_to_buy()) {
|
if (prop_modal->wants_to_buy()) {
|
||||||
const BoardTile* tile = &MONOPOLY_BOARD[p->position];
|
const BoardTile* tile = &MONOPOLY_BOARD[p->position];
|
||||||
|
if (p->balance >= tile->cost) {
|
||||||
p->balance -= tile->cost;
|
p->balance -= tile->cost;
|
||||||
p->properties_owned[p->property_count++] = p->position;
|
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;
|
delete active_modal;
|
||||||
active_modal = nullptr;
|
active_modal = nullptr;
|
||||||
needs_redraw = true;
|
needs_redraw = true;
|
||||||
@@ -135,27 +207,6 @@ bool MonopolyGame::update(const InputEvent& event) {
|
|||||||
default:
|
default:
|
||||||
break;
|
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;
|
return needs_redraw;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,8 +220,11 @@ void MonopolyGame::draw() {
|
|||||||
|
|
||||||
renderer->clear_buffer();
|
renderer->clear_buffer();
|
||||||
|
|
||||||
|
Player* p = &players[current_player_idx];
|
||||||
|
const BoardTile* tile = &MONOPOLY_BOARD[p->position];
|
||||||
|
|
||||||
// --- Draw Board Perimeter ---
|
// --- 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) ---
|
// --- Inner Dashboard (Center Area) ---
|
||||||
int cw = width / 7;
|
int cw = width / 7;
|
||||||
@@ -178,17 +232,14 @@ void MonopolyGame::draw() {
|
|||||||
int ix = cw + 2, iy = ch + 2;
|
int ix = cw + 2, iy = ch + 2;
|
||||||
int iw = width - 2 * cw - 4, ih = height - 2 * ch - 4;
|
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
|
// Stats Window in center
|
||||||
char buf[128];
|
char buf[128];
|
||||||
renderer->draw_string_scaled(ix + 5, iy + 5, "Monopoly", 2);
|
renderer->draw_string_scaled(ix + 5, iy + 5, "Monopoly", 2);
|
||||||
|
|
||||||
int content_y = iy + 25;
|
int content_y = iy + 25;
|
||||||
snprintf(buf, sizeof(buf), "TURN: %s", p->name);
|
snprintf(buf, sizeof(buf), "%s", p->name);
|
||||||
renderer->draw_string_scaled(ix + 5, content_y, buf, 1);
|
renderer->draw_string_scaled(ix + 5, content_y, buf, 2);
|
||||||
content_y += 12;
|
content_y += 20;
|
||||||
|
|
||||||
snprintf(buf, sizeof(buf), "BAL: $%d", p->balance);
|
snprintf(buf, sizeof(buf), "BAL: $%d", p->balance);
|
||||||
renderer->draw_string_scaled(ix + 5, content_y, buf, 1);
|
renderer->draw_string_scaled(ix + 5, content_y, buf, 1);
|
||||||
|
|||||||
12
lib/game.h
12
lib/game.h
@@ -28,6 +28,13 @@ class InputManager;
|
|||||||
*/
|
*/
|
||||||
class Game {
|
class Game {
|
||||||
public:
|
public:
|
||||||
|
enum class Type {
|
||||||
|
BASE,
|
||||||
|
MONOPOLY_DICE,
|
||||||
|
MONOPOLY_PROPERTY,
|
||||||
|
MONOPOLY_BOARD
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Construct a new Game
|
* @brief Construct a new Game
|
||||||
* @param width Display width in pixels
|
* @param width Display width in pixels
|
||||||
@@ -83,6 +90,11 @@ public:
|
|||||||
*/
|
*/
|
||||||
virtual bool wants_to_exit() const { return false; }
|
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:
|
protected:
|
||||||
// Display dimensions
|
// Display dimensions
|
||||||
uint16_t width;
|
uint16_t width;
|
||||||
|
|||||||
Reference in New Issue
Block a user