Compare commits

..

17 Commits

Author SHA1 Message Date
Adolfo Reyna fe6e403a98 Reduce touch overhead and cap frame rate at 24 FPS 2026-02-18 15:53:48 -05:00
Adolfo Reyna 73a78069e3 Stop tracking screenlog.0 2026-02-18 15:44:54 -05:00
Adolfo Reyna 3e54466752 Add Core1 refresh recovery and shared SPI arbitration 2026-02-18 15:43:35 -05:00
Adolfo Reyna ebc58d7e4d Add scene stack menu flow and restore stable TFT sync refresh 2026-02-18 15:10:20 -05:00
Adolfo Reyna a06e0d69fe Refactor main loop touch flow and update docs 2026-02-18 12:08:23 -05:00
Adolfo Reyna be6a217b08 Fix touch event stability and WFI wake filtering 2026-02-18 11:51:29 -05:00
Adolfo Reyna b01cd652a0 Fix build error: Rename BOARD_SIZE to MONOPOLY_BOARD_SIZE to avoid macro collision 2026-02-17 22:11:21 -05:00
Adolfo Reyna b0ca1f1a55 Fix emulator build script, UI rendering, and clean up repo 2026-02-17 21:49:49 -05:00
Adolfo Reyna 6d29e99394 Remove untracked files 2026-02-17 21:17:50 -05:00
Adolfo Reyna b356897387 Merge remote-tracking branch 'gitea/fix-refresh-race'
# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.
2026-02-13 16:16:14 -05:00
Adolfo Reyna 06f5976865 Fix Lua syntax errors caused by bad regex replacement 2026-02-13 15:31:21 -05:00
Adolfo Reyna e406a06f61 Fix race condition in display refresh locking 2026-02-13 13:40:45 -05:00
Adolfo Reyna 034867d2a7 fix sneak game 2026-02-13 13:36:35 -05:00
Adolfo Reyna 76e3d2435e Make game name search case-insensitive
Serial uploader searches for games by filename (e.g., "tetris") but
games are registered by their metadata NAME field (e.g., "Tetris").
This caused launch failures when case didn't match.

Changed select_game_by_name() to use case-insensitive matching:
- strcasecmp() for exact match
- tolower() both strings before strstr() for partial match

Now uploading "tetris.lua" will successfully match and launch "Tetris".

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-12 23:27:48 -05:00
Adolfo Reyna 518bc054c4 Fix SPI bus contention crash on serial game upload
Serial uploader was crashing the Pico when launching games because
it accessed SD card (SPI) while Core 1 was refreshing display (also SPI).
Display and SD card share the same SPI bus and cannot be accessed
simultaneously.

Split game launch into prepare and execute phases:
- prepare: Re-scan games directory (safe, SD access done immediately)
- execute: Load Lua script from SD (deferred until display is idle)

Main loop now checks !is_refresh_in_progress() before completing
launch, preventing SPI conflicts.

Also updated SD card best practices skill to document SPI bus
contention as the #1 most critical issue to avoid.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-12 23:25:31 -05:00
Adolfo Reyna f8fb04db1b improve game 2026-02-12 23:09:13 -05:00
Adolfo Reyna 84b009c33e Add serial upload tool for rapid Lua game iteration
Implements a complete serial upload workflow that allows uploading and
immediately testing Lua games via USB serial connection.

New Components:
- SerialUploader: Receives files via serial, writes to SD card
- upload_game.py: Python tool for sending files from host computer
- Protocol: Text-based with base64 encoding for reliability

Key Features:
- Uploads file to /games folder on SD card
- Overwrites existing files (FA_CREATE_ALWAYS)
- Auto-launches uploaded game immediately
- Proper memory cleanup (prevents Lua state conflicts)

SD Card Fixes:
- Fixed SPI speed management (12.5MHz for SD, 32MHz for display)
- Fixed SD write protocol (poll for data response token)
- Added speed switching wrappers around all FatFS operations
- Cleaned up excessive debug output

Game Launcher Improvements:
- Added clear_games() to prevent duplicate registrations
- Added cleanup in select_game_by_name() to delete old instances
- Added exact match priority in game selection
- LuaGameLoader now has clear_factory_data() for memory cleanup

Integration:
- Added serial_uploader to CMakeLists.txt
- Integrated into main loop in basic1.cpp
- Re-scans games after upload to pick up new files

Documentation:
- UPLOAD_TOOL.md: Usage instructions
- sd_card_best_practices.md: Critical lessons learned

Known Issues:
- Game launch after upload occasionally causes freeze (needs investigation)
- Display may not refresh properly after upload

