Compare commits
17 Commits
b26f3bf775
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| fe6e403a98 | |||
| 73a78069e3 | |||
| 3e54466752 | |||
| ebc58d7e4d | |||
| a06e0d69fe | |||
| be6a217b08 | |||
| b01cd652a0 | |||
| b0ca1f1a55 | |||
| 6d29e99394 | |||
| b356897387 | |||
| 06f5976865 | |||
| e406a06f61 | |||
| 034867d2a7 | |||
| 76e3d2435e | |||
| 518bc054c4 | |||
| f8fb04db1b | |||
| 84b009c33e |
@@ -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
@@ -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*
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
@@ -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
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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]
|
||||||
|
|||||||
@@ -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
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
@@ -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()
|
||||||
Reference in New Issue
Block a user