Usage:
  python upload_game.py games/lua_examples/2048.lua /dev/tty.usbmodem101

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-12 22:52:57 -05:00
48 changed files with 4583 additions and 4369 deletions
+484
View File
@@ -0,0 +1,484 @@
# SD Card Best Practices for RP2350 + FatFS
This document captures critical best practices for working with SD card operations in this project, based on lessons learned during development.
## 1. SPI Bus Contention (CRITICAL)
### ⚠️ CRITICAL: Display and SD Card Share the Same SPI Bus
**The display and SD card use the same SPI bus and CANNOT be accessed simultaneously.** Attempting to do so will cause the Pico to crash or behave unpredictably.
### Real-World Example: Game Launch Crash
The serial uploader originally crashed when launching games because:
1. `SerialUploader::launch_game()` writes file to SD (SPI)
2. Immediately calls `select_game_by_name()`
3. `LuaGame::load_script()` reads from SD (SPI)
4. **Meanwhile**, Core 1 is refreshing the display (also SPI)
5. **CRASH** due to simultaneous SPI access
### Solution: Wait for Display to Be Idle
Before any SD card operation that isn't already protected, ensure no display refresh is in progress:
```cpp
// In main loop:
if (serial_uploader.wants_to_launch_game() && !is_refresh_in_progress()) {
// Safe to launch now - no SPI conflict with display
bool game_launched = serial_uploader.complete_launch();
// ...
}
```
### Key Patterns for Avoiding Contention
**Pattern 1: Check `is_refresh_in_progress()` before SD operations**
```cpp
if (!is_refresh_in_progress()) {
uint prev_speed = sd_card_set_spi_speed();
// SD card operations
sd_card_restore_spi_speed(prev_speed);
}
```
**Pattern 2: Split operations into "prepare" and "execute" phases**
```cpp
// Phase 1: Prepare (safe, no SD access)
void prepare_operation() {
state = READY_TO_EXECUTE;
}
// Phase 2: Execute (only when !is_refresh_in_progress())
bool execute_operation() {
uint prev_speed = sd_card_set_spi_speed();
// SD card operations here
sd_card_restore_spi_speed(prev_speed);
}
```
**Pattern 3: Keep main loop responsive**
```cpp
while (1) {
// Check if we need to do SD operation
if (needs_sd_operation && !is_refresh_in_progress()) {
perform_sd_operation();
}
// Don't sleep if waiting for SD operation window
bool stay_awake = pending_refresh || needs_sd_operation;
if (!stay_awake) {
__wfi(); // Sleep until interrupt
}
}
```
### When This Matters Most
- **Game loading**: Reading Lua scripts from SD during game launch
- **Serial uploads**: Writing files and immediately loading them
- **Save/load operations**: Writing game state to SD
- **Directory scanning**: Re-scanning games while display is active
### The Core Architecture
This project uses **dual-core display refresh**:
- **Core 0**: Main logic, input processing, game updates, SD card operations
- **Core 1**: Display refresh (writes framebuffer to display via SPI)
Core 1 runs asynchronously, so you must explicitly check `is_refresh_in_progress()` before SD operations.
## 2. SPI Speed Management
### Critical Rule: Always Set SD Card Speed Before Operations
The display and SD card share the same SPI bus but operate at different speeds:
- **Display**: 32 MHz (fast)
- **SD Card**: 12.5 MHz (slower, more reliable)
**ALWAYS wrap SD card operations with speed switching:**
```cpp
// Save current speed and switch to SD card speed
uint prev_speed = sd_card_set_spi_speed();
// ... SD card operations here ...
// f_open(), f_write(), f_read(), f_readdir(), etc.
// Restore previous speed for display
sd_card_restore_spi_speed(prev_speed);
```
### Why This Matters
Running SD card operations at the wrong SPI speed causes:
- Unreliable reads/writes
- Corrupted data
- `FR_DISK_ERR` errors from FatFS
- Hardware-level protocol failures (0xFF data responses)
### Where Speed Switching is Already Handled
These functions handle SPI speed internally (you don't need to wrap them):
- `LuaGameLoader::register_all_games()`
- All functions in `sd_card.c` (low-level operations)
### Where You MUST Handle Speed Switching
Any code that calls FatFS functions directly:
- `f_open()`, `f_close()`
- `f_read()`, `f_write()`
- `f_opendir()`, `f_readdir()`, `f_closedir()`
- `f_stat()`, `f_mkdir()`, `f_unlink()`
- `f_getfree()`, `f_sync()`
## 2. SD Card Write Protocol
### The Data Response Polling Issue
When writing to SD card with `CMD24` (write single block), the data response token may not arrive immediately. You must poll for it:
```cpp
// After sending data block and CRC:
uint8_t response = 0xFF;
for (int i = 0; i < 10; i++) {
response = sd_card_transfer(0xFF);
if (response != 0xFF) {
break; // Got the response
}
}
// Check if data was accepted
if ((response & 0x1F) != SD_DATA_ACCEPTED) {
// Write failed
}
```
**Why:** The SD card may need a few clock cycles before sending the data response token. Reading only once may return 0xFF (no response yet).
### Wait for Card Ready After CMD24
After sending the write command and before sending data:
```cpp
// After CMD24 command:
uint8_t ready_byte;
do {
ready_byte = sd_card_transfer(0xFF);
timeout_count++;
if (timeout_count > 1000) {
return false; // Timeout
}
} while (ready_byte != 0xFF);
```
This ensures the card is ready to receive the data block.
## 3. FatFS Best Practices
### Always Check Return Codes
```cpp
FRESULT fr = f_open(&fil, path, FA_CREATE_ALWAYS | FA_WRITE);
if (fr != FR_OK) {
printf("ERROR: f_open failed: %d\n", fr);
// Clean up and return
return false;
}
```
Common FatFS error codes:
- `FR_OK (0)`: Success
- `FR_DISK_ERR (1)`: Low-level disk error (often SPI speed issue)
- `FR_NOT_READY (3)`: Card not initialized
- `FR_NO_FILE (4)`: File not found
- `FR_NO_PATH (5)`: Path not found
- `FR_EXIST (8)`: File/directory already exists
### Use FA_CREATE_ALWAYS to Overwrite
For rapid iteration (like our serial uploader):
```cpp
f_open(&fil, path, FA_CREATE_ALWAYS | FA_WRITE);
```
This overwrites existing files, perfect for development.
### Write in Chunks for Large Files
For files larger than 512 bytes, write in chunks:
```cpp
const uint32_t CHUNK_SIZE = 512;
uint32_t total_written = 0;
while (total_written < total_size) {
uint32_t chunk_size = min(CHUNK_SIZE, total_size - total_written);
UINT bytes_written;
fr = f_write(&fil, buffer + total_written, chunk_size, &bytes_written);
if (fr != FR_OK || bytes_written != chunk_size) {
// Handle error
break;
}
total_written += bytes_written;
}
```
### Always Sync and Close
```cpp
f_sync(&fil); // Ensure data is written to card
f_close(&fil); // Close file and update directory
```
Skipping `f_sync()` can lead to data loss if power is lost.
## 4. Memory Management
### Clean Up After Re-scanning
When re-scanning games (like after upload), clean up old data:
```cpp
// Clear game launcher entries
game_launcher->clear_games();
// Clear Lua game factory data
LuaGameLoader::clear_factory_data();
// Re-scan
LuaGameLoader::register_all_games(game_launcher);
```
**Why:** Without cleanup, you get duplicate registrations and memory leaks.
### Delete Old Game Instances Before Creating New Ones
```cpp
if (selected_game) {
delete selected_game;
selected_game = nullptr;
}
// Now create new game
selected_game = factory(width, height, renderer, gui, input_manager);
```
**Critical for Lua games:** Each LuaGame has a Lua state. Not cleaning up the old one before creating a new one causes conflicts and freezes.
## 5. Debugging Tips
### Add Targeted Debug Output
When debugging SD operations, add prints at key points:
```cpp
printf("✓ Wrote %u bytes to %s\n", bytes_written, filepath);
```
But avoid spamming the console - it slows down operations significantly.
### Check Hardware Layer First
If FatFS returns `FR_DISK_ERR`, the issue is usually at the hardware level:
1. Check SPI speed (most common issue)
2. Check SD card write protection
3. Check physical SD card connection
4. Verify SD card is properly initialized
### Use Root Directory for Testing
When debugging writes, test with root directory first:
```cpp
FIL test_file;
if (f_open(&test_file, "/test.txt", FA_CREATE_ALWAYS | FA_WRITE) == FR_OK) {
printf("Root write OK\n");
f_close(&test_file);
f_unlink("/test.txt");
}
```
This isolates directory-related issues.
## 6. Common Pitfalls
### ❌ DON'T: Access SD Card During Display Refresh (MOST CRITICAL)
```cpp
// BAD - Will crash the Pico!
void launch_game() {
scan_games(); // Reads SD card
selected_game = create_game(); // Reads Lua script from SD
}
// Called directly without checking if display is refreshing
```
### ✅ DO: Wait for Display to Be Idle
```cpp
// GOOD - Wait for safe window
if (!is_refresh_in_progress()) {
uint prev_speed = sd_card_set_spi_speed();
scan_games();
selected_game = create_game();
sd_card_restore_spi_speed(prev_speed);
}
```
### ❌ DON'T: Forget SPI Speed Management
```cpp
// BAD - Will fail or be unreliable
f_open(&fil, "/games/test.lua", FA_WRITE);
```
### ✅ DO: Always Switch Speeds
```cpp
// GOOD
uint prev_speed = sd_card_set_spi_speed();
f_open(&fil, "/games/test.lua", FA_WRITE);
// ... operations ...
sd_card_restore_spi_speed(prev_speed);
```
### ❌ DON'T: Assume Immediate Data Response
```cpp
// BAD - May get 0xFF (no response yet)
uint8_t response = sd_card_transfer(0xFF);
if (response != 0x05) {
// Might incorrectly fail
}
```
### ✅ DO: Poll for Data Response
```cpp
// GOOD - Poll until response arrives
uint8_t response = 0xFF;
for (int i = 0; i < 10; i++) {
response = sd_card_transfer(0xFF);
if (response != 0xFF) break;
}
```
### ❌ DON'T: Skip Error Checking
```cpp
// BAD
f_write(&fil, buffer, size, &bytes_written);
f_close(&fil);
```
### ✅ DO: Check Every Return Value
```cpp
// GOOD
if (f_write(&fil, buffer, size, &bytes_written) != FR_OK) {
printf("Write failed\n");
f_close(&fil);
return false;
}
```
### ❌ DON'T: Create New Games Without Cleanup
```cpp
// BAD - Memory leak and Lua state conflicts
selected_game = new LuaGame(...);
```
### ✅ DO: Clean Up First
```cpp
// GOOD
if (selected_game) {
delete selected_game;
selected_game = nullptr;
}
selected_game = new LuaGame(...);
```
## 7. Serial Upload Pattern
The serial uploader demonstrates the complete pattern:
```cpp
bool SerialUploader::write_file_to_sd() {
// 1. Validate input
if (!file_buffer || bytes_received == 0) return false;
// 2. Set SD card SPI speed
uint prev_speed = sd_card_set_spi_speed();
// 3. Ensure directory exists
f_mkdir("/games");
// 4. Open file (overwrite mode for iteration)
FIL fil;
FRESULT fr = f_open(&fil, filepath, FA_CREATE_ALWAYS | FA_WRITE);
if (fr != FR_OK) {
sd_card_restore_spi_speed(prev_speed);
return false;
}
// 5. Write in chunks
const uint32_t CHUNK_SIZE = 512;
uint32_t total_written = 0;
while (total_written < bytes_received) {
uint32_t chunk = min(CHUNK_SIZE, bytes_received - total_written);
UINT written;
fr = f_write(&fil, file_buffer + total_written, chunk, &written);
if (fr != FR_OK || written != chunk) {
f_close(&fil);
sd_card_restore_spi_speed(prev_speed);
return false;
}
total_written += written;
}
// 6. Sync and close
f_sync(&fil);
f_close(&fil);
// 7. Restore display SPI speed
sd_card_restore_spi_speed(prev_speed);
return true;
}
```
## 9. Testing Checklist
When implementing new SD card functionality:
- [ ] **SPI bus contention checked** - verify `!is_refresh_in_progress()` before SD operations
- [ ] SPI speed switching is in place
- [ ] All FatFS return codes are checked
- [ ] Files are properly closed after operations
- [ ] Memory is cleaned up (no leaks)
- [ ] Error messages are informative
- [ ] Tested with both small and large files
- [ ] Tested overwriting existing files
- [ ] Tested with non-existent directories
- [ ] Verified data integrity (read back after write)
## Summary
The most important rules:
1. **⚠️ CRITICAL: Avoid SPI bus contention** - Check `!is_refresh_in_progress()` before SD operations (display and SD share SPI bus)
2. **Always manage SPI speed** around FatFS operations
3. **Poll for SD card responses** - don't assume immediate response
4. **Check error codes** on every operation
5. **Clean up memory** before creating new game instances
6. **Write in chunks** for large files
7. **Sync before closing** to ensure data is written
Following these practices will save hours of debugging SD card issues!
+10
View File
@@ -10,3 +10,13 @@ CMakeFiles/
*.cmake *.cmake
emulator/build/ emulator/build/
.DS_Store .DS_Store
.cache
/emulator/CMakeFiles/*
/emulator/build/*
/emulator/games/*
/emulator/games/lua_examples/*
/emulator/cmake_install.cmake
/emulator/CMakeCache.txt
/emulator/basic1_emulator
/emulator/Makefile
screenlog*
+2
View File
@@ -48,6 +48,8 @@ add_executable(basic1
basic1.cpp basic1.cpp
lib/input_manager.cpp lib/input_manager.cpp
lib/game_launcher.cpp lib/game_launcher.cpp
lib/shared_spi_bus.c
lib/serial_uploader.cpp
games/tic_tac_toe.cpp games/tic_tac_toe.cpp
games/demo_game.cpp games/demo_game.cpp
games/monopoly/monopoly_game.cpp games/monopoly/monopoly_game.cpp
+12 -6
View File
@@ -21,13 +21,13 @@ The project now uses a **clean, event-driven architecture** perfect for building
-**Event-Driven**: Display only updates when input is received -**Event-Driven**: Display only updates when input is received
- 🔋 **Power Efficient**: Uses `__wfi()` to sleep between inputs (< 1mA idle) - 🔋 **Power Efficient**: Uses `__wfi()` to sleep between inputs (< 1mA idle)
- 📄 **E-ink Optimized**: Minimizes screen refreshes for e-paper displays - 📄 **E-ink Optimized**: Minimizes screen refreshes for e-paper displays
- 🎯 **Interrupt-Driven**: Touch and button handling via hardware interrupts - 🎯 **Hybrid Input**: IRQ wake-up plus active-touch sampling for smoother drag/move handling
- 🧩 **Modular**: Clear separation of input processing, game logic, and rendering - 🧩 **Modular**: Clear separation of input processing, game logic, and rendering
### Architecture Highlights ### Architecture Highlights
``` ```
Interrupt → Set Flag → Wake CPU → Process Input → Update Game → Draw → Refresh → Sleep IRQ/Timer → Set Flag → Wake CPU → Process Input → Update Game → Draw → Refresh → Sleep
``` ```
**Before (Polling Loop):** **Before (Polling Loop):**
@@ -45,8 +45,12 @@ while(1) {
**After (Reactive Template):** **After (Reactive Template):**
```cpp ```cpp
while(1) { while (1) {
bool stay_awake = pending_refresh || touch_event_down || (last_touch_time != 0);
if (!stay_awake) {
__wfi(); // Sleep until interrupt __wfi(); // Sleep until interrupt
if (!has_pending_wake_source()) continue; // Ignore unrelated wake-ups
}
InputEvent input = process_button_input(config); InputEvent input = process_button_input(config);
if (!input.valid) { if (!input.valid) {
@@ -534,7 +538,8 @@ The typical development workflow:
**Interrupt not triggering:** **Interrupt not triggering:**
- Verify `TOUCH_INT_PIN` is correctly defined - Verify `TOUCH_INT_PIN` is correctly defined
- Check INT pin is pulled high (internal pull-up or external resistor) - Check INT pin is pulled high (internal pull-up or external resistor)
- Confirm touch controller is configured for interrupt mode - Confirm touch controller is configured for trigger/interrupt mode
- Touch processing uses hybrid mode: IRQ wake-up plus active-session sampling while touch is down
### SD Card Issues ### SD Card Issues
@@ -579,7 +584,7 @@ The code includes `printf()` statements for debugging touch, display, and SD car
For battery-powered projects: For battery-powered projects:
- E-Paper displays use almost no power when idle - E-Paper displays use almost no power when idle
- Use `__wfi()` (Wait For Interrupt) to sleep between inputs - Use `__wfi()` (Wait For Interrupt) to sleep between event bursts
- Disable TFT backlight when idle - Disable TFT backlight when idle
- Lower SPI/I2C baud rates to reduce power consumption - Lower SPI/I2C baud rates to reduce power consumption
@@ -607,7 +612,8 @@ For battery-powered projects:
### Touch Features ### Touch Features
- Multi-touch support (up to 2 points) - Multi-touch support (up to 2 points)
- Coordinate transformation - Coordinate transformation
- Touch debouncing - Hybrid IRQ + active-session sampling
- Touch debouncing with release hysteresis
- Event types (press, lift, contact) - Event types (press, lift, contact)
### SD Card Features ### SD Card Features
+151
View File
@@ -0,0 +1,151 @@
# Lua Game Upload Tool
Rapidly upload and execute Lua games on your RP2350 via USB serial for quick iteration during development!
## Features
- Upload Lua game files directly to the SD card via USB serial
- Automatically launches the uploaded game immediately
- No need to manually swap SD cards or restart the device
- Perfect for rapid game development and testing
## Requirements
### Computer Side
- Python 3.x
- pyserial library: `pip install pyserial`
### RP2350 Side
- Firmware must be built with serial uploader support (already included)
- SD card inserted and formatted (FAT32)
- USB connection to computer
## Usage
### 1. Connect Your Device
Connect your RP2350 to your computer via USB. The device will appear as a serial port:
- **Linux/Mac**: Usually `/dev/ttyACM0` or `/dev/ttyUSB0`
- **Windows**: Usually `COM3`, `COM4`, etc.
### 2. Upload a Lua Game
Basic usage:
```bash
python upload_game.py my_game.lua
```
Specify a custom serial port:
```bash
python upload_game.py my_game.lua /dev/ttyACM0 # Linux/Mac
python upload_game.py my_game.lua COM3 # Windows
```
The script will:
1. Read your Lua file
2. Upload it to the RP2350 via serial
3. Save it to `/games/<filename>.lua` on the SD card
4. Automatically launch the game!
### 3. View Available Serial Ports
Run the script without arguments to see available ports:
```bash
python upload_game.py
```
## Example Workflow
```bash
# Edit your game
vim snake.lua
# Upload and test (the game starts immediately!)
python upload_game.py snake.lua
# Make changes
vim snake.lua
# Upload again (instantly replaces and restarts)
python upload_game.py snake.lua
```
## Protocol Details
The tool uses a simple text-based protocol over USB serial:
```
UPLOAD <filename> <size_in_bytes>
<base64_encoded_file_content>
END
```
**Response:**
```
OK <bytes_written>
LAUNCHED <game_name>
```
## Troubleshooting
### "No serial port found"
- Make sure your device is connected via USB
- Check if the device appears in your system (use `ls /dev/tty*` on Linux/Mac)
- On Linux, you may need to add your user to the `dialout` group: `sudo usermod -a -G dialout $USER`
### "Permission denied"
On Linux/Mac, you may need permissions to access the serial port:
```bash
sudo chmod 666 /dev/ttyACM0 # Quick fix
# OR
sudo usermod -a -G dialout $USER && newgrp dialout # Permanent fix
```
### "Upload failed" or "ERROR" messages
- Make sure the SD card is properly inserted and formatted (FAT32)
- Check that there's enough space on the SD card
- Verify the Lua file is valid (syntax errors will appear when game launches)
### Game doesn't appear or launch
- Check the serial output from the RP2350 for error messages
- Ensure the Lua file has proper metadata comments at the top:
```lua
-- NAME: My Game
-- DESCRIPTION: A fun game
```
## Tips for Rapid Development
1. **Use a serial terminal** alongside uploads to see debug output:
```bash
screen /dev/ttyACM0 115200
```
2. **Create a watch script** to auto-upload on file changes:
```bash
while inotifywait -e modify my_game.lua; do
python upload_game.py my_game.lua
done
```
3. **Use printf() in Lua** for debugging:
```lua
function update(event)
print("Event type: " .. event.type)
-- your code here
end
```
## File Size Limits
- Maximum file size: 64 KB
- This should be plenty for most Lua games
- If you need more, consider splitting assets or code
## Related Files
- `lib/serial_uploader.h` - C++ header for serial upload handler
- `lib/serial_uploader.cpp` - C++ implementation
- `upload_game.py` - Python upload script
Happy game development! 🎮
+424 -74
View File
@@ -14,7 +14,7 @@
* - Event-driven: Display only updates when input is received * - Event-driven: Display only updates when input is received
* - Power efficient: Uses __wfi() to sleep between inputs * - Power efficient: Uses __wfi() to sleep between inputs
* - E-ink optimized: Minimizes screen refreshes * - E-ink optimized: Minimizes screen refreshes
* - Interrupt-driven: Touch and button handling via interrupts * - Hybrid input handling: IRQ wake-up plus active-touch sampling
* - Modular: Clear separation of input, game logic, and rendering * - Modular: Clear separation of input, game logic, and rendering
* *
* ARCHITECTURE: * ARCHITECTURE:
@@ -36,7 +36,6 @@
#include "pico/stdlib.h" #include "pico/stdlib.h"
#include "pico/binary_info.h" #include "pico/binary_info.h"
#include "hardware/sync.h"
#include "pico/multicore.h" #include "pico/multicore.h"
#include "board_config.h" // Board-specific pin configuration #include "board_config.h" // Board-specific pin configuration
#include "sd_card.h" #include "sd_card.h"
@@ -58,6 +57,9 @@ extern "C" {
#include "demo_game.h" #include "demo_game.h"
#include "monopoly_game.h" #include "monopoly_game.h"
#include "lua_game_loader.h" #include "lua_game_loader.h"
#include "serial_uploader.h"
#include "scene_stack.h"
#include "shared_spi_bus.h"
// Binary info for RP2350 - ensures proper boot image structure // Binary info for RP2350 - ensures proper boot image structure
@@ -74,6 +76,28 @@ volatile bool refresh_requested = false;
volatile bool refresh_in_progress = false; volatile bool refresh_in_progress = false;
const uint8_t* volatile refresh_buffer = nullptr; const uint8_t* volatile refresh_buffer = nullptr;
LowLevelDisplay* volatile refresh_display = nullptr; LowLevelDisplay* volatile refresh_display = nullptr;
volatile uint32_t core1_heartbeat = 0;
void core1_entry();
static void restart_core1_refresh_worker() {
printf("Attempting Core1 restart...\n");
// Stop Core1 and clear in-flight refresh state.
multicore_reset_core1();
refresh_requested = false;
refresh_in_progress = false;
refresh_buffer = nullptr;
refresh_display = nullptr;
core1_heartbeat = 0;
// Recover shared SPI lock state after hard core reset.
shared_spi_bus_force_recover();
multicore_launch_core1(core1_entry);
sleep_ms(20);
printf("Core1 restart complete\n");
}
/** /**
* @brief Core 1 entry point - handles display refresh operations * @brief Core 1 entry point - handles display refresh operations
@@ -85,21 +109,31 @@ void core1_entry() {
printf("Core 1 started - handling display refreshes\n"); printf("Core 1 started - handling display refreshes\n");
while (1) { while (1) {
core1_heartbeat++;
// Wait for refresh request // Wait for refresh request
if (refresh_requested && refresh_buffer && refresh_display) { if (refresh_requested) {
refresh_in_progress = true; if (refresh_buffer && refresh_display) {
// refresh_in_progress is already set by Core 0 to lock the buffer
// Get local copies for safe access // Get local copies for safe access
LowLevelDisplay* display = refresh_display; LowLevelDisplay* display = refresh_display;
const uint8_t* buffer = refresh_buffer; const uint8_t* buffer = refresh_buffer;
// Perform the refresh operation (may be slow for e-ink) // Perform refresh with shared SPI bus lock to avoid SD/display collisions.
shared_spi_bus_lock();
display->draw_buffer(buffer); display->draw_buffer(buffer);
display->refresh(); display->refresh();
shared_spi_bus_unlock();
} else {
// Recovery guard: never leave Core 0 stuck waiting forever if a
// malformed/partial request is observed across cores.
printf("Core1: dropped malformed refresh request\n");
}
// Clear flags // Clear flags in all cases to avoid deadlock on Core 0.
refresh_requested = false; refresh_requested = false;
refresh_in_progress = false; refresh_in_progress = false; // Unlock buffer for Core 0
} }
// Small delay to avoid busy-waiting // Small delay to avoid busy-waiting
@@ -123,6 +157,10 @@ bool refresh_screen_async(const uint8_t *buffer, LowLevelDisplay* display) {
return false; return false;
} }
// Lock the buffer immediately on Core 0 to prevent race condition
// Core 1 will unlock it (set to false) when done
refresh_in_progress = true;
// Queue refresh on Core 1 // Queue refresh on Core 1
refresh_buffer = buffer; refresh_buffer = buffer;
refresh_display = display; refresh_display = display;
@@ -157,8 +195,8 @@ struct GameConfig {
// ============================================================================ // ============================================================================
// Display dimming settings // Display dimming settings
#define DIM_TIMEOUT_MS (2 * 60 * 1000) // 2 minutes to dim #define DEFAULT_DIM_TIMEOUT_MS (2 * 60 * 1000) // 2 minutes to dim
#define SLEEP_TIMEOUT_MS (10 * 60 * 1000) // 10 minutes to sleep #define DEFAULT_SLEEP_TIMEOUT_MS (10 * 60 * 1000) // 10 minutes to sleep
#define DIM_CHECK_INTERVAL_MS 10000 // Check every 10 seconds #define DIM_CHECK_INTERVAL_MS 10000 // Check every 10 seconds
// Display dimming state // Display dimming state
@@ -166,7 +204,8 @@ static uint32_t last_interaction_time = 0; // Last time user interacted
static bool is_idle_2min_triggered = false; // Flag for 2min trigger static bool is_idle_2min_triggered = false; // Flag for 2min trigger
static bool is_idle_10min_triggered = false; // Flag for 10min trigger static bool is_idle_10min_triggered = false; // Flag for 10min trigger
static volatile bool dim_check_flag = false; // Flag set by timer to check dimming static volatile bool dim_check_flag = false; // Flag set by timer to check dimming
static LowLevelDisplay* global_display = nullptr; // Global display pointer for timer callback static uint32_t dim_timeout_ms = DEFAULT_DIM_TIMEOUT_MS;
static uint32_t sleep_timeout_ms = DEFAULT_SLEEP_TIMEOUT_MS;
/** /**
* @brief Update last interaction time and notify display driver * @brief Update last interaction time and notify display driver
@@ -219,14 +258,14 @@ static inline void check_and_apply_dimming(LowLevelDisplay* display) {
uint32_t current_time = to_ms_since_boot(get_absolute_time()); uint32_t current_time = to_ms_since_boot(get_absolute_time());
uint32_t elapsed = current_time - last_interaction_time; uint32_t elapsed = current_time - last_interaction_time;
// Check for 10 minute timeout (Sleep) // Check sleep timeout (if enabled)
if (!is_idle_10min_triggered && elapsed >= SLEEP_TIMEOUT_MS) { if (sleep_timeout_ms > 0 && !is_idle_10min_triggered && elapsed >= sleep_timeout_ms) {
display->on_idle_10min(); display->on_idle_10min();
is_idle_10min_triggered = true; is_idle_10min_triggered = true;
is_idle_2min_triggered = true; // Implicitly triggered is_idle_2min_triggered = true; // Implicitly triggered
} }
// Check for 2 minute timeout (Dim) // Check dim timeout (if enabled)
else if (!is_idle_2min_triggered && elapsed >= DIM_TIMEOUT_MS) { else if (dim_timeout_ms > 0 && !is_idle_2min_triggered && elapsed >= dim_timeout_ms) {
display->on_idle_2min(); display->on_idle_2min();
is_idle_2min_triggered = true; is_idle_2min_triggered = true;
} }
@@ -247,6 +286,121 @@ volatile bool button_key0_pressed = false;
volatile bool button_key1_pressed = false; volatile bool button_key1_pressed = false;
#endif #endif
/**
* @brief Returns true when an application-level wake source is pending.
*
* __wfi() can wake on unrelated interrupts (e.g. USB/background IRQs). This
* guard prevents running full input/game logic unless one of our expected
* event sources actually fired.
*/
static inline bool has_pending_wake_source() {
if (touch_interrupt_flag) return true;
if (touch_event_down) return true;
if (dim_check_flag) return true;
#ifdef BUTTON_KEY0_PIN
if (button_key0_pressed || button_key1_pressed) return true;
#endif
return false;
}
struct SleepOption {
const char* label;
uint32_t sleep_ms;
};
static constexpr SleepOption kSleepOptions[] = {
{"Never", 0},
{"30 sec", 30 * 1000},
{"1 min", 60 * 1000},
{"2 min", 2 * 60 * 1000},
{"5 min", 5 * 60 * 1000},
{"10 min", 10 * 60 * 1000}
};
static constexpr int kSleepOptionCount = sizeof(kSleepOptions) / sizeof(kSleepOptions[0]);
static inline void apply_sleep_option(int option_index) {
if (option_index < 0 || option_index >= kSleepOptionCount) {
return;
}
sleep_timeout_ms = kSleepOptions[option_index].sleep_ms;
if (sleep_timeout_ms == 0) {
dim_timeout_ms = 0;
return;
}
uint32_t candidate_dim = sleep_timeout_ms / 5;
if (candidate_dim < 30 * 1000) {
candidate_dim = 30 * 1000;
}
if (candidate_dim > 2 * 60 * 1000) {
candidate_dim = 2 * 60 * 1000;
}
if (candidate_dim >= sleep_timeout_ms && sleep_timeout_ms > 5000) {
candidate_dim = sleep_timeout_ms - 5000;
}
dim_timeout_ms = candidate_dim;
}
static inline bool in_rect(int16_t x, int16_t y, int rx, int ry, int rw, int rh) {
return x >= rx && x < (rx + rw) && y >= ry && y < (ry + rh);
}
static inline bool is_top_right_start(int16_t x, int16_t y, int width, int height) {
return x >= (width * 3 / 4) && y <= (height / 4);
}
static inline bool is_open_menu_swipe(int16_t sx, int16_t sy, int16_t ex, int16_t ey, int width, int height) {
if (!is_top_right_start(sx, sy, width, height)) {
return false;
}
const bool released_near_center = in_rect(ex, ey, width / 4, height / 4, width / 2, height / 2);
const int dx = ex - sx;
const int dy = ey - sy;
const bool moved_left_and_down = (dx <= -(width / 5)) && (dy >= (height / 10));
printf("Swipe from (%d, %d) to (%d, %d) - released_near_center=%d, moved_left_and_down=%d\n",
sx, sy, ex, ey, released_near_center, moved_left_and_down);
return released_near_center && moved_left_and_down;
}
static void draw_in_game_menu(LowLevelRenderer* renderer, LowLevelGUI* gui, int width, int height, const char* sleep_label) {
const int menu_w = width - 40;
const int menu_h = 190;
const int menu_x = 20;
const int menu_y = (height - menu_h) / 2;
const int row_h = 34;
const int row_x = menu_x + 16;
const int row_w = menu_w - 32;
const int row_start_y = menu_y + 48;
renderer->draw_filled_rectangle(0, 0, width, height, false, 1);
renderer->set_font(&font_5x5_obj);
renderer->set_text_color(true);
LowLevelWindow* win = gui->draw_new_window(menu_x, menu_y, menu_w, menu_h, "Game Menu");
gui->draw_button(win, row_x, row_start_y + (0 * row_h), "Restart Game", false, true);
gui->draw_button(win, row_x, row_start_y + (1 * row_h), "Back to Game Selection", false, true);
char sleep_label_text[64];
snprintf(sleep_label_text, sizeof(sleep_label_text), "Auto Sleep: %s", sleep_label);
gui->draw_button(win, row_x, row_start_y + (2 * row_h), sleep_label_text, false, true);
}
/**
* @brief Returns true when the currently selected game needs frame ticks.
*/
static inline bool game_wants_frame_updates(GameLauncher& launcher) {
if (!launcher.is_game_selected()) {
return false;
}
Game* game = launcher.get_selected_game();
return game && game->wants_frame_updates();
}
/** /**
* @brief Touch interrupt callback handler * @brief Touch interrupt callback handler
* *
@@ -260,18 +414,17 @@ volatile bool button_key1_pressed = false;
* @param events Event mask (GPIO_IRQ_EDGE_FALL and/or GPIO_IRQ_EDGE_RISE) * @param events Event mask (GPIO_IRQ_EDGE_FALL and/or GPIO_IRQ_EDGE_RISE)
*/ */
void touch_interrupt_handler(uint gpio, uint32_t events) { void touch_interrupt_handler(uint gpio, uint32_t events) {
// Set flag to indicate touch event occurred (void)gpio;
// Main loop will handle the actual touch reading
touch_interrupt_flag = true;
// Track which edge triggered (down vs up) // Track which edge triggered (down vs up).
// Keep ISR minimal: do not log/print from interrupt context.
if (events & GPIO_IRQ_EDGE_FALL) { if (events & GPIO_IRQ_EDGE_FALL) {
touch_event_down = true; touch_event_down = true;
printf("INT: FALL\n"); touch_interrupt_flag = true;
} }
if (events & GPIO_IRQ_EDGE_RISE) { if (events & GPIO_IRQ_EDGE_RISE) {
touch_event_down = false; touch_event_down = false;
printf("INT: RISE\n"); touch_interrupt_flag = true;
} }
} }
@@ -306,9 +459,6 @@ void button_interrupt_handler(uint gpio, uint32_t events) {
const int V_WIDTH = DISPLAY_WIDTH; const int V_WIDTH = DISPLAY_WIDTH;
const int V_HEIGHT = DISPLAY_HEIGHT; const int V_HEIGHT = DISPLAY_HEIGHT;
// Touch indicator settings
#define TOUCH_RADIUS 10
uint8_t bit_buffer[V_WIDTH * V_HEIGHT / 8]; uint8_t bit_buffer[V_WIDTH * V_HEIGHT / 8];
/** /**
@@ -350,6 +500,9 @@ int main()
printf("\n=== %s Demo ===\n", BOARD_NAME); printf("\n=== %s Demo ===\n", BOARD_NAME);
printf("Starting dual-core system...\n"); printf("Starting dual-core system...\n");
// Initialize shared SPI lock before SD/display operations start.
shared_spi_bus_init();
// Create display abstraction using factory method // Create display abstraction using factory method
// The factory handles all board-specific configuration internally // The factory handles all board-specific configuration internally
LowLevelDisplay* display = LowLevelDisplay::create((DisplayType)DISPLAY_TYPE_SELECTED, V_WIDTH, V_HEIGHT); LowLevelDisplay* display = LowLevelDisplay::create((DisplayType)DISPLAY_TYPE_SELECTED, V_WIDTH, V_HEIGHT);
@@ -368,11 +521,11 @@ int main()
return -1; return -1;
} }
// Enable dirty rectangle optimization for ST7796 displays // Enable dirty/partial refresh optimization for ST7796.
if (display->get_type() == DISPLAY_TYPE_ST7796) { if (display->get_type() == DISPLAY_TYPE_ST7796) {
LowLevelDisplayST7796* st7796_display = static_cast<LowLevelDisplayST7796*>(display); LowLevelDisplayST7796* st7796_display = static_cast<LowLevelDisplayST7796*>(display);
st7796_display->enable_dirty_rect(true); st7796_display->enable_dirty_rect(true);
printf("Dirty rectangle optimization enabled (4 quadrants: TL/TR/BL/BR split)\n"); printf("Dirty rectangle optimization enabled\n");
} }
// Launch Core 1 for display refresh handling // Launch Core 1 for display refresh handling
@@ -408,7 +561,7 @@ int main()
if (touch) { if (touch) {
printf("Touch initialized successfully\n"); printf("Touch initialized successfully\n");
// Set up interrupt-driven touch detection // Set up touch IRQ wake-up (InputManager handles active-touch sampling)
printf("Setting up touch interrupt callback...\n"); printf("Setting up touch interrupt callback...\n");
touch->set_interrupt_callback(touch_interrupt_handler); touch->set_interrupt_callback(touch_interrupt_handler);
printf("Touch interrupt enabled on INT pin (falling and rising edges)\n"); printf("Touch interrupt enabled on INT pin (falling and rising edges)\n");
@@ -437,6 +590,10 @@ int main()
// Create GameLauncher // Create GameLauncher
GameLauncher launcher(V_WIDTH, V_HEIGHT, &renderer, &gui, &input_manager); GameLauncher launcher(V_WIDTH, V_HEIGHT, &renderer, &gui, &input_manager);
// Create SerialUploader for rapid game iteration
SerialUploader serial_uploader(&launcher);
printf("Serial uploader initialized\n");
// Register available games // Register available games
launcher.register_game("Tic-Tac-Toe", "Classic 2-player game", launcher.register_game("Tic-Tac-Toe", "Classic 2-player game",
[](uint16_t w, uint16_t h, LowLevelRenderer* r, LowLevelGUI* g, InputManager* im) -> Game* { [](uint16_t w, uint16_t h, LowLevelRenderer* r, LowLevelGUI* g, InputManager* im) -> Game* {
@@ -486,7 +643,7 @@ int main()
// Draw launcher menu // Draw launcher menu
launcher.draw(); launcher.draw();
// Refresh the screen with the launcher menu (async on Core 1) // Initial refresh queued on Core 1 (async for all display types in this test mode).
refresh_screen_async(bit_buffer, display); refresh_screen_async(bit_buffer, display);
printf("Initial screen refresh queued on Core 1\n"); printf("Initial screen refresh queued on Core 1\n");
@@ -526,10 +683,11 @@ int main()
// Core 0 (this loop): Handles input and game logic - stays responsive // Core 0 (this loop): Handles input and game logic - stays responsive
// Core 1: Handles display refresh - can take 1-2 seconds for e-ink // Core 1: Handles display refresh - can take 1-2 seconds for e-ink
// //
// The loop sleeps until an interrupt occurs, then: // The loop primarily sleeps on __wfi(), and wakes to:
// 1. Process input (button or touch) // 1. Process input (button or touch)
// 2. Update game state based on input // 2. Update game state based on input
// 3. Queue refresh on Core 1 (non-blocking) // 3. Queue refresh on Core 1 (non-blocking)
// While touch is active or a game needs frame ticks, the loop stays awake.
// This keeps Core 0 responsive even during slow e-ink refreshes // This keeps Core 0 responsive even during slow e-ink refreshes
// ======================================================================== // ========================================================================
@@ -538,50 +696,121 @@ int main()
// Initialize last interaction time to current time // Initialize last interaction time to current time
last_interaction_time = to_ms_since_boot(get_absolute_time()); last_interaction_time = to_ms_since_boot(get_absolute_time());
global_display = display;
// Set up repeating alarm to periodically check dimming status // Set up repeating alarm to periodically check dimming status
// This wakes the CPU from __wfi() every DIM_CHECK_INTERVAL_MS // This wakes the CPU from __wfi() every DIM_CHECK_INTERVAL_MS
add_alarm_in_ms(DIM_CHECK_INTERVAL_MS, dim_check_alarm_callback, nullptr, true); add_alarm_in_ms(DIM_CHECK_INTERVAL_MS, dim_check_alarm_callback, nullptr, true);
int sleep_option_index = 5; // Default: 10 min
apply_sleep_option(sleep_option_index);
if (display->get_type() == DISPLAY_TYPE_ST7796) { if (display->get_type() == DISPLAY_TYPE_ST7796) {
printf("Power saving: Dim at %d min, Sleep at %d min\n", if (sleep_timeout_ms == 0) {
DIM_TIMEOUT_MS / 60000, SLEEP_TIMEOUT_MS / 60000); printf("Power saving: auto sleep disabled\n");
} else { } else {
printf("Power saving: Sleep at %d min\n", SLEEP_TIMEOUT_MS / 60000); printf("Power saving: Dim at %lus, Sleep at %lus\n",
(unsigned long)(dim_timeout_ms / 1000),
(unsigned long)(sleep_timeout_ms / 1000));
}
} else {
if (sleep_timeout_ms == 0) {
printf("Power saving: auto sleep disabled\n");
} else {
printf("Power saving: Sleep at %lus\n", (unsigned long)(sleep_timeout_ms / 1000));
}
} }
printf("Dimming check timer set to %d seconds\n", DIM_CHECK_INTERVAL_MS / 1000); printf("Dimming check timer set to %d seconds\n", DIM_CHECK_INTERVAL_MS / 1000);
printf("\nEntering reactive game loop (Core 0 - input & logic)\n"); printf("\nEntering reactive game loop (Core 0 - input & logic)\n");
printf("Display refreshes handled by Core 1\n"); printf("Display refreshes handled by Core 1\n");
printf("Frame rate limited to 30 FPS (33.3ms per frame)\n\n"); printf("Frame rate limited to 24 FPS (41.7ms per frame)\n\n");
Game* current_game = nullptr; Game* current_game = nullptr;
uint32_t game_start_time = 0; uint32_t game_start_time = 0;
// Frame rate limiting (30 FPS = 33.33ms per frame) // Frame rate limiting (24 FPS = 41.67ms per frame)
const uint32_t TARGET_FRAME_TIME_MS = 33; // 1000ms / 30fps ≈ 33ms const uint32_t TARGET_FRAME_TIME_MS = 42; // 1000ms / 24fps ≈ 41.7ms
uint32_t last_frame_time = 0; uint32_t last_frame_time = 0;
bool needs_refresh = false; // Track if screen needs redraw
bool dirty_rect_opt_state = (display->get_type() == DISPLAY_TYPE_ST7796);
SceneStack scene_stack;
bool force_sync_tft_refresh = false;
bool core1_restart_attempted = false;
uint32_t last_seen_core1_heartbeat = core1_heartbeat;
uint32_t last_core1_heartbeat_ms = to_ms_since_boot(get_absolute_time());
bool swipe_candidate_active = false;
int16_t swipe_start_x = 0;
int16_t swipe_start_y = 0;
int16_t swipe_last_x = 0;
int16_t swipe_last_y = 0;
while (1) { while (1) {
// Core1 liveness watchdog:
// If refresh work is pending/in-progress but Core1 heartbeat stops advancing,
// fall back to synchronous TFT refresh on Core0 to avoid frozen UI.
uint32_t hb_now = core1_heartbeat;
uint32_t now_ms = to_ms_since_boot(get_absolute_time());
if (hb_now != last_seen_core1_heartbeat) {
last_seen_core1_heartbeat = hb_now;
last_core1_heartbeat_ms = now_ms;
core1_restart_attempted = false;
} else if (!force_sync_tft_refresh &&
(refresh_in_progress || pending_refresh || needs_refresh) &&
(now_ms - last_core1_heartbeat_ms) > 500) {
if (!core1_restart_attempted) {
core1_restart_attempted = true;
restart_core1_refresh_worker();
last_seen_core1_heartbeat = core1_heartbeat;
last_core1_heartbeat_ms = to_ms_since_boot(get_absolute_time());
pending_refresh = true;
} else {
force_sync_tft_refresh = true;
refresh_requested = false;
refresh_in_progress = false;
pending_refresh = true;
printf("Core1 heartbeat stalled after restart; switching TFT refresh to synchronous fallback\n");
}
}
// 0. Process serial uploads (for rapid game iteration)
serial_uploader.process(is_refresh_in_progress());
// If serial uploader wants to launch a game, wait until it's safe (no display refresh)
if (serial_uploader.wants_to_launch_game() && !is_refresh_in_progress()) {
// Safe to launch now - no SPI conflict with display
bool game_launched = serial_uploader.complete_launch();
if (game_launched) {
// A new game was uploaded and launched - trigger redraw
needs_refresh = true;
current_game = launcher.get_selected_game();
scene_stack.clear_to_launcher();
scene_stack.push(SceneId::GAME);
// Note: game is already initialized by select_game_by_name()
}
}
// Determine if we should sleep or stay awake for updates // Determine if we should sleep or stay awake for updates
bool stay_awake = false; bool stay_awake = false;
if (needs_refresh) stay_awake = true;
if (pending_refresh) stay_awake = true; if (pending_refresh) stay_awake = true;
if (serial_uploader.wants_to_launch_game()) stay_awake = true; // Don't sleep while waiting to launch
if (touch_event_down) stay_awake = true; // Keep sampling while finger is down
if (last_touch_time != 0) stay_awake = true; // Keep sampling during active touch session
if (launcher.is_game_selected()) { if (scene_stack.is(SceneId::GAME) && game_wants_frame_updates(launcher)) stay_awake = true;
Game* g = launcher.get_selected_game();
if (g && g->wants_frame_updates()) {
stay_awake = true;
}
}
if (!stay_awake) { if (!stay_awake) {
// Sleep until interrupt wakes us up (very power efficient!) // Sleep until interrupt wakes us up (very power efficient!)
__wfi(); // Wait For Interrupt - CPU sleeps until any interrupt occurs __wfi(); // Wait For Interrupt - CPU sleeps until any interrupt occurs
// Ignore unrelated interrupts (USB/background/timer noise).
// Only continue loop work when one of our wake sources is pending.
if (!has_pending_wake_source()) {
continue;
}
} }
InputEvent input = {INPUT_NONE, 0, 0, 0, 0, 0, false}; InputEvent input = {INPUT_NONE, 0, 0, 0, 0, 0, false};
bool needs_refresh = false;
// 1. Process button input first (higher priority) // 1. Process button input first (higher priority)
input = input_manager.process_button_input(); input = input_manager.process_button_input();
@@ -608,40 +837,86 @@ int main()
input.button_id, input.pressure); input.button_id, input.pressure);
} }
if (launcher.is_game_selected()) { SceneId scene = scene_stack.current();
// In game mode - process game input
current_game = launcher.get_selected_game(); if (scene == SceneId::LAUNCHER) {
needs_refresh = current_game->update(input); swipe_candidate_active = false;
// Wait for any active display refresh to finish before potentially loading a game (SD Card I/O)
// This prevents SPI bus conflicts between Core 0 (SD Card) and Core 1 (Display)
while (is_refresh_in_progress()) {
sleep_us(100);
}
bool game_selected = launcher.update(input);
if (game_selected) {
printf("Game launched successfully\n");
game_start_time = 0;
scene_stack.push(SceneId::GAME);
}
needs_refresh = true;
} else if (scene == SceneId::GAME) {
current_game = launcher.get_selected_game();
if (!current_game) {
scene_stack.clear_to_launcher();
needs_refresh = true;
} else {
bool consumed_by_scene = false;
// Swipe gesture candidate for opening menu (evaluated on touch release).
if (input.type == INPUT_TOUCH_DOWN &&
is_top_right_start(input.x, input.y, V_WIDTH, V_HEIGHT)) {
swipe_candidate_active = true;
swipe_start_x = input.x;
swipe_start_y = input.y;
swipe_last_x = input.x;
swipe_last_y = input.y;
consumed_by_scene = true;
}
if (swipe_candidate_active && (input.type == INPUT_TOUCH_MOVE || input.type == INPUT_TOUCH_UP)) {
consumed_by_scene = true;
if (input.type == INPUT_TOUCH_MOVE) {
swipe_last_x = input.x;
swipe_last_y = input.y;
}
if (input.type == INPUT_TOUCH_UP) {
if (is_open_menu_swipe(swipe_start_x, swipe_start_y, swipe_last_x, swipe_last_y, V_WIDTH, V_HEIGHT)) {
scene_stack.push(SceneId::IN_GAME_MENU);
needs_refresh = true;
printf("Opened in-game menu\n");
}
swipe_candidate_active = false;
}
}
if (!consumed_by_scene) {
needs_refresh = current_game->update(input) || needs_refresh;
// Check if game wants to exit
if (current_game->wants_to_exit()) { if (current_game->wants_to_exit()) {
printf("Game requested exit - returning to launcher\n"); printf("Game requested exit - returning to launcher\n");
swipe_candidate_active = false;
launcher.reset(); launcher.reset();
scene_stack.clear_to_launcher();
needs_refresh = true; needs_refresh = true;
// Force full clear for clean transition
display->clear(false);
if (display->get_type() == DISPLAY_TYPE_EPAPER) { if (display->get_type() == DISPLAY_TYPE_EPAPER) {
LowLevelDisplayEPaper* epaper = static_cast<LowLevelDisplayEPaper*>(display); LowLevelDisplayEPaper* epaper = static_cast<LowLevelDisplayEPaper*>(display);
epaper->full_refresh(); epaper->full_refresh();
} }
} }
// Check if player wants to exit (hold for 2+ seconds or special gesture)
// For now, we'll add a simple long-press detection
if (input.type == INPUT_TOUCH_DOWN) { if (input.type == INPUT_TOUCH_DOWN) {
// Record start time on first touch
if (game_start_time == 0) { if (game_start_time == 0) {
game_start_time = to_ms_since_boot(get_absolute_time()); game_start_time = to_ms_since_boot(get_absolute_time());
} }
} else if (input.type == INPUT_TOUCH_UP) { } else if (input.type == INPUT_TOUCH_UP) {
uint32_t now = to_ms_since_boot(get_absolute_time()); uint32_t now = to_ms_since_boot(get_absolute_time());
if (game_start_time > 0 && (now - game_start_time) > 10000) { if (game_start_time > 0 && (now - game_start_time) > 10000) {
// Long press detected - return to menu
printf("Long press detected - returning to launcher\n"); printf("Long press detected - returning to launcher\n");
swipe_candidate_active = false;
launcher.reset(); launcher.reset();
scene_stack.clear_to_launcher();
needs_refresh = true; needs_refresh = true;
// Force full clear for clean transition
display->clear(false);
if (display->get_type() == DISPLAY_TYPE_EPAPER) { if (display->get_type() == DISPLAY_TYPE_EPAPER) {
LowLevelDisplayEPaper* epaper = static_cast<LowLevelDisplayEPaper*>(display); LowLevelDisplayEPaper* epaper = static_cast<LowLevelDisplayEPaper*>(display);
epaper->full_refresh(); epaper->full_refresh();
@@ -649,27 +924,64 @@ int main()
} }
game_start_time = 0; game_start_time = 0;
} }
} else {
// In launcher mode - process menu input
bool game_selected = launcher.update(input);
if (game_selected) {
printf("Game launched successfully\n");
game_start_time = 0;
// Force full clear for clean transition to game
display->clear(false);
// if (display->get_type() == DISPLAY_TYPE_EPAPER) {
// LowLevelDisplayEPaper* epaper = static_cast<LowLevelDisplayEPaper*>(display);
// epaper->full_refresh();
// }
} }
}
} else if (scene == SceneId::IN_GAME_MENU) {
current_game = launcher.get_selected_game();
if (!current_game) {
launcher.return_to_menu();
scene_stack.clear_to_launcher();
needs_refresh = true;
} else {
const int menu_w = V_WIDTH - 40;
const int menu_h = 190;
const int menu_x = 20;
const int menu_y = (V_HEIGHT - menu_h) / 2;
const int row_h = 34;
const int row_x = menu_x + 16;
const int row_w = menu_w - 32;
const int row_start_y = menu_y + 48;
// Menu tap handling on TOUCH_DOWN because TOUCH_UP coordinates can be unreliable (often 0,0).
if (input.type == INPUT_TOUCH_DOWN) {
if (in_rect(input.x, input.y, row_x, row_start_y + (0 * row_h), row_w, row_h)) {
if (launcher.restart_selected_game()) {
scene_stack.pop(); // Back to GAME
needs_refresh = true;
printf("Restarted current game from global menu\n");
}
} else if (in_rect(input.x, input.y, row_x, row_start_y + (1 * row_h), row_w, row_h)) {
swipe_candidate_active = false;
launcher.return_to_menu();
scene_stack.clear_to_launcher();
needs_refresh = true;
printf("Returned to game launcher from global menu\n");
if (display->get_type() == DISPLAY_TYPE_EPAPER) {
LowLevelDisplayEPaper* epaper = static_cast<LowLevelDisplayEPaper*>(display);
epaper->full_refresh();
}
} else if (in_rect(input.x, input.y, row_x, row_start_y + (2 * row_h), row_w, row_h)) {
sleep_option_index = (sleep_option_index + 1) % kSleepOptionCount;
apply_sleep_option(sleep_option_index);
needs_refresh = true;
if (sleep_timeout_ms == 0) {
printf("Auto sleep disabled\n");
} else {
printf("Auto sleep set to %s\n", kSleepOptions[sleep_option_index].label);
}
} else if (!in_rect(input.x, input.y, menu_x, menu_y, menu_w, menu_h)) {
scene_stack.pop(); // Close menu, resume game scene
needs_refresh = true; needs_refresh = true;
} }
} }
}
}
}
if (launcher.is_game_selected()) { if (scene_stack.is(SceneId::GAME) && game_wants_frame_updates(launcher)) {
// No input, but check if game wants continuous updates // No input, but check if game wants continuous updates
current_game = launcher.get_selected_game(); current_game = launcher.get_selected_game();
if (current_game->wants_frame_updates()) { if (current_game) {
// Only send frame tick if we're ready to draw the next frame // Only send frame tick if we're ready to draw the next frame
if (!is_refresh_in_progress()) { if (!is_refresh_in_progress()) {
InputEvent frame_tick = {INPUT_FRAME_TICK, 0, 0, 0, 0, 0, true}; InputEvent frame_tick = {INPUT_FRAME_TICK, 0, 0, 0, 0, 0, true};
@@ -678,7 +990,7 @@ int main()
} }
} }
// 4. Redraw and queue async refresh on Core 1 (with 30 FPS limiting) // 4. Redraw and queue async refresh on Core 1 (with 24 FPS limiting)
if (needs_refresh || pending_refresh) { if (needs_refresh || pending_refresh) {
// Check frame rate limiting // Check frame rate limiting
uint32_t current_time = to_ms_since_boot(get_absolute_time()); uint32_t current_time = to_ms_since_boot(get_absolute_time());
@@ -688,20 +1000,58 @@ int main()
if (time_since_last_frame >= TARGET_FRAME_TIME_MS) { if (time_since_last_frame >= TARGET_FRAME_TIME_MS) {
// Only draw if Core 1 is finished with the buffer // Only draw if Core 1 is finished with the buffer
if (!is_refresh_in_progress()) { if (!is_refresh_in_progress()) {
// Keep dirty rectangle optimization enabled for TFT.
if (display->get_type() == DISPLAY_TYPE_ST7796) {
bool wants_opt = true;
if (dirty_rect_opt_state != wants_opt) {
LowLevelDisplayST7796* st7796_display = static_cast<LowLevelDisplayST7796*>(display);
st7796_display->enable_dirty_rect(wants_opt);
dirty_rect_opt_state = wants_opt;
}
}
// Clear buffer and redraw entire UI with updated state // Clear buffer and redraw entire UI with updated state
memset(bit_buffer, 0, V_WIDTH * V_HEIGHT / 8); memset(bit_buffer, 0, V_WIDTH * V_HEIGHT / 8);
// Reset renderer state so one scene/game cannot leak clip/text/font settings
// into subsequent scenes (e.g. Lua games using clip rects).
renderer.reset_clip_rect();
renderer.set_text_color(true);
renderer.set_font(&font_homespun_obj);
if (launcher.is_game_selected()) { if (scene_stack.is(SceneId::LAUNCHER)) {
launcher.draw();
} else if (scene_stack.is(SceneId::GAME)) {
current_game = launcher.get_selected_game(); current_game = launcher.get_selected_game();
if (current_game) {
current_game->draw(); current_game->draw();
} else { } else {
launcher.draw(); launcher.draw();
scene_stack.clear_to_launcher();
}
} else if (scene_stack.is(SceneId::IN_GAME_MENU)) {
current_game = launcher.get_selected_game();
if (current_game) {
current_game->draw();
draw_in_game_menu(&renderer, &gui, V_WIDTH, V_HEIGHT, kSleepOptions[sleep_option_index].label);
} else {
launcher.draw();
scene_stack.clear_to_launcher();
}
} }
// Request async refresh (non-blocking - handled by Core 1) bool refresh_started = false;
bool refresh_started = refresh_screen_async(bit_buffer, display); if (force_sync_tft_refresh &&
(display->get_type() == DISPLAY_TYPE_ST7796 || display->get_type() == DISPLAY_TYPE_ST7789)) {
refresh_screen(bit_buffer, display);
refresh_started = true;
} else {
// Async refresh test path.
refresh_started = refresh_screen_async(bit_buffer, display);
}
if (refresh_started) { if (refresh_started) {
needs_refresh = false;
pending_refresh = false; // Refresh queued successfully pending_refresh = false; // Refresh queued successfully
last_frame_time = current_time; // Update frame time last_frame_time = current_time; // Update frame time
} else { } else {
+12 -2
View File
@@ -6,6 +6,8 @@
#include "diskio.h" #include "diskio.h"
#include "sd_card.h" #include "sd_card.h"
#include <string.h> #include <string.h>
#include <stdio.h>
#include <stdbool.h>
/* Definitions of physical drive number for each drive */ /* Definitions of physical drive number for each drive */
#define DEV_SD 0 /* SD card */ #define DEV_SD 0 /* SD card */
@@ -21,7 +23,7 @@ DSTATUS disk_status (
if (pdrv != DEV_SD) return STA_NOINIT; if (pdrv != DEV_SD) return STA_NOINIT;
// Assume SD card is always initialized after sd_card_init() is called // Assume SD card is always initialized after sd_card_init() is called
return 0; // OK return 0; // OK - not write protected, not removed
} }
/*-----------------------------------------------------------------------*/ /*-----------------------------------------------------------------------*/
@@ -76,7 +78,10 @@ DRESULT disk_write (
if (pdrv != DEV_SD) return RES_PARERR; if (pdrv != DEV_SD) return RES_PARERR;
for (UINT i = 0; i < count; i++) { for (UINT i = 0; i < count; i++) {
if (!sd_card_write_block(sector + i, (uint8_t*)(buff + (i * 512)))) { bool result = sd_card_write_block(sector + i, (uint8_t*)(buff + (i * 512)));
if (!result) {
printf("ERROR disk_write failed at sector %lu\n", (unsigned long)(sector + i));
return RES_ERROR; return RES_ERROR;
} }
} }
@@ -131,6 +136,11 @@ DRESULT disk_ioctl (
res = RES_OK; res = RES_OK;
break; break;
case CTRL_TRIM:
// Inform device that data on the block of sectors is no longer used (optional)
res = RES_OK;
break;
default: default:
res = RES_PARERR; res = RES_PARERR;
} }
+131 -90
View File
@@ -1,79 +1,100 @@
#include "low_level_render.h"
#include "low_level_gui.h" #include "low_level_gui.h"
#include "low_level_render.h"
#include <cstring> #include <cstring>
#include <ctime> #include <ctime>
LowLevelWindow* validate_or_create_window(LowLevelWindow* window, LowLevelRenderer* renderer) { LowLevelWindow *validate_or_create_window(LowLevelWindow *window,
LowLevelRenderer *renderer) {
if (window == nullptr) { if (window == nullptr) {
return new LowLevelWindow(0, 0, renderer->get_width(), renderer->get_height(), "Default Window"); return new LowLevelWindow(0, 0, renderer->get_width(),
renderer->get_height(), "Default Window");
} }
return window; return window;
} }
LowLevelGUI::LowLevelGUI(LowLevelRenderer *rend, const Font& font) : renderer(rend), current_font(&font) {} LowLevelGUI::LowLevelGUI(LowLevelRenderer *rend, const Font &font)
: renderer(rend), current_font(&font) {}
LowLevelWindow* LowLevelGUI::draw_new_window(int x, int y, int width, int height, const char *title) LowLevelWindow *LowLevelGUI::draw_new_window(int x, int y, int width,
{ int height, const char *title) {
LowLevelWindow* w = new LowLevelWindow(x, y, width, height, title); LowLevelWindow *w = new LowLevelWindow(x, y, width, height, title);
draw_window(w); draw_window(w);
return w; return w;
} }
void LowLevelGUI::draw_window(LowLevelWindow* window){ void LowLevelGUI::draw_window(LowLevelWindow *window) {
// Draw window border // Draw window border
if (use_rounded_corners) if (use_rounded_corners) {
{ // shadow
//shadow renderer->draw_rounded_rectangle(window->x + 3, window->y + 3,
renderer->draw_rounded_rectangle(window->x+3, window->y+3, window->width, window->height, 10, true, true); window->width, window->height, 10, true,
renderer->draw_rounded_rectangle(window->x-2, window->y-2, window->width+2, window->height+2, 10, false, true); true);
renderer->draw_rounded_rectangle(window->x, window->y, window->width, window->height, 10, true); renderer->draw_rounded_rectangle(window->x - 2, window->y - 2,
} window->width + 2, window->height + 2, 10,
else false, true);
{ renderer->draw_rounded_rectangle(window->x, window->y, window->width,
renderer->draw_filled_rectangle(window->x + 3, window->y + 3, window->width + 2, window->height + 2, true, 2); window->height, 10, true);
renderer->draw_filled_rectangle(window->x - 2, window->y - 2, window->width + 2, window->height + 2, false, 2); } else {
renderer->draw_rectangle(window->x, window->y, window->width, window->height, true, 2); renderer->draw_filled_rectangle(window->x + 3, window->y + 3,
window->width + 2, window->height + 2, true,
2);
renderer->draw_filled_rectangle(window->x - 2, window->y - 2,
window->width + 2, window->height + 2,
false, 2);
renderer->draw_rectangle(window->x, window->y, window->width,
window->height, true, 2);
} }
renderer->draw_line(window->x, window->y + 20, window->x + window->width - 1, window->y + 20, true, 1); renderer->draw_line(window->x, window->y + 20, window->x + window->width - 1,
window->y + 20, true, 1);
// draw closing 'X' button // draw closing 'X' button
int close_size = 12; int close_size = 12;
int close_x = window->x + window->width - close_size - 4; int close_x = window->x + window->width - close_size - 4;
int close_y = window->y + 4; int close_y = window->y + 4;
//renderer->draw_rectangle(close_x, close_y, close_size, close_size, true, 1); // renderer->draw_rectangle(close_x, close_y, close_size, close_size, true,
renderer->draw_line(close_x + 3, close_y + 3, close_x + close_size - 4, close_y + close_size - 4, true, 1); // 1);
renderer->draw_line(close_x + close_size - 4, close_y + 3, close_x + 3, close_y + close_size - 4, true, 1); renderer->draw_line(close_x + 3, close_y + 3, close_x + close_size - 4,
close_y + close_size - 4, true, 1);
renderer->draw_line(close_x + close_size - 4, close_y + 3, close_x + 3,
close_y + close_size - 4, true, 1);
const Font* original_font = renderer->get_current_font(); const Font *original_font = renderer->get_current_font();
renderer->set_font(current_font); renderer->set_font(current_font);
renderer->draw_string_scaled(window->x + 10, window->y + 3, window->title, 2); renderer->draw_string_scaled(window->x + 10, window->y + 3, window->title, 2);
renderer->set_font(original_font); renderer->set_font(original_font);
} }
void LowLevelGUI::draw_button(LowLevelWindow* window, int x, int y, const char *label, bool pressed, bool rounded) void LowLevelGUI::draw_button(LowLevelWindow *window, int x, int y,
{ const char *label, bool pressed, bool rounded) {
window = validate_or_create_window(window, renderer); window = validate_or_create_window(window, renderer);
const Font* original_font = renderer->get_current_font(); const Font *original_font = renderer->get_current_font();
bool original_text_color = renderer->get_current_text_color(); bool original_text_color = renderer->get_current_text_color();
renderer->set_font(current_font); renderer->set_font(current_font);
int text_x = window->x + x + 5; int text_x = window->x + x + 5;
int text_y = window->y + y + 5; int text_y = window->y + y + 5;
int height = renderer->get_current_font()->get_char_height() * 2 + 10; int height = renderer->get_current_font()->get_char_height() * 2 + 10;
int width = int(renderer->draw_string_scaled(text_x, text_y, label, 2) * 1) + 30; int width = renderer->get_string_width_scaled(label, 2) + 30;
if (pressed) if (pressed) {
{ renderer->draw_rounded_rectangle(window->x + x - 1, window->y + y - 1,
renderer->draw_rounded_rectangle(window->x + x - 1, window->y + y -1, width + 2, height+ 2, rounded ? 5 : 0, false, true); width + 2, height + 2, rounded ? 5 : 0,
renderer->draw_rounded_rectangle(window->x + x, window->y + y, width, height, rounded ? 5 : 0, true, true); false, true);
renderer->draw_rounded_rectangle(window->x + x + 2, window->y + y + 2, width - 4, height - 4, rounded ? 5 : 0, false, false); renderer->draw_rounded_rectangle(window->x + x, window->y + y, width,
} height, rounded ? 5 : 0, true, true);
else renderer->draw_rounded_rectangle(window->x + x + 2, window->y + y + 2,
{ width - 4, height - 4, rounded ? 5 : 0,
renderer->draw_rounded_rectangle(window->x + x - 1, window->y + y -1, width + 2, height+ 2, rounded ? 5 : 0, false, true); false, false);
renderer->draw_rounded_rectangle(window->x + x, window->y + y, width, height, rounded ? 5 : 0, false, true); } else {
renderer->draw_rounded_rectangle(window->x + x + 2, window->y + y + 2, width - 4, height - 4, rounded ? 5 : 0, true, false); renderer->draw_rounded_rectangle(window->x + x - 1, window->y + y - 1,
width + 2, height + 2, rounded ? 5 : 0,
false, true);
renderer->draw_rounded_rectangle(window->x + x, window->y + y, width,
height, rounded ? 5 : 0, false, true);
renderer->draw_rounded_rectangle(window->x + x + 2, window->y + y + 2,
width - 4, height - 4, rounded ? 5 : 0,
true, false);
} }
renderer->set_text_color(!pressed); renderer->set_text_color(!pressed);
@@ -83,20 +104,22 @@ void LowLevelGUI::draw_button(LowLevelWindow* window, int x, int y, const char *
renderer->set_text_color(original_text_color); renderer->set_text_color(original_text_color);
} }
void LowLevelGUI::draw_checkbox(LowLevelWindow* window, int x, int y, const char *label, bool checked) { void LowLevelGUI::draw_checkbox(LowLevelWindow *window, int x, int y,
const char *label, bool checked) {
window = validate_or_create_window(window, renderer); window = validate_or_create_window(window, renderer);
const Font* original_font = renderer->get_current_font(); const Font *original_font = renderer->get_current_font();
renderer->set_font(current_font); renderer->set_font(current_font);
int box_size = renderer->get_current_font()->get_char_height() * 2 ; int box_size = renderer->get_current_font()->get_char_height() * 2;
int box_x = window->x + x; int box_x = window->x + x;
int box_y = window->y + y; int box_y = window->y + y;
// Draw checkbox square // Draw checkbox square
renderer->draw_rectangle(box_x, box_y, box_size, box_size, true, 1); renderer->draw_rectangle(box_x, box_y, box_size, box_size, true, 1);
if (checked) if (checked) {
{
// Draw check mark // Draw check mark
renderer->draw_line(box_x + 2, box_y + box_size / 2, box_x + box_size / 2, box_y + box_size - 3, true, 1); renderer->draw_line(box_x + 2, box_y + box_size / 2, box_x + box_size / 2,
renderer->draw_line(box_x + box_size / 2, box_y + box_size - 3, box_x + box_size - 2, box_y + 2, true, 1); box_y + box_size - 3, true, 1);
renderer->draw_line(box_x + box_size / 2, box_y + box_size - 3,
box_x + box_size - 2, box_y + 2, true, 1);
} }
// Draw label // Draw label
renderer->set_text_color(true); renderer->set_text_color(true);
@@ -104,72 +127,74 @@ void LowLevelGUI::draw_checkbox(LowLevelWindow* window, int x, int y, const char
renderer->set_font(original_font); renderer->set_font(original_font);
} }
void LowLevelGUI::draw_radio_button(LowLevelWindow* window, int x, int y, const char *label, bool selected) { void LowLevelGUI::draw_radio_button(LowLevelWindow *window, int x, int y,
const char *label, bool selected) {
window = validate_or_create_window(window, renderer); window = validate_or_create_window(window, renderer);
const Font* original_font = renderer->get_current_font(); const Font *original_font = renderer->get_current_font();
renderer->set_font(current_font); renderer->set_font(current_font);
int radius = renderer->get_current_font()->get_char_height(); int radius = renderer->get_current_font()->get_char_height();
int center_x = window->x + x + radius; int center_x = window->x + x + radius;
int center_y = window->y + y + radius; int center_y = window->y + y + radius;
// Draw outer circle // Draw outer circle
renderer->draw_circle(center_x, center_y, radius, true); renderer->draw_circle(center_x, center_y, radius, true);
if (selected) if (selected) {
{
// Draw inner filled circle // Draw inner filled circle
renderer->draw_filled_circle(center_x, center_y, radius - 4, true); renderer->draw_filled_circle(center_x, center_y, radius - 4, true);
} }
// Draw label // Draw label
renderer->set_text_color(true); renderer->set_text_color(true);
renderer->draw_string_scaled(center_x + radius + 5, center_y - radius / 2, label, 2); renderer->draw_string_scaled(center_x + radius + 5, center_y - radius / 2,
label, 2);
renderer->set_font(original_font); renderer->set_font(original_font);
} }
void LowLevelGUI::draw_slider(LowLevelWindow* window, int x, int y, int width, int height, int position, char* label) { void LowLevelGUI::draw_slider(LowLevelWindow *window, int x, int y, int width,
int height, int position, char *label) {
window = validate_or_create_window(window, renderer); window = validate_or_create_window(window, renderer);
int slider_x = window->x + x; int slider_x = window->x + x;
int slider_y = window->y + y + (label != nullptr ? 20 : 0); int slider_y = window->y + y + (label != nullptr ? 20 : 0);
position = std::max(0, std::min(100, position)); position = std::max(0, std::min(100, position));
// Draw slider track // Draw slider track
renderer->draw_filled_rectangle(slider_x, slider_y + height / 2 - 2, width, 4, true, 1); renderer->draw_filled_rectangle(slider_x, slider_y + height / 2 - 2, width, 4,
true, 1);
// Draw slider handle, considering position to be within [0, 100] // Draw slider handle, considering position to be within [0, 100]
int handle_x = slider_x + (position * width / 100); int handle_x = slider_x + (position * width / 100);
renderer->draw_filled_rectangle(handle_x - 5, slider_y, 10, height, true, 1); renderer->draw_filled_rectangle(handle_x - 5, slider_y, 10, height, true, 1);
renderer->draw_rectangle(handle_x - 6, slider_y - 1, 12, height + 2, true, 1); renderer->draw_rectangle(handle_x - 6, slider_y - 1, 12, height + 2, true, 1);
const Font *original_font = renderer->get_current_font();
const Font* original_font = renderer->get_current_font();
renderer->set_font(current_font); renderer->set_font(current_font);
// draw current position value label on top of the slider // draw current position value label on top of the slider
char pos_label[10]; char pos_label[10];
snprintf(pos_label, sizeof(pos_label), "%d", position); snprintf(pos_label, sizeof(pos_label), "%d", position);
renderer->draw_string_scaled(slider_x + width + 10, slider_y + (height / 2) - 5, pos_label, 1); renderer->draw_string_scaled(slider_x + width + 10,
slider_y + (height / 2) - 5, pos_label, 1);
// Draw label if provided // Draw label if provided
if (label != nullptr) { if (label != nullptr) {
renderer->draw_string_scaled(slider_x, window->y + y, label, 2); renderer->draw_string_scaled(slider_x, window->y + y, label, 2);
} }
renderer->set_font(original_font); renderer->set_font(original_font);
} }
void LowLevelGUI::draw_calendar(LowLevelWindow* window, int x, int y, int month, int year) { void LowLevelGUI::draw_calendar(LowLevelWindow *window, int x, int y, int month,
int year) {
window = validate_or_create_window(window, renderer); window = validate_or_create_window(window, renderer);
const Font* original_font = renderer->get_current_font(); const Font *original_font = renderer->get_current_font();
renderer->set_font(current_font); renderer->set_font(current_font);
// 1. Draw Month and Year Header // 1. Draw Month and Year Header
char title[32]; char title[32];
const char* month_names[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; const char *month_names[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
snprintf(title, sizeof(title), "%s %04d", month_names[month - 1], year); snprintf(title, sizeof(title), "%s %04d", month_names[month - 1], year);
renderer->draw_string_scaled(window->x + x, window->y + y, title, 1); renderer->draw_string_scaled(window->x + x, window->y + y, title, 1);
// 2. Draw Days of the Week labels // 2. Draw Days of the Week labels
const char* days[] = {"Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"}; const char *days[] = {"Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"};
for (int i = 0; i < 7; i++) { for (int i = 0; i < 7; i++) {
renderer->draw_string_scaled(window->x + x + (i * 20), window->y + y + 15, days[i], 1); renderer->draw_string_scaled(window->x + x + (i * 20), window->y + y + 15,
days[i], 1);
} }
// 3. Calculate Month Metadata // 3. Calculate Month Metadata
@@ -212,21 +237,24 @@ void LowLevelGUI::draw_calendar(LowLevelWindow* window, int x, int y, int month,
renderer->set_font(original_font); renderer->set_font(original_font);
} }
void LowLevelGUI::draw_textbox(LowLevelWindow* window, int x, int y, int width, int height, const char* content, bool focused) { void LowLevelGUI::draw_textbox(LowLevelWindow *window, int x, int y, int width,
int height, const char *content, bool focused) {
window = validate_or_create_window(window, renderer); window = validate_or_create_window(window, renderer);
// Draw textbox border // Draw textbox border
int box_x = window->x + x; int box_x = window->x + x;
int box_y = window->y + y; int box_y = window->y + y;
if(focused) { if (focused) {
renderer->draw_filled_rectangle(box_x, box_y, width, height, true, 1); renderer->draw_filled_rectangle(box_x, box_y, width, height, true, 1);
renderer->draw_rectangle(box_x - 1, box_y - 1, width + 2, height + 2, false, 1); renderer->draw_rectangle(box_x - 1, box_y - 1, width + 2, height + 2, false,
1);
} else { } else {
renderer->draw_filled_rectangle(box_x, box_y, width, height, false, 1); renderer->draw_filled_rectangle(box_x, box_y, width, height, false, 1);
renderer->draw_rectangle(box_x - 1, box_y - 1, width + 2, height + 2, true, 1); renderer->draw_rectangle(box_x - 1, box_y - 1, width + 2, height + 2, true,
1);
} }
// Draw content text inside the textbox // Draw content text inside the textbox
const Font* original_font = renderer->get_current_font(); const Font *original_font = renderer->get_current_font();
renderer->set_font(current_font); renderer->set_font(current_font);
renderer->set_text_color(true); renderer->set_text_color(true);
@@ -237,37 +265,44 @@ void LowLevelGUI::draw_textbox(LowLevelWindow* window, int x, int y, int width,
renderer->set_font(original_font); renderer->set_font(original_font);
} }
void LowLevelGUI::draw_tab(LowLevelWindow* window, int x, int y, int width, int height, const char* label, bool selected) { void LowLevelGUI::draw_tab(LowLevelWindow *window, int x, int y, int width,
int height, const char *label, bool selected) {
window = validate_or_create_window(window, renderer); window = validate_or_create_window(window, renderer);
int tab_x = window->x + x; int tab_x = window->x + x;
int tab_y = window->y + y; int tab_y = window->y + y;
if (selected) { if (selected) {
renderer->draw_filled_rectangle(tab_x, tab_y, width, height, true, 1); renderer->draw_filled_rectangle(tab_x, tab_y, width, height, true, 1);
renderer->draw_rectangle(tab_x - 1, tab_y - 1, width + 2, height + 2, false, 1); renderer->draw_rectangle(tab_x - 1, tab_y - 1, width + 2, height + 2, false,
1);
} else { } else {
renderer->draw_filled_rectangle(tab_x, tab_y, width, height, false, 1); renderer->draw_filled_rectangle(tab_x, tab_y, width, height, false, 1);
renderer->draw_rectangle(tab_x - 1, tab_y - 1, width + 2, height + 2, true, 1); renderer->draw_rectangle(tab_x - 1, tab_y - 1, width + 2, height + 2, true,
1);
} }
const Font* original_font = renderer->get_current_font(); const Font *original_font = renderer->get_current_font();
renderer->set_font(current_font); renderer->set_font(current_font);
renderer->set_text_color(true); renderer->set_text_color(true);
// Center the label within the tab // Center the label within the tab
int text_width = int(renderer->draw_string_scaled(0, 0, label, 1) * 0.75); int text_width = renderer->get_string_width_scaled(label, 1);
int text_x = tab_x + (width - text_width) / 2; int text_x = tab_x + (width - text_width) / 2;
int text_y = tab_y + (height - renderer->get_current_font()->get_char_height()) / 2; int text_y =
tab_y + (height - renderer->get_current_font()->get_char_height()) / 2;
renderer->draw_string_scaled(text_x, text_y, label, 1); renderer->draw_string_scaled(text_x, text_y, label, 1);
renderer->set_font(original_font); renderer->set_font(original_font);
} }
void LowLevelGUI::draw_status_bar(LowLevelWindow* window, int x, int y, int width, const char* label, const char* sublabel, int percentage, const char* value_text) { void LowLevelGUI::draw_status_bar(LowLevelWindow *window, int x, int y,
int width, const char *label,
const char *sublabel, int percentage,
const char *value_text) {
window = validate_or_create_window(window, renderer); window = validate_or_create_window(window, renderer);
int base_x = window->x + x; int base_x = window->x + x;
int base_y = window->y + y; int base_y = window->y + y;
const Font* original_font = renderer->get_current_font(); const Font *original_font = renderer->get_current_font();
renderer->set_font(current_font); renderer->set_font(current_font);
// Draw Main Label (e.g., "PANELS") // Draw Main Label (e.g., "PANELS")
@@ -276,7 +311,8 @@ void LowLevelGUI::draw_status_bar(LowLevelWindow* window, int x, int y, int widt
renderer->draw_string_scaled(base_x, base_y + 15, sublabel, 1); renderer->draw_string_scaled(base_x, base_y + 15, sublabel, 1);
int val_width = strlen(value_text) * 8; // Approximation int val_width = strlen(value_text) * 8; // Approximation
renderer->draw_string_scaled(base_x + width - val_width, base_y + 15, value_text, 1); renderer->draw_string_scaled(base_x + width - val_width, base_y + 15,
value_text, 1);
// Draw Bar Container (Rounded) // Draw Bar Container (Rounded)
int bar_y = base_y + 30; int bar_y = base_y + 30;
@@ -286,22 +322,26 @@ void LowLevelGUI::draw_status_bar(LowLevelWindow* window, int x, int y, int widt
// Draw Progress Fill // Draw Progress Fill
int fill_width = (percentage * width) / 100; int fill_width = (percentage * width) / 100;
if (fill_width > 4) { if (fill_width > 4) {
renderer->draw_rounded_rectangle(base_x + 2, bar_y + 2, fill_width - 4, bar_height - 4, 4, true, true); renderer->draw_rounded_rectangle(base_x + 2, bar_y + 2, fill_width - 4,
bar_height - 4, 4, true, true);
} }
renderer->set_font(original_font); renderer->set_font(original_font);
} }
void LowLevelGUI::draw_circular_gauge(LowLevelWindow* window, int x, int y, int width, const char* label, int percentage) { void LowLevelGUI::draw_circular_gauge(LowLevelWindow *window, int x, int y,
int width, const char *label,
int percentage) {
window = validate_or_create_window(window, renderer); window = validate_or_create_window(window, renderer);
int base_x = window->x + x; int base_x = window->x + x;
int base_y = window->y + y; int base_y = window->y + y;
int height = 50; int height = 50;
// Draw pill-shaped container // Draw pill-shaped container
renderer->draw_rounded_rectangle(base_x, base_y, width, height, height/2, true); renderer->draw_rounded_rectangle(base_x, base_y, width, height, height / 2,
true);
const Font* original_font = renderer->get_current_font(); const Font *original_font = renderer->get_current_font();
renderer->set_font(current_font); renderer->set_font(current_font);
// Draw Label // Draw Label
@@ -326,8 +366,9 @@ void LowLevelGUI::draw_circular_gauge(LowLevelWindow* window, int x, int y, int
renderer->set_font(original_font); renderer->set_font(original_font);
} }
void LowLevelGUI::draw_notification(LowLevelWindow *window, int x, int y,
void LowLevelGUI::draw_notification(LowLevelWindow* window, int x, int y, int width, const char* time, const char* message) { int width, const char *time,
const char *message) {
// window = validate_or_create_window(window, renderer); // window = validate_or_create_window(window, renderer);
int base_x = window->x + x; int base_x = window->x + x;
int base_y = window->y + y; int base_y = window->y + y;
@@ -335,7 +376,7 @@ void LowLevelGUI::draw_notification(LowLevelWindow* window, int x, int y, int wi
// Draw dark background // Draw dark background
renderer->draw_rounded_rectangle(base_x, base_y, width, 100, 15, true, true); renderer->draw_rounded_rectangle(base_x, base_y, width, 100, 15, true, true);
const Font* original_font = renderer->get_current_font(); const Font *original_font = renderer->get_current_font();
renderer->set_font(current_font); renderer->set_font(current_font);
renderer->set_text_color(false); // Assume false is white/light on dark renderer->set_text_color(false); // Assume false is white/light on dark
@@ -350,12 +391,12 @@ void LowLevelGUI::draw_notification(LowLevelWindow* window, int x, int y, int wi
renderer->set_font(original_font); renderer->set_font(original_font);
} }
void LowLevelGUI::draw_large_clock(LowLevelWindow* window, int x, int y, const char* time_str) { void LowLevelGUI::draw_large_clock(LowLevelWindow *window, int x, int y,
const char *time_str) {
window = validate_or_create_window(window, renderer); window = validate_or_create_window(window, renderer);
const Font* original_font = renderer->get_current_font(); const Font *original_font = renderer->get_current_font();
renderer->set_font(current_font); renderer->set_font(current_font);
// Draw the time significantly larger (scale 5 or 6) // Draw the time significantly larger (scale 5 or 6)
renderer->draw_string_scaled(window->x + x, window->y + y, time_str, 6); renderer->draw_string_scaled(window->x + x, window->y + y, time_str, 6);
renderer->set_font(original_font); renderer->set_font(original_font);
} }
+54
View File
@@ -671,3 +671,57 @@ int LowLevelRenderer::draw_string_scaled(int x, int y, const char* text, int sca
} }
return current_x; return current_x;
} }
int LowLevelRenderer::get_char_width_scaled(char c, int scale) {
if (!current_font)
return 0;
if (c < 32 || c > 127)
return 0;
if (scale < 1)
scale = 1;
int font_idx = c - 32;
const unsigned char *char_data = current_font->get_char_data(font_idx);
if (!char_data)
return 0;
int bytes_per_char = current_font->get_bytes_per_char();
// Find the actual width by skipping trailing empty columns
int actual_width = 0;
for (int col = bytes_per_char - 1; col >= 0; col--) {
if (char_data[col] != 0) {
actual_width = col + 1;
break;
}
}
return actual_width * scale;
}
int LowLevelRenderer::get_string_width_scaled(const char *text, int scale,
int spacing) {
if (!current_font)
return 0;
int width = 0;
int i = 0;
while (text[i] != '\0') {
int char_width = get_char_width_scaled(text[i], scale);
// Add spacing only if it's not the last character, but logic usually adds
// spacing after each char In drawn_string_scaled: current_x += char_width +
// (spacing * scale); So width accumulates char_width + spacing*scale.
// However, the last character shouldn't really have spacing if we want
// exact bounding box, but let's match draw_string_scaled behavior which
// effectively advances cursor. Wait, draw_string_scaled returns
// `current_x`. If x=0, current_x ends up at sum(char_width +
// spacing*scale).
width += char_width + (spacing * scale);
i++;
}
// Correction: draw_string_scaled includes spacing after the last character.
// If we want exact pixel width of the visible text, we might want to subtract
// the last spacing. But for UI alignment, usually cursor advancement is fine.
// Let's stick to returning what draw_string_scaled would add to x.
return width;
}
+48 -30
View File
@@ -1,9 +1,8 @@
// class that handles low-level rendering operations, such as drawing pixels and shapes to the display. // class that handles low-level rendering operations, such as drawing pixels and
// This class is framework-agnostic and focuses solely on manipulating a 1-bit per pixel buffer. // shapes to the display. This class is framework-agnostic and focuses solely on
// Constructor Args: // manipulating a 1-bit per pixel buffer. Constructor Args: uint8_t* buffer:
// uint8_t* buffer: Pointer to the bit buffer // Pointer to the bit buffer int width: Display width in pixels int height:
// int width: Display width in pixels // Display height in pixels
// int height: Display height in pixels
#ifndef LOW_LEVEL_RENDER_H #ifndef LOW_LEVEL_RENDER_H
#define LOW_LEVEL_RENDER_H #define LOW_LEVEL_RENDER_H
@@ -15,24 +14,26 @@
// Font class that holds font data and dimensions // Font class that holds font data and dimensions
class Font { class Font {
private: private:
const unsigned char* data; const unsigned char *data;
int num_chars; int num_chars;
int bytes_per_char; int bytes_per_char;
int char_height; int char_height;
public: public:
Font(const unsigned char* font_data, int num_chars, int bytes_per_char, int char_height) Font(const unsigned char *font_data, int num_chars, int bytes_per_char,
int char_height)
: data(font_data), num_chars(num_chars), bytes_per_char(bytes_per_char), : data(font_data), num_chars(num_chars), bytes_per_char(bytes_per_char),
char_height(char_height) {} char_height(char_height) {}
const unsigned char* get_data() const { return data; } const unsigned char *get_data() const { return data; }
int get_num_chars() const { return num_chars; } int get_num_chars() const { return num_chars; }
int get_bytes_per_char() const { return bytes_per_char; } int get_bytes_per_char() const { return bytes_per_char; }
int get_char_height() const { return char_height; } int get_char_height() const { return char_height; }
// Get a specific character's data // Get a specific character's data
const unsigned char* get_char_data(int char_index) const { const unsigned char *get_char_data(int char_index) const {
if (char_index < 0 || char_index >= num_chars) return nullptr; if (char_index < 0 || char_index >= num_chars)
return nullptr;
return data + (char_index * bytes_per_char); return data + (char_index * bytes_per_char);
} }
}; };
@@ -93,25 +94,28 @@ extern Font font_zxpix_obj;
class LowLevelRenderer { class LowLevelRenderer {
private: private:
uint8_t* bit_buffer; uint8_t *bit_buffer;
int V_WIDTH; int V_WIDTH;
int V_HEIGHT; int V_HEIGHT;
const Font* current_font; const Font *current_font;
bool clipping_enabled; bool clipping_enabled;
int clip_x, clip_y, clip_width, clip_height; int clip_x, clip_y, clip_width, clip_height;
bool text_color; bool text_color;
void draw_corner_arc(int center_x, int center_y, int radius, int quadrant, bool on); void draw_corner_arc(int center_x, int center_y, int radius, int quadrant,
void fill_bottom_flat_triangle(int x1, int y1, int x2, int y2, int x3, int y3, bool on); bool on);
void fill_top_flat_triangle(int x1, int y1, int x2, int y2, int x3, int y3, bool on); void fill_bottom_flat_triangle(int x1, int y1, int x2, int y2, int x3, int y3,
bool on);
void fill_top_flat_triangle(int x1, int y1, int x2, int y2, int x3, int y3,
bool on);
bool is_point_in_clip_rect(int x, int y); bool is_point_in_clip_rect(int x, int y);
public: public:
LowLevelRenderer(uint8_t* buffer, int width, int height); LowLevelRenderer(uint8_t *buffer, int width, int height);
// Font management // Font management
void set_font(const Font* font); void set_font(const Font *font);
void set_text_color(bool color); void set_text_color(bool color);
const Font* get_current_font() const { return current_font; } const Font *get_current_font() const { return current_font; }
bool get_current_text_color() const { return text_color; } bool get_current_text_color() const { return text_color; }
int get_width() const { return V_WIDTH; } int get_width() const { return V_WIDTH; }
int get_height() const { return V_HEIGHT; } int get_height() const { return V_HEIGHT; }
@@ -119,19 +123,28 @@ public:
// --- 1-BIT DRAWING PRIMITIVES --- // --- 1-BIT DRAWING PRIMITIVES ---
void set_pixel(int x, int y, bool on); void set_pixel(int x, int y, bool on);
void draw_line(int x0, int y0, int x1, int y1, bool on, int width = 1); void draw_line(int x0, int y0, int x1, int y1, bool on, int width = 1);
void draw_rectangle(int x, int y, int width, int height, bool on, int line_width); void draw_rectangle(int x, int y, int width, int height, bool on,
void draw_filled_rectangle(int x, int y, int width, int height, bool on, int line_width); int line_width);
void draw_rounded_rectangle(int x, int y, int width, int height, int radius, bool on, bool filled = false); void draw_filled_rectangle(int x, int y, int width, int height, bool on,
int line_width);
void draw_rounded_rectangle(int x, int y, int width, int height, int radius,
bool on, bool filled = false);
void draw_triangle(int x1, int y1, int x2, int y2, int x3, int y3, bool on); void draw_triangle(int x1, int y1, int x2, int y2, int x3, int y3, bool on);
void draw_filled_triangle(int x1, int y1, int x2, int y2, int x3, int y3, bool on); void draw_filled_triangle(int x1, int y1, int x2, int y2, int x3, int y3,
void draw_ellipse(int center_x, int center_y, int radius_x, int radius_y, bool on); bool on);
void draw_filled_ellipse(int center_x, int center_y, int radius_x, int radius_y, bool on); void draw_ellipse(int center_x, int center_y, int radius_x, int radius_y,
void draw_polygon(const std::vector<std::pair<int, int>>& points, bool on); bool on);
void draw_filled_polygon(const std::vector<std::pair<int, int>>& points, bool on); void draw_filled_ellipse(int center_x, int center_y, int radius_x,
void draw_arc(int center_x, int center_y, int radius, int start_angle, int end_angle, bool on); int radius_y, bool on);
void draw_polygon(const std::vector<std::pair<int, int>> &points, bool on);
void draw_filled_polygon(const std::vector<std::pair<int, int>> &points,
bool on);
void draw_arc(int center_x, int center_y, int radius, int start_angle,
int end_angle, bool on);
// Bitmap drawing // Bitmap drawing
void draw_bitmap(const unsigned char* bitmap, int x, int y, int width, int height, bool invert = false); void draw_bitmap(const unsigned char *bitmap, int x, int y, int width,
int height, bool invert = false);
// Clipping functions // Clipping functions
void set_clip_rect(int x, int y, int width, int height); void set_clip_rect(int x, int y, int width, int height);
@@ -147,7 +160,12 @@ public:
int draw_char_vcol(int x, int y, char c); int draw_char_vcol(int x, int y, char c);
void draw_string(int x, int y, const std::string &text, int spacing = 1); void draw_string(int x, int y, const std::string &text, int spacing = 1);
int draw_char_scaled(int x, int y, char c, int scale); int draw_char_scaled(int x, int y, char c, int scale);
int draw_string_scaled(int x, int y, const char* text, int scale, int spacing = 1); int draw_string_scaled(int x, int y, const char *text, int scale,
int spacing = 1);
// Width calculation without drawing
int get_char_width_scaled(char c, int scale);
int get_string_width_scaled(const char *text, int scale, int spacing = 1);
}; };
#endif // LOW_LEVEL_RENDER_H #endif // LOW_LEVEL_RENDER_H
-383
View File
@@ -1,383 +0,0 @@
# This is the CMakeCache file.
# For build in directory: /Users/adolforeyna/Projects/basic1/emulator
# It was generated by CMake: /opt/homebrew/bin/cmake
# You can edit this file to change values found and used by cmake.
# If you do not want to change any of the values, simply exit the editor.
# If you do want to change a value, simply edit, save, and exit the editor.
# The syntax for the file is as follows:
# KEY:TYPE=VALUE
# KEY is the name of a variable in the cache.
# TYPE is a hint to GUIs for the type of VALUE, DO NOT EDIT TYPE!.
# VALUE is the current value for the KEY.
########################
# EXTERNAL cache entries
########################
//Path to a program.
CMAKE_ADDR2LINE:FILEPATH=CMAKE_ADDR2LINE-NOTFOUND
//Path to a program.
CMAKE_AR:FILEPATH=/usr/bin/ar
//Choose the type of build, options are: None Debug Release RelWithDebInfo
// MinSizeRel ...
CMAKE_BUILD_TYPE:STRING=
//Enable/Disable color output during build.
CMAKE_COLOR_MAKEFILE:BOOL=ON
//CXX compiler
CMAKE_CXX_COMPILER:FILEPATH=/usr/bin/c++
//Flags used by the CXX compiler during all build types.
CMAKE_CXX_FLAGS:STRING=
//Flags used by the CXX compiler during DEBUG builds.
CMAKE_CXX_FLAGS_DEBUG:STRING=-g
//Flags used by the CXX compiler during MINSIZEREL builds.
CMAKE_CXX_FLAGS_MINSIZEREL:STRING=-Os -DNDEBUG
//Flags used by the CXX compiler during RELEASE builds.
CMAKE_CXX_FLAGS_RELEASE:STRING=-O3 -DNDEBUG
//Flags used by the CXX compiler during RELWITHDEBINFO builds.
CMAKE_CXX_FLAGS_RELWITHDEBINFO:STRING=-O2 -g -DNDEBUG
//C compiler
CMAKE_C_COMPILER:FILEPATH=/usr/bin/cc
//Flags used by the C compiler during all build types.
CMAKE_C_FLAGS:STRING=
//Flags used by the C compiler during DEBUG builds.
CMAKE_C_FLAGS_DEBUG:STRING=-g
//Flags used by the C compiler during MINSIZEREL builds.
CMAKE_C_FLAGS_MINSIZEREL:STRING=-Os -DNDEBUG
//Flags used by the C compiler during RELEASE builds.
CMAKE_C_FLAGS_RELEASE:STRING=-O3 -DNDEBUG
//Flags used by the C compiler during RELWITHDEBINFO builds.
CMAKE_C_FLAGS_RELWITHDEBINFO:STRING=-O2 -g -DNDEBUG
//Path to a program.
CMAKE_DLLTOOL:FILEPATH=CMAKE_DLLTOOL-NOTFOUND
//Flags used by the linker during all build types.
CMAKE_EXE_LINKER_FLAGS:STRING=
//Flags used by the linker during DEBUG builds.
CMAKE_EXE_LINKER_FLAGS_DEBUG:STRING=
//Flags used by the linker during MINSIZEREL builds.
CMAKE_EXE_LINKER_FLAGS_MINSIZEREL:STRING=
//Flags used by the linker during RELEASE builds.
CMAKE_EXE_LINKER_FLAGS_RELEASE:STRING=
//Flags used by the linker during RELWITHDEBINFO builds.
CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO:STRING=
//Enable/Disable output of compile commands during generation.
CMAKE_EXPORT_COMPILE_COMMANDS:BOOL=
//Value Computed by CMake.
CMAKE_FIND_PACKAGE_REDIRECTS_DIR:STATIC=/Users/adolforeyna/Projects/basic1/emulator/CMakeFiles/pkgRedirects
//Path to a program.
CMAKE_INSTALL_NAME_TOOL:FILEPATH=/usr/bin/install_name_tool
//Install path prefix, prepended onto install directories.
CMAKE_INSTALL_PREFIX:PATH=/usr/local
//Path to a program.
CMAKE_LINKER:FILEPATH=/usr/bin/ld
//Path to a program.
CMAKE_MAKE_PROGRAM:FILEPATH=/usr/bin/make
//Flags used by the linker during the creation of modules during
// all build types.
CMAKE_MODULE_LINKER_FLAGS:STRING=
//Flags used by the linker during the creation of modules during
// DEBUG builds.
CMAKE_MODULE_LINKER_FLAGS_DEBUG:STRING=
//Flags used by the linker during the creation of modules during
// MINSIZEREL builds.
CMAKE_MODULE_LINKER_FLAGS_MINSIZEREL:STRING=
//Flags used by the linker during the creation of modules during
// RELEASE builds.
CMAKE_MODULE_LINKER_FLAGS_RELEASE:STRING=
//Flags used by the linker during the creation of modules during
// RELWITHDEBINFO builds.
CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO:STRING=
//Path to a program.
CMAKE_NM:FILEPATH=/usr/bin/nm
//Path to a program.
CMAKE_OBJCOPY:FILEPATH=CMAKE_OBJCOPY-NOTFOUND
//Path to a program.
CMAKE_OBJDUMP:FILEPATH=/usr/bin/objdump
//Build architectures for OSX
CMAKE_OSX_ARCHITECTURES:STRING=
//Minimum OS X version to target for deployment (at runtime); newer
// APIs weak linked. Set to empty string for default value.
CMAKE_OSX_DEPLOYMENT_TARGET:STRING=
//The product will be built against the headers and libraries located
// inside the indicated SDK.
CMAKE_OSX_SYSROOT:STRING=
//Value Computed by CMake
CMAKE_PROJECT_COMPAT_VERSION:STATIC=
//Value Computed by CMake
CMAKE_PROJECT_DESCRIPTION:STATIC=
//Value Computed by CMake
CMAKE_PROJECT_HOMEPAGE_URL:STATIC=
//Value Computed by CMake
CMAKE_PROJECT_NAME:STATIC=basic1_emulator
//Value Computed by CMake
CMAKE_PROJECT_SPDX_LICENSE:STATIC=
//Path to a program.
CMAKE_RANLIB:FILEPATH=/usr/bin/ranlib
//Path to a program.
CMAKE_READELF:FILEPATH=CMAKE_READELF-NOTFOUND
//Flags used by the linker during the creation of shared libraries
// during all build types.
CMAKE_SHARED_LINKER_FLAGS:STRING=
//Flags used by the linker during the creation of shared libraries
// during DEBUG builds.
CMAKE_SHARED_LINKER_FLAGS_DEBUG:STRING=
//Flags used by the linker during the creation of shared libraries
// during MINSIZEREL builds.
CMAKE_SHARED_LINKER_FLAGS_MINSIZEREL:STRING=
//Flags used by the linker during the creation of shared libraries
// during RELEASE builds.
CMAKE_SHARED_LINKER_FLAGS_RELEASE:STRING=
//Flags used by the linker during the creation of shared libraries
// during RELWITHDEBINFO builds.
CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO:STRING=
//If set, runtime paths are not added when installing shared libraries,
// but are added when building.
CMAKE_SKIP_INSTALL_RPATH:BOOL=NO
//If set, runtime paths are not added when using shared libraries.
CMAKE_SKIP_RPATH:BOOL=NO
//Flags used by the archiver during the creation of static libraries
// during all build types.
CMAKE_STATIC_LINKER_FLAGS:STRING=
//Flags used by the archiver during the creation of static libraries
// during DEBUG builds.
CMAKE_STATIC_LINKER_FLAGS_DEBUG:STRING=
//Flags used by the archiver during the creation of static libraries
// during MINSIZEREL builds.
CMAKE_STATIC_LINKER_FLAGS_MINSIZEREL:STRING=
//Flags used by the archiver during the creation of static libraries
// during RELEASE builds.
CMAKE_STATIC_LINKER_FLAGS_RELEASE:STRING=
//Flags used by the archiver during the creation of static libraries
// during RELWITHDEBINFO builds.
CMAKE_STATIC_LINKER_FLAGS_RELWITHDEBINFO:STRING=
//Path to a program.
CMAKE_STRIP:FILEPATH=/usr/bin/strip
//Path to a program.
CMAKE_TAPI:FILEPATH=/Volumes/ReynaFamily/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/tapi
//If this value is on, makefiles will be generated without the
// .SILENT directive, and all commands will be echoed to the console
// during the make. This is useful for debugging only. With Visual
// Studio IDE projects all commands are done without /nologo.
CMAKE_VERBOSE_MAKEFILE:BOOL=FALSE
//The directory containing a CMake configuration file for SFML.
SFML_DIR:PATH=/opt/homebrew/lib/cmake/SFML
//Path to a file.
SFML_DOC_DIR:PATH=/opt/homebrew/share/doc/SFML
//Value Computed by CMake
basic1_emulator_BINARY_DIR:STATIC=/Users/adolforeyna/Projects/basic1/emulator
//Value Computed by CMake
basic1_emulator_IS_TOP_LEVEL:STATIC=ON
//Value Computed by CMake
basic1_emulator_SOURCE_DIR:STATIC=/Users/adolforeyna/Projects/basic1/emulator
########################
# INTERNAL cache entries
########################
//ADVANCED property for variable: CMAKE_ADDR2LINE
CMAKE_ADDR2LINE-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_AR
CMAKE_AR-ADVANCED:INTERNAL=1
//This is the directory where this CMakeCache.txt was created
CMAKE_CACHEFILE_DIR:INTERNAL=/Users/adolforeyna/Projects/basic1/emulator
//Major version of cmake used to create the current loaded cache
CMAKE_CACHE_MAJOR_VERSION:INTERNAL=4
//Minor version of cmake used to create the current loaded cache
CMAKE_CACHE_MINOR_VERSION:INTERNAL=2
//Patch version of cmake used to create the current loaded cache
CMAKE_CACHE_PATCH_VERSION:INTERNAL=3
//ADVANCED property for variable: CMAKE_COLOR_MAKEFILE
CMAKE_COLOR_MAKEFILE-ADVANCED:INTERNAL=1
//Path to CMake executable.
CMAKE_COMMAND:INTERNAL=/opt/homebrew/bin/cmake
//Path to cpack program executable.
CMAKE_CPACK_COMMAND:INTERNAL=/opt/homebrew/bin/cpack
//Path to ctest program executable.
CMAKE_CTEST_COMMAND:INTERNAL=/opt/homebrew/bin/ctest
//ADVANCED property for variable: CMAKE_CXX_COMPILER
CMAKE_CXX_COMPILER-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_CXX_FLAGS
CMAKE_CXX_FLAGS-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_CXX_FLAGS_DEBUG
CMAKE_CXX_FLAGS_DEBUG-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_CXX_FLAGS_MINSIZEREL
CMAKE_CXX_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_CXX_FLAGS_RELEASE
CMAKE_CXX_FLAGS_RELEASE-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_CXX_FLAGS_RELWITHDEBINFO
CMAKE_CXX_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_C_COMPILER
CMAKE_C_COMPILER-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_C_FLAGS
CMAKE_C_FLAGS-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_C_FLAGS_DEBUG
CMAKE_C_FLAGS_DEBUG-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_C_FLAGS_MINSIZEREL
CMAKE_C_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_C_FLAGS_RELEASE
CMAKE_C_FLAGS_RELEASE-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_C_FLAGS_RELWITHDEBINFO
CMAKE_C_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_DLLTOOL
CMAKE_DLLTOOL-ADVANCED:INTERNAL=1
//Path to cache edit program executable.
CMAKE_EDIT_COMMAND:INTERNAL=/opt/homebrew/bin/ccmake
//Executable file format
CMAKE_EXECUTABLE_FORMAT:INTERNAL=MACHO
//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS
CMAKE_EXE_LINKER_FLAGS-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS_DEBUG
CMAKE_EXE_LINKER_FLAGS_DEBUG-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS_MINSIZEREL
CMAKE_EXE_LINKER_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS_RELEASE
CMAKE_EXE_LINKER_FLAGS_RELEASE-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO
CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_EXPORT_COMPILE_COMMANDS
CMAKE_EXPORT_COMPILE_COMMANDS-ADVANCED:INTERNAL=1
//Name of external makefile project generator.
CMAKE_EXTRA_GENERATOR:INTERNAL=
//Name of generator.
CMAKE_GENERATOR:INTERNAL=Unix Makefiles
//Generator instance identifier.
CMAKE_GENERATOR_INSTANCE:INTERNAL=
//Name of generator platform.
CMAKE_GENERATOR_PLATFORM:INTERNAL=
//Name of generator toolset.
CMAKE_GENERATOR_TOOLSET:INTERNAL=
//Source directory with the top level CMakeLists.txt file for this
// project
CMAKE_HOME_DIRECTORY:INTERNAL=/Users/adolforeyna/Projects/basic1/emulator
//ADVANCED property for variable: CMAKE_INSTALL_NAME_TOOL
CMAKE_INSTALL_NAME_TOOL-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_LINKER
CMAKE_LINKER-ADVANCED:INTERNAL=1
//Name of CMakeLists files to read
CMAKE_LIST_FILE_NAME:INTERNAL=CMakeLists.txt
//ADVANCED property for variable: CMAKE_MAKE_PROGRAM
CMAKE_MAKE_PROGRAM-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS
CMAKE_MODULE_LINKER_FLAGS-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS_DEBUG
CMAKE_MODULE_LINKER_FLAGS_DEBUG-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS_MINSIZEREL
CMAKE_MODULE_LINKER_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS_RELEASE
CMAKE_MODULE_LINKER_FLAGS_RELEASE-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO
CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_NM
CMAKE_NM-ADVANCED:INTERNAL=1
//number of local generators
CMAKE_NUMBER_OF_MAKEFILES:INTERNAL=1
//ADVANCED property for variable: CMAKE_OBJCOPY
CMAKE_OBJCOPY-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_OBJDUMP
CMAKE_OBJDUMP-ADVANCED:INTERNAL=1
//Platform information initialized
CMAKE_PLATFORM_INFO_INITIALIZED:INTERNAL=1
//ADVANCED property for variable: CMAKE_RANLIB
CMAKE_RANLIB-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_READELF
CMAKE_READELF-ADVANCED:INTERNAL=1
//Path to CMake installation.
CMAKE_ROOT:INTERNAL=/opt/homebrew/share/cmake
//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS
CMAKE_SHARED_LINKER_FLAGS-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS_DEBUG
CMAKE_SHARED_LINKER_FLAGS_DEBUG-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS_MINSIZEREL
CMAKE_SHARED_LINKER_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS_RELEASE
CMAKE_SHARED_LINKER_FLAGS_RELEASE-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO
CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_SKIP_INSTALL_RPATH
CMAKE_SKIP_INSTALL_RPATH-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_SKIP_RPATH
CMAKE_SKIP_RPATH-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS
CMAKE_STATIC_LINKER_FLAGS-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS_DEBUG
CMAKE_STATIC_LINKER_FLAGS_DEBUG-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS_MINSIZEREL
CMAKE_STATIC_LINKER_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS_RELEASE
CMAKE_STATIC_LINKER_FLAGS_RELEASE-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS_RELWITHDEBINFO
CMAKE_STATIC_LINKER_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_STRIP
CMAKE_STRIP-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_TAPI
CMAKE_TAPI-ADVANCED:INTERNAL=1
//uname command
CMAKE_UNAME:INTERNAL=/usr/bin/uname
//ADVANCED property for variable: CMAKE_VERBOSE_MAKEFILE
CMAKE_VERBOSE_MAKEFILE-ADVANCED:INTERNAL=1
-1315
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
+3 -2
View File
@@ -1,8 +1,9 @@
#!/bin/bash #!/bin/bash
set -e set -e
echo "[Emulator] Cleaning old build..." echo "[Emulator] Generating build files..."
make clean cmake .
echo "[Emulator] Building..." echo "[Emulator] Building..."
make make
-61
View File
@@ -1,61 +0,0 @@
# Install script for directory: /Users/adolforeyna/Projects/basic1/emulator
# Set the install prefix
if(NOT DEFINED CMAKE_INSTALL_PREFIX)
set(CMAKE_INSTALL_PREFIX "/usr/local")
endif()
string(REGEX REPLACE "/$" "" CMAKE_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}")
# Set the install configuration name.
if(NOT DEFINED CMAKE_INSTALL_CONFIG_NAME)
if(BUILD_TYPE)
string(REGEX REPLACE "^[^A-Za-z0-9_]+" ""
CMAKE_INSTALL_CONFIG_NAME "${BUILD_TYPE}")
else()
set(CMAKE_INSTALL_CONFIG_NAME "")
endif()
message(STATUS "Install configuration: \"${CMAKE_INSTALL_CONFIG_NAME}\"")
endif()
# Set the component getting installed.
if(NOT CMAKE_INSTALL_COMPONENT)
if(COMPONENT)
message(STATUS "Install component: \"${COMPONENT}\"")
set(CMAKE_INSTALL_COMPONENT "${COMPONENT}")
else()
set(CMAKE_INSTALL_COMPONENT)
endif()
endif()
# Is this installation the result of a crosscompile?
if(NOT DEFINED CMAKE_CROSSCOMPILING)
set(CMAKE_CROSSCOMPILING "FALSE")
endif()
# Set path to fallback-tool for dependency-resolution.
if(NOT DEFINED CMAKE_OBJDUMP)
set(CMAKE_OBJDUMP "/usr/bin/objdump")
endif()
string(REPLACE ";" "\n" CMAKE_INSTALL_MANIFEST_CONTENT
"${CMAKE_INSTALL_MANIFEST_FILES}")
if(CMAKE_INSTALL_LOCAL_ONLY)
file(WRITE "/Users/adolforeyna/Projects/basic1/emulator/install_local_manifest.txt"
"${CMAKE_INSTALL_MANIFEST_CONTENT}")
endif()
if(CMAKE_INSTALL_COMPONENT)
if(CMAKE_INSTALL_COMPONENT MATCHES "^[a-zA-Z0-9_.+-]+$")
set(CMAKE_INSTALL_MANIFEST "install_manifest_${CMAKE_INSTALL_COMPONENT}.txt")
else()
string(MD5 CMAKE_INST_COMP_HASH "${CMAKE_INSTALL_COMPONENT}")
set(CMAKE_INSTALL_MANIFEST "install_manifest_${CMAKE_INST_COMP_HASH}.txt")
unset(CMAKE_INST_COMP_HASH)
endif()
else()
set(CMAKE_INSTALL_MANIFEST "install_manifest.txt")
endif()
if(NOT CMAKE_INSTALL_LOCAL_ONLY)
file(WRITE "/Users/adolforeyna/Projects/basic1/emulator/${CMAKE_INSTALL_MANIFEST}"
"${CMAKE_INSTALL_MANIFEST_CONTENT}")
endif()
+32 -18
View File
@@ -1,15 +1,21 @@
// Copy of game_launcher.cpp for emulator build // Copy of game_launcher.cpp for emulator build
#include "game_launcher.h" #include "game_launcher.h"
#include "input_manager.h"
#include "../display/low_level_render.h"
#include "../display/low_level_gui.h" #include "../display/low_level_gui.h"
#include "../display/low_level_render.h"
#include "input_manager.h"
#include <stdio.h> #include <stdio.h>
extern Font font_5x5_obj; extern Font font_5x5_obj;
GameLauncher::GameLauncher(uint16_t width, uint16_t height, LowLevelRenderer* renderer, LowLevelGUI* gui, InputManager* input_manager) GameLauncher::GameLauncher(uint16_t width, uint16_t height,
: width(width), height(height), renderer(renderer), gui(gui), input_manager(input_manager), LowLevelRenderer *renderer, LowLevelGUI *gui,
selected_index(0), selected_game(nullptr), current_page(0) {} InputManager *input_manager)
void GameLauncher::register_game(const char* name, const char* description, : width(width), height(height), renderer(renderer), gui(gui),
std::function<Game*(uint16_t, uint16_t, LowLevelRenderer*, LowLevelGUI*, InputManager*)> factory) { input_manager(input_manager), selected_index(0), selected_game(nullptr),
current_page(0) {}
void GameLauncher::register_game(
const char *name, const char *description,
std::function<Game *(uint16_t, uint16_t, LowLevelRenderer *, LowLevelGUI *,
InputManager *)>
factory) {
GameEntry entry; GameEntry entry;
entry.name = name; entry.name = name;
entry.description = description; entry.description = description;
@@ -18,12 +24,14 @@ void GameLauncher::register_game(const char* name, const char* description,
printf("Registered game: %s - %s\n", name, description); printf("Registered game: %s - %s\n", name, description);
} }
void GameLauncher::draw() { void GameLauncher::draw() {
LowLevelWindow* window = gui->draw_new_window(10, 10, width - 20, height - 20, "Game Launcher"); LowLevelWindow *window =
gui->draw_new_window(10, 10, width - 20, height - 20, "Game Launcher");
renderer->set_font(&font_5x5_obj); renderer->set_font(&font_5x5_obj);
int total_pages = get_total_pages(); int total_pages = get_total_pages();
char title[64]; char title[64];
snprintf(title, sizeof(title), "Select a Game: (Page %d/%d)", current_page + 1, total_pages); snprintf(title, sizeof(title), "Select a Game: (Page %d/%d)",
current_page + 1, total_pages);
renderer->draw_string_scaled(30, 40, title, 2); renderer->draw_string_scaled(30, 40, title, 2);
int page_start = get_page_start_index(); int page_start = get_page_start_index();
@@ -41,16 +49,19 @@ void GameLauncher::draw() {
if (total_pages > 1) { if (total_pages > 1) {
int button_y = height - 65; int button_y = height - 65;
gui->draw_button(window, PREV_BUTTON_X, button_y, "< PREV", false, true);
gui->draw_button(window, NEXT_BUTTON_X, button_y, "NEXT >", false, true);
renderer->set_font(&font_5x5_obj); renderer->set_font(&font_5x5_obj);
renderer->draw_string_scaled(30, height - 25, "Touch buttons or KEY0/KEY1", 1); gui->draw_button(window, PREV_BUTTON_X, button_y, "PREV", false, true);
gui->draw_button(window, NEXT_BUTTON_X, button_y, "NEXT", false, true);
renderer->set_font(&font_5x5_obj);
renderer->draw_string_scaled(30, height - 25, "Touch buttons or KEY0/KEY1",
1);
} else { } else {
renderer->set_font(&font_5x5_obj); renderer->set_font(&font_5x5_obj);
renderer->draw_string_scaled(30, height - 35, "KEY0: Navigate | KEY1: Select | Touch to play", 1); renderer->draw_string_scaled(
30, height - 35, "KEY0: Navigate | KEY1: Select | Touch to play", 1);
} }
} }
bool GameLauncher::update(const InputEvent& event) { bool GameLauncher::update(const InputEvent &event) {
bool needs_refresh = false; bool needs_refresh = false;
int total_pages = get_total_pages(); int total_pages = get_total_pages();
@@ -85,7 +96,8 @@ bool GameLauncher::update(const InputEvent& event) {
int item_index = i - page_start; int item_index = i - page_start;
int y = MENU_Y_START + (item_index * MENU_ITEM_HEIGHT); int y = MENU_Y_START + (item_index * MENU_ITEM_HEIGHT);
if (event.y >= y - 5 && event.y < y + MENU_ITEM_HEIGHT - 5) { if (event.y >= y - 5 && event.y < y + MENU_ITEM_HEIGHT - 5) {
selected_game = games[i].factory(width, height, renderer, gui, input_manager); selected_game =
games[i].factory(width, height, renderer, gui, input_manager);
if (selected_game) { if (selected_game) {
selected_game->init(); selected_game->init();
return true; return true;
@@ -127,7 +139,8 @@ bool GameLauncher::update(const InputEvent& event) {
} }
case INPUT_BUTTON_1: { case INPUT_BUTTON_1: {
if (selected_index >= 0 && selected_index < (int)games.size()) { if (selected_index >= 0 && selected_index < (int)games.size()) {
selected_game = games[selected_index].factory(width, height, renderer, gui, input_manager); selected_game = games[selected_index].factory(width, height, renderer,
gui, input_manager);
if (selected_game) { if (selected_game) {
selected_game->init(); selected_game->init();
return true; return true;
@@ -140,7 +153,7 @@ bool GameLauncher::update(const InputEvent& event) {
} }
return needs_refresh; return needs_refresh;
} }
Game* GameLauncher::get_selected_game() { return selected_game; } Game *GameLauncher::get_selected_game() { return selected_game; }
void GameLauncher::reset() { void GameLauncher::reset() {
if (selected_game) { if (selected_game) {
delete selected_game; delete selected_game;
@@ -151,7 +164,8 @@ void GameLauncher::reset() {
} }
int GameLauncher::get_total_pages() const { int GameLauncher::get_total_pages() const {
if (games.empty()) return 1; if (games.empty())
return 1;
return (games.size() + GAMES_PER_PAGE - 1) / GAMES_PER_PAGE; return (games.size() + GAMES_PER_PAGE - 1) / GAMES_PER_PAGE;
} }
-84
View File
@@ -1,84 +0,0 @@
-- NAME: Bouncing Ball
-- DESC: Physics demo with state management
-- States
local STATE_PAUSED = 0
local STATE_RUNNING = 1
function init()
game.vars.state = STATE_PAUSED
game.vars.ball_x = game.width() / 2
game.vars.ball_y = game.height() / 2
game.vars.vel_x = 3
game.vars.vel_y = 2
game.vars.radius = 10
game.vars.frame_count = 0
-- Enable continuous frame updates for smooth animation
game.set_frame_updates(true)
print("Bouncing Ball initialized")
end
function update(event)
-- Toggle pause on tap
if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.BUTTON_0 or event.type == INPUT.BUTTON_1 then
if game.vars.state == STATE_PAUSED then
game.vars.state = STATE_RUNNING
else
game.vars.state = STATE_PAUSED
end
return true
end
-- Update physics if running (on any frame tick)
if event.type == INPUT.FRAME_TICK and game.vars.state == STATE_RUNNING then
-- Move ball
game.vars.ball_x = game.vars.ball_x + game.vars.vel_x
game.vars.ball_y = game.vars.ball_y + game.vars.vel_y
-- Bounce off walls
if game.vars.ball_x - game.vars.radius < 0 or game.vars.ball_x + game.vars.radius > game.width() then
game.vars.vel_x = -game.vars.vel_x
game.vars.ball_x = math.max(game.vars.radius, math.min(game.width() - game.vars.radius, game.vars.ball_x))
end
if game.vars.ball_y - game.vars.radius < 0 or game.vars.ball_y + game.vars.radius > game.height() then
game.vars.vel_y = -game.vars.vel_y
game.vars.ball_y = math.max(game.vars.radius, math.min(game.height() - game.vars.radius, game.vars.ball_y))
end
game.vars.frame_count = game.vars.frame_count + 1
return true -- Always redraw when running
end
return false
end
function draw()
renderer.clear(false)
-- Draw ball
renderer.circle(game.vars.ball_x, game.vars.ball_y, game.vars.radius, true, true)
-- Draw trail (previous positions)
local trail_radius = game.vars.radius - 2
if trail_radius > 2 then
renderer.circle(game.vars.ball_x - game.vars.vel_x,
game.vars.ball_y - game.vars.vel_y,
trail_radius, true, false)
end
-- Draw status
if game.vars.state == STATE_PAUSED then
renderer.text(10, 10, "PAUSED - Tap to start", true)
else
renderer.text(10, 10, "Frames: " .. tostring(game.vars.frame_count), true)
renderer.text(10, 25, "Tap to pause", true)
end
-- Draw velocity vector
local arrow_x = game.vars.ball_x + game.vars.vel_x * 3
local arrow_y = game.vars.ball_y + game.vars.vel_y * 3
renderer.line(game.vars.ball_x, game.vars.ball_y, arrow_x, arrow_y, true, 2)
end
-49
View File
@@ -1,49 +0,0 @@
-- NAME: Touch Counter
-- DESC: Simple tap counter demo
-- Initialize game state
function init()
game.vars.count = 0
game.vars.last_x = 0
game.vars.last_y = 0
print("Counter initialized")
end
-- Update game logic based on input
function update(event)
-- Check if touch/button pressed
if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.BUTTON_0 or event.type == INPUT.BUTTON_1 then
game.vars.count = game.vars.count + 1
game.vars.last_x = event.x
game.vars.last_y = event.y
print("Count: " .. game.vars.count)
return true -- Request redraw
end
return false -- No redraw needed
end
-- Draw the game
function draw()
-- Clear screen
renderer.clear(true)
-- Draw title
renderer.text(20, 20, "Touch Counter", true)
-- Draw count (centered)
local count_text = "Count: " .. tostring(game.vars.count)
renderer.text(game.width() / 2 - 40, game.height() / 2 - 10, count_text, true)
-- Draw last touch position
if game.vars.count > 0 then
local pos_text = "Last: (" .. tostring(game.vars.last_x) .. ", " .. tostring(game.vars.last_y) .. ")"
renderer.text(20, game.height() - 30, pos_text, true)
-- Draw marker at last touch
renderer.circle(game.vars.last_x, game.vars.last_y, 5, true, false)
end
-- Draw instructions
renderer.text(20, 50, "Tap screen to increment", true)
end
-219
View File
@@ -1,219 +0,0 @@
-- NAME: Snake Game
-- DESC: Classic snake with state machine
-- Game states
local STATE_MENU = 0
local STATE_PLAYING = 1
local STATE_GAME_OVER = 2
-- Game constants
local CELL_SIZE = 10
local GRID_W = 40
local GRID_H = 28
-- Initialize game
function init()
game.vars.state = STATE_MENU
game.vars.score = 0
game.vars.high_score = 0
-- Snake as array of {x, y} positions
game.vars.snake = {}
game.vars.snake[1] = {x = 20, y = 14}
game.vars.snake[2] = {x = 19, y = 14}
game.vars.snake[3] = {x = 18, y = 14}
game.vars.dir_x = 1
game.vars.dir_y = 0
game.vars.food_x = 30
game.vars.food_y = 14
game.vars.frame_count = 0
game.vars.move_speed = 10 -- Frames between moves
-- Enable continuous frame updates
game.set_frame_updates(true)
print("Snake Game initialized")
end
-- Update game logic
function update(event)
local state = game.vars.state
-- State: MENU
if state == STATE_MENU then
if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.BUTTON_0 or event.type == INPUT.BUTTON_1 then
game.vars.state = STATE_PLAYING
game.vars.score = 0
init_snake()
spawn_food()
return true
end
-- State: PLAYING
elseif state == STATE_PLAYING then
-- Handle input for direction change
if event.type == INPUT.TOUCH_DOWN then
local head = game.vars.snake[1]
local head_screen_x = head.x * CELL_SIZE
local head_screen_y = head.y * CELL_SIZE
local dx = event.x - head_screen_x
local dy = event.y - head_screen_y
-- Change direction based on touch relative to head
if math.abs(dx) > math.abs(dy) then
if dx > 0 and game.vars.dir_x ~= -1 then
game.vars.dir_x = 1
game.vars.dir_y = 0
elseif dx < 0 and game.vars.dir_x ~= 1 then
game.vars.dir_x = -1
game.vars.dir_y = 0
end
else
if dy > 0 and game.vars.dir_y ~= -1 then
game.vars.dir_x = 0
game.vars.dir_y = 1
elseif dy < 0 and game.vars.dir_y ~= 1 then
game.vars.dir_x = 0
game.vars.dir_y = -1
end
end
end
-- Move snake every N frames
game.vars.frame_count = game.vars.frame_count + 1
if game.vars.frame_count >= game.vars.move_speed then
game.vars.frame_count = 0
move_snake()
return true
end
-- State: GAME_OVER
elseif state == STATE_GAME_OVER then
if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.BUTTON_0 or event.type == INPUT.BUTTON_1 then
game.vars.state = STATE_MENU
return true
end
end
return false
end
-- Draw game
function draw()
renderer.clear(false)
local state = game.vars.state
-- Draw: MENU
if state == STATE_MENU then
renderer.text(game.width() / 2 - 30, game.height() / 2 - 20, "SNAKE", true)
renderer.text(game.width() / 2 - 50, game.height() / 2, "Tap to Start", true)
if game.vars.high_score > 0 then
local hs_text = "High: " .. tostring(game.vars.high_score)
renderer.text(game.width() / 2 - 30, game.height() / 2 + 20, hs_text, true)
end
-- Draw: PLAYING
elseif state == STATE_PLAYING then
-- Draw snake
for i = 1, #game.vars.snake do
local seg = game.vars.snake[i]
local filled = (i == 1) -- Head filled, body outline
renderer.rect(seg.x * CELL_SIZE, seg.y * CELL_SIZE, CELL_SIZE, CELL_SIZE, true, filled)
end
-- Draw food
renderer.circle(game.vars.food_x * CELL_SIZE + CELL_SIZE / 2,
game.vars.food_y * CELL_SIZE + CELL_SIZE / 2,
CELL_SIZE / 2, true, true)
-- Draw score
local score_text = "Score: " .. tostring(game.vars.score)
renderer.text(5, 5, score_text, true)
-- Draw: GAME_OVER
elseif state == STATE_GAME_OVER then
renderer.text(game.width() / 2 - 40, game.height() / 2 - 20, "GAME OVER", true)
local score_text = "Score: " .. tostring(game.vars.score)
renderer.text(game.width() / 2 - 40, game.height() / 2, score_text, true)
renderer.text(game.width() / 2 - 60, game.height() / 2 + 20, "Tap to Continue", true)
end
end
-- Helper: Initialize snake
function init_snake()
game.vars.snake = {}
game.vars.snake[1] = {x = 20, y = 14}
game.vars.snake[2] = {x = 19, y = 14}
game.vars.snake[3] = {x = 18, y = 14}
game.vars.dir_x = 1
game.vars.dir_y = 0
end
-- Helper: Spawn food at random position
function spawn_food()
game.vars.food_x = math.random(0, GRID_W - 1)
game.vars.food_y = math.random(0, GRID_H - 1)
end
-- Helper: Move snake
function move_snake()
local head = game.vars.snake[1]
local new_head = {
x = head.x + game.vars.dir_x,
y = head.y + game.vars.dir_y
}
-- Check wall collision
if new_head.x < 0 or new_head.x >= GRID_W or new_head.y < 0 or new_head.y >= GRID_H then
game_over()
return
end
-- Check self collision
for i = 1, #game.vars.snake do
local seg = game.vars.snake[i]
if new_head.x == seg.x and new_head.y == seg.y then
game_over()
return
end
end
-- Check food collision
local ate_food = false
if new_head.x == game.vars.food_x and new_head.y == game.vars.food_y then
ate_food = true
game.vars.score = game.vars.score + 10
spawn_food()
-- Increase speed slightly
if game.vars.move_speed > 3 then
game.vars.move_speed = game.vars.move_speed - 1
end
end
-- Move snake
table.insert(game.vars.snake, 1, new_head)
if not ate_food then
table.remove(game.vars.snake) -- Remove tail
end
end
-- Helper: Game over
function game_over()
game.vars.state = STATE_GAME_OVER
if game.vars.score > game.vars.high_score then
game.vars.high_score = game.vars.score
end
print("Game Over! Score: " .. game.vars.score)
end
+22 -5
View File
@@ -49,7 +49,7 @@ function init()
game.vars.won = false game.vars.won = false
-- Enable continuous updates -- Enable continuous updates
game.set_frame_updates(true) -- game.set_frame_updates(true)
print("2048 initialized") print("2048 initialized")
end end
@@ -124,6 +124,7 @@ function draw()
if state == STATE_MENU then if state == STATE_MENU then
renderer.text_scaled(game.width() / 2 - 15, game.height() / 2 - 30, "2048", true, 2) renderer.text_scaled(game.width() / 2 - 15, game.height() / 2 - 30, "2048", true, 2)
renderer.text_scaled(game.width() / 2 - 50, game.height() / 2, "Tap to Start", true, 2) renderer.text_scaled(game.width() / 2 - 50, game.height() / 2, "Tap to Start", true, 2)
renderer.text_scaled(game.width() / 2 - 50, game.height() / 2 + 30, "Welcome Adolfo", true, 2)
elseif state == STATE_PLAYING or state == STATE_WIN or state == STATE_GAME_OVER then elseif state == STATE_PLAYING or state == STATE_WIN or state == STATE_GAME_OVER then
-- Draw grid -- Draw grid
@@ -137,15 +138,31 @@ function draw()
-- Empty tile -- Empty tile
renderer.rect(tile_x, tile_y, tile_size, tile_size, true, false) renderer.rect(tile_x, tile_y, tile_size, tile_size, true, false)
else else
local reduce_size
if value == 2 then
reduce_size = 10
elseif value == 4 then
reduce_size = 8
elseif value == 8 then
reduce_size = 6
elseif value == 16 then
reduce_size = 4
elseif value == 32 then
reduce_size = 2
else
reduce_size = 0
end
-- Empty tile
renderer.rect(tile_x, tile_y, tile_size, tile_size, true, false)
-- Filled tile -- Filled tile
renderer.rect(tile_x, tile_y, tile_size, tile_size, true, true) renderer.rect(tile_x+reduce_size, tile_y+reduce_size, tile_size-reduce_size*2, tile_size-reduce_size*2, true, true)
-- Draw value (simplified) -- Draw value (simplified)
local text = tostring(value) local text = tostring(value)
if string.len(text) <= 2 then if string.len(text) < 2 then
renderer.text_scaled(tile_x + 2, tile_y + 2, text, false, 2) renderer.text_scaled(tile_x + tile_size / 2 - 4, tile_y + tile_size / 2 - 8, text, false, 2)
else else
renderer.text_scaled(tile_x + 1, tile_y + 2, text, false, 2) renderer.text_scaled(tile_x + tile_size / 2 - 12, tile_y + tile_size / 2 - 8, text, false, 2)
end end
end end
end end
+8 -8
View File
@@ -87,9 +87,9 @@ function draw()
local state = game.vars.state local state = game.vars.state
if state == STATE_MENU then if state == STATE_MENU then
renderer.text_scaled(game.width(, 2) / 2 - 35, game.height() / 2 - 30, "AIR HOCKEY", true) renderer.text_scaled(game.width() / 2 - 35, game.height() / 2 - 30, "AIR HOCKEY", true, 2)
renderer.text_scaled(game.width(, 2) / 2 - 50, game.height() / 2, "Tap to Start", true) renderer.text_scaled(game.width() / 2 - 50, game.height() / 2, "Tap to Start", true, 2)
renderer.text_scaled(game.width(, 2) / 2 - 60, game.height() / 2 + 20, "First to " .. tostring(MAX_SCORE), true) renderer.text_scaled(game.width() / 2 - 60, game.height() / 2 + 20, "First to " .. tostring(MAX_SCORE), true, 2)
elseif state == STATE_PLAYING or state == STATE_GAME_OVER then elseif state == STATE_PLAYING or state == STATE_GAME_OVER then
-- Draw center line -- Draw center line
@@ -109,14 +109,14 @@ function draw()
renderer.circle(math.floor(game.vars.puck_x + 0.5), math.floor(game.vars.puck_y + 0.5), PUCK_RADIUS, true, true) renderer.circle(math.floor(game.vars.puck_x + 0.5), math.floor(game.vars.puck_y + 0.5), PUCK_RADIUS, true, true)
-- Draw scores -- Draw scores
renderer.text_scaled(game.width(, 2) / 2 - 30, 5, tostring(game.vars.paddle_left_score), true) renderer.text_scaled(game.width() / 2 - 30, 5, tostring(game.vars.paddle_left_score), true, 2)
renderer.text_scaled(game.width(, 2) / 2 + 20, 5, tostring(game.vars.paddle_right_score), true) renderer.text_scaled(game.width() / 2 + 20, 5, tostring(game.vars.paddle_right_score), true, 2)
if state == STATE_GAME_OVER then if state == STATE_GAME_OVER then
local winner = game.vars.paddle_left_score > game.vars.paddle_right_score and "Player 1" or "Player 2" local winner = game.vars.paddle_left_score > game.vars.paddle_right_score and "Player 1" or "Player 2"
renderer.text_scaled(game.width(, 2) / 2 - 50, game.height() / 2 - 20, "GAME OVER", true) renderer.text_scaled(game.width() / 2 - 50, game.height() / 2 - 20, "GAME OVER", true, 2)
renderer.text_scaled(game.width(, 2) / 2 - 40, game.height() / 2, winner .. " Wins!", true) renderer.text_scaled(game.width() / 2 - 40, game.height() / 2, winner .. " Wins!", true, 2)
renderer.text_scaled(game.width(, 2) / 2 - 60, game.height() / 2 + 20, "Tap to Menu", true) renderer.text_scaled(game.width() / 2 - 60, game.height() / 2 + 20, "Tap to Menu", true, 2)
end end
end end
end end
+6 -6
View File
@@ -100,8 +100,8 @@ function draw()
local state = game.vars.state local state = game.vars.state
if state == STATE_MENU then if state == STATE_MENU then
renderer.text_scaled(game.width(, 2) / 2 - 35, game.height() / 2 - 30, "ASTEROIDS", true) renderer.text_scaled(game.width() / 2 - 35, game.height() / 2 - 30, "ASTEROIDS", true, 2)
renderer.text_scaled(game.width(, 2) / 2 - 50, game.height() / 2, "Tap to Start", true) renderer.text_scaled(game.width() / 2 - 50, game.height() / 2, "Tap to Start", true, 2)
elseif state == STATE_PLAYING or state == STATE_GAME_OVER then elseif state == STATE_PLAYING or state == STATE_GAME_OVER then
-- Draw asteroids (convert to integers) -- Draw asteroids (convert to integers)
@@ -134,12 +134,12 @@ function draw()
end end
-- Draw score -- Draw score
renderer.text_scaled(10, 10, "Score: " .. tostring(game.vars.score, 2), true) renderer.text_scaled(10, 10, "Score: " .. tostring(game.vars.score), true, 2)
renderer.text_scaled(10, 20, "Level: " .. tostring(game.vars.level, 2), true) renderer.text_scaled(10, 20, "Level: " .. tostring(game.vars.level), true, 2)
if state == STATE_GAME_OVER then if state == STATE_GAME_OVER then
renderer.text_scaled(game.width(, 2) / 2 - 40, game.height() / 2, "GAME OVER", true) renderer.text_scaled(game.width() / 2 - 40, game.height() / 2, "GAME OVER", true, 2)
renderer.text_scaled(game.width(, 2) / 2 - 50, game.height() / 2 + 20, "Tap to Menu", true) renderer.text_scaled(game.width() / 2 - 50, game.height() / 2 + 20, "Tap to Menu", true, 2)
end end
end end
end end
+1 -1
View File
@@ -73,7 +73,7 @@ function draw()
if game.vars.state == STATE_PAUSED then if game.vars.state == STATE_PAUSED then
renderer.text_scaled(10, 10, "PAUSED - Tap to start", true, 2) renderer.text_scaled(10, 10, "PAUSED - Tap to start", true, 2)
else else
renderer.text_scaled(10, 10, "Frames: " .. tostring(game.vars.frame_count, 2), true) renderer.text_scaled(10, 10, "Frames: " .. tostring(game.vars.frame_count), true, 2)
renderer.text_scaled(10, 25, "Tap to pause", true, 2) renderer.text_scaled(10, 25, "Tap to pause", true, 2)
end end
+10 -10
View File
@@ -92,8 +92,8 @@ function draw()
local state = game.vars.state local state = game.vars.state
if state == STATE_MENU then if state == STATE_MENU then
renderer.text_scaled(game.width(, 2) / 2 - 30, game.height() / 2 - 30, "BREAKOUT", true) renderer.text_scaled(game.width() / 2 - 30, game.height() / 2 - 30, "BREAKOUT", true, 2)
renderer.text_scaled(game.width(, 2) / 2 - 50, game.height() / 2, "Tap to Start", true) renderer.text_scaled(game.width() / 2 - 50, game.height() / 2, "Tap to Start", true, 2)
elseif state == STATE_PLAYING then elseif state == STATE_PLAYING then
-- Draw bricks -- Draw bricks
@@ -115,18 +115,18 @@ function draw()
renderer.circle(math.floor(game.vars.ball_x + 0.5), math.floor(game.vars.ball_y + 0.5), BALL_RADIUS, true, true) renderer.circle(math.floor(game.vars.ball_x + 0.5), math.floor(game.vars.ball_y + 0.5), BALL_RADIUS, true, true)
-- Draw score and lives -- Draw score and lives
renderer.text_scaled(5, 5, "Score: " .. tostring(game.vars.score, 2), true) renderer.text_scaled(5, 5, "Score: " .. tostring(game.vars.score), true, 2)
renderer.text_scaled(game.width(, 2) - 50, 5, "Lives: " .. tostring(game.vars.lives), true) renderer.text_scaled(game.width() - 50, 5, "Lives: " .. tostring(game.vars.lives), true, 2)
elseif state == STATE_GAME_OVER then elseif state == STATE_GAME_OVER then
renderer.text_scaled(game.width(, 2) / 2 - 40, game.height() / 2 - 20, "GAME OVER", true) renderer.text_scaled(game.width() / 2 - 40, game.height() / 2 - 20, "GAME OVER", true, 2)
renderer.text_scaled(game.width(, 2) / 2 - 30, game.height() / 2, "Score: " .. tostring(game.vars.score), true) renderer.text_scaled(game.width() / 2 - 30, game.height() / 2, "Score: " .. tostring(game.vars.score), true, 2)
renderer.text_scaled(game.width(, 2) / 2 - 60, game.height() / 2 + 20, "Tap to Menu", true) renderer.text_scaled(game.width() / 2 - 60, game.height() / 2 + 20, "Tap to Menu", true, 2)
elseif state == STATE_LEVEL_COMPLETE then elseif state == STATE_LEVEL_COMPLETE then
renderer.text_scaled(game.width(, 2) / 2 - 50, game.height() / 2 - 20, "LEVEL COMPLETE!", true) renderer.text_scaled(game.width() / 2 - 50, game.height() / 2 - 20, "LEVEL COMPLETE!", true, 2)
renderer.text_scaled(game.width(, 2) / 2 - 30, game.height() / 2, "Score: " .. tostring(game.vars.score), true) renderer.text_scaled(game.width() / 2 - 30, game.height() / 2, "Score: " .. tostring(game.vars.score), true, 2)
renderer.text_scaled(game.width(, 2) / 2 - 60, game.height() / 2 + 20, "Tap to Menu", true) renderer.text_scaled(game.width() / 2 - 60, game.height() / 2 + 20, "Tap to Menu", true, 2)
end end
end end
+2 -2
View File
@@ -33,12 +33,12 @@ function draw()
-- Draw count (centered) -- Draw count (centered)
local count_text = "Count: " .. tostring(game.vars.count) local count_text = "Count: " .. tostring(game.vars.count)
renderer.text_scaled(game.width(, 2) / 2 - 40, game.height() / 2 - 10, count_text, true) renderer.text_scaled(game.width() / 2 - 40, game.height() / 2 - 10, count_text, true, 2)
-- Draw last touch position -- Draw last touch position
if game.vars.count > 0 then if game.vars.count > 0 then
local pos_text = "Last: (" .. tostring(game.vars.last_x) .. ", " .. tostring(game.vars.last_y) .. ")" local pos_text = "Last: (" .. tostring(game.vars.last_x) .. ", " .. tostring(game.vars.last_y) .. ")"
renderer.text_scaled(20, game.height(, 2) - 30, pos_text, true) renderer.text_scaled(20, game.height() - 30, pos_text, true, 2)
-- Draw marker at last touch (convert to integers) -- Draw marker at last touch (convert to integers)
renderer.circle(math.floor(game.vars.last_x + 0.5), math.floor(game.vars.last_y + 0.5), 5, true, false) renderer.circle(math.floor(game.vars.last_x + 0.5), math.floor(game.vars.last_y + 0.5), 5, true, false)
+6 -6
View File
@@ -74,8 +74,8 @@ function draw()
local state = game.vars.state local state = game.vars.state
if state == STATE_MENU then if state == STATE_MENU then
renderer.text_scaled(game.width(, 2) / 2 - 40, game.height() / 2 - 30, "FLAPPY BIRD", true) renderer.text_scaled(game.width() / 2 - 40, game.height() / 2 - 30, "FLAPPY BIRD", true, 2)
renderer.text_scaled(game.width(, 2) / 2 - 50, game.height() / 2, "Tap to Start", true) renderer.text_scaled(game.width() / 2 - 50, game.height() / 2, "Tap to Start", true, 2)
elseif state == STATE_PLAYING then elseif state == STATE_PLAYING then
-- Draw bird (convert to integer) -- Draw bird (convert to integer)
@@ -94,12 +94,12 @@ function draw()
end end
-- Draw score -- Draw score
renderer.text_scaled(10, 10, "Score: " .. tostring(game.vars.score, 2), true) renderer.text_scaled(10, 10, "Score: " .. tostring(game.vars.score), true, 2)
elseif state == STATE_GAME_OVER then elseif state == STATE_GAME_OVER then
renderer.text_scaled(game.width(, 2) / 2 - 40, game.height() / 2 - 30, "GAME OVER", true) renderer.text_scaled(game.width() / 2 - 40, game.height() / 2 - 30, "GAME OVER", true, 2)
renderer.text_scaled(game.width(, 2) / 2 - 30, game.height() / 2, "Score: " .. tostring(game.vars.score), true) renderer.text_scaled(game.width() / 2 - 30, game.height() / 2, "Score: " .. tostring(game.vars.score), true, 2)
renderer.text_scaled(game.width(, 2) / 2 - 60, game.height() / 2 + 20, "Tap to Restart", true) renderer.text_scaled(game.width() / 2 - 60, game.height() / 2 + 20, "Tap to Restart", true, 2)
end end
end end
+10 -10
View File
@@ -81,9 +81,9 @@ function draw()
local state = game.vars.state local state = game.vars.state
if state == STATE_MENU then if state == STATE_MENU then
renderer.text_scaled(game.width(, 2) / 2 - 50, game.height() / 2 - 30, "LUNAR LANDER", true) renderer.text_scaled(game.width() / 2 - 50, game.height() / 2 - 30, "LUNAR LANDER", true, 2)
renderer.text_scaled(game.width(, 2) / 2 - 70, game.height() / 2 - 5, "Land in the zone safely", true) renderer.text_scaled(game.width() / 2 - 70, game.height() / 2 - 5, "Land in the zone safely", true, 2)
renderer.text_scaled(game.width(, 2) / 2 - 50, game.height() / 2 + 20, "Tap to Start", true) renderer.text_scaled(game.width() / 2 - 50, game.height() / 2 + 20, "Tap to Start", true, 2)
elseif state == STATE_PLAYING or state == STATE_LANDED or state == STATE_CRASHED then elseif state == STATE_PLAYING or state == STATE_LANDED or state == STATE_CRASHED then
-- Draw terrain -- Draw terrain
@@ -111,18 +111,18 @@ function draw()
end end
-- Draw stats -- Draw stats
renderer.text_scaled(5, 5, "Fuel: " .. tostring(math.floor(game.vars.fuel, 2)), true) renderer.text_scaled(5, 5, "Fuel: " .. tostring(math.floor(game.vars.fuel)), true, 2)
renderer.text_scaled(5, 15, "Speed: " .. tostring(math.floor(game.vars.lander_vel_y * 10, 2)), true) renderer.text_scaled(5, 15, "Speed: " .. tostring(math.floor(game.vars.lander_vel_y * 10)), true, 2)
if state == STATE_LANDED then if state == STATE_LANDED then
renderer.text_scaled(game.width(, 2) / 2 - 40, game.height() / 2 - 20, "LANDED!", true) renderer.text_scaled(game.width() / 2 - 40, game.height() / 2 - 20, "LANDED!", true, 2)
renderer.text_scaled(game.width(, 2) / 2 - 30, game.height() / 2, "Score: " .. tostring(game.vars.score), true) renderer.text_scaled(game.width() / 2 - 30, game.height() / 2, "Score: " .. tostring(game.vars.score), true, 2)
renderer.text_scaled(game.width(, 2) / 2 - 60, game.height() / 2 + 20, "Tap to Menu", true) renderer.text_scaled(game.width() / 2 - 60, game.height() / 2 + 20, "Tap to Menu", true, 2)
end end
if state == STATE_CRASHED then if state == STATE_CRASHED then
renderer.text_scaled(game.width(, 2) / 2 - 40, game.height() / 2 - 20, "CRASHED!", true) renderer.text_scaled(game.width() / 2 - 40, game.height() / 2 - 20, "CRASHED!", true, 2)
renderer.text_scaled(game.width(, 2) / 2 - 60, game.height() / 2 + 20, "Tap to Menu", true) renderer.text_scaled(game.width() / 2 - 60, game.height() / 2 + 20, "Tap to Menu", true, 2)
end end
end end
end end
+7 -7
View File
@@ -110,12 +110,12 @@ function draw()
-- Draw: MENU -- Draw: MENU
if state == STATE_MENU then if state == STATE_MENU then
renderer.text_scaled(game.width(, 2) / 2 - 30, game.height() / 2 - 20, "SNAKE", true) renderer.text(game.width() / 2 - 30, game.height() / 2 - 20, "SNAKE", true)
renderer.text_scaled(game.width(, 2) / 2 - 50, game.height() / 2, "Tap to Start", true) renderer.text(game.width() / 2 - 50, game.height() / 2, "Tap to Start", true)
if game.vars.high_score > 0 then if game.vars.high_score > 0 then
local hs_text = "High: " .. tostring(game.vars.high_score) local hs_text = "High: " .. tostring(game.vars.high_score)
renderer.text_scaled(game.width(, 2) / 2 - 30, game.height() / 2 + 20, hs_text, true) renderer.text(game.width() / 2 - 30, game.height() / 2 + 20, hs_text, true)
end end
-- Draw: PLAYING -- Draw: PLAYING
@@ -134,16 +134,16 @@ function draw()
-- Draw score -- Draw score
local score_text = "Score: " .. tostring(game.vars.score) local score_text = "Score: " .. tostring(game.vars.score)
renderer.text_scaled(5, 5, score_text, true, 2) renderer.text(5, 5, score_text, true)
-- Draw: GAME_OVER -- Draw: GAME_OVER
elseif state == STATE_GAME_OVER then elseif state == STATE_GAME_OVER then
renderer.text_scaled(game.width(, 2) / 2 - 40, game.height() / 2 - 20, "GAME OVER", true) renderer.text(game.width() / 2 - 40, game.height() / 2 - 20, "GAME OVER", true)
local score_text = "Score: " .. tostring(game.vars.score) local score_text = "Score: " .. tostring(game.vars.score)
renderer.text_scaled(game.width(, 2) / 2 - 40, game.height() / 2, score_text, true) renderer.text(game.width() / 2 - 40, game.height() / 2, score_text, true)
renderer.text_scaled(game.width(, 2) / 2 - 60, game.height() / 2 + 20, "Tap to Continue", true) renderer.text(game.width() / 2 - 60, game.height() / 2 + 20, "Tap to Continue", true)
end end
end end
+6 -6
View File
@@ -177,8 +177,8 @@ function draw()
local state = game.vars.state local state = game.vars.state
if state == STATE_MENU then if state == STATE_MENU then
renderer.text_scaled(game.width(, 2) / 2 - 20, game.height() / 2 - 30, "TETRIS", true) renderer.text_scaled(game.width() / 2 - 20, game.height() / 2 - 30, "TETRIS", true, 2)
renderer.text_scaled(game.width(, 2) / 2 - 50, game.height() / 2, "Tap to Start", true) renderer.text_scaled(game.width() / 2 - 50, game.height() / 2, "Tap to Start", true, 2)
elseif state == STATE_PLAYING or state == STATE_GAME_OVER then elseif state == STATE_PLAYING or state == STATE_GAME_OVER then
-- Draw grid -- Draw grid
@@ -203,12 +203,12 @@ function draw()
end end
-- Draw score -- Draw score
renderer.text_scaled(game.width(, 2) - 50, 10, "Score: " .. tostring(game.vars.score), true) renderer.text_scaled(game.width() - 50, 10, "Score: " .. tostring(game.vars.score), true, 2)
renderer.text_scaled(game.width(, 2) - 50, 20, "Lines: " .. tostring(game.vars.lines), true) renderer.text_scaled(game.width() - 50, 20, "Lines: " .. tostring(game.vars.lines), true, 2)
if state == STATE_GAME_OVER then if state == STATE_GAME_OVER then
renderer.text_scaled(game.width(, 2) / 2 - 40, game.height() / 2, "GAME OVER", true) renderer.text_scaled(game.width() / 2 - 40, game.height() / 2, "GAME OVER", true, 2)
renderer.text_scaled(game.width(, 2) / 2 - 50, game.height() / 2 + 20, "Tap to Menu", true) renderer.text_scaled(game.width() / 2 - 50, game.height() / 2 + 20, "Tap to Menu", true, 2)
end end
end end
end end
+9
View File
@@ -23,6 +23,15 @@ struct LuaGameFactoryData {
static std::vector<LuaGameFactoryData*> factory_data_list; static std::vector<LuaGameFactoryData*> factory_data_list;
void LuaGameLoader::clear_factory_data() {
// Delete all factory data and clear the vector
for (LuaGameFactoryData* data : factory_data_list) {
delete data;
}
factory_data_list.clear();
printf("LuaGameLoader: Cleared all factory data\n");
}
// Factory wrapper that captures script path // Factory wrapper that captures script path
static Game* lua_game_factory_wrapper(uint16_t width, uint16_t height, static Game* lua_game_factory_wrapper(uint16_t width, uint16_t height,
LowLevelRenderer* renderer, LowLevelGUI* gui, LowLevelRenderer* renderer, LowLevelGUI* gui,
+5
View File
@@ -20,6 +20,11 @@ public:
*/ */
static int register_all_games(GameLauncher* launcher); static int register_all_games(GameLauncher* launcher);
/**
* @brief Clear all factory data (useful before re-scanning)
*/
static void clear_factory_data();
private: private:
/** /**
* @brief Parse metadata from Lua script comments * @brief Parse metadata from Lua script comments
+53 -29
View File
@@ -1,18 +1,18 @@
// DiceModalGame.h // DiceModalGame.h
#pragma once #pragma once
#include "../../lib/game.h"
#include "../../display/low_level_render.h"
#include "../../display/low_level_gui.h" #include "../../display/low_level_gui.h"
#include "input_manager.h" #include "../../display/low_level_render.h"
#include "MonopolyBoardRenderer.h" #include "../../lib/game.h"
#include "ModalButtonHelper.h" #include "ModalButtonHelper.h"
#include <stdlib.h> #include "MonopolyBoardRenderer.h"
#include "input_manager.h"
#include <stdio.h> #include <stdio.h>
#include <stdlib.h>
class DiceModalGame : public Game { class DiceModalGame : public Game {
int dice1, dice2; int dice1, dice2;
const BoardTile *from_tile, *to_tile; const BoardTile *from_tile, *to_tile;
Player* players; Player *players;
int players_count; int players_count;
bool dismissed; bool dismissed;
@@ -38,10 +38,13 @@ class DiceModalGame : public Game {
int b = 3 * size / 4; int b = 3 * size / 4;
auto draw_dot = [&](int dx, int dy) { auto draw_dot = [&](int dx, int dy) {
renderer->draw_filled_rectangle(x + dx - dot_size / 2, y + dy - dot_size / 2, dot_size, dot_size, true, 1); renderer->draw_filled_rectangle(x + dx - dot_size / 2,
y + dy - dot_size / 2, dot_size, dot_size,
true, 1);
}; };
if (value % 2 == 1) draw_dot(m, m); // Center dot for 1, 3, 5 if (value % 2 == 1)
draw_dot(m, m); // Center dot for 1, 3, 5
if (value > 1) { if (value > 1) {
draw_dot(l, t); draw_dot(l, t);
draw_dot(r, b); draw_dot(r, b);
@@ -57,29 +60,36 @@ class DiceModalGame : public Game {
} }
public: 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,
: Game(width, height, renderer, gui, input_manager), dice1(d1), dice2(d2), from_tile(from), to_tile(to), players(p), players_count(count), dismissed(false) { 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) {
// Find from_pos // Find from_pos
from_pos = 0; from_pos = 0;
for(int i=0; i<40; i++) { for (int i = 0; i < 40; i++) {
if(&MONOPOLY_BOARD[i] == from_tile) { if (&MONOPOLY_BOARD[i] == from_tile) {
from_pos = i; from_pos = i;
break; break;
} }
} }
correct_destination = (from_pos + dice1 + dice2) % BOARD_SIZE; correct_destination = (from_pos + dice1 + dice2) % MONOPOLY_BOARD_SIZE;
selected_choice = -1; selected_choice = -1;
show_error = false; show_error = false;
for(int i=0; i<3; i++) option_visible[i] = true; for (int i = 0; i < 3; i++)
option_visible[i] = true;
// Generate fake options // Generate fake options
int fake1 = (from_pos + (rand() % 11 + 2)) % BOARD_SIZE; int fake1 = (from_pos + (rand() % 11 + 2)) % MONOPOLY_BOARD_SIZE;
if (fake1 == correct_destination) fake1 = (fake1 + 1) % BOARD_SIZE; if (fake1 == correct_destination)
fake1 = (fake1 + 1) % MONOPOLY_BOARD_SIZE;
int fake2 = (from_pos + (rand() % 11 + 2)) % BOARD_SIZE; int fake2 = (from_pos + (rand() % 11 + 2)) % MONOPOLY_BOARD_SIZE;
while (fake2 == correct_destination || fake2 == fake1) { while (fake2 == correct_destination || fake2 == fake1) {
fake2 = (from_pos + (rand() % 11 + 2)) % BOARD_SIZE; fake2 = (from_pos + (rand() % 11 + 2)) % MONOPOLY_BOARD_SIZE;
} }
int rand_pos = rand() % 3; int rand_pos = rand() % 3;
@@ -102,24 +112,27 @@ public:
dismissed = false; dismissed = false;
selected_choice = -1; selected_choice = -1;
show_error = false; show_error = false;
for(int i=0; i<3; i++) option_visible[i] = true; for (int i = 0; i < 3; i++)
option_visible[i] = true;
ModalButtonHelper::set_monopoly_regions(input_manager, width, height); ModalButtonHelper::set_monopoly_regions(input_manager, width, height);
} }
Type get_type() const override { return Type::MONOPOLY_DICE; } 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) { // Select if (event.type == INPUT_BUTTON_0) { // Select
int start_choice = selected_choice; int start_choice = selected_choice;
do { do {
selected_choice = (selected_choice + 1) % 3; selected_choice = (selected_choice + 1) % 3;
} while (!option_visible[selected_choice] && selected_choice != start_choice); } while (!option_visible[selected_choice] &&
selected_choice != start_choice);
show_error = false; show_error = false;
return true; return true;
} }
if (event.type == INPUT_BUTTON_1) { if (event.type == INPUT_BUTTON_1) {
if (selected_choice == -1 || !option_visible[selected_choice]) return false; if (selected_choice == -1 || !option_visible[selected_choice])
return false;
if (options[selected_choice] == correct_destination) { if (options[selected_choice] == correct_destination) {
dismissed = true; dismissed = true;
@@ -137,7 +150,9 @@ public:
renderer->clear_buffer(); renderer->clear_buffer();
// Draw the restricted board perimeter // Draw the restricted board perimeter
MonopolyBoardRenderer::draw_board_perimeter(renderer, width, height, players, players_count, -1, -1, from_pos, (from_pos + 12) % BOARD_SIZE); MonopolyBoardRenderer::draw_board_perimeter(
renderer, width, height, players, players_count, -1, -1, from_pos,
(from_pos + 12) % MONOPOLY_BOARD_SIZE);
// --- Inner UI (Center Area) --- // --- Inner UI (Center Area) ---
int cw = width / 7; int cw = width / 7;
@@ -155,7 +170,9 @@ public:
// Header // Header
renderer->draw_filled_rectangle(ix + 4, iy + 4, iw - 8, 30, true, 1); renderer->draw_filled_rectangle(ix + 4, iy + 4, iw - 8, 30, true, 1);
renderer->set_text_color(false); renderer->set_text_color(false);
renderer->draw_string_scaled(ix + (iw - (int)strlen("DICE CHALLENGE") * 12) / 2, iy + 10, "DICE CHALLENGE", 2); renderer->draw_string_scaled(
ix + (iw - (int)strlen("DICE CHALLENGE") * 12) / 2, iy + 10,
"DICE CHALLENGE", 2);
renderer->set_text_color(true); renderer->set_text_color(true);
// Dice // Dice
@@ -176,20 +193,27 @@ public:
continue; continue;
} }
char opt_buf[64]; char opt_buf[64];
snprintf(opt_buf, sizeof(opt_buf), "%s %d: %s", (selected_choice == i ? ">" : " "), i+1, MONOPOLY_BOARD[options[i]].name); snprintf(opt_buf, sizeof(opt_buf), "%s %d: %s",
(selected_choice == i ? ">" : " "), i + 1,
MONOPOLY_BOARD[options[i]].name);
if (selected_choice == i) renderer->draw_filled_rectangle(ix + 15, opt_y - 2, iw - 30, 22, true, 1); if (selected_choice == i)
if (selected_choice == i) renderer->set_text_color(false); renderer->draw_filled_rectangle(ix + 15, opt_y - 2, iw - 30, 22, true,
1);
if (selected_choice == i)
renderer->set_text_color(false);
// Truncate name if too long // Truncate name if too long
if (strlen(opt_buf) > 30) opt_buf[30] = '\0'; if (strlen(opt_buf) > 30)
opt_buf[30] = '\0';
renderer->draw_string_scaled(ix + 20, opt_y, opt_buf, 2); renderer->draw_string_scaled(ix + 20, opt_y, opt_buf, 2);
renderer->set_text_color(true); renderer->set_text_color(true);
opt_y += 25; opt_y += 25;
} }
if (show_error) { if (show_error) {
renderer->draw_string_scaled(ix + (iw - 10 * 12) / 2, iy + ih - 30, "TRY AGAIN!", 2); renderer->draw_string_scaled(ix + (iw - 10 * 12) / 2, iy + ih - 30,
"TRY AGAIN!", 2);
} }
ModalButtonHelper::draw_virtual_buttons(renderer, input_manager); ModalButtonHelper::draw_virtual_buttons(renderer, input_manager);
+76 -33
View File
@@ -6,8 +6,12 @@
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, int currentPlayerPos = -1, int observer_idx = -1) { static void draw_tile(LowLevelRenderer *renderer, int x, int y, int w, int h,
if (index < 0 || index >= BOARD_SIZE) return; int index, bool is_corner, Player *players,
int players_count, int orientation = 0,
int currentPlayerPos = -1, int observer_idx = -1) {
if (index < 0 || index >= MONOPOLY_BOARD_SIZE)
return;
// Find owner // Find owner
int owner_id = -1; int owner_id = -1;
@@ -18,7 +22,8 @@ public:
break; break;
} }
} }
if (owner_id != -1) break; if (owner_id != -1)
break;
} }
bool isInverted = false; bool isInverted = false;
@@ -35,7 +40,7 @@ public:
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;
if (!is_corner && tile.type == TILE_PROPERTY) { if (!is_corner && tile.type == TILE_PROPERTY) {
@@ -44,16 +49,20 @@ public:
if (orientation == 0) { // Bottom row (Bar on top) if (orientation == 0) { // Bottom row (Bar on top)
bh = bar_size; bh = bar_size;
content_y += bar_size; content_h -= bar_size; content_y += bar_size;
content_h -= bar_size;
} else if (orientation == 1) { // Left column (Bar on right) } else if (orientation == 1) { // Left column (Bar on right)
bx = x + w - bar_size; bw = bar_size; bx = x + w - bar_size;
bw = bar_size;
content_w -= bar_size; content_w -= bar_size;
} else if (orientation == 2) { // Top row (Bar on bottom) } else if (orientation == 2) { // Top row (Bar on bottom)
by = y + h - bar_size; bh = bar_size; by = y + h - bar_size;
bh = bar_size;
content_h -= bar_size; content_h -= bar_size;
} else if (orientation == 3) { // Right column (Bar on left) } else if (orientation == 3) { // Right column (Bar on left)
bw = bar_size; bw = bar_size;
content_x += bar_size; content_w -= bar_size; content_x += bar_size;
content_w -= bar_size;
} }
if (isInverted) { if (isInverted) {
@@ -66,48 +75,61 @@ public:
} }
// Group number // Group number
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);
if (isInverted) renderer->set_text_color(false); if (isInverted)
else renderer->set_text_color(true); renderer->set_text_color(false);
else
renderer->set_text_color(true);
} }
char short_name[10] = {0}; char short_name[10] = {0};
int s_ptr = 0; int s_ptr = 0;
bool isCurrentPos = (index == currentPlayerPos && observer_idx != -1); bool isCurrentPos = (index == currentPlayerPos && observer_idx != -1);
if (isCurrentPos) short_name[s_ptr++] = '-'; if (isCurrentPos)
short_name[s_ptr++] = '-';
// Add * if owned by someone else // Add * if owned by someone else
if (owner_id != -1 && observer_idx != -1 && owner_id != observer_idx) { if (owner_id != -1 && observer_idx != -1 && owner_id != observer_idx) {
short_name[s_ptr++] = '*'; short_name[s_ptr++] = '*';
} }
const char* full_name = tile.name; const char *full_name = tile.name;
if (is_corner) { if (is_corner) {
int len = strlen(full_name); int len = strlen(full_name);
if (len > 3) len = 3; if (len > 3)
for(int i=0; i<len; i++) short_name[s_ptr++] = full_name[i]; len = 3;
for (int i = 0; i < len; i++)
short_name[s_ptr++] = full_name[i];
} else { } else {
short_name[s_ptr++] = full_name[0]; short_name[s_ptr++] = full_name[0];
const char* space = strchr(full_name, ' '); const char *space = strchr(full_name, ' ');
if (space && space[1] != '\0') short_name[s_ptr++] = space[1]; if (space && space[1] != '\0')
short_name[s_ptr++] = space[1];
} }
if (isCurrentPos) short_name[s_ptr++] = '-'; if (isCurrentPos)
short_name[s_ptr++] = '-';
short_name[s_ptr] = '\0'; short_name[s_ptr] = '\0';
for (int i = 0; short_name[i]; i++) if(short_name[i] >= 'a' && short_name[i] <= 'z') short_name[i] -= 32; for (int i = 0; short_name[i]; i++)
if (short_name[i] >= 'a' && short_name[i] <= 'z')
short_name[i] -= 32;
renderer->draw_string_scaled(content_x + (content_w - (int)strlen(short_name) * 6) / 2, content_y + (content_h - 8) / 2, short_name, 1); renderer->draw_string_scaled(
content_x + (content_w - (int)strlen(short_name) * 6) / 2,
content_y + (content_h - 8) / 2, short_name, 1);
// Draw player markers // Draw player markers
int p_count = 0; int p_count = 0;
for (int i = 0; i < players_count; ++i) { for (int i = 0; i < players_count; ++i) {
if (players[i].position == index) { if (players[i].position == index) {
char mark[2] = { (players[i].token ? players[i].token[0] : 'P'), '\0' }; char mark[2] = {(players[i].token ? players[i].token[0] : 'P'), '\0'};
renderer->draw_string_scaled(content_x + 2 + (p_count * 8), content_y + 2, mark, 1); renderer->draw_string_scaled(content_x + 2 + (p_count * 8),
content_y + 2, mark, 1);
p_count++; p_count++;
} }
} }
@@ -117,14 +139,19 @@ public:
} }
} }
static void draw_board_perimeter(LowLevelRenderer* renderer, int width, int height, Player* players, int players_count, int currentPlayerPos = -1, int observer_idx = -1, int limit_start = -1, int limit_end = -1) { static void draw_board_perimeter(LowLevelRenderer *renderer, int width,
int height, Player *players,
int players_count, int currentPlayerPos = -1,
int observer_idx = -1, int limit_start = -1,
int limit_end = -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
auto should_draw = [&](int index) { auto should_draw = [&](int index) {
if (limit_start == -1 || limit_end == -1) return true; if (limit_start == -1 || limit_end == -1)
return true;
if (limit_start <= limit_end) { if (limit_start <= limit_end) {
return index >= limit_start && index <= limit_end; return index >= limit_start && index <= limit_end;
} else { } else {
@@ -133,27 +160,43 @@ public:
}; };
// --- Bottom Row: 0 to 10 (Right to Left) --- // --- Bottom Row: 0 to 10 (Right to Left) ---
if (should_draw(0)) draw_tile(renderer, width - cw, height - ch, cw, ch, 0, true, players, players_count, 0, currentPlayerPos, observer_idx); // GO if (should_draw(0))
draw_tile(renderer, width - cw, height - ch, cw, ch, 0, true, players,
players_count, 0, currentPlayerPos, observer_idx); // GO
for (int i = 1; i < 10; ++i) { for (int i = 1; i < 10; ++i) {
if (should_draw(i)) draw_tile(renderer, width - cw - i * rw, height - ch, rw, ch, i, false, players, players_count, 0, currentPlayerPos, observer_idx); if (should_draw(i))
draw_tile(renderer, width - cw - i * rw, height - ch, rw, ch, i, false,
players, players_count, 0, currentPlayerPos, observer_idx);
} }
if (should_draw(10)) draw_tile(renderer, 0, height - ch, cw, ch, 10, true, players, players_count, 1, currentPlayerPos, observer_idx); // JAIL if (should_draw(10))
draw_tile(renderer, 0, height - ch, cw, ch, 10, true, players,
players_count, 1, currentPlayerPos, observer_idx); // 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) {
if (should_draw(i)) draw_tile(renderer, 0, height - ch - (i - 10) * rh, cw, rh, i, false, players, players_count, 1, currentPlayerPos, observer_idx); if (should_draw(i))
draw_tile(renderer, 0, height - ch - (i - 10) * rh, cw, rh, i, false,
players, players_count, 1, currentPlayerPos, observer_idx);
} }
// --- Top Row: 20 to 30 (Left to Right) --- // --- Top Row: 20 to 30 (Left to Right) ---
if (should_draw(20)) draw_tile(renderer, 0, 0, cw, ch, 20, true, players, players_count, 2, currentPlayerPos, observer_idx); // FREE PARKING if (should_draw(20))
draw_tile(renderer, 0, 0, cw, ch, 20, true, players, players_count, 2,
currentPlayerPos, observer_idx); // FREE PARKING
for (int i = 21; i < 30; ++i) { for (int i = 21; i < 30; ++i) {
if (should_draw(i)) draw_tile(renderer, cw + (i - 21) * rw, 0, rw, ch, i, false, players, players_count, 2, currentPlayerPos, observer_idx); if (should_draw(i))
draw_tile(renderer, cw + (i - 21) * rw, 0, rw, ch, i, false, players,
players_count, 2, currentPlayerPos, observer_idx);
} }
if (should_draw(30)) draw_tile(renderer, width - cw, 0, cw, ch, 30, true, players, players_count, 3, currentPlayerPos, observer_idx); // GO TO JAIL if (should_draw(30))
draw_tile(renderer, width - cw, 0, cw, ch, 30, true, players,
players_count, 3, currentPlayerPos, observer_idx); // 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) {
if (should_draw(i)) draw_tile(renderer, width - cw, ch + (i - 31) * rh, cw, rh, i, false, players, players_count, 3, currentPlayerPos, observer_idx); if (should_draw(i))
draw_tile(renderer, width - cw, ch + (i - 31) * rh, cw, rh, i, false,
players, players_count, 3, currentPlayerPos, observer_idx);
} }
} }
}; };
+97 -152
View File
@@ -1,10 +1,10 @@
#include "chance.h"
#include "community_chest.h"
#include "monopoly_board.h"
#include "player.h"
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <time.h> #include <time.h>
#include "monopoly_board.h"
#include "player.h"
#include "chance.h"
#include "community_chest.h"
// Forward Declarations // Forward Declarations
void handle_roll(Player *p, bool *has_rolled, int *double_rolls); void handle_roll(Player *p, bool *has_rolled, int *double_rolls);
@@ -13,9 +13,10 @@ void handle_trade(Player *p);
void handle_build(Player *p); void handle_build(Player *p);
void process_landing(Player *p); void process_landing(Player *p);
void handle_property_landing(Player *p, const BoardTile *tile); void handle_property_landing(Player *p, const BoardTile *tile);
void handle_chance(Player* p); void handle_chance(Player *p);
void find_properties_on_group_from_position(int position, int positions_found[4], int *count); void find_properties_on_group_from_position(int position,
void handle_community_chest(Player* p); int positions_found[4], int *count);
void handle_community_chest(Player *p);
void handle_jail_options(Player *p); void handle_jail_options(Player *p);
bool attempt_jail_escape(Player *p); bool attempt_jail_escape(Player *p);
@@ -26,8 +27,7 @@ int players_count = 2;
int current_player_idx = 0; int current_player_idx = 0;
bool just_sent_to_jail = false; // Flag to end turn when sent to jail bool just_sent_to_jail = false; // Flag to end turn when sent to jail
int main() int main() {
{
srand(time(NULL)); srand(time(NULL));
// Initialize hardcoded players // Initialize hardcoded players
@@ -44,18 +44,16 @@ int main()
bool has_rolled = false; // Turn state tracker bool has_rolled = false; // Turn state tracker
int double_rolls = 0; int double_rolls = 0;
current_player_idx = 0; current_player_idx = 0;
while (running) while (running) {
{
Player *p = &players[current_player_idx]; Player *p = &players[current_player_idx];
printf("\n╔═════════════════════════════════════════╗\n"); printf("\n╔═════════════════════════════════════════╗\n");
printf("║ %s's Turn (%s)\n", p->name, p->token); printf("║ %s's Turn (%s)\n", p->name, p->token);
printf("║ 💰 Balance: $%d | 📍 %s\n", p->balance, MONOPOLY_BOARD[p->position].name); printf("║ 💰 Balance: $%d | 📍 %s\n", p->balance,
MONOPOLY_BOARD[p->position].name);
printf("╚═════════════════════════════════════════╝\n\n"); printf("╚═════════════════════════════════════════╝\n\n");
// Handle jail status at start of turn // Handle jail status at start of turn
if (p->is_in_jail) if (p->is_in_jail) {
{ if (just_sent_to_jail) {
if (just_sent_to_jail)
{
// Player was just sent to jail this turn, end their turn // Player was just sent to jail this turn, end their turn
printf("\n🚨 You have been sent to Jail! Your turn ends.\n"); printf("\n🚨 You have been sent to Jail! Your turn ends.\n");
current_player_idx = (current_player_idx + 1) % players_count; current_player_idx = (current_player_idx + 1) % players_count;
@@ -68,13 +66,10 @@ int main()
printf(" Turns in jail: %d/3\n", p->jail_turns); printf(" Turns in jail: %d/3\n", p->jail_turns);
handle_jail_options(p); handle_jail_options(p);
if (!p->is_in_jail) if (!p->is_in_jail) {
{
// Player escaped jail, now they can roll // Player escaped jail, now they can roll
printf("\n📋 %s escaped jail!\n\n", p->name); printf("\n📋 %s escaped jail!\n\n", p->name);
} } else {
else
{
// Player remains in jail, end turn // Player remains in jail, end turn
printf("\n📋 %s remains in jail.\n", p->name); printf("\n📋 %s remains in jail.\n", p->name);
current_player_idx = (current_player_idx + 1) % players_count; current_player_idx = (current_player_idx + 1) % players_count;
@@ -98,8 +93,7 @@ int main()
int choice; int choice;
scanf("%d", &choice); scanf("%d", &choice);
switch (choice) switch (choice) {
{
case 1: case 1:
handle_roll(p, &has_rolled, &double_rolls); handle_roll(p, &has_rolled, &double_rolls);
break; break;
@@ -113,15 +107,12 @@ int main()
handle_trade(p); handle_trade(p);
break; break;
case 5: case 5:
if (has_rolled) if (has_rolled) {
{
current_player_idx = (current_player_idx + 1) % players_count; current_player_idx = (current_player_idx + 1) % players_count;
has_rolled = false; // Reset for next player has_rolled = false; // Reset for next player
just_sent_to_jail = false; // Reset jail flag just_sent_to_jail = false; // Reset jail flag
printf("✓ Turn ended.\n"); printf("✓ Turn ended.\n");
} } else {
else
{
printf("❌ You must roll before ending your turn!\n"); printf("❌ You must roll before ending your turn!\n");
} }
break; break;
@@ -136,36 +127,33 @@ int main()
return 0; return 0;
} }
void find_properties_on_group_from_position(int position, int positions_found[4], int *count) void find_properties_on_group_from_position(int position,
{ int positions_found[4],
int *count) {
const BoardTile *tile = &MONOPOLY_BOARD[position]; const BoardTile *tile = &MONOPOLY_BOARD[position];
if (tile->type == TILE_PROPERTY || tile->type == TILE_RAILROAD || tile->type == TILE_UTILITY) if (tile->type == TILE_PROPERTY || tile->type == TILE_RAILROAD ||
{ tile->type == TILE_UTILITY) {
int group_id = tile->group[0]; int group_id = tile->group[0];
*count = 0; *count = 0;
for (int i = 0; i < BOARD_SIZE; i++) for (int i = 0; i < MONOPOLY_BOARD_SIZE; i++) {
{
if (i == position) if (i == position)
continue; continue;
const BoardTile *t = &MONOPOLY_BOARD[i]; const BoardTile *t = &MONOPOLY_BOARD[i];
if (t->group[0] == group_id) if (t->group[0] == group_id) {
{
positions_found[(*count)++] = i; positions_found[(*count)++] = i;
} }
} }
} }
} }
bool handle_if_owned(Player *p, const BoardTile *tile){ bool handle_if_owned(Player *p, const BoardTile *tile) {
if (tile->type == TILE_PROPERTY || tile->type == TILE_RAILROAD || tile->type == TILE_UTILITY){ if (tile->type == TILE_PROPERTY || tile->type == TILE_RAILROAD ||
tile->type == TILE_UTILITY) {
bool is_owned = false; bool is_owned = false;
int i = 0; int i = 0;
for (i = 0; i < MAX_PLAYERS; i++) for (i = 0; i < MAX_PLAYERS; i++) {
{ for (int j = 0; j < players[i].property_count; j++) {
for (int j = 0; j < players[i].property_count; j++) if (players[i].properties_owned[j] == p->position) {
{
if (players[i].properties_owned[j] == p->position)
{
is_owned = true; is_owned = true;
printf("🏠 This property is owned by %s.\n", players[i].name); printf("🏠 This property is owned by %s.\n", players[i].name);
break; break;
@@ -174,10 +162,8 @@ bool handle_if_owned(Player *p, const BoardTile *tile){
if (is_owned) if (is_owned)
break; break;
} }
if(is_owned) if (is_owned) {
{ if (players[i].id == p->id) {
if(players[i].id == p->id)
{
printf("✓ You own this property.\n"); printf("✓ You own this property.\n");
return true; return true;
} }
@@ -191,17 +177,14 @@ bool handle_if_owned(Player *p, const BoardTile *tile){
return false; return false;
} }
void handle_roll(Player *p, bool *has_rolled, int *double_rolls) void handle_roll(Player *p, bool *has_rolled, int *double_rolls) {
{ if (*has_rolled) {
if (*has_rolled)
{
printf("❌ You have already moved this turn!\n"); printf("❌ You have already moved this turn!\n");
return; return;
} }
// Check if player is in jail and trying to escape // Check if player is in jail and trying to escape
if (p->is_in_jail) if (p->is_in_jail) {
{
printf("❌ You are in jail! Use the jail menu to escape first.\n"); printf("❌ You are in jail! Use the jail menu to escape first.\n");
return; return;
} }
@@ -210,13 +193,11 @@ void handle_roll(Player *p, bool *has_rolled, int *double_rolls)
int dice2 = (rand() % 6) + 1; int dice2 = (rand() % 6) + 1;
printf("\n🎲 Rolled: [%d] + [%d] = %d\n", dice1, dice2, dice1 + dice2); printf("\n🎲 Rolled: [%d] + [%d] = %d\n", dice1, dice2, dice1 + dice2);
int total = dice1 + dice2; int total = dice1 + dice2;
p->position = (p->position + total) % BOARD_SIZE; p->position = (p->position + total) % MONOPOLY_BOARD_SIZE;
if(dice1 == dice2) if (dice1 == dice2) {
{
(*double_rolls)++; (*double_rolls)++;
if(*double_rolls >= 3) if (*double_rolls >= 3) {
{
printf("🚨 Three doubles in a row! Sent directly to JAIL!\n"); printf("🚨 Three doubles in a row! Sent directly to JAIL!\n");
p->position = 10; // Jail position p->position = 10; // Jail position
p->jail_turns = 0; p->jail_turns = 0;
@@ -225,34 +206,28 @@ void handle_roll(Player *p, bool *has_rolled, int *double_rolls)
*has_rolled = true; *has_rolled = true;
just_sent_to_jail = true; just_sent_to_jail = true;
return; return;
} } else {
else
{
printf("✨ Doubles! You get another turn!\n"); printf("✨ Doubles! You get another turn!\n");
} }
} } else {
else
{
*double_rolls = 0; // Reset double rolls count *double_rolls = 0; // Reset double rolls count
} }
*has_rolled = true; *has_rolled = true;
// Check for passing GO // Check for passing GO
if (p->position < (p->position - total)) if (p->position < (p->position - total)) { // Simplistic wrap-around check
{ // Simplistic wrap-around check
p->balance += 200; p->balance += 200;
printf("Passed GO! Collected $200.\n"); printf("Passed GO! Collected $200.\n");
} }
printf("➜ Moved %d spaces to: %s\n\n", total, MONOPOLY_BOARD[p->position].name); printf("➜ Moved %d spaces to: %s\n\n", total,
MONOPOLY_BOARD[p->position].name);
process_landing(p); process_landing(p);
} }
void handle_buy(Player *p, bool has_rolled) void handle_buy(Player *p, bool has_rolled) {
{ if (!has_rolled) {
if (!has_rolled)
{
printf("❌ You can't buy anything until you roll and land on a tile!\n"); printf("❌ You can't buy anything until you roll and land on a tile!\n");
return; return;
} }
@@ -260,48 +235,42 @@ void handle_buy(Player *p, bool has_rolled)
const BoardTile *tile = &MONOPOLY_BOARD[p->position]; const BoardTile *tile = &MONOPOLY_BOARD[p->position];
// Check if it's even a purchasable type // Check if it's even a purchasable type
if (tile->type != TILE_PROPERTY && tile->type != TILE_RAILROAD && tile->type != TILE_UTILITY) if (tile->type != TILE_PROPERTY && tile->type != TILE_RAILROAD &&
{ tile->type != TILE_UTILITY) {
printf("❌ This location (%s) cannot be purchased.\n", tile->name); printf("❌ This location (%s) cannot be purchased.\n", tile->name);
return; return;
} }
// Check if someone already owns it (basic check) // Check if someone already owns it (basic check)
// In a full game, we'd iterate through all players' properties_owned arrays // In a full game, we'd iterate through all players' properties_owned arrays
for (int i = 0; i < MAX_PLAYERS; i++) for (int i = 0; i < MAX_PLAYERS; i++) {
{ for (int j = 0; j < players[i].property_count; j++) {
for (int j = 0; j < players[i].property_count; j++) if (players[i].properties_owned[j] == p->position) {
{
if (players[i].properties_owned[j] == p->position)
{
printf("❌ This property is already owned by %s.\n", players[i].name); printf("❌ This property is already owned by %s.\n", players[i].name);
return; return;
} }
} }
} }
if (p->balance >= tile->cost) 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;
printf("✓ Bought %s for $%d! (Balance: $%d)\n", tile->name, tile->cost, p->balance); printf("✓ Bought %s for $%d! (Balance: $%d)\n", tile->name, tile->cost,
} p->balance);
else } else {
{ printf("❌ Insufficient funds! Cost: $%d | Your Balance: $%d\n", tile->cost,
printf("❌ Insufficient funds! Cost: $%d | Your Balance: $%d\n", tile->cost, p->balance); p->balance);
} }
} }
void process_landing(Player *p) void process_landing(Player *p) {
{
const BoardTile *tile = &MONOPOLY_BOARD[p->position]; const BoardTile *tile = &MONOPOLY_BOARD[p->position];
printf("\n─────────────────────────────────────────\n"); printf("\n─────────────────────────────────────────\n");
printf("📍 Landed on: %s\n", tile->name); printf("📍 Landed on: %s\n", tile->name);
printf("─────────────────────────────────────────\n"); printf("─────────────────────────────────────────\n");
switch (tile->type) switch (tile->type) {
{
case TILE_PROPERTY: case TILE_PROPERTY:
case TILE_RAILROAD: case TILE_RAILROAD:
case TILE_UTILITY: case TILE_UTILITY:
@@ -330,23 +299,19 @@ void process_landing(Player *p)
printf("📌 You're at: %s\n", tile->name); printf("📌 You're at: %s\n", tile->name);
break; break;
default: default:
break; break;
} }
} }
void handle_property_landing(Player *p, const BoardTile *tile) void handle_property_landing(Player *p, const BoardTile *tile) {
{
bool is_owned = handle_if_owned(p, tile); bool is_owned = handle_if_owned(p, tile);
if (is_owned) if (is_owned) {
{
return; return;
} }
printf("\n💰 Purchase Price: $%d\n", tile->cost); printf("\n💰 Purchase Price: $%d\n", tile->cost);
if (tile->type == TILE_PROPERTY) if (tile->type == TILE_PROPERTY) {
{
// rent data // rent data
printf("\n📋 Rent Schedule:\n"); printf("\n📋 Rent Schedule:\n");
printf(" Vacant.....................$%d\n", tile->rent[0]); printf(" Vacant.....................$%d\n", tile->rent[0]);
@@ -359,25 +324,21 @@ void handle_property_landing(Player *p, const BoardTile *tile)
} }
// show if the other properties in the group are owned // show if the other properties in the group are owned
printf("\n🏘️ Property Group: %d | Position %d/%d in group\n", printf("\n🏘️ Property Group: %d | Position %d/%d in group\n", tile->group[0],
tile->group[0], tile->group[1], tile->group[2]); tile->group[1], tile->group[2]);
int group_positions[4]; int group_positions[4];
int group_count = 0; int group_count = 0;
find_properties_on_group_from_position(p->position, group_positions, &group_count); find_properties_on_group_from_position(p->position, group_positions,
if (group_count > 0) &group_count);
{ if (group_count > 0) {
printf("\n Related properties:\n"); printf("\n Related properties:\n");
for (int i = 0; i < group_count; i++) for (int i = 0; i < group_count; i++) {
{
const BoardTile *gtile = &MONOPOLY_BOARD[group_positions[i]]; const BoardTile *gtile = &MONOPOLY_BOARD[group_positions[i]];
// Check if owned by any player // Check if owned by any player
bool is_owned_in_group = false; bool is_owned_in_group = false;
for (int j = 0; j < MAX_PLAYERS; j++) for (int j = 0; j < MAX_PLAYERS; j++) {
{ for (int k = 0; k < players[j].property_count; k++) {
for (int k = 0; k < players[j].property_count; k++) if (players[j].properties_owned[k] == group_positions[i]) {
{
if (players[j].properties_owned[k] == group_positions[i])
{
is_owned_in_group = true; is_owned_in_group = true;
printf(" 🔒 %s (Owned by %s)\n", gtile->name, players[j].name); printf(" 🔒 %s (Owned by %s)\n", gtile->name, players[j].name);
break; break;
@@ -386,21 +347,18 @@ void handle_property_landing(Player *p, const BoardTile *tile)
if (is_owned_in_group) if (is_owned_in_group)
break; break;
} }
if (!is_owned_in_group) if (!is_owned_in_group) {
{
printf(" 🔓 %s - $%d (available)\n", gtile->name, gtile->cost); printf(" 🔓 %s - $%d (available)\n", gtile->name, gtile->cost);
} }
} }
} }
} }
void handle_trade(Player *p) void handle_trade(Player *p) {
{
printf("⚙️ Trade functionality is not yet implemented.\n"); printf("⚙️ Trade functionality is not yet implemented.\n");
} }
void handle_jail_options(Player *p) void handle_jail_options(Player *p) {
{
bool escaped = false; bool escaped = false;
printf("\n╔─ Jail Options ──────────────────────────────╗\n"); printf("\n╔─ Jail Options ──────────────────────────────╗\n");
@@ -414,39 +372,32 @@ void handle_jail_options(Player *p)
int choice; int choice;
scanf("%d", &choice); scanf("%d", &choice);
switch(choice) switch (choice) {
{
case 1: case 1:
// Try to roll doubles // Try to roll doubles
escaped = attempt_jail_escape(p); escaped = attempt_jail_escape(p);
break; break;
case 2: case 2:
// Pay bail // Pay bail
if (p->balance >= 50) if (p->balance >= 50) {
{
p->balance -= 50; p->balance -= 50;
p->is_in_jail = false; p->is_in_jail = false;
p->jail_turns = 0; p->jail_turns = 0;
escaped = true; escaped = true;
printf("\n✓ Paid $50 bail! You are now free.\n"); printf("\n✓ Paid $50 bail! You are now free.\n");
} } else {
else
{
printf("\n❌ Insufficient funds! You need $50.\n"); printf("\n❌ Insufficient funds! You need $50.\n");
} }
break; break;
case 3: case 3:
if (p->jail_free_cards > 0) if (p->jail_free_cards > 0) {
{
p->jail_free_cards--; p->jail_free_cards--;
p->is_in_jail = false; p->is_in_jail = false;
p->jail_turns = 0; p->jail_turns = 0;
escaped = true; escaped = true;
printf("\n✓ Used a Get Out of Jail Free card! You are now free.\n"); printf("\n✓ Used a Get Out of Jail Free card! You are now free.\n");
printf(" Cards remaining: %d\n", p->jail_free_cards); printf(" Cards remaining: %d\n", p->jail_free_cards);
} } else {
else
{
printf("\n❌ You don't have any Get Out of Jail Free cards!\n"); printf("\n❌ You don't have any Get Out of Jail Free cards!\n");
} }
break; break;
@@ -455,11 +406,9 @@ void handle_jail_options(Player *p)
break; break;
} }
if (!escaped) if (!escaped) {
{
p->jail_turns++; p->jail_turns++;
if (p->jail_turns >= 3) if (p->jail_turns >= 3) {
{
// Force payment after 3 turns // Force payment after 3 turns
printf("\n⏰ You've been in jail for 3 turns. You must pay $50 bail!\n"); printf("\n⏰ You've been in jail for 3 turns. You must pay $50 bail!\n");
p->balance -= 50; p->balance -= 50;
@@ -470,26 +419,21 @@ void handle_jail_options(Player *p)
} }
} }
bool attempt_jail_escape(Player *p) bool attempt_jail_escape(Player *p) {
{
int dice1 = (rand() % 6) + 1; int dice1 = (rand() % 6) + 1;
int dice2 = (rand() % 6) + 1; int dice2 = (rand() % 6) + 1;
printf("\n🎲 Rolling to escape...\n"); printf("\n🎲 Rolling to escape...\n");
printf(" Rolled: [%d] + [%d]\n", dice1, dice2); printf(" Rolled: [%d] + [%d]\n", dice1, dice2);
if (dice1 == dice2) if (dice1 == dice2) {
{
printf("\n✨ DOUBLES! You escaped jail!\n"); printf("\n✨ DOUBLES! You escaped jail!\n");
p->is_in_jail = false; p->is_in_jail = false;
p->jail_turns = 0; p->jail_turns = 0;
return true; return true;
} } else {
else
{
printf("\n❌ No doubles. You remain in jail.\n"); printf("\n❌ No doubles. You remain in jail.\n");
p->jail_turns++; p->jail_turns++;
if (p->jail_turns >= 3) if (p->jail_turns >= 3) {
{
printf("\n⏰ You've been in jail for 3 turns. You must pay $50 bail!\n"); printf("\n⏰ You've been in jail for 3 turns. You must pay $50 bail!\n");
p->balance -= 50; p->balance -= 50;
p->is_in_jail = false; p->is_in_jail = false;
@@ -500,15 +444,14 @@ bool attempt_jail_escape(Player *p)
} }
} }
void handle_build(Player *p) void handle_build(Player *p) {
{
printf("⚙️ Build functionality is not yet implemented.\n"); printf("⚙️ Build functionality is not yet implemented.\n");
} }
void handle_chance(Player* p) { void handle_chance(Player *p) {
// In a real game, you'd pull from a shuffled deck of indices // In a real game, you'd pull from a shuffled deck of indices
int card_idx = rand() % CHANCE_DECK_SIZE; int card_idx = rand() % CHANCE_DECK_SIZE;
const ChanceCard* card = &CHANCE_DECK[card_idx]; const ChanceCard *card = &CHANCE_DECK[card_idx];
printf("\n✨ %s\n", card->description); printf("\n✨ %s\n", card->description);
@@ -519,11 +462,11 @@ void handle_chance(Player* p) {
// Handle special "Nearest" logic // Handle special "Nearest" logic
if (target == TARGET_NEAREST_UTILITY) { if (target == TARGET_NEAREST_UTILITY) {
while (MONOPOLY_BOARD[p->position].type != TILE_UTILITY) { while (MONOPOLY_BOARD[p->position].type != TILE_UTILITY) {
p->position = (p->position + 1) % BOARD_SIZE; p->position = (p->position + 1) % MONOPOLY_BOARD_SIZE;
} }
} else if (target == TARGET_NEAREST_RAILROAD) { } else if (target == TARGET_NEAREST_RAILROAD) {
while (MONOPOLY_BOARD[p->position].type != TILE_RAILROAD) { while (MONOPOLY_BOARD[p->position].type != TILE_RAILROAD) {
p->position = (p->position + 1) % BOARD_SIZE; p->position = (p->position + 1) % MONOPOLY_BOARD_SIZE;
} }
} else { } else {
// Check for passing GO during standard advance // Check for passing GO during standard advance
@@ -547,7 +490,8 @@ void handle_chance(Player* p) {
break; break;
case CHANCE_BACK: case CHANCE_BACK:
p->position = (p->position - card->value + BOARD_SIZE) % BOARD_SIZE; p->position =
(p->position - card->value + MONOPOLY_BOARD_SIZE) % MONOPOLY_BOARD_SIZE;
printf("⬅️ Moved back to: %s\n", MONOPOLY_BOARD[p->position].name); printf("⬅️ Moved back to: %s\n", MONOPOLY_BOARD[p->position].name);
process_landing(p); process_landing(p);
break; break;
@@ -571,7 +515,8 @@ void handle_chance(Player* p) {
case CHANCE_JAIL_FREE: case CHANCE_JAIL_FREE:
p->jail_free_cards++; p->jail_free_cards++;
printf("🔑 You received a 'Get Out of Jail Free' card! (Total: %d)\n", p->jail_free_cards); printf("🔑 You received a 'Get Out of Jail Free' card! (Total: %d)\n",
p->jail_free_cards);
break; break;
case CHANCE_REPAIRS: case CHANCE_REPAIRS:
@@ -581,9 +526,9 @@ void handle_chance(Player* p) {
} }
} }
void handle_community_chest(Player* p) { void handle_community_chest(Player *p) {
int card_idx = rand() % COMMUNITY_DECK_SIZE; int card_idx = rand() % COMMUNITY_DECK_SIZE;
const CommunityCard* card = &COMMUNITY_DECK[card_idx]; const CommunityCard *card = &COMMUNITY_DECK[card_idx];
printf("\n📦 %s\n", card->description); printf("\n📦 %s\n", card->description);
+188 -28
View File
@@ -18,59 +18,219 @@ typedef enum {
} TileType; } TileType;
typedef struct { typedef struct {
const char* name; const char *name;
TileType type; TileType type;
bool is_corner; bool is_corner;
int cost; // 0 if not applicable int cost; // 0 if not applicable
const char* color; // Hex string, NULL if not property const char *color; // Hex string, NULL if not property
int rent[6]; // Base, 1H, 2H, 3H, 4H, Hotel int rent[6]; // Base, 1H, 2H, 3H, 4H, Hotel
int group[3]; // Group ID, Position in group, Total in group int group[3]; // Group ID, Position in group, Total in group
int house_cost; // Cost to build int house_cost; // Cost to build
} BoardTile; } BoardTile;
#define BOARD_SIZE 40 #define MONOPOLY_BOARD_SIZE 40
static const BoardTile MONOPOLY_BOARD[BOARD_SIZE] = { static const BoardTile MONOPOLY_BOARD[MONOPOLY_BOARD_SIZE] = {
{"Go", TILE_GO, true, 0, NULL, {0}, {0}, 0}, {"Go", TILE_GO, true, 0, NULL, {0}, {0}, 0},
{"Mediterranean Avenue", TILE_PROPERTY, false, 60, "#955438", {2, 10, 30, 90, 160, 250}, {1, 1, 2}, 50}, {"Mediterranean Avenue",
TILE_PROPERTY,
false,
60,
"#955438",
{2, 10, 30, 90, 160, 250},
{1, 1, 2},
50},
{"Community Chest", TILE_COMMUNITY_CHEST, false, 0, NULL, {0}, {0}, 0}, {"Community Chest", TILE_COMMUNITY_CHEST, false, 0, NULL, {0}, {0}, 0},
{"Baltic Avenue", TILE_PROPERTY, false, 60, "#955438", {4, 20, 60, 180, 320, 450}, {1, 2, 2}, 50}, {"Baltic Avenue",
TILE_PROPERTY,
false,
60,
"#955438",
{4, 20, 60, 180, 320, 450},
{1, 2, 2},
50},
{"Income Tax", TILE_TAX, false, 200, NULL, {0}, {0}, 0}, {"Income Tax", TILE_TAX, false, 200, NULL, {0}, {0}, 0},
{"Reading Railroad", TILE_RAILROAD, false, 200, NULL, {0}, {9, 1, 4}, 0}, {"Reading Railroad", TILE_RAILROAD, false, 200, NULL, {0}, {9, 1, 4}, 0},
{"Rhode Island Avenue", TILE_PROPERTY, false, 100, "#aae0fa", {6, 30, 90, 270, 400, 550}, {2, 1, 3}, 50}, {"Rhode Island Avenue",
TILE_PROPERTY,
false,
100,
"#aae0fa",
{6, 30, 90, 270, 400, 550},
{2, 1, 3},
50},
{"Chance", TILE_CHANCE, false, 0, NULL, {0}, {0}, 0}, {"Chance", TILE_CHANCE, false, 0, NULL, {0}, {0}, 0},
{"Vermont Avenue", TILE_PROPERTY, false, 100, "#aae0fa", {6, 30, 90, 270, 400, 550}, {2, 2, 3}, 50}, {"Vermont Avenue",
{"Connecticut Avenue", TILE_PROPERTY, false, 120, "#aae0fa", {8, 40, 100, 300, 450, 600}, {2, 3, 3}, 50}, TILE_PROPERTY,
false,
100,
"#aae0fa",
{6, 30, 90, 270, 400, 550},
{2, 2, 3},
50},
{"Connecticut Avenue",
TILE_PROPERTY,
false,
120,
"#aae0fa",
{8, 40, 100, 300, 450, 600},
{2, 3, 3},
50},
{"Jail", TILE_JAIL, true, 0, NULL, {0}, {0}, 0}, {"Jail", TILE_JAIL, true, 0, NULL, {0}, {0}, 0},
{"St. Charles Place", TILE_PROPERTY, false, 140, "#d93a96", {10, 50, 150, 450, 625, 750}, {3, 1, 3}, 100}, {"St. Charles Place",
TILE_PROPERTY,
false,
140,
"#d93a96",
{10, 50, 150, 450, 625, 750},
{3, 1, 3},
100},
{"Electric Company", TILE_UTILITY, false, 150, NULL, {0}, {10, 1, 2}, 0}, {"Electric Company", TILE_UTILITY, false, 150, NULL, {0}, {10, 1, 2}, 0},
{"States Avenue", TILE_PROPERTY, false, 140, "#d93a96", {10, 50, 150, 450, 625, 750}, {3, 2, 3}, 100}, {"States Avenue",
{"Virginia Avenue", TILE_PROPERTY, false, 160, "#d93a96", {12, 60, 180, 500, 700, 900}, {3, 3, 3}, 100}, TILE_PROPERTY,
{"Pennsylvania Railroad", TILE_RAILROAD, false, 200, NULL, {0}, {9, 2, 4}, 0}, false,
{"St. James Place", TILE_PROPERTY, false, 180, "#f7941d", {14, 70, 200, 550, 750, 950}, {4, 1, 3}, 100}, 140,
"#d93a96",
{10, 50, 150, 450, 625, 750},
{3, 2, 3},
100},
{"Virginia Avenue",
TILE_PROPERTY,
false,
160,
"#d93a96",
{12, 60, 180, 500, 700, 900},
{3, 3, 3},
100},
{"Pennsylvania Railroad",
TILE_RAILROAD,
false,
200,
NULL,
{0},
{9, 2, 4},
0},
{"St. James Place",
TILE_PROPERTY,
false,
180,
"#f7941d",
{14, 70, 200, 550, 750, 950},
{4, 1, 3},
100},
{"Community Chest", TILE_COMMUNITY_CHEST, false, 0, NULL, {0}, {0}, 0}, {"Community Chest", TILE_COMMUNITY_CHEST, false, 0, NULL, {0}, {0}, 0},
{"Tennessee Avenue", TILE_PROPERTY, false, 180, "#f7941d", {14, 70, 200, 550, 750, 950}, {4, 2, 3}, 100}, {"Tennessee Avenue",
{"New York Avenue", TILE_PROPERTY, false, 200, "#f7941d", {16, 80, 220, 600, 800, 1000}, {4, 3, 3}, 100}, TILE_PROPERTY,
false,
180,
"#f7941d",
{14, 70, 200, 550, 750, 950},
{4, 2, 3},
100},
{"New York Avenue",
TILE_PROPERTY,
false,
200,
"#f7941d",
{16, 80, 220, 600, 800, 1000},
{4, 3, 3},
100},
{"Free Parking", TILE_FREE_PARKING, true, 0, NULL, {0}, {0}, 0}, {"Free Parking", TILE_FREE_PARKING, true, 0, NULL, {0}, {0}, 0},
{"Kentucky Avenue", TILE_PROPERTY, false, 220, "#ed1b24", {18, 90, 250, 700, 875, 1050}, {5, 1, 3}, 150}, {"Kentucky Avenue",
TILE_PROPERTY,
false,
220,
"#ed1b24",
{18, 90, 250, 700, 875, 1050},
{5, 1, 3},
150},
{"Chance", TILE_CHANCE, false, 0, NULL, {0}, {0}, 0}, {"Chance", TILE_CHANCE, false, 0, NULL, {0}, {0}, 0},
{"Indiana Avenue", TILE_PROPERTY, false, 220, "#ed1b24", {18, 90, 250, 700, 875, 1050}, {5, 2, 3}, 150}, {"Indiana Avenue",
{"Illinois Avenue", TILE_PROPERTY, false, 240, "#ed1b24", {20, 100, 300, 750, 925, 1100}, {5, 3, 3}, 150}, TILE_PROPERTY,
false,
220,
"#ed1b24",
{18, 90, 250, 700, 875, 1050},
{5, 2, 3},
150},
{"Illinois Avenue",
TILE_PROPERTY,
false,
240,
"#ed1b24",
{20, 100, 300, 750, 925, 1100},
{5, 3, 3},
150},
{"B. & O. Railroad", TILE_RAILROAD, false, 200, NULL, {0}, {9, 3, 4}, 0}, {"B. & O. Railroad", TILE_RAILROAD, false, 200, NULL, {0}, {9, 3, 4}, 0},
{"Atlantic Avenue", TILE_PROPERTY, false, 260, "#fef200", {22, 110, 330, 800, 975, 1150}, {6, 1, 3}, 150}, {"Atlantic Avenue",
{"Ventnor Avenue", TILE_PROPERTY, false, 260, "#fef200", {22, 110, 330, 800, 975, 1150}, {6, 2, 3}, 150}, TILE_PROPERTY,
false,
260,
"#fef200",
{22, 110, 330, 800, 975, 1150},
{6, 1, 3},
150},
{"Ventnor Avenue",
TILE_PROPERTY,
false,
260,
"#fef200",
{22, 110, 330, 800, 975, 1150},
{6, 2, 3},
150},
{"Water Works", TILE_UTILITY, false, 150, NULL, {0}, {10, 2, 2}, 0}, {"Water Works", TILE_UTILITY, false, 150, NULL, {0}, {10, 2, 2}, 0},
{"Marvin Gardens", TILE_PROPERTY, false, 280, "#fef200", {24, 120, 360, 850, 1025, 1200}, {6, 3, 3}, 150}, {"Marvin Gardens",
TILE_PROPERTY,
false,
280,
"#fef200",
{24, 120, 360, 850, 1025, 1200},
{6, 3, 3},
150},
{"Go To Jail", TILE_GO_TO_JAIL, true, 0, NULL, {0}, {0}, 0}, {"Go To Jail", TILE_GO_TO_JAIL, true, 0, NULL, {0}, {0}, 0},
{"Pacific Avenue", TILE_PROPERTY, false, 300, "#1fb25a", {26, 130, 390, 900, 1100, 1275}, {7, 1, 3}, 200}, {"Pacific Avenue",
{"North Carolina Avenue", TILE_PROPERTY, false, 300, "#1fb25a", {26, 130, 390, 900, 1100, 1275}, {7, 2, 3}, 200}, TILE_PROPERTY,
false,
300,
"#1fb25a",
{26, 130, 390, 900, 1100, 1275},
{7, 1, 3},
200},
{"North Carolina Avenue",
TILE_PROPERTY,
false,
300,
"#1fb25a",
{26, 130, 390, 900, 1100, 1275},
{7, 2, 3},
200},
{"Community Chest", TILE_COMMUNITY_CHEST, false, 0, NULL, {0}, {0}, 0}, {"Community Chest", TILE_COMMUNITY_CHEST, false, 0, NULL, {0}, {0}, 0},
{"Pennsylvania Avenue", TILE_PROPERTY, false, 320, "#1fb25a", {28, 150, 450, 1000, 1200, 1400}, {7, 3, 3}, 200}, {"Pennsylvania Avenue",
TILE_PROPERTY,
false,
320,
"#1fb25a",
{28, 150, 450, 1000, 1200, 1400},
{7, 3, 3},
200},
{"Short Line", TILE_RAILROAD, false, 200, NULL, {0}, {9, 4, 4}, 0}, {"Short Line", TILE_RAILROAD, false, 200, NULL, {0}, {9, 4, 4}, 0},
{"Chance", TILE_CHANCE, false, 0, NULL, {0}, {0}, 0}, {"Chance", TILE_CHANCE, false, 0, NULL, {0}, {0}, 0},
{"Park Place", TILE_PROPERTY, false, 350, "#0072bb", {35, 175, 500, 1100, 1300, 1500}, {8, 1, 2}, 200}, {"Park Place",
TILE_PROPERTY,
false,
350,
"#0072bb",
{35, 175, 500, 1100, 1300, 1500},
{8, 1, 2},
200},
{"Luxury Tax", TILE_TAX, false, 100, NULL, {0}, {0}, 0}, {"Luxury Tax", TILE_TAX, false, 100, NULL, {0}, {0}, 0},
{"Boardwalk", TILE_PROPERTY, false, 400, "#0072bb", {50, 200, 600, 1400, 1700, 2000}, {8, 2, 2}, 200} {"Boardwalk",
}; TILE_PROPERTY,
false,
400,
"#0072bb",
{50, 200, 600, 1400, 1700, 2000},
{8, 2, 2},
200}};
#endif #endif
+256 -125
View File
@@ -1,7 +1,8 @@
// ============================================================================ // ============================================================================
// MONOPOLY GAME IMPLEMENTATION (for custom console) // MONOPOLY GAME IMPLEMENTATION (for custom console)
// ============================================================================ // ============================================================================
// Refactored from console version to use Game interface and rendering/input system // Refactored from console version to use Game interface and rendering/input
// system
#include "monopoly_game.h" #include "monopoly_game.h"
#ifdef __cplusplus #ifdef __cplusplus
@@ -12,25 +13,25 @@ extern "C" {
} }
#endif #endif
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include "DiceModalGame.h"
#include "PropertyModalGame.h"
#include "BoardModalGame.h" #include "BoardModalGame.h"
#include "ChanceModalGame.h" #include "ChanceModalGame.h"
#include "CommunityChestModalGame.h" #include "CommunityChestModalGame.h"
#include "TurnModalGame.h" #include "DiceModalGame.h"
#include "PaymentModalGame.h"
#include "ModalButtonHelper.h" #include "ModalButtonHelper.h"
#include "sprites.h"
#include "MonopolyBoardRenderer.h" #include "MonopolyBoardRenderer.h"
#include "PaymentModalGame.h"
#include "PropertyModalGame.h"
#include "TurnModalGame.h"
#include "sprites.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
// --- Constructor --- // --- Constructor ---
MonopolyGame::MonopolyGame(uint16_t width, uint16_t height, LowLevelRenderer* renderer, LowLevelGUI* gui, InputManager* input_manager) MonopolyGame::MonopolyGame(uint16_t width, uint16_t height,
LowLevelRenderer *renderer, LowLevelGUI *gui,
InputManager *input_manager)
: Game(width, height, renderer, gui, input_manager) { : Game(width, height, renderer, gui, input_manager) {
players_count = 2; players_count = 2;
current_player_idx = 0; current_player_idx = 0;
@@ -56,9 +57,13 @@ void MonopolyGame::init() {
srand(time(NULL)); srand(time(NULL));
shuffle_chance_deck(); shuffle_chance_deck();
shuffle_community_deck(); shuffle_community_deck();
//renderer->set_font(&font_tama_mini02_obj); // renderer->set_font(&font_tama_mini02_obj);
if (active_modal) { delete active_modal; active_modal = nullptr; } if (active_modal) {
active_modal = new TurnModalGame(width, height, renderer, gui, input_manager, &players[current_player_idx]); delete active_modal;
active_modal = nullptr;
}
active_modal = new TurnModalGame(width, height, renderer, gui, input_manager,
&players[current_player_idx]);
// TODO: Reset all board state, property ownership, etc. // TODO: Reset all board state, property ownership, etc.
} }
@@ -89,15 +94,16 @@ void MonopolyGame::shuffle_community_deck() {
} }
// --- Handle input events (minimal: roll, buy, end turn) --- // --- Handle input events (minimal: roll, buy, end turn) ---
bool MonopolyGame::update(const InputEvent& event) { bool MonopolyGame::update(const InputEvent &event) {
Player* p = &players[current_player_idx]; Player *p = &players[current_player_idx];
bool needs_redraw = false; bool needs_redraw = false;
// Calculate available actions // Calculate available actions
int menu_count = 0; int menu_count = 0;
if (!has_rolled) { if (!has_rolled) {
menu_count++; // Roll Dice menu_count++; // Roll Dice
if (p->is_in_jail) menu_count++; // Pay $50 if (p->is_in_jail)
menu_count++; // Pay $50
} else { } else {
menu_count++; // End Turn menu_count++; // End Turn
} }
@@ -106,16 +112,39 @@ bool MonopolyGame::update(const InputEvent& event) {
// If a modal is active, delegate input and check for dismissal // If a modal is active, delegate input and check for dismissal
if (active_modal) { if (active_modal) {
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 using custom get_type() // Check for specific modal types to handle their results using custom
DiceModalGame* dice_modal = (active_modal->get_type() == Game::Type::MONOPOLY_DICE) ? static_cast<DiceModalGame*>(active_modal) : nullptr; // get_type()
PropertyModalGame* prop_modal = (active_modal->get_type() == Game::Type::MONOPOLY_PROPERTY) ? static_cast<PropertyModalGame*>(active_modal) : nullptr; DiceModalGame *dice_modal =
BoardModalGame* board_modal = (active_modal->get_type() == Game::Type::MONOPOLY_BOARD) ? static_cast<BoardModalGame*>(active_modal) : nullptr; (active_modal->get_type() == Game::Type::MONOPOLY_DICE)
ChanceModalGame* chance_modal = (active_modal->get_type() == Game::Type::MONOPOLY_CHANCE) ? static_cast<ChanceModalGame*>(active_modal) : nullptr; ? static_cast<DiceModalGame *>(active_modal)
CommunityChestModalGame* community_modal = (active_modal->get_type() == Game::Type::MONOPOLY_COMMUNITY_CHEST) ? static_cast<CommunityChestModalGame*>(active_modal) : nullptr; : nullptr;
TurnModalGame* turn_modal = (active_modal->get_type() == Game::Type::MONOPOLY_TURN) ? static_cast<TurnModalGame*>(active_modal) : nullptr; PropertyModalGame *prop_modal =
PaymentModalGame* pay_modal = (active_modal->get_type() == Game::Type::MONOPOLY_PAYMENT) ? static_cast<PaymentModalGame*>(active_modal) : nullptr; (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;
ChanceModalGame *chance_modal =
(active_modal->get_type() == Game::Type::MONOPOLY_CHANCE)
? static_cast<ChanceModalGame *>(active_modal)
: nullptr;
CommunityChestModalGame *community_modal =
(active_modal->get_type() == Game::Type::MONOPOLY_COMMUNITY_CHEST)
? static_cast<CommunityChestModalGame *>(active_modal)
: nullptr;
TurnModalGame *turn_modal =
(active_modal->get_type() == Game::Type::MONOPOLY_TURN)
? static_cast<TurnModalGame *>(active_modal)
: nullptr;
PaymentModalGame *pay_modal =
(active_modal->get_type() == Game::Type::MONOPOLY_PAYMENT)
? static_cast<PaymentModalGame *>(active_modal)
: nullptr;
if (pay_modal && pay_modal->is_dismissed()) { if (pay_modal && pay_modal->is_dismissed()) {
delete active_modal; delete active_modal;
@@ -135,7 +164,7 @@ bool MonopolyGame::update(const InputEvent& event) {
// Immediately check if we need to show a property modal // Immediately check if we need to show a property modal
if (modal_property_index >= 0) { if (modal_property_index >= 0) {
bool is_owned = false; bool is_owned = false;
const char* owner_name = nullptr; const char *owner_name = nullptr;
int owner_id = -1; int owner_id = -1;
for (int i = 0; i < players_count; ++i) { for (int i = 0; i < players_count; ++i) {
for (int j = 0; j < players[i].property_count; ++j) { for (int j = 0; j < players[i].property_count; ++j) {
@@ -146,23 +175,36 @@ bool MonopolyGame::update(const InputEvent& event) {
break; break;
} }
} }
if (is_owned) break; if (is_owned)
break;
} }
bool can_afford = (p->balance >= MONOPOLY_BOARD[modal_property_index].cost); bool can_afford =
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, current_player_idx); (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, current_player_idx);
modal_property_index = -1; modal_property_index = -1;
} else if (last_drawn_chance_idx >= 0) { } else if (last_drawn_chance_idx >= 0) {
active_modal = new ChanceModalGame(width, height, renderer, gui, input_manager, &CHANCE_DECK[last_drawn_chance_idx], players, players_count, p->position); active_modal =
new ChanceModalGame(width, height, renderer, gui, input_manager,
&CHANCE_DECK[last_drawn_chance_idx], players,
players_count, p->position);
// We'll apply the effect when ChanceModal is dismissed // We'll apply the effect when ChanceModal is dismissed
} else if (last_drawn_community_idx >= 0) { } else if (last_drawn_community_idx >= 0) {
active_modal = new CommunityChestModalGame(width, height, renderer, gui, input_manager, &COMMUNITY_DECK[last_drawn_community_idx], players, players_count, p->position); active_modal = new CommunityChestModalGame(
width, height, renderer, gui, input_manager,
&COMMUNITY_DECK[last_drawn_community_idx], players, players_count,
p->position);
// We'll apply the effect when CommunityChestModal is dismissed // We'll apply the effect when CommunityChestModal is dismissed
} }
if (active_modal) active_modal->init(); if (active_modal)
else selected_action = -1; active_modal->init();
else
selected_action = -1;
return needs_redraw; return needs_redraw;
} else if (chance_modal && chance_modal->is_dismissed()) { } else if (chance_modal && chance_modal->is_dismissed()) {
const ChanceCard* card = &CHANCE_DECK[last_drawn_chance_idx]; const ChanceCard *card = &CHANCE_DECK[last_drawn_chance_idx];
last_drawn_chance_idx = -1; last_drawn_chance_idx = -1;
delete active_modal; delete active_modal;
active_modal = nullptr; active_modal = nullptr;
@@ -177,31 +219,36 @@ bool MonopolyGame::update(const InputEvent& event) {
p->balance += card->value; p->balance += card->value;
break; break;
case CHANCE_SPEND: case CHANCE_SPEND:
active_modal = new PaymentModalGame(width, height, renderer, gui, input_manager, p, nullptr, card->value, "CHANCE CARD"); active_modal =
if (active_modal) active_modal->init(); new PaymentModalGame(width, height, renderer, gui, input_manager, p,
nullptr, card->value, "CHANCE CARD");
if (active_modal)
active_modal->init();
break; break;
case CHANCE_ADVANCE: { case CHANCE_ADVANCE: {
int target = card->value; int target = card->value;
if (target == TARGET_NEAREST_UTILITY) { if (target == TARGET_NEAREST_UTILITY) {
target = (p->position + 1) % BOARD_SIZE; target = (p->position + 1) % MONOPOLY_BOARD_SIZE;
while (MONOPOLY_BOARD[target].type != TILE_UTILITY) { while (MONOPOLY_BOARD[target].type != TILE_UTILITY) {
target = (target + 1) % BOARD_SIZE; target = (target + 1) % MONOPOLY_BOARD_SIZE;
} }
force_utility_10x = true; force_utility_10x = true;
} else if (target == TARGET_NEAREST_RAILROAD) { } else if (target == TARGET_NEAREST_RAILROAD) {
target = (p->position + 1) % BOARD_SIZE; target = (p->position + 1) % MONOPOLY_BOARD_SIZE;
while (MONOPOLY_BOARD[target].type != TILE_RAILROAD) { while (MONOPOLY_BOARD[target].type != TILE_RAILROAD) {
target = (target + 1) % BOARD_SIZE; target = (target + 1) % MONOPOLY_BOARD_SIZE;
} }
rent_multiplier = 2; rent_multiplier = 2;
} }
p->position = target; p->position = target;
if (p->position < old_pos) p->balance += 200; if (p->position < old_pos)
p->balance += 200;
position_changed = true; position_changed = true;
break; break;
} }
case CHANCE_BACK: case CHANCE_BACK:
p->position = (p->position - card->value + BOARD_SIZE) % BOARD_SIZE; p->position = (p->position - card->value + MONOPOLY_BOARD_SIZE) %
MONOPOLY_BOARD_SIZE;
position_changed = true; position_changed = true;
break; break;
case CHANCE_JAIL: case CHANCE_JAIL:
@@ -231,10 +278,11 @@ bool MonopolyGame::update(const InputEvent& event) {
active_modal->init(); active_modal->init();
} else if (position_changed) { } else if (position_changed) {
// If we moved, check if we landed on a property // If we moved, check if we landed on a property
const BoardTile* landed = &MONOPOLY_BOARD[p->position]; const BoardTile *landed = &MONOPOLY_BOARD[p->position];
if (landed->type == TILE_PROPERTY || landed->type == TILE_RAILROAD || landed->type == TILE_UTILITY) { if (landed->type == TILE_PROPERTY || landed->type == TILE_RAILROAD ||
landed->type == TILE_UTILITY) {
bool is_owned = false; bool is_owned = false;
const char* owner_name = nullptr; const char *owner_name = nullptr;
int owner_id = -1; int owner_id = -1;
for (int i = 0; i < players_count; ++i) { for (int i = 0; i < players_count; ++i) {
for (int j = 0; j < players[i].property_count; ++j) { for (int j = 0; j < players[i].property_count; ++j) {
@@ -247,12 +295,16 @@ bool MonopolyGame::update(const InputEvent& event) {
} }
} }
bool can_afford = (p->balance >= landed->cost); bool can_afford = (p->balance >= landed->cost);
active_modal = new PropertyModalGame(width, height, renderer, gui, input_manager, landed, is_owned, owner_name, owner_id, can_afford, players, players_count, current_player_idx); active_modal = new PropertyModalGame(
if (active_modal) active_modal->init(); width, height, renderer, gui, input_manager, landed, is_owned,
owner_name, owner_id, can_afford, players, players_count,
current_player_idx);
if (active_modal)
active_modal->init();
} }
} }
} else if (community_modal && community_modal->is_dismissed()) { } else if (community_modal && community_modal->is_dismissed()) {
const CommunityCard* card = &COMMUNITY_DECK[last_drawn_community_idx]; const CommunityCard *card = &COMMUNITY_DECK[last_drawn_community_idx];
last_drawn_community_idx = -1; last_drawn_community_idx = -1;
delete active_modal; delete active_modal;
@@ -266,12 +318,16 @@ bool MonopolyGame::update(const InputEvent& event) {
p->balance += card->value; p->balance += card->value;
break; break;
case COMMUNITY_SPEND: case COMMUNITY_SPEND:
active_modal = new PaymentModalGame(width, height, renderer, gui, input_manager, p, nullptr, card->value, "COMMUNITY CHEST"); active_modal =
if (active_modal) active_modal->init(); new PaymentModalGame(width, height, renderer, gui, input_manager, p,
nullptr, card->value, "COMMUNITY CHEST");
if (active_modal)
active_modal->init();
break; break;
case COMMUNITY_ADVANCE: case COMMUNITY_ADVANCE:
p->position = card->value; p->position = card->value;
if (p->position < old_pos) p->balance += 200; if (p->position < old_pos)
p->balance += 200;
position_changed = true; position_changed = true;
break; break;
case COMMUNITY_JAIL: case COMMUNITY_JAIL:
@@ -300,10 +356,11 @@ bool MonopolyGame::update(const InputEvent& event) {
if (active_modal) { if (active_modal) {
active_modal->init(); active_modal->init();
} else if (position_changed) { } else if (position_changed) {
const BoardTile* landed = &MONOPOLY_BOARD[p->position]; const BoardTile *landed = &MONOPOLY_BOARD[p->position];
if (landed->type == TILE_PROPERTY || landed->type == TILE_RAILROAD || landed->type == TILE_UTILITY) { if (landed->type == TILE_PROPERTY || landed->type == TILE_RAILROAD ||
landed->type == TILE_UTILITY) {
bool is_owned = false; bool is_owned = false;
const char* owner_name = nullptr; const char *owner_name = nullptr;
int owner_id = -1; int owner_id = -1;
for (int i = 0; i < players_count; ++i) { for (int i = 0; i < players_count; ++i) {
for (int j = 0; j < players[i].property_count; ++j) { for (int j = 0; j < players[i].property_count; ++j) {
@@ -316,8 +373,12 @@ bool MonopolyGame::update(const InputEvent& event) {
} }
} }
bool can_afford = (p->balance >= landed->cost); bool can_afford = (p->balance >= landed->cost);
active_modal = new PropertyModalGame(width, height, renderer, gui, input_manager, landed, is_owned, owner_name, owner_id, can_afford, players, players_count, current_player_idx); active_modal = new PropertyModalGame(
if (active_modal) active_modal->init(); width, height, renderer, gui, input_manager, landed, is_owned,
owner_name, owner_id, can_afford, players, players_count,
current_player_idx);
if (active_modal)
active_modal->init();
} }
} }
} else if (prop_modal && prop_modal->is_dismissed()) { } else if (prop_modal && prop_modal->is_dismissed()) {
@@ -329,38 +390,49 @@ bool MonopolyGame::update(const InputEvent& event) {
active_modal = nullptr; active_modal = nullptr;
if (wants_buy) { if (wants_buy) {
const BoardTile* tile = &MONOPOLY_BOARD[p->position]; const BoardTile *tile = &MONOPOLY_BOARD[p->position];
if (p->balance >= tile->cost) { if (p->balance >= tile->cost) {
active_modal = new PaymentModalGame(width, height, renderer, gui, input_manager, p, nullptr, tile->cost, "BUY PROPERTY"); active_modal =
new PaymentModalGame(width, height, renderer, gui, input_manager,
p, nullptr, tile->cost, "BUY PROPERTY");
p->properties_owned[p->property_count++] = p->position; p->properties_owned[p->property_count++] = p->position;
} }
} else if (wants_rent) { } else if (wants_rent) {
const BoardTile* tile = &MONOPOLY_BOARD[p->position]; const BoardTile *tile = &MONOPOLY_BOARD[p->position];
int rent = 0; int rent = 0;
if (tile->type == TILE_PROPERTY) { if (tile->type == TILE_PROPERTY) {
rent = tile->rent[0]; rent = tile->rent[0];
} } else if (tile->type == TILE_RAILROAD) {
else if (tile->type == TILE_RAILROAD) {
int rr_count = 0; int rr_count = 0;
if (owner_id_from_modal != -1) { if (owner_id_from_modal != -1) {
for (int i = 0; i < players[owner_id_from_modal].property_count; ++i) { for (int i = 0; i < players[owner_id_from_modal].property_count;
if (MONOPOLY_BOARD[players[owner_id_from_modal].properties_owned[i]].type == TILE_RAILROAD) { ++i) {
if (MONOPOLY_BOARD[players[owner_id_from_modal]
.properties_owned[i]]
.type == TILE_RAILROAD) {
rr_count++; rr_count++;
} }
} }
} }
if (rr_count == 1) rent = 25; if (rr_count == 1)
else if (rr_count == 2) rent = 50; rent = 25;
else if (rr_count == 3) rent = 100; else if (rr_count == 2)
else if (rr_count == 4) rent = 200; rent = 50;
else rent = 25; else if (rr_count == 3)
} rent = 100;
else if (tile->type == TILE_UTILITY) { else if (rr_count == 4)
rent = 200;
else
rent = 25;
} else if (tile->type == TILE_UTILITY) {
int total_dice = last_dice1 + last_dice2; int total_dice = last_dice1 + last_dice2;
int utility_count = 0; int utility_count = 0;
if (owner_id_from_modal != -1) { if (owner_id_from_modal != -1) {
for (int i = 0; i < players[owner_id_from_modal].property_count; ++i) { for (int i = 0; i < players[owner_id_from_modal].property_count;
if (MONOPOLY_BOARD[players[owner_id_from_modal].properties_owned[i]].type == TILE_UTILITY) { ++i) {
if (MONOPOLY_BOARD[players[owner_id_from_modal]
.properties_owned[i]]
.type == TILE_UTILITY) {
utility_count++; utility_count++;
} }
} }
@@ -374,8 +446,11 @@ bool MonopolyGame::update(const InputEvent& event) {
rent *= rent_multiplier; rent *= rent_multiplier;
if (owner_id_from_modal != -1 && (int)current_player_idx != owner_id_from_modal) { if (owner_id_from_modal != -1 &&
active_modal = new PaymentModalGame(width, height, renderer, gui, input_manager, p, &players[owner_id_from_modal], rent, "RENT"); (int)current_player_idx != owner_id_from_modal) {
active_modal = new PaymentModalGame(
width, height, renderer, gui, input_manager, p,
&players[owner_id_from_modal], rent, "RENT");
} }
} }
// Reset multipliers // Reset multipliers
@@ -384,7 +459,8 @@ bool MonopolyGame::update(const InputEvent& event) {
needs_redraw = true; needs_redraw = true;
ModalButtonHelper::set_monopoly_regions(input_manager, width, height); ModalButtonHelper::set_monopoly_regions(input_manager, width, height);
if (active_modal) active_modal->init(); if (active_modal)
active_modal->init();
} else if (board_modal && board_modal->is_dismissed()) { } else if (board_modal && board_modal->is_dismissed()) {
delete active_modal; delete active_modal;
active_modal = nullptr; active_modal = nullptr;
@@ -409,40 +485,56 @@ bool MonopolyGame::update(const InputEvent& event) {
needs_redraw = true; needs_redraw = true;
break; break;
case INPUT_BUTTON_1: case INPUT_BUTTON_1:
if (selected_action == -1) return false; if (selected_action == -1)
return false;
if (p->is_in_jail && !has_rolled && selected_action == 1) { if (p->is_in_jail && !has_rolled && selected_action == 1) {
active_modal = new PaymentModalGame(width, height, renderer, gui, input_manager, p, nullptr, 50, "JAIL BAIL"); active_modal =
if (active_modal) active_modal->init(); new PaymentModalGame(width, height, renderer, gui, input_manager, p,
nullptr, 50, "JAIL BAIL");
if (active_modal)
active_modal->init();
p->is_in_jail = false; p->is_in_jail = false;
p->jail_turns = 0; p->jail_turns = 0;
selected_action = -1; selected_action = -1;
needs_redraw = true; needs_redraw = true;
return true; return true;
} }
if (active_modal) delete active_modal; if (active_modal)
delete active_modal;
if (selected_action == (menu_count - 1)) { if (selected_action == (menu_count - 1)) {
active_modal = new BoardModalGame(width, height, renderer, gui, input_manager, players, players_count, current_player_idx); active_modal =
new BoardModalGame(width, height, renderer, gui, input_manager,
players, players_count, current_player_idx);
needs_redraw = true; needs_redraw = true;
} else if (!has_rolled) { } else if (!has_rolled) {
roll_dice_logic: roll_dice_logic:
int d1 = (rand() % 6) + 1; int d1 = (rand() % 6) + 1;
int d2 = (rand() % 6) + 1; int d2 = (rand() % 6) + 1;
bool is_db = (d1 == d2); bool is_db = (d1 == d2);
int old_pos = p->position; int old_pos = p->position;
if (p->is_in_jail) { if (p->is_in_jail) {
if (is_db) { if (is_db) {
p->is_in_jail = false; p->jail_turns = 0; p->is_in_jail = false;
p->jail_turns = 0;
} else { } else {
p->jail_turns++; p->jail_turns++;
if (p->jail_turns >= 3) { if (p->jail_turns >= 3) {
active_modal = new PaymentModalGame(width, height, renderer, gui, input_manager, p, nullptr, 50, "JAIL BAIL (FORCED)"); active_modal = new PaymentModalGame(width, height, renderer, gui,
if (active_modal) active_modal->init(); input_manager, p, nullptr, 50,
p->is_in_jail = false; p->jail_turns = 0; "JAIL BAIL (FORCED)");
if (active_modal)
active_modal->init();
p->is_in_jail = false;
p->jail_turns = 0;
} else { } else {
has_rolled = true; has_rolled = true;
last_dice1 = d1; last_dice2 = d2; last_dice1 = d1;
active_modal = new DiceModalGame(width, height, renderer, gui, input_manager, d1, d2, &MONOPOLY_BOARD[old_pos], &MONOPOLY_BOARD[old_pos], players, players_count); last_dice2 = d2;
active_modal = new DiceModalGame(
width, height, renderer, gui, input_manager, d1, d2,
&MONOPOLY_BOARD[old_pos], &MONOPOLY_BOARD[old_pos], players,
players_count);
needs_redraw = true; needs_redraw = true;
return true; return true;
} }
@@ -450,10 +542,17 @@ roll_dice_logic:
} else if (is_db) { } else if (is_db) {
double_rolls++; double_rolls++;
if (double_rolls >= 3) { if (double_rolls >= 3) {
p->position = 10; p->is_in_jail = true; p->jail_turns = 0; p->position = 10;
has_rolled = true; double_rolls = 0; p->is_in_jail = true;
last_dice1 = d1; last_dice2 = d2; p->jail_turns = 0;
active_modal = new DiceModalGame(width, height, renderer, gui, input_manager, d1, d2, &MONOPOLY_BOARD[old_pos], &MONOPOLY_BOARD[10], players, players_count); has_rolled = true;
double_rolls = 0;
last_dice1 = d1;
last_dice2 = d2;
active_modal =
new DiceModalGame(width, height, renderer, gui, input_manager, d1,
d2, &MONOPOLY_BOARD[old_pos],
&MONOPOLY_BOARD[10], players, players_count);
needs_redraw = true; needs_redraw = true;
return true; return true;
} }
@@ -462,39 +561,60 @@ roll_dice_logic:
} }
int total = d1 + d2; int total = d1 + d2;
p->position = (p->position + total) % BOARD_SIZE; p->position = (p->position + total) % MONOPOLY_BOARD_SIZE;
if (p->position < old_pos) p->balance += 200; if (p->position < old_pos)
p->balance += 200;
has_rolled = !is_db; has_rolled = !is_db;
last_dice1 = d1; last_dice2 = d2; last_dice1 = d1;
active_modal = new DiceModalGame(width, height, renderer, gui, input_manager, d1, d2, &MONOPOLY_BOARD[old_pos], &MONOPOLY_BOARD[p->position], players, players_count); last_dice2 = d2;
active_modal = new DiceModalGame(
width, height, renderer, gui, input_manager, d1, d2,
&MONOPOLY_BOARD[old_pos], &MONOPOLY_BOARD[p->position], players,
players_count);
const BoardTile* lnd = &MONOPOLY_BOARD[p->position]; const BoardTile *lnd = &MONOPOLY_BOARD[p->position];
if (lnd->type == TILE_GO_TO_JAIL) { if (lnd->type == TILE_GO_TO_JAIL) {
p->position = 10; p->is_in_jail = true; p->jail_turns = 0; p->position = 10;
has_rolled = true; double_rolls = 0; p->is_in_jail = true;
} else if (lnd->type == TILE_PROPERTY || lnd->type == TILE_RAILROAD || lnd->type == TILE_UTILITY) { p->jail_turns = 0;
has_rolled = true;
double_rolls = 0;
} else if (lnd->type == TILE_PROPERTY || lnd->type == TILE_RAILROAD ||
lnd->type == TILE_UTILITY) {
modal_property_index = p->position; modal_property_index = p->position;
} else if (lnd->type == TILE_CHANCE) { } else if (lnd->type == TILE_CHANCE) {
last_drawn_chance_idx = chance_deck[current_chance_idx]; last_drawn_chance_idx = chance_deck[current_chance_idx];
current_chance_idx = (current_chance_idx + 1) % CHANCE_DECK_SIZE; current_chance_idx = (current_chance_idx + 1) % CHANCE_DECK_SIZE;
if (current_chance_idx == 0) shuffle_chance_deck(); if (current_chance_idx == 0)
shuffle_chance_deck();
} else if (lnd->type == TILE_COMMUNITY_CHEST) { } else if (lnd->type == TILE_COMMUNITY_CHEST) {
last_drawn_community_idx = community_deck[current_community_idx]; last_drawn_community_idx = community_deck[current_community_idx];
current_community_idx = (current_community_idx + 1) % COMMUNITY_DECK_SIZE; current_community_idx =
if (current_community_idx == 0) shuffle_community_deck(); (current_community_idx + 1) % COMMUNITY_DECK_SIZE;
if (current_community_idx == 0)
shuffle_community_deck();
} else if (lnd->type == TILE_TAX) { } else if (lnd->type == TILE_TAX) {
active_modal = new PaymentModalGame(width, height, renderer, gui, input_manager, p, nullptr, lnd->cost, "TAXES"); active_modal =
if (active_modal) active_modal->init(); new PaymentModalGame(width, height, renderer, gui, input_manager, p,
nullptr, lnd->cost, "TAXES");
if (active_modal)
active_modal->init();
} }
needs_redraw = true; needs_redraw = true;
} else { } else {
current_player_idx = (current_player_idx + 1) % players_count; current_player_idx = (current_player_idx + 1) % players_count;
has_rolled = false; double_rolls = 0; just_sent_to_jail = false; selected_action = -1; has_rolled = false;
active_modal = new TurnModalGame(width, height, renderer, gui, input_manager, &players[current_player_idx]); double_rolls = 0;
just_sent_to_jail = false;
selected_action = -1;
active_modal =
new TurnModalGame(width, height, renderer, gui, input_manager,
&players[current_player_idx]);
needs_redraw = true; needs_redraw = true;
} }
break; break;
default: break; default:
break;
} }
return needs_redraw; return needs_redraw;
} }
@@ -509,11 +629,12 @@ void MonopolyGame::draw() {
renderer->clear_buffer(); renderer->clear_buffer();
Player* p = &players[current_player_idx]; Player *p = &players[current_player_idx];
const BoardTile* tile = &MONOPOLY_BOARD[p->position]; const BoardTile *tile = &MONOPOLY_BOARD[p->position];
// --- Draw Board Perimeter --- // --- Draw Board Perimeter ---
MonopolyBoardRenderer::draw_board_perimeter(renderer, width, height, players, players_count, p->position); 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;
@@ -531,10 +652,12 @@ void MonopolyGame::draw() {
renderer->draw_filled_rectangle(ix + 4, iy + 4, iw - 8, 35, true, 1); renderer->draw_filled_rectangle(ix + 4, iy + 4, iw - 8, 35, true, 1);
renderer->set_text_color(false); // White text renderer->set_text_color(false); // White text
snprintf(buf, sizeof(buf), "%s'S TURN", p->name); snprintf(buf, sizeof(buf), "%s'S TURN", p->name);
renderer->draw_string_scaled(ix + (iw - (int)strlen(buf) * 12) / 2, iy + 12, buf, 2); renderer->draw_string_scaled(ix + (iw - (int)strlen(buf) * 12) / 2, iy + 12,
buf, 2);
renderer->set_text_color(true); renderer->set_text_color(true);
renderer->draw_string_scaled(ix + (iw - 8 * 18) / 2, iy + ih - 35, "Monopoly", 3); renderer->draw_string_scaled(ix + (iw - 8 * 18) / 2, iy + ih - 35, "Monopoly",
3);
int content_y = iy + 50; int content_y = iy + 50;
@@ -547,14 +670,20 @@ void MonopolyGame::draw() {
content_y += 20; content_y += 20;
// Draw special tile sprite // Draw special tile sprite
const unsigned char* sprite = nullptr; const unsigned char *sprite = nullptr;
if (tile->type == TILE_COMMUNITY_CHEST) sprite = epd_bitmap_CommunityChest; if (tile->type == TILE_COMMUNITY_CHEST)
else if (tile->type == TILE_CHANCE) sprite = epd_bitmap_Chance; sprite = epd_bitmap_CommunityChest;
else if (tile->type == TILE_FREE_PARKING) sprite = epd_bitmap_FreeParking; else if (tile->type == TILE_CHANCE)
else if (tile->type == TILE_GO_TO_JAIL) sprite = epd_bitmap_GoToJail; sprite = epd_bitmap_Chance;
else if (tile->type == TILE_FREE_PARKING)
sprite = epd_bitmap_FreeParking;
else if (tile->type == TILE_GO_TO_JAIL)
sprite = epd_bitmap_GoToJail;
else if (tile->type == TILE_UTILITY) { else if (tile->type == TILE_UTILITY) {
if (strstr(tile->name, "Electric")) sprite = epd_bitmap_ElectricCompany; if (strstr(tile->name, "Electric"))
else if (strstr(tile->name, "Water")) sprite = epd_bitmap_WaterWorks; sprite = epd_bitmap_ElectricCompany;
else if (strstr(tile->name, "Water"))
sprite = epd_bitmap_WaterWorks;
} }
if (sprite) { if (sprite) {
@@ -566,18 +695,20 @@ void MonopolyGame::draw() {
renderer->draw_line(ix + 10, content_y, ix + iw - 10, content_y, true); renderer->draw_line(ix + 10, content_y, ix + iw - 10, content_y, true);
content_y += 15; content_y += 15;
// Draw action menu // Draw action menu
const char* actions[3]; const char *actions[3];
int menu_count = 0; int menu_count = 0;
if (!has_rolled) { if (!has_rolled) {
actions[menu_count++] = "Roll Dice"; actions[menu_count++] = "Roll Dice";
if (p->is_in_jail) actions[menu_count++] = "Pay $50"; if (p->is_in_jail)
actions[menu_count++] = "Pay $50";
} else { } else {
actions[menu_count++] = "End Turn"; actions[menu_count++] = "End Turn";
} }
actions[menu_count++] = "View Board"; actions[menu_count++] = "View Board";
for (int i = 0; i < menu_count; ++i) { for (int i = 0; i < menu_count; ++i) {
snprintf(buf, sizeof(buf), "%s%s", (i == selected_action) ? "> " : " ", actions[i]); snprintf(buf, sizeof(buf), "%s%s", (i == selected_action) ? "> " : " ",
actions[i]);
renderer->draw_string_scaled(ix + 15, content_y, buf, 2); renderer->draw_string_scaled(ix + 15, content_y, buf, 2);
content_y += 25; content_y += 25;
} }
+6 -9
View File
@@ -27,8 +27,9 @@ static bool ft6336u_read_reg(uint8_t reg, uint8_t *value) {
continue; continue;
} }
// Add delay after write, before read (like Arduino library does) // Short settle time between register address write and read.
sleep_us(10000); // 10ms delay // 10ms here heavily throttles touch sampling during hold/move.
sleep_us(100); // 0.1ms
result = i2c_read_blocking(g_config->i2c, FT6336U_ADDR, value, 1, false); result = i2c_read_blocking(g_config->i2c, FT6336U_ADDR, value, 1, false);
if (result == 1) { if (result == 1) {
@@ -50,8 +51,9 @@ static bool ft6336u_read_regs(uint8_t reg, uint8_t *buf, size_t len) {
continue; continue;
} }
// Add delay after write, before read (like Arduino library does) // Short settle time between register address write and read.
sleep_us(10000); // 10ms delay // 10ms here heavily throttles touch sampling during hold/move.
sleep_us(100); // 0.1ms
result = i2c_read_blocking(g_config->i2c, FT6336U_ADDR, buf, len, false); result = i2c_read_blocking(g_config->i2c, FT6336U_ADDR, buf, len, false);
if (result == (int)len) { if (result == (int)len) {
@@ -143,11 +145,6 @@ bool ft6336u_init(const ft6336u_config_t *config) {
printf("[FT6336U] WARNING: Failed to set CTRL mode\n"); printf("[FT6336U] WARNING: Failed to set CTRL mode\n");
} }
// Enable polling mode - INT pin stays LOW while touched, goes HIGH when released
printf("[FT6336U] Enabling polling mode...\n");
if (!ft6336u_write_reg(FT6336U_REG_G_MODE, FT6336U_G_MODE_POLLING)) {
printf("[FT6336U] WARNING: Failed to set G_MODE\n");
}
// Configure gesture parameters for better detection // Configure gesture parameters for better detection
printf("[FT6336U] Configuring gesture detection parameters...\n"); printf("[FT6336U] Configuring gesture detection parameters...\n");
+111
View File
@@ -8,6 +8,8 @@
#include "display/low_level_render.h" #include "display/low_level_render.h"
#include "display/low_level_gui.h" #include "display/low_level_gui.h"
#include <stdio.h> #include <stdio.h>
#include <cstring>
#include <cctype>
GameLauncher::GameLauncher(uint16_t width, uint16_t height, GameLauncher::GameLauncher(uint16_t width, uint16_t height,
LowLevelRenderer* renderer, LowLevelGUI* gui, InputManager* input_manager) LowLevelRenderer* renderer, LowLevelGUI* gui, InputManager* input_manager)
@@ -127,6 +129,7 @@ bool GameLauncher::update(const InputEvent& event) {
if (event.y >= y - 5 && event.y < y + MENU_ITEM_HEIGHT - 5) { if (event.y >= y - 5 && event.y < y + MENU_ITEM_HEIGHT - 5) {
// Game selected - create instance // Game selected - create instance
printf("Selected game: %s\n", games[i].name); printf("Selected game: %s\n", games[i].name);
selected_index = i;
selected_game = games[i].factory(width, height, renderer, gui, input_manager); selected_game = games[i].factory(width, height, renderer, gui, input_manager);
if (selected_game) { if (selected_game) {
selected_game->init(); selected_game->init();
@@ -212,6 +215,114 @@ void GameLauncher::reset() {
printf("Launcher reset - returning to menu\n"); printf("Launcher reset - returning to menu\n");
} }
void GameLauncher::clear_games() {
// Clean up currently selected game first
if (selected_game) {
delete selected_game;
selected_game = nullptr;
}
// Clear the games vector
games.clear();
selected_index = 0;
current_page = 0;
printf("Launcher: Cleared all registered games\n");
}
bool GameLauncher::select_game_by_name(const char* name) {
// Clean up old game first if one exists
if (selected_game) {
printf("Cleaning up previous game...\n");
delete selected_game;
selected_game = nullptr;
}
// First pass: search for exact match (case-insensitive)
for (size_t i = 0; i < games.size(); i++) {
if (strcasecmp(games[i].name, name) == 0) {
printf("Found exact match: %s\n", games[i].name);
// Create and initialize the game
selected_game = games[i].factory(width, height, renderer, gui, input_manager);
if (selected_game) {
printf("Game instance created, initializing...\n");
selected_game->init();
selected_index = i;
current_page = i / GAMES_PER_PAGE;
return true;
} else {
printf("Failed to create game instance\n");
return false;
}
}
}
// Second pass: search in reverse order for partial match (case-insensitive, newest files first)
for (int i = games.size() - 1; i >= 0; i--) {
// Convert both strings to lowercase for comparison
char game_name_lower[256];
char search_name_lower[256];
strncpy(game_name_lower, games[i].name, sizeof(game_name_lower) - 1);
game_name_lower[sizeof(game_name_lower) - 1] = '\0';
strncpy(search_name_lower, name, sizeof(search_name_lower) - 1);
search_name_lower[sizeof(search_name_lower) - 1] = '\0';
// Convert to lowercase
for (size_t j = 0; game_name_lower[j]; j++) {
game_name_lower[j] = tolower(game_name_lower[j]);
}
for (size_t j = 0; search_name_lower[j]; j++) {
search_name_lower[j] = tolower(search_name_lower[j]);
}
if (strstr(game_name_lower, search_name_lower) != nullptr) {
printf("Found partial match: %s (searching for: %s)\n", games[i].name, name);
// Create and initialize the game
selected_game = games[i].factory(width, height, renderer, gui, input_manager);
if (selected_game) {
printf("Game instance created, initializing...\n");
selected_game->init();
selected_index = i;
current_page = i / GAMES_PER_PAGE;
return true;
} else {
printf("Failed to create game instance\n");
return false;
}
}
}
printf("Game not found: %s\n", name);
return false;
}
bool GameLauncher::restart_selected_game() {
if (selected_index < 0 || selected_index >= (int)games.size()) {
return false;
}
if (selected_game) {
delete selected_game;
selected_game = nullptr;
}
selected_game = games[selected_index].factory(width, height, renderer, gui, input_manager);
if (!selected_game) {
return false;
}
selected_game->init();
current_page = selected_index / GAMES_PER_PAGE;
return true;
}
void GameLauncher::return_to_menu() {
reset();
}
int GameLauncher::get_total_pages() const { int GameLauncher::get_total_pages() const {
if (games.empty()) return 1; if (games.empty()) return 1;
return (games.size() + GAMES_PER_PAGE - 1) / GAMES_PER_PAGE; return (games.size() + GAMES_PER_PAGE - 1) / GAMES_PER_PAGE;
+23
View File
@@ -79,12 +79,35 @@ public:
*/ */
void reset(); void reset();
/**
* @brief Clear all registered games (useful before re-scanning)
*/
void clear_games();
/** /**
* @brief Check if a game is currently selected * @brief Check if a game is currently selected
* @return true if game selected, false if in menu * @return true if game selected, false if in menu
*/ */
bool is_game_selected() const { return selected_game != nullptr; } bool is_game_selected() const { return selected_game != nullptr; }
/**
* @brief Select a game by name (for programmatic launching)
* @param name Game name to select (partial match supported)
* @return true if game was found and launched, false otherwise
*/
bool select_game_by_name(const char* name);
/**
* @brief Restart the currently selected game.
* @return true if restart succeeded, false otherwise
*/
bool restart_selected_game();
/**
* @brief Exit current game and return to launcher menu.
*/
void return_to_menu();
private: private:
uint16_t width; uint16_t width;
uint16_t height; uint16_t height;
+43 -35
View File
@@ -10,7 +10,6 @@
// External interrupt flags from basic1.cpp // External interrupt flags from basic1.cpp
extern volatile bool touch_interrupt_flag; extern volatile bool touch_interrupt_flag;
extern volatile bool touch_event_down;
extern volatile bool button_key0_pressed; extern volatile bool button_key0_pressed;
extern volatile bool button_key1_pressed; extern volatile bool button_key1_pressed;
@@ -41,71 +40,80 @@ bool InputManager::has_buttons() const {
InputEvent InputManager::process_touch_input(uint32_t* last_time) { InputEvent InputManager::process_touch_input(uint32_t* last_time) {
InputEvent event = {INPUT_NONE, 0, 0, 0, 0, 0, false}; InputEvent event = {INPUT_NONE, 0, 0, 0, 0, 0, false};
const uint32_t now = to_ms_since_boot(get_absolute_time());
static constexpr uint32_t ACTIVE_TOUCH_SAMPLE_INTERVAL_MS = 8;
// Check if touch interrupt flag is set // Process immediately on IRQ, and continue sampling while a touch is active.
if (!touch_interrupt_flag) { // Some controllers only IRQ on edge transitions, so move events require polling.
if (!touch_interrupt_flag && *last_time == 0) {
return event; // No touch event return event; // No touch event
} }
printf("Processing touch: flag=%d, event_down=%d\n", touch_interrupt_flag, touch_event_down); // While a touch session is active, avoid reading touch hardware every loop
// iteration. IRQ edges (down/up) still bypass this throttle for responsiveness.
if (!touch_interrupt_flag && *last_time != 0) {
uint32_t elapsed = now - last_touch_sample_ms;
if (elapsed < ACTIVE_TOUCH_SAMPLE_INTERVAL_MS) {
return event;
}
}
// Don't clear the flag yet - we may still be processing continuous touch // Always validate via controller state instead of relying on edge flag alone.
// Edge chatter can flip touch_event_down without a real touch transition.
// Check if touch is active TouchData touch_data;
if (!touch_event_down) { if (!touch || !touch->read_touch(&touch_data)) {
// Touch released - reset timing for next touch last_touch_sample_ms = now;
touch_interrupt_flag = false; touch_interrupt_flag = false;
*last_time = 0; // Reset so next touch is treated as new touch-down
event.type = INPUT_TOUCH_UP;
event.valid = true;
printf("Touch UP\n");
return event; return event;
} }
// Touch is down - check debounce timing last_touch_sample_ms = now;
uint32_t now = to_ms_since_boot(get_absolute_time());
if (now - *last_time < config->touch_debounce_ms) {
return event; // Too soon, skip
}
// Read touch data
TouchData touch_data;
if (!touch || !touch->read_touch(&touch_data)) {
// Clear flag even if read failed to prevent getting stuck
touch_interrupt_flag = false;
printf("Touch read FAILED\n");
return event; // Read failed
}
// Clear the interrupt flag after successfully reading touch data
// This allows the next touch interrupt to be detected
touch_interrupt_flag = false; touch_interrupt_flag = false;
printf("Touch DOWN at (%d,%d)\n", touch_data.points[0].x, touch_data.points[0].y); // No active touch points: require consecutive empty reads before release to
// avoid false TOUCH_UP events from transient controller jitter.
if (touch_data.touch_count == 0) {
if (*last_time != 0) {
no_touch_samples++;
if (no_touch_samples >= 2) {
*last_time = 0;
last_touch_sample_ms = 0;
no_touch_samples = 0;
event.type = INPUT_TOUCH_UP;
event.valid = true;
}
}
return event;
}
no_touch_samples = 0;
// Populate event structure // Debounce touch-down/move updates.
if (*last_time != 0 && (now - *last_time) < config->touch_debounce_ms) {
return event;
}
// Populate event from first touch point.
event.x = touch_data.points[0].x; event.x = touch_data.points[0].x;
event.y = touch_data.points[0].y; event.y = touch_data.points[0].y;
event.pressure = touch_data.points[0].pressure; event.pressure = touch_data.points[0].pressure;
event.gesture_code = touch_data.gesture; event.gesture_code = touch_data.gesture;
event.valid = true; event.valid = true;
// Determine event type
if (*last_time == 0) { if (*last_time == 0) {
event.type = INPUT_TOUCH_DOWN; event.type = INPUT_TOUCH_DOWN;
// Check for virtual buttons
InputType virtual_type; InputType virtual_type;
if (check_virtual_buttons(event.x, event.y, virtual_type)) { if (check_virtual_buttons(event.x, event.y, virtual_type)) {
event.type = virtual_type; event.type = virtual_type;
event.button_id = (virtual_type == INPUT_BUTTON_0) ? 0 : 1; event.button_id = (virtual_type == INPUT_BUTTON_0) ? 0 : 1;
if (config->debug_verbose) {
printf("Virtual button %d pressed via touch\n", event.button_id); printf("Virtual button %d pressed via touch\n", event.button_id);
} }
}
} else { } else {
event.type = INPUT_TOUCH_MOVE; event.type = INPUT_TOUCH_MOVE;
} }
// Handle gesture events
if (config->enable_gestures && touch_data.gesture != 0) { if (config->enable_gestures && touch_data.gesture != 0) {
event.type = INPUT_GESTURE; event.type = INPUT_GESTURE;
if (config->debug_verbose) { if (config->debug_verbose) {
+8 -3
View File
@@ -37,9 +37,9 @@ public:
InputManager(LowLevelTouch* touch, const GameConfig* config); InputManager(LowLevelTouch* touch, const GameConfig* config);
/** /**
* @brief Process touch input from controller * @brief Process touch input using hybrid IRQ + active-session sampling.
* @param last_time Pointer to last touch timestamp for debouncing * @param last_time Pointer to last touch timestamp (0 means no active touch session)
* @return InputEvent (valid=false if no valid input) * @return InputEvent (valid=false if no state change/update is emitted)
*/ */
InputEvent process_touch_input(uint32_t* last_time); InputEvent process_touch_input(uint32_t* last_time);
@@ -97,6 +97,11 @@ public:
private: private:
LowLevelTouch* touch; LowLevelTouch* touch;
const GameConfig* config; const GameConfig* config;
// Last time we sampled touch over I2C while a touch session is active.
uint32_t last_touch_sample_ms = 0;
// Consecutive touch_count==0 samples while a touch session is active.
// Used to suppress false TOUCH_UP events from transient touch controller jitter.
uint8_t no_touch_samples = 0;
// Virtual button regions // Virtual button regions
int v_button_a[4] = {0, 0, 0, 0}; // [x, y, w, h] int v_button_a[4] = {0, 0, 0, 0}; // [x, y, w, h]
+45
View File
@@ -0,0 +1,45 @@
#ifndef SCENE_STACK_H
#define SCENE_STACK_H
#include <vector>
enum class SceneId {
LAUNCHER = 0,
GAME,
IN_GAME_MENU
};
class SceneStack {
public:
SceneStack() {
stack.push_back(SceneId::LAUNCHER);
}
SceneId current() const {
return stack.empty() ? SceneId::LAUNCHER : stack.back();
}
bool is(SceneId scene) const {
return current() == scene;
}
void push(SceneId scene) {
stack.push_back(scene);
}
void pop() {
if (stack.size() > 1) {
stack.pop_back();
}
}
void clear_to_launcher() {
stack.clear();
stack.push_back(SceneId::LAUNCHER);
}
private:
std::vector<SceneId> stack;
};
#endif // SCENE_STACK_H
+40 -4
View File
@@ -5,6 +5,7 @@
#include "sd_card.h" #include "sd_card.h"
#include "hardware/gpio.h" #include "hardware/gpio.h"
#include "board_config.h" #include "board_config.h"
#include "shared_spi_bus.h"
#include "ff.h" // FatFS #include "ff.h" // FatFS
#include <string.h> #include <string.h>
#include <stdio.h> #include <stdio.h>
@@ -323,7 +324,9 @@ bool sd_card_read_blocks(uint32_t block_addr, uint32_t num_blocks, uint8_t *buff
} }
bool sd_card_write_block(uint32_t block_addr, const uint8_t *buffer) { bool sd_card_write_block(uint32_t block_addr, const uint8_t *buffer) {
if (!g_card_info.initialized || buffer == NULL) return false; if (!g_card_info.initialized || buffer == NULL) {
return false;
}
// For non-SDHC cards, convert block address to byte address // For non-SDHC cards, convert block address to byte address
if (g_card_info.type != SD_CARD_TYPE_SDHC) { if (g_card_info.type != SD_CARD_TYPE_SDHC) {
@@ -334,11 +337,24 @@ bool sd_card_write_block(uint32_t block_addr, const uint8_t *buffer) {
// Send write command // Send write command
uint8_t r1 = sd_card_send_command(SD_CMD24, block_addr); uint8_t r1 = sd_card_send_command(SD_CMD24, block_addr);
if (r1 != SD_R1_READY) { if (r1 != SD_R1_READY) {
sd_card_deselect(); sd_card_deselect();
return false; return false;
} }
// Wait for card to be ready to receive data
uint32_t timeout_count = 0;
uint8_t ready_byte;
do {
ready_byte = sd_card_transfer(0xFF);
timeout_count++;
if (timeout_count > 1000) {
sd_card_deselect();
return false;
}
} while (ready_byte != 0xFF);
// Send start token // Send start token
sd_card_transfer(SD_START_TOKEN); sd_card_transfer(SD_START_TOKEN);
@@ -351,8 +367,15 @@ bool sd_card_write_block(uint32_t block_addr, const uint8_t *buffer) {
sd_card_transfer(0xFF); sd_card_transfer(0xFF);
sd_card_transfer(0xFF); sd_card_transfer(0xFF);
// Check data response // Check data response - may need to read several bytes before getting response
uint8_t response = sd_card_transfer(0xFF); uint8_t response = 0xFF;
for (int i = 0; i < 10; i++) {
response = sd_card_transfer(0xFF);
if (response != 0xFF) {
break;
}
}
if ((response & 0x1F) != SD_DATA_ACCEPTED) { if ((response & 0x1F) != SD_DATA_ACCEPTED) {
sd_card_deselect(); sd_card_deselect();
return false; return false;
@@ -409,6 +432,9 @@ bool sd_card_init_with_board_config(void) {
uint sd_card_set_spi_speed(void) { uint sd_card_set_spi_speed(void) {
if (!g_config) return 0; if (!g_config) return 0;
// SD file operations run with exclusive ownership of shared SPI bus.
shared_spi_bus_lock();
// Save current speed and set to SD card speed // Save current speed and set to SD card speed
uint current_speed = spi_get_baudrate(g_config->spi); uint current_speed = spi_get_baudrate(g_config->spi);
spi_set_baudrate(g_config->spi, 12500 * 1000); // 12.5 MHz for SD card spi_set_baudrate(g_config->spi, 12500 * 1000); // 12.5 MHz for SD card
@@ -416,8 +442,18 @@ uint sd_card_set_spi_speed(void) {
} }
void sd_card_restore_spi_speed(uint baudrate) { void sd_card_restore_spi_speed(uint baudrate) {
if (!g_config || baudrate == 0) return; if (!g_config) {
shared_spi_bus_unlock();
return;
}
if (baudrate != 0) {
spi_set_baudrate(g_config->spi, baudrate); spi_set_baudrate(g_config->spi, baudrate);
}
// Leave SD deselected before releasing bus.
gpio_put(g_config->gpio_cs, 1);
shared_spi_bus_unlock();
} }
bool sd_card_test_fatfs(void) { bool sd_card_test_fatfs(void) {
+333
View File
@@ -0,0 +1,333 @@
#include "serial_uploader.h"
#include "game_launcher.h"
#include "lua_game_loader.h"
#include "sd_card.h"
#include "pico/stdlib.h"
#include <cstring>
#include <cstdio>
#include <cctype>
// Maximum file size: 64KB (should be plenty for Lua games)
#define MAX_FILE_SIZE (64 * 1024)
SerialUploader::SerialUploader(GameLauncher* launcher)
: state(IDLE)
, game_launcher(launcher)
, file_size(0)
, bytes_received(0)
, file_buffer(nullptr)
, base64_index(0)
{
filename[0] = '\0';
last_uploaded_name[0] = '\0';
}
void SerialUploader::reset() {
state = IDLE;
file_size = 0;
bytes_received = 0;
base64_index = 0;
filename[0] = '\0';
if (file_buffer) {
delete[] file_buffer;
file_buffer = nullptr;
}
}
uint8_t SerialUploader::decode_base64_char(char c) {
if (c >= 'A' && c <= 'Z') return c - 'A';
if (c >= 'a' && c <= 'z') return c - 'a' + 26;
if (c >= '0' && c <= '9') return c - '0' + 52;
if (c == '+') return 62;
if (c == '/') return 63;
return 0;
}
void SerialUploader::decode_base64_block(const char* input, uint8_t* output) {
uint32_t combined = (decode_base64_char(input[0]) << 18) |
(decode_base64_char(input[1]) << 12) |
(decode_base64_char(input[2]) << 6) |
decode_base64_char(input[3]);
output[0] = (combined >> 16) & 0xFF;
output[1] = (combined >> 8) & 0xFF;
output[2] = combined & 0xFF;
}
bool SerialUploader::write_file_to_sd() {
if (bytes_received == 0) {
printf("ERROR No data received!\n");
return false;
}
if (!file_buffer) {
printf("ERROR File buffer is NULL!\n");
return false;
}
// Set SPI speed to SD card speed (12.5 MHz) and save previous speed
uint prev_speed = sd_card_set_spi_speed();
// Ensure /games directory exists
FRESULT fr = f_mkdir("/games");
if (fr != FR_OK && fr != FR_EXIST) {
printf("ERROR Failed to create /games directory: %d\n", fr);
// Try to continue anyway in case it already exists
}
// Build file path - overwrite if file exists
char filepath[128];
snprintf(filepath, sizeof(filepath), "/games/%s", filename);
FIL fil;
fr = f_open(&fil, filepath, FA_CREATE_ALWAYS | FA_WRITE);
if (fr != FR_OK) {
printf("ERROR Failed to open file for writing: %d\n", fr);
sd_card_restore_spi_speed(prev_speed);
return false;
}
// Write data in chunks (more reliable for large files)
const uint32_t CHUNK_SIZE = 512;
uint32_t total_written = 0;
while (total_written < bytes_received) {
uint32_t chunk = (bytes_received - total_written > CHUNK_SIZE) ? CHUNK_SIZE : (bytes_received - total_written);
UINT bytes_written = 0;
fr = f_write(&fil, file_buffer + total_written, chunk, &bytes_written);
if (fr != FR_OK) {
printf("ERROR f_write failed at byte %u: error %d\n", total_written, fr);
f_close(&fil);
sd_card_restore_spi_speed(prev_speed);
return false;
}
if (bytes_written != chunk) {
printf("ERROR Partial write: wrote %u/%u bytes at offset %u\n", bytes_written, chunk, total_written);
f_close(&fil);
sd_card_restore_spi_speed(prev_speed);
return false;
}
total_written += bytes_written;
}
// Sync to ensure data is written to SD card
fr = f_sync(&fil);
if (fr != FR_OK) {
printf("ERROR f_sync failed: %d\n", fr);
}
f_close(&fil);
printf("✓ Wrote %u bytes to %s\n", total_written, filepath);
// Restore display SPI speed
sd_card_restore_spi_speed(prev_speed);
return true;
}
void SerialUploader::launch_game() {
// Extract base game name (remove .lua extension)
strncpy(last_uploaded_name, filename, sizeof(last_uploaded_name) - 1);
last_uploaded_name[sizeof(last_uploaded_name) - 1] = '\0';
// Remove .lua extension
char* ext = strstr(last_uploaded_name, ".lua");
if (ext) {
*ext = '\0';
}
printf("Re-scanning Lua games to pick up new file...\n");
// Clear existing games before re-scanning (prevents duplicates and memory leaks)
game_launcher->clear_games();
LuaGameLoader::clear_factory_data();
// Re-scan Lua games to pick up the newly uploaded file
// Note: LuaGameLoader::register_all_games handles SPI speed internally
int new_games = LuaGameLoader::register_all_games(game_launcher);
printf("Found %d Lua games after re-scan\n", new_games);
}
bool SerialUploader::complete_launch() {
// This should only be called when it's safe (no display refresh in progress)
// Now try to launch the newly uploaded game by name
printf("Attempting to launch game: %s\n", last_uploaded_name);
bool launched = game_launcher->select_game_by_name(last_uploaded_name);
if (launched) {
printf("LAUNCHED %s\n", last_uploaded_name);
} else {
printf("ERROR Failed to launch game: %s\n", last_uploaded_name);
}
// Reset state back to IDLE
reset();
return launched;
}
bool SerialUploader::process(bool spi_busy) {
if (state == IDLE) {
// Check for "UPLOAD" command
int c = getchar_timeout_us(0);
if (c == PICO_ERROR_TIMEOUT) return false;
if (c == 'U') {
// Check for "UPLOAD "
static char cmd_buffer[8];
cmd_buffer[0] = c;
int idx = 1;
// Read "PLOAD "
for (int i = 0; i < 6; i++) {
c = getchar_timeout_us(100000); // 100ms timeout
if (c == PICO_ERROR_TIMEOUT) return false;
cmd_buffer[idx++] = c;
}
cmd_buffer[idx] = '\0';
if (strcmp(cmd_buffer, "UPLOAD ") == 0) {
state = RECEIVING_FILENAME;
filename[0] = '\0';
return false;
}
}
}
if (state == RECEIVING_FILENAME) {
// Read filename until space
int idx = strlen(filename);
while (idx < sizeof(filename) - 1) {
int c = getchar_timeout_us(100000);
if (c == PICO_ERROR_TIMEOUT) break;
if (c == ' ') {
filename[idx] = '\0';
state = RECEIVING_SIZE;
file_size = 0;
return false;
}
filename[idx++] = (char)c;
}
}
if (state == RECEIVING_SIZE) {
// Read file size until newline
char size_buffer[16];
int idx = 0;
while (idx < sizeof(size_buffer) - 1) {
int c = getchar_timeout_us(100000);
if (c == PICO_ERROR_TIMEOUT) break;
if (c == '\n' || c == '\r') {
size_buffer[idx] = '\0';
file_size = atoi(size_buffer);
if (file_size == 0 || file_size > MAX_FILE_SIZE) {
printf("ERROR Invalid file size: %u\n", file_size);
reset();
return false;
}
// Allocate buffer for decoded data
// file_size is the ORIGINAL (decoded) size, so allocate exactly that
file_buffer = new uint8_t[file_size];
if (!file_buffer) {
printf("ERROR Failed to allocate %u bytes for file buffer\n", file_size);
reset();
return false;
}
bytes_received = 0;
base64_index = 0;
state = RECEIVING_DATA;
printf("Receiving %u bytes for %s...\n", file_size, filename);
return false;
}
size_buffer[idx++] = (char)c;
}
}
if (state == RECEIVING_DATA) {
// Read base64 data until "END"
while (true) {
int c = getchar_timeout_us(1000);
if (c == PICO_ERROR_TIMEOUT) break;
// Check for "END" marker
static char end_check[4] = {0, 0, 0, 0};
end_check[0] = end_check[1];
end_check[1] = end_check[2];
end_check[2] = end_check[3];
end_check[3] = (char)c;
if (end_check[0] == '\n' && end_check[1] == 'E' && end_check[2] == 'N' && end_check[3] == 'D') {
state = WRITING_FILE;
return false;
}
// Skip whitespace
if (isspace(c)) continue;
// Accumulate base64 characters
if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
(c >= '0' && c <= '9') || c == '+' || c == '/' || c == '=') {
base64_buffer[base64_index++] = (char)c;
// Decode every 4 characters
if (base64_index == 4) {
uint8_t decoded[3];
decode_base64_block(base64_buffer, decoded);
// Handle padding
int decoded_count = 3;
if (base64_buffer[2] == '=') decoded_count = 1;
else if (base64_buffer[3] == '=') decoded_count = 2;
// Copy decoded bytes
for (int i = 0; i < decoded_count; i++) {
if (bytes_received < file_size) {
file_buffer[bytes_received++] = decoded[i];
}
}
base64_index = 0;
}
}
}
}
if (state == WRITING_FILE) {
// Wait if SPI bus is busy (e.g. display refresh in progress on other core)
if (spi_busy) return false;
if (write_file_to_sd()) {
state = LAUNCHING_GAME;
// Prepare for launch by scanning games, but don't actually launch yet
launch_game();
} else {
reset();
}
return false;
}
if (state == LAUNCHING_GAME) {
// Stay in this state until main loop calls complete_launch() when safe
return false;
}
return false;
}
+60
View File
@@ -0,0 +1,60 @@
#ifndef SERIAL_UPLOADER_H
#define SERIAL_UPLOADER_H
#include <cstdint>
#include "ff.h"
class GameLauncher;
class SerialUploader {
public:
SerialUploader(GameLauncher* launcher);
// Process incoming serial data (call this frequently in main loop)
// Returns true if a game was launched
// spi_busy: set to true if SPI bus is currently in use by another core (e.g. display refresh)
bool process(bool spi_busy = false);
// Check if uploader wants to launch a game (after upload complete)
bool wants_to_launch_game() const { return state == LAUNCHING_GAME; }
// Complete the game launch (call only when safe - no display refresh in progress)
// Returns true if launch succeeded
bool complete_launch();
// Get the filename of the last uploaded game (without .lua extension)
const char* get_last_uploaded_filename() const { return last_uploaded_name; }
private:
enum State {
IDLE,
RECEIVING_FILENAME,
RECEIVING_SIZE,
RECEIVING_DATA,
WRITING_FILE,
LAUNCHING_GAME
};
State state;
GameLauncher* game_launcher;
// Upload state
char filename[64];
char last_uploaded_name[64]; // Game name without .lua extension
uint32_t file_size;
uint32_t bytes_received;
uint8_t* file_buffer;
// Base64 decoding buffer
char base64_buffer[4];
int base64_index;
// Helper methods
void reset();
bool write_file_to_sd();
void launch_game();
uint8_t decode_base64_char(char c);
void decode_base64_block(const char* input, uint8_t* output);
};
#endif // SERIAL_UPLOADER_H
+65
View File
@@ -0,0 +1,65 @@
#include "shared_spi_bus.h"
#include "pico/mutex.h"
#include "pico/multicore.h"
#include <stdbool.h>
static mutex_t g_spi_bus_mutex;
static bool g_spi_bus_initialized = false;
static uint32_t g_core_depth[2] = {0, 0};
void shared_spi_bus_init(void) {
if (g_spi_bus_initialized) {
return;
}
mutex_init(&g_spi_bus_mutex);
g_spi_bus_initialized = true;
}
void shared_spi_bus_lock(void) {
if (!g_spi_bus_initialized) {
shared_spi_bus_init();
}
int core = get_core_num();
if (core < 0 || core > 1) {
core = 0;
}
// Re-entrant lock on same core (needed for nested SD helpers).
if (g_core_depth[core] > 0) {
g_core_depth[core]++;
return;
}
mutex_enter_blocking(&g_spi_bus_mutex);
g_core_depth[core] = 1;
}
void shared_spi_bus_unlock(void) {
if (!g_spi_bus_initialized) {
return;
}
int core = get_core_num();
if (core < 0 || core > 1) {
core = 0;
}
if (g_core_depth[core] == 0) {
return;
}
g_core_depth[core]--;
if (g_core_depth[core] == 0) {
mutex_exit(&g_spi_bus_mutex);
}
}
void shared_spi_bus_force_recover(void) {
// Used after Core1 reset to avoid stale mutex/depth state.
mutex_init(&g_spi_bus_mutex);
g_core_depth[0] = 0;
g_core_depth[1] = 0;
g_spi_bus_initialized = true;
}
+18
View File
@@ -0,0 +1,18 @@
#ifndef SHARED_SPI_BUS_H
#define SHARED_SPI_BUS_H
#ifdef __cplusplus
extern "C" {
#endif
// Cross-core SPI bus lock for shared SD/display SPI usage.
void shared_spi_bus_init(void);
void shared_spi_bus_lock(void);
void shared_spi_bus_unlock(void);
void shared_spi_bus_force_recover(void);
#ifdef __cplusplus
}
#endif
#endif // SHARED_SPI_BUS_H
Executable
+149
View File
@@ -0,0 +1,149 @@
#!/usr/bin/env python3
"""
Lua Game Uploader for RP2350
Rapidly upload and execute Lua games via USB serial for quick iteration.
Usage:
python upload_game.py <lua_file> [serial_port]
Example:
python upload_game.py my_game.lua /dev/ttyACM0
python upload_game.py my_game.lua COM3
"""
import sys
import os
import base64
import serial
import time
from pathlib import Path
def find_serial_port():
"""Auto-detect the RP2350 serial port"""
import serial.tools.list_ports
# Look for Pico/RP2350 devices
ports = list(serial.tools.list_ports.comports())
for port in ports:
# Common VID/PID for Raspberry Pi Pico
if 'Pico' in port.description or \
'RP2350' in port.description or \
'RP2040' in port.description or \
(port.vid == 0x2E8A): # Raspberry Pi VID
return port.device
# Fallback: return first available port
if ports:
print(f"Warning: Could not find RP2350, using first available port: {ports[0].device}")
return ports[0].device
return None
def upload_game(lua_file, serial_port=None):
"""Upload a Lua game file to the RP2350 and execute it"""
# Check if file exists
if not os.path.exists(lua_file):
print(f"Error: File '{lua_file}' not found")
return False
# Read the file
with open(lua_file, 'rb') as f:
file_data = f.read()
file_size = len(file_data)
filename = os.path.basename(lua_file)
# Ensure filename has .lua extension
if not filename.endswith('.lua'):
filename += '.lua'
print(f"Uploading {filename} ({file_size} bytes)...")
# Auto-detect serial port if not provided
if serial_port is None:
serial_port = find_serial_port()
if serial_port is None:
print("Error: No serial port found. Please specify manually.")
return False
print(f"Using serial port: {serial_port}")
try:
# Open serial connection
ser = serial.Serial(serial_port, 115200, timeout=5)
time.sleep(0.1) # Wait for connection to stabilize
# Encode file data as base64
base64_data = base64.b64encode(file_data).decode('ascii')
# Send upload command
command = f"UPLOAD {filename} {file_size}\n"
print(f"Sending command: {command.strip()}")
ser.write(command.encode('ascii'))
# Send base64-encoded file data
print("Sending file data...")
ser.write(base64_data.encode('ascii'))
ser.write(b'\n')
# Send END marker
ser.write(b'END\n')
print("Upload complete, waiting for confirmation...")
# Read response from device
start_time = time.time()
response_lines = []
while time.time() - start_time < 10: # 10 second timeout
if ser.in_waiting > 0:
line = ser.readline().decode('utf-8', errors='ignore').strip()
if line:
print(f" << {line}")
response_lines.append(line)
# Check for success
if line.startswith('OK'):
print(f"✓ File written successfully!")
if line.startswith('LAUNCHED'):
game_name = line.split(' ', 1)[1] if ' ' in line else filename
print(f"✓ Game '{game_name}' launched!")
ser.close()
return True
# Check for error
if line.startswith('ERROR'):
print(f"✗ Upload failed: {line}")
ser.close()
return False
print("Warning: No response received from device")
ser.close()
return False
except serial.SerialException as e:
print(f"Serial error: {e}")
return False
except Exception as e:
print(f"Unexpected error: {e}")
return False
def main():
if len(sys.argv) < 2:
print(__doc__)
print("\nAvailable serial ports:")
try:
import serial.tools.list_ports
for port in serial.tools.list_ports.comports():
print(f" {port.device}: {port.description}")
except ImportError:
print(" (install pyserial to see available ports)")
sys.exit(1)
lua_file = sys.argv[1]
serial_port = sys.argv[2] if len(sys.argv) > 2 else None
success = upload_game(lua_file, serial_port)
sys.exit(0 if success else 1)
if __name__ == '__main__':
main()