Compare commits

..

11 Commits

Author SHA1 Message Date
Adolfo Reyna
fe6e403a98 Reduce touch overhead and cap frame rate at 24 FPS 2026-02-18 15:53:48 -05:00
Adolfo Reyna
73a78069e3 Stop tracking screenlog.0 2026-02-18 15:44:54 -05:00
Adolfo Reyna
3e54466752 Add Core1 refresh recovery and shared SPI arbitration 2026-02-18 15:43:35 -05:00
Adolfo Reyna
ebc58d7e4d Add scene stack menu flow and restore stable TFT sync refresh 2026-02-18 15:10:20 -05:00
Adolfo Reyna
a06e0d69fe Refactor main loop touch flow and update docs 2026-02-18 12:08:23 -05:00
Adolfo Reyna
be6a217b08 Fix touch event stability and WFI wake filtering 2026-02-18 11:51:29 -05:00
Adolfo Reyna
b01cd652a0 Fix build error: Rename BOARD_SIZE to MONOPOLY_BOARD_SIZE to avoid macro collision 2026-02-17 22:11:21 -05:00
Adolfo Reyna
b0ca1f1a55 Fix emulator build script, UI rendering, and clean up repo 2026-02-17 21:49:49 -05:00
Adolfo Reyna
6d29e99394 Remove untracked files 2026-02-17 21:17:50 -05:00
Adolfo Reyna
b356897387 Merge remote-tracking branch 'gitea/fix-refresh-race'
# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.
2026-02-13 16:16:14 -05:00
Adolfo Reyna
e406a06f61 Fix race condition in display refresh locking 2026-02-13 13:40:45 -05:00
33 changed files with 3136 additions and 4807 deletions

12
.gitignore vendored
View File

@@ -9,4 +9,14 @@ build*/
CMakeFiles/
*.cmake
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*

View File

@@ -48,6 +48,7 @@ add_executable(basic1
basic1.cpp
lib/input_manager.cpp
lib/game_launcher.cpp
lib/shared_spi_bus.c
lib/serial_uploader.cpp
games/tic_tac_toe.cpp
games/demo_game.cpp

View File

@@ -21,13 +21,13 @@ The project now uses a **clean, event-driven architecture** perfect for building
-**Event-Driven**: Display only updates when input is received
- 🔋 **Power Efficient**: Uses `__wfi()` to sleep between inputs (< 1mA idle)
- 📄 **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
### 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):**
@@ -45,14 +45,18 @@ while(1) {
**After (Reactive Template):**
```cpp
while(1) {
__wfi(); // Sleep until interrupt
while (1) {
bool stay_awake = pending_refresh || touch_event_down || (last_touch_time != 0);
if (!stay_awake) {
__wfi(); // Sleep until interrupt
if (!has_pending_wake_source()) continue; // Ignore unrelated wake-ups
}
InputEvent input = process_button_input(config);
if (!input.valid) {
input = process_touch_input(config, &last_touch_time);
}
if (input.valid && game_update(&game_state, input, config, &renderer)) {
game_draw(&game_state, &renderer, &gui);
refresh_screen(bit_buffer, display);
@@ -534,7 +538,8 @@ The typical development workflow:
**Interrupt not triggering:**
- Verify `TOUCH_INT_PIN` is correctly defined
- 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
@@ -579,7 +584,7 @@ The code includes `printf()` statements for debugging touch, display, and SD car
For battery-powered projects:
- 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
- Lower SPI/I2C baud rates to reduce power consumption
@@ -607,7 +612,8 @@ For battery-powered projects:
### Touch Features
- Multi-touch support (up to 2 points)
- Coordinate transformation
- Touch debouncing
- Hybrid IRQ + active-session sampling
- Touch debouncing with release hysteresis
- Event types (press, lift, contact)
### SD Card Features

View File

@@ -14,7 +14,7 @@
* - Event-driven: Display only updates when input is received
* - Power efficient: Uses __wfi() to sleep between inputs
* - 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
*
* ARCHITECTURE:
@@ -36,7 +36,6 @@
#include "pico/stdlib.h"
#include "pico/binary_info.h"
#include "hardware/sync.h"
#include "pico/multicore.h"
#include "board_config.h" // Board-specific pin configuration
#include "sd_card.h"
@@ -59,6 +58,8 @@ extern "C" {
#include "monopoly_game.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
@@ -75,6 +76,28 @@ volatile bool refresh_requested = false;
volatile bool refresh_in_progress = false;
const uint8_t* volatile refresh_buffer = 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
@@ -86,21 +109,31 @@ void core1_entry() {
printf("Core 1 started - handling display refreshes\n");
while (1) {
core1_heartbeat++;
// Wait for refresh request
if (refresh_requested && refresh_buffer && refresh_display) {
refresh_in_progress = true;
// Get local copies for safe access
LowLevelDisplay* display = refresh_display;
const uint8_t* buffer = refresh_buffer;
// Perform the refresh operation (may be slow for e-ink)
display->draw_buffer(buffer);
display->refresh();
// Clear flags
if (refresh_requested) {
if (refresh_buffer && refresh_display) {
// refresh_in_progress is already set by Core 0 to lock the buffer
// Get local copies for safe access
LowLevelDisplay* display = refresh_display;
const uint8_t* buffer = refresh_buffer;
// Perform refresh with shared SPI bus lock to avoid SD/display collisions.
shared_spi_bus_lock();
display->draw_buffer(buffer);
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 in all cases to avoid deadlock on Core 0.
refresh_requested = false;
refresh_in_progress = false;
refresh_in_progress = false; // Unlock buffer for Core 0
}
// Small delay to avoid busy-waiting
@@ -124,6 +157,10 @@ bool refresh_screen_async(const uint8_t *buffer, LowLevelDisplay* display) {
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
refresh_buffer = buffer;
refresh_display = display;
@@ -158,16 +195,17 @@ struct GameConfig {
// ============================================================================
// Display dimming settings
#define DIM_TIMEOUT_MS (2 * 60 * 1000) // 2 minutes to dim
#define SLEEP_TIMEOUT_MS (10 * 60 * 1000) // 10 minutes to sleep
#define DIM_CHECK_INTERVAL_MS 10000 // Check every 10 seconds
#define DEFAULT_DIM_TIMEOUT_MS (2 * 60 * 1000) // 2 minutes to dim
#define DEFAULT_SLEEP_TIMEOUT_MS (10 * 60 * 1000) // 10 minutes to sleep
#define DIM_CHECK_INTERVAL_MS 10000 // Check every 10 seconds
// Display dimming state
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_10min_triggered = false; // Flag for 10min trigger
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
@@ -220,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 elapsed = current_time - last_interaction_time;
// Check for 10 minute timeout (Sleep)
if (!is_idle_10min_triggered && elapsed >= SLEEP_TIMEOUT_MS) {
// Check sleep timeout (if enabled)
if (sleep_timeout_ms > 0 && !is_idle_10min_triggered && elapsed >= sleep_timeout_ms) {
display->on_idle_10min();
is_idle_10min_triggered = true;
is_idle_2min_triggered = true; // Implicitly triggered
}
// Check for 2 minute timeout (Dim)
else if (!is_idle_2min_triggered && elapsed >= DIM_TIMEOUT_MS) {
// Check dim timeout (if enabled)
else if (dim_timeout_ms > 0 && !is_idle_2min_triggered && elapsed >= dim_timeout_ms) {
display->on_idle_2min();
is_idle_2min_triggered = true;
}
@@ -248,6 +286,121 @@ volatile bool button_key0_pressed = false;
volatile bool button_key1_pressed = false;
#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
*
@@ -261,18 +414,17 @@ volatile bool button_key1_pressed = false;
* @param events Event mask (GPIO_IRQ_EDGE_FALL and/or GPIO_IRQ_EDGE_RISE)
*/
void touch_interrupt_handler(uint gpio, uint32_t events) {
// Set flag to indicate touch event occurred
// Main loop will handle the actual touch reading
touch_interrupt_flag = true;
// Track which edge triggered (down vs up)
if (events & GPIO_IRQ_EDGE_FALL) {
(void)gpio;
// Track which edge triggered (down vs up).
// Keep ISR minimal: do not log/print from interrupt context.
if (events & GPIO_IRQ_EDGE_FALL) {
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;
printf("INT: RISE\n");
touch_interrupt_flag = true;
}
}
@@ -307,9 +459,6 @@ void button_interrupt_handler(uint gpio, uint32_t events) {
const int V_WIDTH = DISPLAY_WIDTH;
const int V_HEIGHT = DISPLAY_HEIGHT;
// Touch indicator settings
#define TOUCH_RADIUS 10
uint8_t bit_buffer[V_WIDTH * V_HEIGHT / 8];
/**
@@ -350,6 +499,9 @@ int main()
printf("\n=== %s Demo ===\n", BOARD_NAME);
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
// The factory handles all board-specific configuration internally
@@ -369,11 +521,11 @@ int main()
return -1;
}
// Enable dirty rectangle optimization for ST7796 displays
// Enable dirty/partial refresh optimization for ST7796.
if (display->get_type() == DISPLAY_TYPE_ST7796) {
LowLevelDisplayST7796* st7796_display = static_cast<LowLevelDisplayST7796*>(display);
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
@@ -409,7 +561,7 @@ int main()
if (touch) {
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");
touch->set_interrupt_callback(touch_interrupt_handler);
printf("Touch interrupt enabled on INT pin (falling and rising edges)\n");
@@ -491,7 +643,7 @@ int main()
// Draw launcher menu
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);
printf("Initial screen refresh queued on Core 1\n");
@@ -531,10 +683,11 @@ int main()
// Core 0 (this loop): Handles input and game logic - stays responsive
// 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)
// 2. Update game state based on input
// 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
// ========================================================================
@@ -543,36 +696,84 @@ int main()
// Initialize last interaction time to current time
last_interaction_time = to_ms_since_boot(get_absolute_time());
global_display = display;
// Set up repeating alarm to periodically check dimming status
// 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);
int sleep_option_index = 5; // Default: 10 min
apply_sleep_option(sleep_option_index);
if (display->get_type() == DISPLAY_TYPE_ST7796) {
printf("Power saving: Dim at %d min, Sleep at %d min\n",
DIM_TIMEOUT_MS / 60000, SLEEP_TIMEOUT_MS / 60000);
if (sleep_timeout_ms == 0) {
printf("Power saving: auto sleep disabled\n");
} else {
printf("Power saving: Dim at %lus, Sleep at %lus\n",
(unsigned long)(dim_timeout_ms / 1000),
(unsigned long)(sleep_timeout_ms / 1000));
}
} else {
printf("Power saving: Sleep at %d min\n", SLEEP_TIMEOUT_MS / 60000);
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("\nEntering reactive game loop (Core 0 - input & logic)\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;
uint32_t game_start_time = 0;
// Frame rate limiting (30 FPS = 33.33ms per frame)
const uint32_t TARGET_FRAME_TIME_MS = 33; // 1000ms / 30fps ≈ 33ms
// Frame rate limiting (24 FPS = 41.67ms per frame)
const uint32_t TARGET_FRAME_TIME_MS = 42; // 1000ms / 24fps ≈ 41.7ms
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) {
// 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();
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()) {
@@ -582,25 +783,31 @@ int main()
// 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
bool stay_awake = false;
if (needs_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()) {
Game* g = launcher.get_selected_game();
if (g && g->wants_frame_updates()) {
stay_awake = true;
}
}
if (scene_stack.is(SceneId::GAME) && game_wants_frame_updates(launcher)) stay_awake = true;
if (!stay_awake) {
// Sleep until interrupt wakes us up (very power efficient!)
__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};
@@ -630,68 +837,151 @@ int main()
input.button_id, input.pressure);
}
if (launcher.is_game_selected()) {
// In game mode - process game input
current_game = launcher.get_selected_game();
needs_refresh = current_game->update(input);
// Check if game wants to exit
if (current_game->wants_to_exit()) {
printf("Game requested exit - returning to launcher\n");
launcher.reset();
needs_refresh = true;
// Force full clear for clean transition
display->clear(false);
if (display->get_type() == DISPLAY_TYPE_EPAPER) {
LowLevelDisplayEPaper* epaper = static_cast<LowLevelDisplayEPaper*>(display);
epaper->full_refresh();
}
SceneId scene = scene_stack.current();
if (scene == SceneId::LAUNCHER) {
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);
}
// 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) {
// Record start time on first touch
if (game_start_time == 0) {
game_start_time = to_ms_since_boot(get_absolute_time());
}
} else if (input.type == INPUT_TOUCH_UP) {
uint32_t now = to_ms_since_boot(get_absolute_time());
if (game_start_time > 0 && (now - game_start_time) > 10000) {
// Long press detected - return to menu
printf("Long press detected - returning to launcher\n");
launcher.reset();
needs_refresh = true;
// Force full clear for clean transition
display->clear(false);
if (display->get_type() == DISPLAY_TYPE_EPAPER) {
LowLevelDisplayEPaper* epaper = static_cast<LowLevelDisplayEPaper*>(display);
epaper->full_refresh();
}
}
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();
// }
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;
if (current_game->wants_to_exit()) {
printf("Game requested exit - returning to launcher\n");
swipe_candidate_active = false;
launcher.reset();
scene_stack.clear_to_launcher();
needs_refresh = true;
if (display->get_type() == DISPLAY_TYPE_EPAPER) {
LowLevelDisplayEPaper* epaper = static_cast<LowLevelDisplayEPaper*>(display);
epaper->full_refresh();
}
}
if (input.type == INPUT_TOUCH_DOWN) {
if (game_start_time == 0) {
game_start_time = to_ms_since_boot(get_absolute_time());
}
} else if (input.type == INPUT_TOUCH_UP) {
uint32_t now = to_ms_since_boot(get_absolute_time());
if (game_start_time > 0 && (now - game_start_time) > 10000) {
printf("Long press detected - returning to launcher\n");
swipe_candidate_active = false;
launcher.reset();
scene_stack.clear_to_launcher();
needs_refresh = true;
if (display->get_type() == DISPLAY_TYPE_EPAPER) {
LowLevelDisplayEPaper* epaper = static_cast<LowLevelDisplayEPaper*>(display);
epaper->full_refresh();
}
}
game_start_time = 0;
}
}
}
} 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;
}
}
}
}
}
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
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
if (!is_refresh_in_progress()) {
InputEvent frame_tick = {INPUT_FRAME_TICK, 0, 0, 0, 0, 0, true};
@@ -700,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) {
// Check frame rate limiting
uint32_t current_time = to_ms_since_boot(get_absolute_time());
@@ -710,20 +1000,58 @@ int main()
if (time_since_last_frame >= TARGET_FRAME_TIME_MS) {
// Only draw if Core 1 is finished with the buffer
if (!is_refresh_in_progress()) {
// Clear buffer and redraw entire UI with updated state
memset(bit_buffer, 0, V_WIDTH * V_HEIGHT / 8);
// Keep dirty rectangle optimization enabled for TFT.
if (display->get_type() == DISPLAY_TYPE_ST7796) {
bool wants_opt = true;
if (launcher.is_game_selected()) {
current_game = launcher.get_selected_game();
current_game->draw();
} else {
launcher.draw();
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;
}
}
// Request async refresh (non-blocking - handled by Core 1)
bool refresh_started = refresh_screen_async(bit_buffer, display);
// Clear buffer and redraw entire UI with updated state
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 (scene_stack.is(SceneId::LAUNCHER)) {
launcher.draw();
} else if (scene_stack.is(SceneId::GAME)) {
current_game = launcher.get_selected_game();
if (current_game) {
current_game->draw();
} else {
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();
}
}
bool refresh_started = false;
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) {
needs_refresh = false;
pending_refresh = false; // Refresh queued successfully
last_frame_time = current_time; // Update frame time
} else {

View File

@@ -1,361 +1,402 @@
#include "low_level_render.h"
#include "low_level_gui.h"
#include "low_level_render.h"
#include <cstring>
#include <ctime>
LowLevelWindow* validate_or_create_window(LowLevelWindow* window, LowLevelRenderer* renderer) {
if (window == nullptr) {
return new LowLevelWindow(0, 0, renderer->get_width(), renderer->get_height(), "Default Window");
}
return window;
LowLevelWindow *validate_or_create_window(LowLevelWindow *window,
LowLevelRenderer *renderer) {
if (window == nullptr) {
return new LowLevelWindow(0, 0, renderer->get_width(),
renderer->get_height(), "Default 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* w = new LowLevelWindow(x, y, width, height, title);
draw_window(w);
return w;
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);
draw_window(w);
return w;
}
void LowLevelGUI::draw_window(LowLevelWindow* window){
// Draw window border
if (use_rounded_corners)
{
//shadow
renderer->draw_rounded_rectangle(window->x+3, window->y+3, window->width, window->height, 10, true, true);
renderer->draw_rounded_rectangle(window->x-2, window->y-2, window->width+2, window->height+2, 10, false, true);
renderer->draw_rounded_rectangle(window->x, window->y, window->width, window->height, 10, true);
}
else
{
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);
}
void LowLevelGUI::draw_window(LowLevelWindow *window) {
// Draw window border
renderer->draw_line(window->x, window->y + 20, window->x + window->width - 1, window->y + 20, true, 1);
// draw closing 'X' button
int close_size = 12;
int close_x = window->x + window->width - close_size - 4;
int close_y = window->y + 4;
//renderer->draw_rectangle(close_x, close_y, close_size, close_size, 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);
if (use_rounded_corners) {
// shadow
renderer->draw_rounded_rectangle(window->x + 3, window->y + 3,
window->width, window->height, 10, true,
true);
renderer->draw_rounded_rectangle(window->x - 2, window->y - 2,
window->width + 2, window->height + 2, 10,
false, true);
renderer->draw_rounded_rectangle(window->x, window->y, window->width,
window->height, 10, true);
} else {
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);
}
const Font* original_font = renderer->get_current_font();
renderer->set_font(current_font);
renderer->draw_string_scaled(window->x + 10, window->y + 3, window->title, 2);
renderer->draw_line(window->x, window->y + 20, window->x + window->width - 1,
window->y + 20, true, 1);
// draw closing 'X' button
int close_size = 12;
int close_x = window->x + window->width - close_size - 4;
int close_y = window->y + 4;
// renderer->draw_rectangle(close_x, close_y, close_size, close_size, 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);
renderer->set_font(original_font);
const Font *original_font = renderer->get_current_font();
renderer->set_font(current_font);
renderer->draw_string_scaled(window->x + 10, window->y + 3, window->title, 2);
renderer->set_font(original_font);
}
void LowLevelGUI::draw_button(LowLevelWindow* window, int x, int y, const char *label, bool pressed, bool rounded)
{
window = validate_or_create_window(window, renderer);
const Font* original_font = renderer->get_current_font();
bool original_text_color = renderer->get_current_text_color();
renderer->set_font(current_font);
int text_x = window->x + x + 5;
int text_y = window->y + y + 5;
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;
void LowLevelGUI::draw_button(LowLevelWindow *window, int x, int y,
const char *label, bool pressed, bool rounded) {
window = validate_or_create_window(window, renderer);
const Font *original_font = renderer->get_current_font();
bool original_text_color = renderer->get_current_text_color();
renderer->set_font(current_font);
int text_x = window->x + x + 5;
int text_y = window->y + y + 5;
int height = renderer->get_current_font()->get_char_height() * 2 + 10;
int width = renderer->get_string_width_scaled(label, 2) + 30;
if (pressed)
{
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, true, true);
renderer->draw_rounded_rectangle(window->x + x + 2, window->y + y + 2, width - 4, height - 4, rounded ? 5 : 0, false, false);
}
else
{
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->draw_string_scaled(text_x, text_y, label, 2);
renderer->set_font(original_font);
renderer->set_text_color(original_text_color);
if (pressed) {
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, true, true);
renderer->draw_rounded_rectangle(window->x + x + 2, window->y + y + 2,
width - 4, height - 4, rounded ? 5 : 0,
false, false);
} else {
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->draw_string_scaled(text_x, text_y, label, 2);
renderer->set_font(original_font);
renderer->set_text_color(original_text_color);
}
void LowLevelGUI::draw_checkbox(LowLevelWindow* window, int x, int y, const char *label, bool checked) {
window = validate_or_create_window(window, renderer);
const Font* original_font = renderer->get_current_font();
renderer->set_font(current_font);
int box_size = renderer->get_current_font()->get_char_height() * 2 ;
int box_x = window->x + x;
int box_y = window->y + y;
// Draw checkbox square
renderer->draw_rectangle(box_x, box_y, box_size, box_size, true, 1);
if (checked)
{
// 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 + box_size / 2, box_y + box_size - 3, box_x + box_size - 2, box_y + 2, true, 1);
}
// Draw label
renderer->set_text_color(true);
renderer->draw_string_scaled(box_x + box_size + 5, box_y - 1, label, 2);
renderer->set_font(original_font);
void LowLevelGUI::draw_checkbox(LowLevelWindow *window, int x, int y,
const char *label, bool checked) {
window = validate_or_create_window(window, renderer);
const Font *original_font = renderer->get_current_font();
renderer->set_font(current_font);
int box_size = renderer->get_current_font()->get_char_height() * 2;
int box_x = window->x + x;
int box_y = window->y + y;
// Draw checkbox square
renderer->draw_rectangle(box_x, box_y, box_size, box_size, true, 1);
if (checked) {
// 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 + box_size / 2, box_y + box_size - 3,
box_x + box_size - 2, box_y + 2, true, 1);
}
// Draw label
renderer->set_text_color(true);
renderer->draw_string_scaled(box_x + box_size + 5, box_y - 1, label, 2);
renderer->set_font(original_font);
}
void LowLevelGUI::draw_radio_button(LowLevelWindow* window, int x, int y, const char *label, bool selected) {
window = validate_or_create_window(window, renderer);
const Font* original_font = renderer->get_current_font();
renderer->set_font(current_font);
int radius = renderer->get_current_font()->get_char_height();
int center_x = window->x + x + radius;
int center_y = window->y + y + radius;
// Draw outer circle
renderer->draw_circle(center_x, center_y, radius, true);
if (selected)
{
// Draw inner filled circle
renderer->draw_filled_circle(center_x, center_y, radius - 4, true);
}
// Draw label
renderer->set_text_color(true);
renderer->draw_string_scaled(center_x + radius + 5, center_y - radius / 2, label, 2);
renderer->set_font(original_font);
void LowLevelGUI::draw_radio_button(LowLevelWindow *window, int x, int y,
const char *label, bool selected) {
window = validate_or_create_window(window, renderer);
const Font *original_font = renderer->get_current_font();
renderer->set_font(current_font);
int radius = renderer->get_current_font()->get_char_height();
int center_x = window->x + x + radius;
int center_y = window->y + y + radius;
// Draw outer circle
renderer->draw_circle(center_x, center_y, radius, true);
if (selected) {
// Draw inner filled circle
renderer->draw_filled_circle(center_x, center_y, radius - 4, true);
}
// Draw label
renderer->set_text_color(true);
renderer->draw_string_scaled(center_x + radius + 5, center_y - radius / 2,
label, 2);
renderer->set_font(original_font);
}
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);
int slider_x = window->x + x;
int slider_y = window->y + y + (label != nullptr ? 20 : 0);
position = std::max(0, std::min(100, position));
// Draw slider track
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]
int handle_x = slider_x + (position * width / 100);
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);
const Font* original_font = renderer->get_current_font();
renderer->set_font(current_font);
// draw current position value label on top of the slider
char pos_label[10];
snprintf(pos_label, sizeof(pos_label), "%d", position);
renderer->draw_string_scaled(slider_x + width + 10, slider_y + (height / 2) - 5, pos_label, 1);
// Draw label if provided
if (label != nullptr) {
renderer->draw_string_scaled(slider_x, window->y + y, label, 2);
}
renderer->set_font(original_font);
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);
int slider_x = window->x + x;
int slider_y = window->y + y + (label != nullptr ? 20 : 0);
position = std::max(0, std::min(100, position));
// Draw slider track
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]
int handle_x = slider_x + (position * width / 100);
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);
const Font *original_font = renderer->get_current_font();
renderer->set_font(current_font);
// draw current position value label on top of the slider
char pos_label[10];
snprintf(pos_label, sizeof(pos_label), "%d", position);
renderer->draw_string_scaled(slider_x + width + 10,
slider_y + (height / 2) - 5, pos_label, 1);
// Draw label if provided
if (label != nullptr) {
renderer->draw_string_scaled(slider_x, window->y + y, label, 2);
}
renderer->set_font(original_font);
}
void LowLevelGUI::draw_calendar(LowLevelWindow* window, int x, int y, int month, int year) {
window = validate_or_create_window(window, renderer);
const Font* original_font = renderer->get_current_font();
renderer->set_font(current_font);
void LowLevelGUI::draw_calendar(LowLevelWindow *window, int x, int y, int month,
int year) {
window = validate_or_create_window(window, renderer);
const Font *original_font = renderer->get_current_font();
renderer->set_font(current_font);
// 1. Draw Month and Year Header
char title[32];
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);
renderer->draw_string_scaled(window->x + x, window->y + y, title, 1);
// 1. Draw Month and Year Header
char title[32];
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);
renderer->draw_string_scaled(window->x + x, window->y + y, title, 1);
// 2. Draw Days of the Week labels
const char* days[] = {"Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"};
for (int i = 0; i < 7; i++) {
renderer->draw_string_scaled(window->x + x + (i * 20), window->y + y + 15, days[i], 1);
}
// 2. Draw Days of the Week labels
const char *days[] = {"Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"};
for (int i = 0; i < 7; i++) {
renderer->draw_string_scaled(window->x + x + (i * 20), window->y + y + 15,
days[i], 1);
}
// 3. Calculate Month Metadata
// Get starting day of the week (0 = Sunday) using a simplified formula
struct tm first_day = {0};
first_day.tm_mday = 1;
first_day.tm_mon = month - 1;
first_day.tm_year = year - 1900;
mktime(&first_day);
int start_col = first_day.tm_wday;
// 3. Calculate Month Metadata
// Get starting day of the week (0 = Sunday) using a simplified formula
struct tm first_day = {0};
first_day.tm_mday = 1;
first_day.tm_mon = month - 1;
first_day.tm_year = year - 1900;
mktime(&first_day);
int start_col = first_day.tm_wday;
// Get number of days in the month
int days_in_month[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))) {
days_in_month[1] = 29;
}
int total_days = days_in_month[month - 1];
// Get number of days in the month
int days_in_month[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))) {
days_in_month[1] = 29;
}
int total_days = days_in_month[month - 1];
// 4. Draw the Days Grid
for (int day = 1; day <= total_days; day++) {
int index = start_col + day - 1;
int col = index % 7;
int row = index / 7;
// 4. Draw the Days Grid
for (int day = 1; day <= total_days; day++) {
int index = start_col + day - 1;
int col = index % 7;
int row = index / 7;
int cell_x = window->x + x + (col * 20);
int cell_y = window->y + y + 30 + (row * 20);
int cell_x = window->x + x + (col * 20);
int cell_y = window->y + y + 30 + (row * 20);
// Draw cell border/background
renderer->draw_rectangle(cell_x, cell_y, 20, 20, false, 1);
// Draw cell border/background
renderer->draw_rectangle(cell_x, cell_y, 20, 20, false, 1);
// Draw day number
char day_str[3];
snprintf(day_str, sizeof(day_str), "%d", day);
// Center the text slightly within the 20x20 cell
int offset_x = (day < 10) ? 7 : 2;
renderer->draw_string_scaled(cell_x + offset_x, cell_y + 5, day_str, 1);
}
// Draw day number
char day_str[3];
snprintf(day_str, sizeof(day_str), "%d", day);
renderer->set_font(original_font);
// Center the text slightly within the 20x20 cell
int offset_x = (day < 10) ? 7 : 2;
renderer->draw_string_scaled(cell_x + offset_x, cell_y + 5, day_str, 1);
}
renderer->set_font(original_font);
}
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);
// Draw textbox border
int box_x = window->x + x;
int box_y = window->y + y;
if(focused) {
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);
} else {
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);
}
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);
// Draw textbox border
int box_x = window->x + x;
int box_y = window->y + y;
if (focused) {
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);
} else {
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);
}
// Draw content text inside the textbox
const Font* original_font = renderer->get_current_font();
renderer->set_font(current_font);
renderer->set_text_color(true);
// Draw content text inside the textbox
const Font *original_font = renderer->get_current_font();
renderer->set_font(current_font);
renderer->set_text_color(true);
int text_x = box_x + 5;
int text_y = box_y + 5;
renderer->draw_string_scaled(text_x, text_y, content, 1);
int text_x = box_x + 5;
int text_y = box_y + 5;
renderer->draw_string_scaled(text_x, text_y, content, 1);
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) {
window = validate_or_create_window(window, renderer);
int tab_x = window->x + x;
int tab_y = window->y + y;
if (selected) {
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);
} else {
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);
}
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);
int tab_x = window->x + x;
int tab_y = window->y + y;
if (selected) {
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);
} else {
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);
}
const Font* original_font = renderer->get_current_font();
renderer->set_font(current_font);
renderer->set_text_color(true);
const Font *original_font = renderer->get_current_font();
renderer->set_font(current_font);
renderer->set_text_color(true);
// Center the label within the tab
int text_width = int(renderer->draw_string_scaled(0, 0, label, 1) * 0.75);
int text_x = tab_x + (width - text_width) / 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);
// Center the label within the tab
int text_width = renderer->get_string_width_scaled(label, 1);
int text_x = tab_x + (width - text_width) / 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->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) {
window = validate_or_create_window(window, renderer);
int base_x = window->x + x;
int base_y = window->y + y;
const Font* original_font = renderer->get_current_font();
renderer->set_font(current_font);
// Draw Main Label (e.g., "PANELS")
renderer->draw_string_scaled(base_x, base_y, label, 2);
// Draw Sublabel and Value (e.g., "Weekly Average Charge" and "190KWH")
renderer->draw_string_scaled(base_x, base_y + 15, sublabel, 1);
int val_width = strlen(value_text) * 8; // Approximation
renderer->draw_string_scaled(base_x + width - val_width, base_y + 15, value_text, 1);
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);
int base_x = window->x + x;
int base_y = window->y + y;
// Draw Bar Container (Rounded)
int bar_y = base_y + 30;
int bar_height = 12;
renderer->draw_rounded_rectangle(base_x, bar_y, width, bar_height, 6, true);
// Draw Progress Fill
int fill_width = (percentage * width) / 100;
if (fill_width > 4) {
renderer->draw_rounded_rectangle(base_x + 2, bar_y + 2, fill_width - 4, bar_height - 4, 4, true, true);
}
renderer->set_font(original_font);
const Font *original_font = renderer->get_current_font();
renderer->set_font(current_font);
// Draw Main Label (e.g., "PANELS")
renderer->draw_string_scaled(base_x, base_y, label, 2);
// Draw Sublabel and Value (e.g., "Weekly Average Charge" and "190KWH")
renderer->draw_string_scaled(base_x, base_y + 15, sublabel, 1);
int val_width = strlen(value_text) * 8; // Approximation
renderer->draw_string_scaled(base_x + width - val_width, base_y + 15,
value_text, 1);
// Draw Bar Container (Rounded)
int bar_y = base_y + 30;
int bar_height = 12;
renderer->draw_rounded_rectangle(base_x, bar_y, width, bar_height, 6, true);
// Draw Progress Fill
int fill_width = (percentage * width) / 100;
if (fill_width > 4) {
renderer->draw_rounded_rectangle(base_x + 2, bar_y + 2, fill_width - 4,
bar_height - 4, 4, true, true);
}
renderer->set_font(original_font);
}
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);
int base_x = window->x + x;
int base_y = window->y + y;
int height = 50;
// Draw pill-shaped container
renderer->draw_rounded_rectangle(base_x, base_y, width, height, height/2, true);
const Font* original_font = renderer->get_current_font();
renderer->set_font(current_font);
// Draw Label
renderer->draw_string_scaled(base_x + 20, base_y + 18, label, 2);
// Draw Circular Gauge on the right
int centerX = base_x + width - 30;
int centerY = base_y + 25;
int radius = 18;
// Draw background track (dimmed)
renderer->draw_circle(centerX, centerY, radius, true);
// Draw percentage text inside circle
char buf[5];
snprintf(buf, sizeof(buf), "%d%%", percentage);
renderer->draw_string_scaled(centerX - 10, centerY - 5, buf, 1);
// Note: If your renderer supports arcs:
// renderer->draw_arc(centerX, centerY, radius, 0, (percentage * 360) / 100);
renderer->set_font(original_font);
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);
int base_x = window->x + x;
int base_y = window->y + y;
int height = 50;
// Draw pill-shaped container
renderer->draw_rounded_rectangle(base_x, base_y, width, height, height / 2,
true);
const Font *original_font = renderer->get_current_font();
renderer->set_font(current_font);
// Draw Label
renderer->draw_string_scaled(base_x + 20, base_y + 18, label, 2);
// Draw Circular Gauge on the right
int centerX = base_x + width - 30;
int centerY = base_y + 25;
int radius = 18;
// Draw background track (dimmed)
renderer->draw_circle(centerX, centerY, radius, true);
// Draw percentage text inside circle
char buf[5];
snprintf(buf, sizeof(buf), "%d%%", percentage);
renderer->draw_string_scaled(centerX - 10, centerY - 5, buf, 1);
// Note: If your renderer supports arcs:
// renderer->draw_arc(centerX, centerY, radius, 0, (percentage * 360) / 100);
renderer->set_font(original_font);
}
void LowLevelGUI::draw_notification(LowLevelWindow *window, int x, int y,
int width, const char *time,
const char *message) {
// window = validate_or_create_window(window, renderer);
int base_x = window->x + x;
int base_y = window->y + y;
void LowLevelGUI::draw_notification(LowLevelWindow* window, int x, int y, int width, const char* time, const char* message) {
// window = validate_or_create_window(window, renderer);
int base_x = window->x + x;
int base_y = window->y + y;
// Draw dark background
renderer->draw_rounded_rectangle(base_x, base_y, width, 100, 15, true, true);
const Font* original_font = renderer->get_current_font();
renderer->set_font(current_font);
renderer->set_text_color(false); // Assume false is white/light on dark
renderer->draw_string_scaled(base_x + 15, base_y + 10, time, 1);
renderer->draw_string_scaled(base_x + width - 20, base_y + 10, "x", 1);
// Simple word wrap or multi-line manual draw for the message
// This is a simplified version
renderer->draw_string_scaled(base_x + 15, base_y + 30, message, 2);
renderer->set_text_color(true);
renderer->set_font(original_font);
// Draw dark background
renderer->draw_rounded_rectangle(base_x, base_y, width, 100, 15, true, true);
const Font *original_font = renderer->get_current_font();
renderer->set_font(current_font);
renderer->set_text_color(false); // Assume false is white/light on dark
renderer->draw_string_scaled(base_x + 15, base_y + 10, time, 1);
renderer->draw_string_scaled(base_x + width - 20, base_y + 10, "x", 1);
// Simple word wrap or multi-line manual draw for the message
// This is a simplified version
renderer->draw_string_scaled(base_x + 15, base_y + 30, message, 2);
renderer->set_text_color(true);
renderer->set_font(original_font);
}
void LowLevelGUI::draw_large_clock(LowLevelWindow* window, int x, int y, const char* time_str) {
window = validate_or_create_window(window, renderer);
const Font* original_font = renderer->get_current_font();
renderer->set_font(current_font);
// Draw the time significantly larger (scale 5 or 6)
renderer->draw_string_scaled(window->x + x, window->y + y, time_str, 6);
renderer->set_font(original_font);
void LowLevelGUI::draw_large_clock(LowLevelWindow *window, int x, int y,
const char *time_str) {
window = validate_or_create_window(window, renderer);
const Font *original_font = renderer->get_current_font();
renderer->set_font(current_font);
// Draw the time significantly larger (scale 5 or 6)
renderer->draw_string_scaled(window->x + x, window->y + y, time_str, 6);
renderer->set_font(original_font);
}

View File

@@ -671,3 +671,57 @@ int LowLevelRenderer::draw_string_scaled(int x, int y, const char* text, int sca
}
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;
}

View File

@@ -1,9 +1,8 @@
// class that handles low-level rendering operations, such as drawing pixels and shapes to the display.
// This class is framework-agnostic and focuses solely on manipulating a 1-bit per pixel buffer.
// Constructor Args:
// uint8_t* buffer: Pointer to the bit buffer
// int width: Display width in pixels
// int height: Display height in pixels
// class that handles low-level rendering operations, such as drawing pixels and
// shapes to the display. This class is framework-agnostic and focuses solely on
// manipulating a 1-bit per pixel buffer. Constructor Args: uint8_t* buffer:
// Pointer to the bit buffer int width: Display width in pixels int height:
// Display height in pixels
#ifndef LOW_LEVEL_RENDER_H
#define LOW_LEVEL_RENDER_H
@@ -15,26 +14,28 @@
// Font class that holds font data and dimensions
class Font {
private:
const unsigned char* data;
int num_chars;
int bytes_per_char;
int char_height;
const unsigned char *data;
int num_chars;
int bytes_per_char;
int char_height;
public:
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),
char_height(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),
char_height(char_height) {}
const unsigned char* get_data() const { return data; }
int get_num_chars() const { return num_chars; }
int get_bytes_per_char() const { return bytes_per_char; }
int get_char_height() const { return char_height; }
// Get a specific character's data
const unsigned char* get_char_data(int char_index) const {
if (char_index < 0 || char_index >= num_chars) return nullptr;
return data + (char_index * bytes_per_char);
}
const unsigned char *get_data() const { return data; }
int get_num_chars() const { return num_chars; }
int get_bytes_per_char() const { return bytes_per_char; }
int get_char_height() const { return char_height; }
// Get a specific character's data
const unsigned char *get_char_data(int char_index) const {
if (char_index < 0 || char_index >= num_chars)
return nullptr;
return data + (char_index * bytes_per_char);
}
};
// Font extern declarations
@@ -93,61 +94,78 @@ extern Font font_zxpix_obj;
class LowLevelRenderer {
private:
uint8_t* bit_buffer;
int V_WIDTH;
int V_HEIGHT;
const Font* current_font;
bool clipping_enabled;
int clip_x, clip_y, clip_width, clip_height;
bool text_color;
void draw_corner_arc(int center_x, int center_y, int radius, int quadrant, 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);
uint8_t *bit_buffer;
int V_WIDTH;
int V_HEIGHT;
const Font *current_font;
bool clipping_enabled;
int clip_x, clip_y, clip_width, clip_height;
bool text_color;
void draw_corner_arc(int center_x, int center_y, int radius, int quadrant,
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);
public:
LowLevelRenderer(uint8_t* buffer, int width, int height);
LowLevelRenderer(uint8_t *buffer, int width, int height);
// Font management
void set_font(const Font* font);
void set_text_color(bool color);
const Font* get_current_font() const { return current_font; }
bool get_current_text_color() const { return text_color; }
int get_width() const { return V_WIDTH; }
int get_height() const { return V_HEIGHT; }
// Font management
void set_font(const Font *font);
void set_text_color(bool color);
const Font *get_current_font() const { return current_font; }
bool get_current_text_color() const { return text_color; }
int get_width() const { return V_WIDTH; }
int get_height() const { return V_HEIGHT; }
// --- 1-BIT DRAWING PRIMITIVES ---
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_rectangle(int x, int y, int width, int height, bool on, int line_width);
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_filled_triangle(int x1, int y1, int x2, int y2, int x3, int y3, bool on);
void draw_ellipse(int center_x, int center_y, int radius_x, int radius_y, bool on);
void draw_filled_ellipse(int center_x, int center_y, int radius_x, 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
void draw_bitmap(const unsigned char* bitmap, int x, int y, int width, int height, bool invert = false);
// Clipping functions
void set_clip_rect(int x, int y, int width, int height);
void reset_clip_rect();
bool is_clipping_enabled() const;
// Buffer operations
void invert_buffer();
void clear_buffer();
void draw_circle(int x, int y, int radius, bool on);
void draw_filled_circle(int x, int y, int radius, bool on);
int draw_char_vcol(int x, int y, char c);
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_string_scaled(int x, int y, const char* text, int scale, int spacing = 1);
// --- 1-BIT DRAWING PRIMITIVES ---
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_rectangle(int x, int y, int width, int height, bool on,
int line_width);
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_filled_triangle(int x1, int y1, int x2, int y2, int x3, int y3,
bool on);
void draw_ellipse(int center_x, int center_y, int radius_x, int radius_y,
bool on);
void draw_filled_ellipse(int center_x, int center_y, int radius_x,
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
void draw_bitmap(const unsigned char *bitmap, int x, int y, int width,
int height, bool invert = false);
// Clipping functions
void set_clip_rect(int x, int y, int width, int height);
void reset_clip_rect();
bool is_clipping_enabled() const;
// Buffer operations
void invert_buffer();
void clear_buffer();
void draw_circle(int x, int y, int radius, bool on);
void draw_filled_circle(int x, int y, int radius, bool on);
int draw_char_vcol(int x, int y, char c);
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_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

View File

@@ -1,383 +0,0 @@
# This is the CMakeCache file.
# For build in directory: /Users/adolforeyna/Projects/basic1/emulator
# It was generated by CMake: /opt/homebrew/bin/cmake
# You can edit this file to change values found and used by cmake.
# If you do not want to change any of the values, simply exit the editor.
# If you do want to change a value, simply edit, save, and exit the editor.
# The syntax for the file is as follows:
# KEY:TYPE=VALUE
# KEY is the name of a variable in the cache.
# TYPE is a hint to GUIs for the type of VALUE, DO NOT EDIT TYPE!.
# VALUE is the current value for the KEY.
########################
# EXTERNAL cache entries
########################
//Path to a program.
CMAKE_ADDR2LINE:FILEPATH=CMAKE_ADDR2LINE-NOTFOUND
//Path to a program.
CMAKE_AR:FILEPATH=/usr/bin/ar
//Choose the type of build, options are: None Debug Release RelWithDebInfo
// MinSizeRel ...
CMAKE_BUILD_TYPE:STRING=
//Enable/Disable color output during build.
CMAKE_COLOR_MAKEFILE:BOOL=ON
//CXX compiler
CMAKE_CXX_COMPILER:FILEPATH=/usr/bin/c++
//Flags used by the CXX compiler during all build types.
CMAKE_CXX_FLAGS:STRING=
//Flags used by the CXX compiler during DEBUG builds.
CMAKE_CXX_FLAGS_DEBUG:STRING=-g
//Flags used by the CXX compiler during MINSIZEREL builds.
CMAKE_CXX_FLAGS_MINSIZEREL:STRING=-Os -DNDEBUG
//Flags used by the CXX compiler during RELEASE builds.
CMAKE_CXX_FLAGS_RELEASE:STRING=-O3 -DNDEBUG
//Flags used by the CXX compiler during RELWITHDEBINFO builds.
CMAKE_CXX_FLAGS_RELWITHDEBINFO:STRING=-O2 -g -DNDEBUG
//C compiler
CMAKE_C_COMPILER:FILEPATH=/usr/bin/cc
//Flags used by the C compiler during all build types.
CMAKE_C_FLAGS:STRING=
//Flags used by the C compiler during DEBUG builds.
CMAKE_C_FLAGS_DEBUG:STRING=-g
//Flags used by the C compiler during MINSIZEREL builds.
CMAKE_C_FLAGS_MINSIZEREL:STRING=-Os -DNDEBUG
//Flags used by the C compiler during RELEASE builds.
CMAKE_C_FLAGS_RELEASE:STRING=-O3 -DNDEBUG
//Flags used by the C compiler during RELWITHDEBINFO builds.
CMAKE_C_FLAGS_RELWITHDEBINFO:STRING=-O2 -g -DNDEBUG
//Path to a program.
CMAKE_DLLTOOL:FILEPATH=CMAKE_DLLTOOL-NOTFOUND
//Flags used by the linker during all build types.
CMAKE_EXE_LINKER_FLAGS:STRING=
//Flags used by the linker during DEBUG builds.
CMAKE_EXE_LINKER_FLAGS_DEBUG:STRING=
//Flags used by the linker during MINSIZEREL builds.
CMAKE_EXE_LINKER_FLAGS_MINSIZEREL:STRING=
//Flags used by the linker during RELEASE builds.
CMAKE_EXE_LINKER_FLAGS_RELEASE:STRING=
//Flags used by the linker during RELWITHDEBINFO builds.
CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO:STRING=
//Enable/Disable output of compile commands during generation.
CMAKE_EXPORT_COMPILE_COMMANDS:BOOL=
//Value Computed by CMake.
CMAKE_FIND_PACKAGE_REDIRECTS_DIR:STATIC=/Users/adolforeyna/Projects/basic1/emulator/CMakeFiles/pkgRedirects
//Path to a program.
CMAKE_INSTALL_NAME_TOOL:FILEPATH=/usr/bin/install_name_tool
//Install path prefix, prepended onto install directories.
CMAKE_INSTALL_PREFIX:PATH=/usr/local
//Path to a program.
CMAKE_LINKER:FILEPATH=/usr/bin/ld
//Path to a program.
CMAKE_MAKE_PROGRAM:FILEPATH=/usr/bin/make
//Flags used by the linker during the creation of modules during
// all build types.
CMAKE_MODULE_LINKER_FLAGS:STRING=
//Flags used by the linker during the creation of modules during
// DEBUG builds.
CMAKE_MODULE_LINKER_FLAGS_DEBUG:STRING=
//Flags used by the linker during the creation of modules during
// MINSIZEREL builds.
CMAKE_MODULE_LINKER_FLAGS_MINSIZEREL:STRING=
//Flags used by the linker during the creation of modules during
// RELEASE builds.
CMAKE_MODULE_LINKER_FLAGS_RELEASE:STRING=
//Flags used by the linker during the creation of modules during
// RELWITHDEBINFO builds.
CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO:STRING=
//Path to a program.
CMAKE_NM:FILEPATH=/usr/bin/nm
//Path to a program.
CMAKE_OBJCOPY:FILEPATH=CMAKE_OBJCOPY-NOTFOUND
//Path to a program.
CMAKE_OBJDUMP:FILEPATH=/usr/bin/objdump
//Build architectures for OSX
CMAKE_OSX_ARCHITECTURES:STRING=
//Minimum OS X version to target for deployment (at runtime); newer
// APIs weak linked. Set to empty string for default value.
CMAKE_OSX_DEPLOYMENT_TARGET:STRING=
//The product will be built against the headers and libraries located
// inside the indicated SDK.
CMAKE_OSX_SYSROOT:STRING=
//Value Computed by CMake
CMAKE_PROJECT_COMPAT_VERSION:STATIC=
//Value Computed by CMake
CMAKE_PROJECT_DESCRIPTION:STATIC=
//Value Computed by CMake
CMAKE_PROJECT_HOMEPAGE_URL:STATIC=
//Value Computed by CMake
CMAKE_PROJECT_NAME:STATIC=basic1_emulator
//Value Computed by CMake
CMAKE_PROJECT_SPDX_LICENSE:STATIC=
//Path to a program.
CMAKE_RANLIB:FILEPATH=/usr/bin/ranlib
//Path to a program.
CMAKE_READELF:FILEPATH=CMAKE_READELF-NOTFOUND
//Flags used by the linker during the creation of shared libraries
// during all build types.
CMAKE_SHARED_LINKER_FLAGS:STRING=
//Flags used by the linker during the creation of shared libraries
// during DEBUG builds.
CMAKE_SHARED_LINKER_FLAGS_DEBUG:STRING=
//Flags used by the linker during the creation of shared libraries
// during MINSIZEREL builds.
CMAKE_SHARED_LINKER_FLAGS_MINSIZEREL:STRING=
//Flags used by the linker during the creation of shared libraries
// during RELEASE builds.
CMAKE_SHARED_LINKER_FLAGS_RELEASE:STRING=
//Flags used by the linker during the creation of shared libraries
// during RELWITHDEBINFO builds.
CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO:STRING=
//If set, runtime paths are not added when installing shared libraries,
// but are added when building.
CMAKE_SKIP_INSTALL_RPATH:BOOL=NO
//If set, runtime paths are not added when using shared libraries.
CMAKE_SKIP_RPATH:BOOL=NO
//Flags used by the archiver during the creation of static libraries
// during all build types.
CMAKE_STATIC_LINKER_FLAGS:STRING=
//Flags used by the archiver during the creation of static libraries
// during DEBUG builds.
CMAKE_STATIC_LINKER_FLAGS_DEBUG:STRING=
//Flags used by the archiver during the creation of static libraries
// during MINSIZEREL builds.
CMAKE_STATIC_LINKER_FLAGS_MINSIZEREL:STRING=
//Flags used by the archiver during the creation of static libraries
// during RELEASE builds.
CMAKE_STATIC_LINKER_FLAGS_RELEASE:STRING=
//Flags used by the archiver during the creation of static libraries
// during RELWITHDEBINFO builds.
CMAKE_STATIC_LINKER_FLAGS_RELWITHDEBINFO:STRING=
//Path to a program.
CMAKE_STRIP:FILEPATH=/usr/bin/strip
//Path to a program.
CMAKE_TAPI:FILEPATH=/Volumes/ReynaFamily/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/tapi
//If this value is on, makefiles will be generated without the
// .SILENT directive, and all commands will be echoed to the console
// during the make. This is useful for debugging only. With Visual
// Studio IDE projects all commands are done without /nologo.
CMAKE_VERBOSE_MAKEFILE:BOOL=FALSE
//The directory containing a CMake configuration file for SFML.
SFML_DIR:PATH=/opt/homebrew/lib/cmake/SFML
//Path to a file.
SFML_DOC_DIR:PATH=/opt/homebrew/share/doc/SFML
//Value Computed by CMake
basic1_emulator_BINARY_DIR:STATIC=/Users/adolforeyna/Projects/basic1/emulator
//Value Computed by CMake
basic1_emulator_IS_TOP_LEVEL:STATIC=ON
//Value Computed by CMake
basic1_emulator_SOURCE_DIR:STATIC=/Users/adolforeyna/Projects/basic1/emulator
########################
# INTERNAL cache entries
########################
//ADVANCED property for variable: CMAKE_ADDR2LINE
CMAKE_ADDR2LINE-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_AR
CMAKE_AR-ADVANCED:INTERNAL=1
//This is the directory where this CMakeCache.txt was created
CMAKE_CACHEFILE_DIR:INTERNAL=/Users/adolforeyna/Projects/basic1/emulator
//Major version of cmake used to create the current loaded cache
CMAKE_CACHE_MAJOR_VERSION:INTERNAL=4
//Minor version of cmake used to create the current loaded cache
CMAKE_CACHE_MINOR_VERSION:INTERNAL=2
//Patch version of cmake used to create the current loaded cache
CMAKE_CACHE_PATCH_VERSION:INTERNAL=3
//ADVANCED property for variable: CMAKE_COLOR_MAKEFILE
CMAKE_COLOR_MAKEFILE-ADVANCED:INTERNAL=1
//Path to CMake executable.
CMAKE_COMMAND:INTERNAL=/opt/homebrew/bin/cmake
//Path to cpack program executable.
CMAKE_CPACK_COMMAND:INTERNAL=/opt/homebrew/bin/cpack
//Path to ctest program executable.
CMAKE_CTEST_COMMAND:INTERNAL=/opt/homebrew/bin/ctest
//ADVANCED property for variable: CMAKE_CXX_COMPILER
CMAKE_CXX_COMPILER-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_CXX_FLAGS
CMAKE_CXX_FLAGS-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_CXX_FLAGS_DEBUG
CMAKE_CXX_FLAGS_DEBUG-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_CXX_FLAGS_MINSIZEREL
CMAKE_CXX_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_CXX_FLAGS_RELEASE
CMAKE_CXX_FLAGS_RELEASE-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_CXX_FLAGS_RELWITHDEBINFO
CMAKE_CXX_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_C_COMPILER
CMAKE_C_COMPILER-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_C_FLAGS
CMAKE_C_FLAGS-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_C_FLAGS_DEBUG
CMAKE_C_FLAGS_DEBUG-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_C_FLAGS_MINSIZEREL
CMAKE_C_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_C_FLAGS_RELEASE
CMAKE_C_FLAGS_RELEASE-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_C_FLAGS_RELWITHDEBINFO
CMAKE_C_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_DLLTOOL
CMAKE_DLLTOOL-ADVANCED:INTERNAL=1
//Path to cache edit program executable.
CMAKE_EDIT_COMMAND:INTERNAL=/opt/homebrew/bin/ccmake
//Executable file format
CMAKE_EXECUTABLE_FORMAT:INTERNAL=MACHO
//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS
CMAKE_EXE_LINKER_FLAGS-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS_DEBUG
CMAKE_EXE_LINKER_FLAGS_DEBUG-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS_MINSIZEREL
CMAKE_EXE_LINKER_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS_RELEASE
CMAKE_EXE_LINKER_FLAGS_RELEASE-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO
CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_EXPORT_COMPILE_COMMANDS
CMAKE_EXPORT_COMPILE_COMMANDS-ADVANCED:INTERNAL=1
//Name of external makefile project generator.
CMAKE_EXTRA_GENERATOR:INTERNAL=
//Name of generator.
CMAKE_GENERATOR:INTERNAL=Unix Makefiles
//Generator instance identifier.
CMAKE_GENERATOR_INSTANCE:INTERNAL=
//Name of generator platform.
CMAKE_GENERATOR_PLATFORM:INTERNAL=
//Name of generator toolset.
CMAKE_GENERATOR_TOOLSET:INTERNAL=
//Source directory with the top level CMakeLists.txt file for this
// project
CMAKE_HOME_DIRECTORY:INTERNAL=/Users/adolforeyna/Projects/basic1/emulator
//ADVANCED property for variable: CMAKE_INSTALL_NAME_TOOL
CMAKE_INSTALL_NAME_TOOL-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_LINKER
CMAKE_LINKER-ADVANCED:INTERNAL=1
//Name of CMakeLists files to read
CMAKE_LIST_FILE_NAME:INTERNAL=CMakeLists.txt
//ADVANCED property for variable: CMAKE_MAKE_PROGRAM
CMAKE_MAKE_PROGRAM-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS
CMAKE_MODULE_LINKER_FLAGS-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS_DEBUG
CMAKE_MODULE_LINKER_FLAGS_DEBUG-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS_MINSIZEREL
CMAKE_MODULE_LINKER_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS_RELEASE
CMAKE_MODULE_LINKER_FLAGS_RELEASE-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO
CMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_NM
CMAKE_NM-ADVANCED:INTERNAL=1
//number of local generators
CMAKE_NUMBER_OF_MAKEFILES:INTERNAL=1
//ADVANCED property for variable: CMAKE_OBJCOPY
CMAKE_OBJCOPY-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_OBJDUMP
CMAKE_OBJDUMP-ADVANCED:INTERNAL=1
//Platform information initialized
CMAKE_PLATFORM_INFO_INITIALIZED:INTERNAL=1
//ADVANCED property for variable: CMAKE_RANLIB
CMAKE_RANLIB-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_READELF
CMAKE_READELF-ADVANCED:INTERNAL=1
//Path to CMake installation.
CMAKE_ROOT:INTERNAL=/opt/homebrew/share/cmake
//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS
CMAKE_SHARED_LINKER_FLAGS-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS_DEBUG
CMAKE_SHARED_LINKER_FLAGS_DEBUG-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS_MINSIZEREL
CMAKE_SHARED_LINKER_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS_RELEASE
CMAKE_SHARED_LINKER_FLAGS_RELEASE-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO
CMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_SKIP_INSTALL_RPATH
CMAKE_SKIP_INSTALL_RPATH-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_SKIP_RPATH
CMAKE_SKIP_RPATH-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS
CMAKE_STATIC_LINKER_FLAGS-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS_DEBUG
CMAKE_STATIC_LINKER_FLAGS_DEBUG-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS_MINSIZEREL
CMAKE_STATIC_LINKER_FLAGS_MINSIZEREL-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS_RELEASE
CMAKE_STATIC_LINKER_FLAGS_RELEASE-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_STATIC_LINKER_FLAGS_RELWITHDEBINFO
CMAKE_STATIC_LINKER_FLAGS_RELWITHDEBINFO-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_STRIP
CMAKE_STRIP-ADVANCED:INTERNAL=1
//ADVANCED property for variable: CMAKE_TAPI
CMAKE_TAPI-ADVANCED:INTERNAL=1
//uname command
CMAKE_UNAME:INTERNAL=/usr/bin/uname
//ADVANCED property for variable: CMAKE_VERBOSE_MAKEFILE
CMAKE_VERBOSE_MAKEFILE-ADVANCED:INTERNAL=1

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -1,8 +1,9 @@
#!/bin/bash
set -e
echo "[Emulator] Cleaning old build..."
make clean
echo "[Emulator] Generating build files..."
cmake .
echo "[Emulator] Building..."
make

View File

@@ -1,61 +0,0 @@
# Install script for directory: /Users/adolforeyna/Projects/basic1/emulator
# Set the install prefix
if(NOT DEFINED CMAKE_INSTALL_PREFIX)
set(CMAKE_INSTALL_PREFIX "/usr/local")
endif()
string(REGEX REPLACE "/$" "" CMAKE_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}")
# Set the install configuration name.
if(NOT DEFINED CMAKE_INSTALL_CONFIG_NAME)
if(BUILD_TYPE)
string(REGEX REPLACE "^[^A-Za-z0-9_]+" ""
CMAKE_INSTALL_CONFIG_NAME "${BUILD_TYPE}")
else()
set(CMAKE_INSTALL_CONFIG_NAME "")
endif()
message(STATUS "Install configuration: \"${CMAKE_INSTALL_CONFIG_NAME}\"")
endif()
# Set the component getting installed.
if(NOT CMAKE_INSTALL_COMPONENT)
if(COMPONENT)
message(STATUS "Install component: \"${COMPONENT}\"")
set(CMAKE_INSTALL_COMPONENT "${COMPONENT}")
else()
set(CMAKE_INSTALL_COMPONENT)
endif()
endif()
# Is this installation the result of a crosscompile?
if(NOT DEFINED CMAKE_CROSSCOMPILING)
set(CMAKE_CROSSCOMPILING "FALSE")
endif()
# Set path to fallback-tool for dependency-resolution.
if(NOT DEFINED CMAKE_OBJDUMP)
set(CMAKE_OBJDUMP "/usr/bin/objdump")
endif()
string(REPLACE ";" "\n" CMAKE_INSTALL_MANIFEST_CONTENT
"${CMAKE_INSTALL_MANIFEST_FILES}")
if(CMAKE_INSTALL_LOCAL_ONLY)
file(WRITE "/Users/adolforeyna/Projects/basic1/emulator/install_local_manifest.txt"
"${CMAKE_INSTALL_MANIFEST_CONTENT}")
endif()
if(CMAKE_INSTALL_COMPONENT)
if(CMAKE_INSTALL_COMPONENT MATCHES "^[a-zA-Z0-9_.+-]+$")
set(CMAKE_INSTALL_MANIFEST "install_manifest_${CMAKE_INSTALL_COMPONENT}.txt")
else()
string(MD5 CMAKE_INST_COMP_HASH "${CMAKE_INSTALL_COMPONENT}")
set(CMAKE_INSTALL_MANIFEST "install_manifest_${CMAKE_INST_COMP_HASH}.txt")
unset(CMAKE_INST_COMP_HASH)
endif()
else()
set(CMAKE_INSTALL_MANIFEST "install_manifest.txt")
endif()
if(NOT CMAKE_INSTALL_LOCAL_ONLY)
file(WRITE "/Users/adolforeyna/Projects/basic1/emulator/${CMAKE_INSTALL_MANIFEST}"
"${CMAKE_INSTALL_MANIFEST_CONTENT}")
endif()

View File

@@ -1,164 +1,178 @@
// Copy of game_launcher.cpp for emulator build
#include "game_launcher.h"
#include "input_manager.h"
#include "../display/low_level_render.h"
#include "../display/low_level_gui.h"
#include "../display/low_level_render.h"
#include "input_manager.h"
#include <stdio.h>
extern Font font_5x5_obj;
GameLauncher::GameLauncher(uint16_t width, uint16_t height, LowLevelRenderer* renderer, LowLevelGUI* gui, InputManager* input_manager)
: width(width), height(height), renderer(renderer), gui(gui), 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;
entry.name = name;
entry.description = description;
entry.factory = factory;
games.push_back(entry);
printf("Registered game: %s - %s\n", name, description);
GameLauncher::GameLauncher(uint16_t width, uint16_t height,
LowLevelRenderer *renderer, LowLevelGUI *gui,
InputManager *input_manager)
: width(width), height(height), renderer(renderer), gui(gui),
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;
entry.name = name;
entry.description = description;
entry.factory = factory;
games.push_back(entry);
printf("Registered game: %s - %s\n", name, description);
}
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);
int total_pages = get_total_pages();
char title[64];
snprintf(title, sizeof(title), "Select a Game: (Page %d/%d)",
current_page + 1, total_pages);
renderer->draw_string_scaled(30, 40, title, 2);
int page_start = get_page_start_index();
int page_end = get_page_end_index();
for (int i = page_start; i < page_end && i < (int)games.size(); i++) {
int item_index = i - page_start;
int y = MENU_Y_START + (item_index * MENU_ITEM_HEIGHT);
bool is_selected = (i == selected_index);
gui->draw_button(window, 20, y, games[i].name, is_selected, true);
renderer->set_font(&font_5x5_obj);
int total_pages = get_total_pages();
char title[64];
snprintf(title, sizeof(title), "Select a Game: (Page %d/%d)", current_page + 1, total_pages);
renderer->draw_string_scaled(30, 40, title, 2);
renderer->set_text_color(true);
renderer->draw_string_scaled(50, y + 36, games[i].description, 1);
}
if (total_pages > 1) {
int button_y = height - 65;
renderer->set_font(&font_5x5_obj);
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 {
renderer->set_font(&font_5x5_obj);
renderer->draw_string_scaled(
30, height - 35, "KEY0: Navigate | KEY1: Select | Touch to play", 1);
}
}
bool GameLauncher::update(const InputEvent &event) {
bool needs_refresh = false;
int total_pages = get_total_pages();
switch (event.type) {
case INPUT_TOUCH_DOWN: {
// Check if touch is on navigation buttons (if multiple pages)
if (total_pages > 1) {
if (event.x >= PREV_BUTTON_X && event.x < PREV_BUTTON_X + BUTTON_WIDTH &&
event.y >= NAV_BUTTON_Y && event.y < NAV_BUTTON_Y + BUTTON_HEIGHT) {
if (current_page > 0) {
current_page--;
selected_index = get_page_start_index();
needs_refresh = true;
}
break;
}
if (event.x >= NEXT_BUTTON_X && event.x < NEXT_BUTTON_X + BUTTON_WIDTH &&
event.y >= NAV_BUTTON_Y && event.y < NAV_BUTTON_Y + BUTTON_HEIGHT) {
if (current_page + 1 < total_pages) {
current_page++;
selected_index = get_page_start_index();
needs_refresh = true;
}
break;
}
}
int page_start = get_page_start_index();
int page_end = get_page_end_index();
for (int i = page_start; i < page_end && i < (int)games.size(); i++) {
int item_index = i - page_start;
int y = MENU_Y_START + (item_index * MENU_ITEM_HEIGHT);
bool is_selected = (i == selected_index);
gui->draw_button(window, 20, y, games[i].name, is_selected, true);
renderer->set_font(&font_5x5_obj);
renderer->set_text_color(true);
renderer->draw_string_scaled(50, y + 36, games[i].description, 1);
int item_index = i - page_start;
int y = MENU_Y_START + (item_index * MENU_ITEM_HEIGHT);
if (event.y >= y - 5 && event.y < y + MENU_ITEM_HEIGHT - 5) {
selected_game =
games[i].factory(width, height, renderer, gui, input_manager);
if (selected_game) {
selected_game->init();
return true;
}
break;
}
}
if (total_pages > 1) {
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->draw_string_scaled(30, height - 25, "Touch buttons or KEY0/KEY1", 1);
break;
}
case INPUT_BUTTON_0: {
int page_start = get_page_start_index();
int page_end = get_page_end_index();
if (page_end - page_start > 1) {
int old_index = selected_index;
selected_index++;
if (selected_index >= page_end) {
if (current_page + 1 < total_pages) {
current_page++;
selected_index = get_page_start_index();
} else {
current_page = 0;
selected_index = 0;
}
} else if (selected_index < page_start) {
selected_index = page_start;
}
} else {
renderer->set_font(&font_5x5_obj);
renderer->draw_string_scaled(30, height - 35, "KEY0: Navigate | KEY1: Select | Touch to play", 1);
if (current_page + 1 < total_pages) {
current_page++;
selected_index = get_page_start_index();
} else {
current_page = 0;
selected_index = 0;
}
}
}
bool GameLauncher::update(const InputEvent& event) {
bool needs_refresh = false;
int total_pages = get_total_pages();
switch (event.type) {
case INPUT_TOUCH_DOWN: {
// Check if touch is on navigation buttons (if multiple pages)
if (total_pages > 1) {
if (event.x >= PREV_BUTTON_X && event.x < PREV_BUTTON_X + BUTTON_WIDTH &&
event.y >= NAV_BUTTON_Y && event.y < NAV_BUTTON_Y + BUTTON_HEIGHT) {
if (current_page > 0) {
current_page--;
selected_index = get_page_start_index();
needs_refresh = true;
}
break;
}
if (event.x >= NEXT_BUTTON_X && event.x < NEXT_BUTTON_X + BUTTON_WIDTH &&
event.y >= NAV_BUTTON_Y && event.y < NAV_BUTTON_Y + BUTTON_HEIGHT) {
if (current_page + 1 < total_pages) {
current_page++;
selected_index = get_page_start_index();
needs_refresh = true;
}
break;
}
}
int page_start = get_page_start_index();
int page_end = get_page_end_index();
for (int i = page_start; i < page_end && i < (int)games.size(); i++) {
int item_index = i - page_start;
int y = MENU_Y_START + (item_index * MENU_ITEM_HEIGHT);
if (event.y >= y - 5 && event.y < y + MENU_ITEM_HEIGHT - 5) {
selected_game = games[i].factory(width, height, renderer, gui, input_manager);
if (selected_game) {
selected_game->init();
return true;
}
break;
}
}
break;
}
case INPUT_BUTTON_0: {
int page_start = get_page_start_index();
int page_end = get_page_end_index();
if (page_end - page_start > 1) {
int old_index = selected_index;
selected_index++;
if (selected_index >= page_end) {
if (current_page + 1 < total_pages) {
current_page++;
selected_index = get_page_start_index();
} else {
current_page = 0;
selected_index = 0;
}
} else if (selected_index < page_start) {
selected_index = page_start;
}
} else {
if (current_page + 1 < total_pages) {
current_page++;
selected_index = get_page_start_index();
} else {
current_page = 0;
selected_index = 0;
}
}
needs_refresh = true;
break;
}
case INPUT_BUTTON_1: {
if (selected_index >= 0 && selected_index < (int)games.size()) {
selected_game = games[selected_index].factory(width, height, renderer, gui, input_manager);
if (selected_game) {
selected_game->init();
return true;
}
}
break;
}
default:
break;
needs_refresh = true;
break;
}
case INPUT_BUTTON_1: {
if (selected_index >= 0 && selected_index < (int)games.size()) {
selected_game = games[selected_index].factory(width, height, renderer,
gui, input_manager);
if (selected_game) {
selected_game->init();
return true;
}
}
return needs_refresh;
break;
}
default:
break;
}
return needs_refresh;
}
Game* GameLauncher::get_selected_game() { return selected_game; }
Game *GameLauncher::get_selected_game() { return selected_game; }
void GameLauncher::reset() {
if (selected_game) {
delete selected_game;
selected_game = nullptr;
}
selected_index = 0;
current_page = 0;
if (selected_game) {
delete selected_game;
selected_game = nullptr;
}
selected_index = 0;
current_page = 0;
}
int GameLauncher::get_total_pages() const {
if (games.empty()) return 1;
return (games.size() + GAMES_PER_PAGE - 1) / GAMES_PER_PAGE;
if (games.empty())
return 1;
return (games.size() + GAMES_PER_PAGE - 1) / GAMES_PER_PAGE;
}
int GameLauncher::get_page_start_index() const {
return current_page * GAMES_PER_PAGE;
return current_page * GAMES_PER_PAGE;
}
int GameLauncher::get_page_end_index() const {
return (current_page + 1) * GAMES_PER_PAGE;
return (current_page + 1) * GAMES_PER_PAGE;
}

View File

@@ -1,84 +0,0 @@
-- NAME: Bouncing Ball
-- DESC: Physics demo with state management
-- States
local STATE_PAUSED = 0
local STATE_RUNNING = 1
function init()
game.vars.state = STATE_PAUSED
game.vars.ball_x = game.width() / 2
game.vars.ball_y = game.height() / 2
game.vars.vel_x = 3
game.vars.vel_y = 2
game.vars.radius = 10
game.vars.frame_count = 0
-- Enable continuous frame updates for smooth animation
game.set_frame_updates(true)
print("Bouncing Ball initialized")
end
function update(event)
-- Toggle pause on tap
if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.BUTTON_0 or event.type == INPUT.BUTTON_1 then
if game.vars.state == STATE_PAUSED then
game.vars.state = STATE_RUNNING
else
game.vars.state = STATE_PAUSED
end
return true
end
-- Update physics if running (on any frame tick)
if event.type == INPUT.FRAME_TICK and game.vars.state == STATE_RUNNING then
-- Move ball
game.vars.ball_x = game.vars.ball_x + game.vars.vel_x
game.vars.ball_y = game.vars.ball_y + game.vars.vel_y
-- Bounce off walls
if game.vars.ball_x - game.vars.radius < 0 or game.vars.ball_x + game.vars.radius > game.width() then
game.vars.vel_x = -game.vars.vel_x
game.vars.ball_x = math.max(game.vars.radius, math.min(game.width() - game.vars.radius, game.vars.ball_x))
end
if game.vars.ball_y - game.vars.radius < 0 or game.vars.ball_y + game.vars.radius > game.height() then
game.vars.vel_y = -game.vars.vel_y
game.vars.ball_y = math.max(game.vars.radius, math.min(game.height() - game.vars.radius, game.vars.ball_y))
end
game.vars.frame_count = game.vars.frame_count + 1
return true -- Always redraw when running
end
return false
end
function draw()
renderer.clear(false)
-- Draw ball
renderer.circle(game.vars.ball_x, game.vars.ball_y, game.vars.radius, true, true)
-- Draw trail (previous positions)
local trail_radius = game.vars.radius - 2
if trail_radius > 2 then
renderer.circle(game.vars.ball_x - game.vars.vel_x,
game.vars.ball_y - game.vars.vel_y,
trail_radius, true, false)
end
-- Draw status
if game.vars.state == STATE_PAUSED then
renderer.text(10, 10, "PAUSED - Tap to start", true)
else
renderer.text(10, 10, "Frames: " .. tostring(game.vars.frame_count), true)
renderer.text(10, 25, "Tap to pause", true)
end
-- Draw velocity vector
local arrow_x = game.vars.ball_x + game.vars.vel_x * 3
local arrow_y = game.vars.ball_y + game.vars.vel_y * 3
renderer.line(game.vars.ball_x, game.vars.ball_y, arrow_x, arrow_y, true, 2)
end

View File

@@ -1,49 +0,0 @@
-- NAME: Touch Counter
-- DESC: Simple tap counter demo
-- Initialize game state
function init()
game.vars.count = 0
game.vars.last_x = 0
game.vars.last_y = 0
print("Counter initialized")
end
-- Update game logic based on input
function update(event)
-- Check if touch/button pressed
if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.BUTTON_0 or event.type == INPUT.BUTTON_1 then
game.vars.count = game.vars.count + 1
game.vars.last_x = event.x
game.vars.last_y = event.y
print("Count: " .. game.vars.count)
return true -- Request redraw
end
return false -- No redraw needed
end
-- Draw the game
function draw()
-- Clear screen
renderer.clear(true)
-- Draw title
renderer.text(20, 20, "Touch Counter", true)
-- Draw count (centered)
local count_text = "Count: " .. tostring(game.vars.count)
renderer.text(game.width() / 2 - 40, game.height() / 2 - 10, count_text, true)
-- Draw last touch position
if game.vars.count > 0 then
local pos_text = "Last: (" .. tostring(game.vars.last_x) .. ", " .. tostring(game.vars.last_y) .. ")"
renderer.text(20, game.height() - 30, pos_text, true)
-- Draw marker at last touch
renderer.circle(game.vars.last_x, game.vars.last_y, 5, true, false)
end
-- Draw instructions
renderer.text(20, 50, "Tap screen to increment", true)
end

View File

@@ -1,219 +0,0 @@
-- NAME: Snake Game
-- DESC: Classic snake with state machine
-- Game states
local STATE_MENU = 0
local STATE_PLAYING = 1
local STATE_GAME_OVER = 2
-- Game constants
local CELL_SIZE = 10
local GRID_W = 40
local GRID_H = 28
-- Initialize game
function init()
game.vars.state = STATE_MENU
game.vars.score = 0
game.vars.high_score = 0
-- Snake as array of {x, y} positions
game.vars.snake = {}
game.vars.snake[1] = {x = 20, y = 14}
game.vars.snake[2] = {x = 19, y = 14}
game.vars.snake[3] = {x = 18, y = 14}
game.vars.dir_x = 1
game.vars.dir_y = 0
game.vars.food_x = 30
game.vars.food_y = 14
game.vars.frame_count = 0
game.vars.move_speed = 10 -- Frames between moves
-- Enable continuous frame updates
game.set_frame_updates(true)
print("Snake Game initialized")
end
-- Update game logic
function update(event)
local state = game.vars.state
-- State: MENU
if state == STATE_MENU then
if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.BUTTON_0 or event.type == INPUT.BUTTON_1 then
game.vars.state = STATE_PLAYING
game.vars.score = 0
init_snake()
spawn_food()
return true
end
-- State: PLAYING
elseif state == STATE_PLAYING then
-- Handle input for direction change
if event.type == INPUT.TOUCH_DOWN then
local head = game.vars.snake[1]
local head_screen_x = head.x * CELL_SIZE
local head_screen_y = head.y * CELL_SIZE
local dx = event.x - head_screen_x
local dy = event.y - head_screen_y
-- Change direction based on touch relative to head
if math.abs(dx) > math.abs(dy) then
if dx > 0 and game.vars.dir_x ~= -1 then
game.vars.dir_x = 1
game.vars.dir_y = 0
elseif dx < 0 and game.vars.dir_x ~= 1 then
game.vars.dir_x = -1
game.vars.dir_y = 0
end
else
if dy > 0 and game.vars.dir_y ~= -1 then
game.vars.dir_x = 0
game.vars.dir_y = 1
elseif dy < 0 and game.vars.dir_y ~= 1 then
game.vars.dir_x = 0
game.vars.dir_y = -1
end
end
end
-- Move snake every N frames
game.vars.frame_count = game.vars.frame_count + 1
if game.vars.frame_count >= game.vars.move_speed then
game.vars.frame_count = 0
move_snake()
return true
end
-- State: GAME_OVER
elseif state == STATE_GAME_OVER then
if event.type == INPUT.TOUCH_DOWN or event.type == INPUT.BUTTON_0 or event.type == INPUT.BUTTON_1 then
game.vars.state = STATE_MENU
return true
end
end
return false
end
-- Draw game
function draw()
renderer.clear(false)
local state = game.vars.state
-- Draw: MENU
if state == STATE_MENU then
renderer.text(game.width() / 2 - 30, game.height() / 2 - 20, "SNAKE", true)
renderer.text(game.width() / 2 - 50, game.height() / 2, "Tap to Start", true)
if game.vars.high_score > 0 then
local hs_text = "High: " .. tostring(game.vars.high_score)
renderer.text(game.width() / 2 - 30, game.height() / 2 + 20, hs_text, true)
end
-- Draw: PLAYING
elseif state == STATE_PLAYING then
-- Draw snake
for i = 1, #game.vars.snake do
local seg = game.vars.snake[i]
local filled = (i == 1) -- Head filled, body outline
renderer.rect(seg.x * CELL_SIZE, seg.y * CELL_SIZE, CELL_SIZE, CELL_SIZE, true, filled)
end
-- Draw food
renderer.circle(game.vars.food_x * CELL_SIZE + CELL_SIZE / 2,
game.vars.food_y * CELL_SIZE + CELL_SIZE / 2,
CELL_SIZE / 2, true, true)
-- Draw score
local score_text = "Score: " .. tostring(game.vars.score)
renderer.text(5, 5, score_text, true)
-- Draw: GAME_OVER
elseif state == STATE_GAME_OVER then
renderer.text(game.width() / 2 - 40, game.height() / 2 - 20, "GAME OVER", true)
local score_text = "Score: " .. tostring(game.vars.score)
renderer.text(game.width() / 2 - 40, game.height() / 2, score_text, true)
renderer.text(game.width() / 2 - 60, game.height() / 2 + 20, "Tap to Continue", true)
end
end
-- Helper: Initialize snake
function init_snake()
game.vars.snake = {}
game.vars.snake[1] = {x = 20, y = 14}
game.vars.snake[2] = {x = 19, y = 14}
game.vars.snake[3] = {x = 18, y = 14}
game.vars.dir_x = 1
game.vars.dir_y = 0
end
-- Helper: Spawn food at random position
function spawn_food()
game.vars.food_x = math.random(0, GRID_W - 1)
game.vars.food_y = math.random(0, GRID_H - 1)
end
-- Helper: Move snake
function move_snake()
local head = game.vars.snake[1]
local new_head = {
x = head.x + game.vars.dir_x,
y = head.y + game.vars.dir_y
}
-- Check wall collision
if new_head.x < 0 or new_head.x >= GRID_W or new_head.y < 0 or new_head.y >= GRID_H then
game_over()
return
end
-- Check self collision
for i = 1, #game.vars.snake do
local seg = game.vars.snake[i]
if new_head.x == seg.x and new_head.y == seg.y then
game_over()
return
end
end
-- Check food collision
local ate_food = false
if new_head.x == game.vars.food_x and new_head.y == game.vars.food_y then
ate_food = true
game.vars.score = game.vars.score + 10
spawn_food()
-- Increase speed slightly
if game.vars.move_speed > 3 then
game.vars.move_speed = game.vars.move_speed - 1
end
end
-- Move snake
table.insert(game.vars.snake, 1, new_head)
if not ate_food then
table.remove(game.vars.snake) -- Remove tail
end
end
-- Helper: Game over
function game_over()
game.vars.state = STATE_GAME_OVER
if game.vars.score > game.vars.high_score then
game.vars.high_score = game.vars.score
end
print("Game Over! Score: " .. game.vars.score)
end

View File

@@ -1,198 +1,222 @@
// DiceModalGame.h
#pragma once
#include "../../lib/game.h"
#include "../../display/low_level_render.h"
#include "../../display/low_level_gui.h"
#include "input_manager.h"
#include "MonopolyBoardRenderer.h"
#include "../../display/low_level_render.h"
#include "../../lib/game.h"
#include "ModalButtonHelper.h"
#include <stdlib.h>
#include "MonopolyBoardRenderer.h"
#include "input_manager.h"
#include <stdio.h>
#include <stdlib.h>
class DiceModalGame : public Game {
int dice1, dice2;
const BoardTile *from_tile, *to_tile;
Player* players;
int players_count;
bool dismissed;
int dice1, dice2;
const BoardTile *from_tile, *to_tile;
Player *players;
int players_count;
bool dismissed;
int from_pos;
int correct_destination;
int options[3];
bool option_visible[3];
int selected_choice;
bool show_error;
int from_pos;
int correct_destination;
int options[3];
bool option_visible[3];
int selected_choice;
bool show_error;
void draw_die(int x, int y, int size, int value) {
// Die base
renderer->draw_rounded_rectangle(x, y, size, size, 4, true, false);
// Shadow (offset 2,2)
renderer->draw_line(x + size, y + 2, x + size, y + size, true);
renderer->draw_line(x + 2, y + size, x + size, y + size, true);
void draw_die(int x, int y, int size, int value) {
// Die base
renderer->draw_rounded_rectangle(x, y, size, size, 4, true, false);
// Shadow (offset 2,2)
renderer->draw_line(x + size, y + 2, x + size, y + size, true);
renderer->draw_line(x + 2, y + size, x + size, y + size, true);
int dot_size = size / 6;
int m = size / 2;
int l = size / 4;
int r = 3 * size / 4;
int t = size / 4;
int b = 3 * size / 4;
int dot_size = size / 6;
int m = size / 2;
int l = size / 4;
int r = 3 * size / 4;
int t = size / 4;
int b = 3 * size / 4;
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);
};
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);
};
if (value % 2 == 1) draw_dot(m, m); // Center dot for 1, 3, 5
if (value > 1) {
draw_dot(l, t);
draw_dot(r, b);
}
if (value > 3) {
draw_dot(r, t);
draw_dot(l, b);
}
if (value == 6) {
draw_dot(l, m);
draw_dot(r, m);
}
if (value % 2 == 1)
draw_dot(m, m); // Center dot for 1, 3, 5
if (value > 1) {
draw_dot(l, t);
draw_dot(r, b);
}
if (value > 3) {
draw_dot(r, t);
draw_dot(l, b);
}
if (value == 6) {
draw_dot(l, m);
draw_dot(r, m);
}
}
public:
DiceModalGame(uint16_t width, uint16_t height, LowLevelRenderer* renderer, LowLevelGUI* gui, InputManager* input_manager, int d1, int d2, const BoardTile* from, const BoardTile* to, Player* p, int count)
: Game(width, height, renderer, gui, input_manager), dice1(d1), dice2(d2), from_tile(from), to_tile(to), players(p), players_count(count), dismissed(false) {
// Find from_pos
from_pos = 0;
for(int i=0; i<40; i++) {
if(&MONOPOLY_BOARD[i] == from_tile) {
from_pos = i;
break;
}
}
correct_destination = (from_pos + dice1 + dice2) % BOARD_SIZE;
selected_choice = -1;
show_error = false;
for(int i=0; i<3; i++) option_visible[i] = true;
DiceModalGame(uint16_t width, uint16_t height, LowLevelRenderer *renderer,
LowLevelGUI *gui, InputManager *input_manager, int d1, int d2,
const BoardTile *from, const BoardTile *to, Player *p,
int count)
: Game(width, height, renderer, gui, input_manager), dice1(d1), dice2(d2),
from_tile(from), to_tile(to), players(p), players_count(count),
dismissed(false) {
// Generate fake options
int fake1 = (from_pos + (rand() % 11 + 2)) % BOARD_SIZE;
if (fake1 == correct_destination) fake1 = (fake1 + 1) % BOARD_SIZE;
int fake2 = (from_pos + (rand() % 11 + 2)) % BOARD_SIZE;
while (fake2 == correct_destination || fake2 == fake1) {
fake2 = (from_pos + (rand() % 11 + 2)) % BOARD_SIZE;
}
// Find from_pos
from_pos = 0;
for (int i = 0; i < 40; i++) {
if (&MONOPOLY_BOARD[i] == from_tile) {
from_pos = i;
break;
}
}
correct_destination = (from_pos + dice1 + dice2) % MONOPOLY_BOARD_SIZE;
selected_choice = -1;
show_error = false;
for (int i = 0; i < 3; i++)
option_visible[i] = true;
int rand_pos = rand() % 3;
if (rand_pos == 0) {
options[0] = correct_destination;
options[1] = fake1;
options[2] = fake2;
} else if (rand_pos == 1) {
options[0] = fake1;
options[1] = correct_destination;
options[2] = fake2;
} else {
options[0] = fake1;
options[1] = fake2;
options[2] = correct_destination;
}
// Generate fake options
int fake1 = (from_pos + (rand() % 11 + 2)) % MONOPOLY_BOARD_SIZE;
if (fake1 == correct_destination)
fake1 = (fake1 + 1) % MONOPOLY_BOARD_SIZE;
int fake2 = (from_pos + (rand() % 11 + 2)) % MONOPOLY_BOARD_SIZE;
while (fake2 == correct_destination || fake2 == fake1) {
fake2 = (from_pos + (rand() % 11 + 2)) % MONOPOLY_BOARD_SIZE;
}
void init() override {
dismissed = false;
selected_choice = -1;
show_error = false;
for(int i=0; i<3; i++) option_visible[i] = true;
ModalButtonHelper::set_monopoly_regions(input_manager, width, height);
int rand_pos = rand() % 3;
if (rand_pos == 0) {
options[0] = correct_destination;
options[1] = fake1;
options[2] = fake2;
} else if (rand_pos == 1) {
options[0] = fake1;
options[1] = correct_destination;
options[2] = fake2;
} else {
options[0] = fake1;
options[1] = fake2;
options[2] = correct_destination;
}
Type get_type() const override { return Type::MONOPOLY_DICE; }
}
bool update(const InputEvent& event) override {
if (event.type == INPUT_BUTTON_0) { // Select
int start_choice = selected_choice;
do {
selected_choice = (selected_choice + 1) % 3;
} while (!option_visible[selected_choice] && selected_choice != start_choice);
show_error = false;
return true;
}
void init() override {
dismissed = false;
selected_choice = -1;
show_error = false;
for (int i = 0; i < 3; i++)
option_visible[i] = true;
ModalButtonHelper::set_monopoly_regions(input_manager, width, height);
}
Type get_type() const override { return Type::MONOPOLY_DICE; }
if (event.type == INPUT_BUTTON_1) {
if (selected_choice == -1 || !option_visible[selected_choice]) return false;
bool update(const InputEvent &event) override {
if (event.type == INPUT_BUTTON_0) { // Select
int start_choice = selected_choice;
do {
selected_choice = (selected_choice + 1) % 3;
} while (!option_visible[selected_choice] &&
selected_choice != start_choice);
if (options[selected_choice] == correct_destination) {
dismissed = true;
} else {
option_visible[selected_choice] = false;
show_error = true;
selected_choice = -1;
}
return true;
}
show_error = false;
return true;
}
if (event.type == INPUT_BUTTON_1) {
if (selected_choice == -1 || !option_visible[selected_choice])
return false;
if (options[selected_choice] == correct_destination) {
dismissed = true;
} else {
option_visible[selected_choice] = false;
show_error = true;
selected_choice = -1;
}
return true;
}
return false;
}
void draw() override {
renderer->clear_buffer();
// Draw the restricted board perimeter
MonopolyBoardRenderer::draw_board_perimeter(renderer, width, height, players, players_count, -1, -1, from_pos, (from_pos + 12) % BOARD_SIZE);
void draw() override {
renderer->clear_buffer();
// --- Inner UI (Center Area) ---
int cw = width / 7;
int ch = height / 7;
int ix = cw + 2;
int iy = ch + 2;
int iw = width - 2 * cw - 4;
int ih = height - 2 * ch - 4;
// Window background (White box)
renderer->draw_filled_rectangle(ix, iy, iw, ih, false, 0);
renderer->draw_rectangle(ix, iy, iw, ih, true, 2);
renderer->draw_rectangle(ix + 3, iy + 3, iw - 6, ih - 6, true, 1);
// Header
renderer->draw_filled_rectangle(ix + 4, iy + 4, iw - 8, 30, true, 1);
// Draw the restricted board perimeter
MonopolyBoardRenderer::draw_board_perimeter(
renderer, width, height, players, players_count, -1, -1, from_pos,
(from_pos + 12) % MONOPOLY_BOARD_SIZE);
// --- Inner UI (Center Area) ---
int cw = width / 7;
int ch = height / 7;
int ix = cw + 2;
int iy = ch + 2;
int iw = width - 2 * cw - 4;
int ih = height - 2 * ch - 4;
// Window background (White box)
renderer->draw_filled_rectangle(ix, iy, iw, ih, false, 0);
renderer->draw_rectangle(ix, iy, iw, ih, true, 2);
renderer->draw_rectangle(ix + 3, iy + 3, iw - 6, ih - 6, true, 1);
// Header
renderer->draw_filled_rectangle(ix + 4, iy + 4, iw - 8, 30, true, 1);
renderer->set_text_color(false);
renderer->draw_string_scaled(
ix + (iw - (int)strlen("DICE CHALLENGE") * 12) / 2, iy + 10,
"DICE CHALLENGE", 2);
renderer->set_text_color(true);
// Dice
int dice_size = 40;
int dice_y = iy + 45;
draw_die(ix + 20, dice_y, dice_size, dice1);
draw_die(ix + 20 + dice_size + 10, dice_y, dice_size, dice2);
// --- Prompt ---
int prompt_y = dice_y + dice_size + 10;
renderer->draw_string_scaled(ix + 20, prompt_y, "WHERE WILL YOU LAND?", 1);
// --- Options ---
int opt_y = prompt_y + 20;
for (int i = 0; i < 3; i++) {
if (!option_visible[i]) {
opt_y += 25;
continue;
}
char opt_buf[64];
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)
renderer->set_text_color(false);
renderer->draw_string_scaled(ix + (iw - (int)strlen("DICE CHALLENGE") * 12) / 2, iy + 10, "DICE CHALLENGE", 2);
renderer->set_text_color(true);
// Dice
int dice_size = 40;
int dice_y = iy + 45;
draw_die(ix + 20, dice_y, dice_size, dice1);
draw_die(ix + 20 + dice_size + 10, dice_y, dice_size, dice2);
// --- Prompt ---
int prompt_y = dice_y + dice_size + 10;
renderer->draw_string_scaled(ix + 20, prompt_y, "WHERE WILL YOU LAND?", 1);
// --- Options ---
int opt_y = prompt_y + 20;
for (int i = 0; i < 3; i++) {
if (!option_visible[i]) {
opt_y += 25;
continue;
}
char opt_buf[64];
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) renderer->set_text_color(false);
// Truncate name if too long
if (strlen(opt_buf) > 30) opt_buf[30] = '\0';
renderer->draw_string_scaled(ix + 20, opt_y, opt_buf, 2);
renderer->set_text_color(true);
opt_y += 25;
}
if (show_error) {
renderer->draw_string_scaled(ix + (iw - 10 * 12) / 2, iy + ih - 30, "TRY AGAIN!", 2);
}
ModalButtonHelper::draw_virtual_buttons(renderer, input_manager);
// Truncate name if too long
if (strlen(opt_buf) > 30)
opt_buf[30] = '\0';
renderer->draw_string_scaled(ix + 20, opt_y, opt_buf, 2);
renderer->set_text_color(true);
opt_y += 25;
}
bool is_dismissed() const { return dismissed; }
if (show_error) {
renderer->draw_string_scaled(ix + (iw - 10 * 12) / 2, iy + ih - 30,
"TRY AGAIN!", 2);
}
ModalButtonHelper::draw_virtual_buttons(renderer, input_manager);
}
bool is_dismissed() const { return dismissed; }
};

View File

@@ -6,154 +6,197 @@
class MonopolyBoardRenderer {
public:
static void draw_tile(LowLevelRenderer* renderer, int x, int y, int w, int h, int index, bool is_corner, Player* players, int players_count, int orientation = 0, int currentPlayerPos = -1, int observer_idx = -1) {
if (index < 0 || index >= BOARD_SIZE) return;
// Find owner
int owner_id = -1;
for (int i = 0; i < players_count; i++) {
for (int j = 0; j < players[i].property_count; j++) {
if (players[i].properties_owned[j] == index) {
owner_id = i;
break;
}
}
if (owner_id != -1) break;
}
bool isInverted = false;
if (observer_idx != -1) {
isInverted = (owner_id == observer_idx);
} else {
isInverted = (index == currentPlayerPos);
}
if (isInverted) {
renderer->draw_filled_rectangle(x, y, w, h, true, 1);
renderer->set_text_color(false); // Black text on white background
} else {
renderer->draw_rectangle(x, y, w, h, true, 1);
}
const BoardTile& tile = MONOPOLY_BOARD[index];
int content_x = x, content_y = y, content_w = w, content_h = h;
if (!is_corner && tile.type == TILE_PROPERTY) {
int bar_size = 10;
int bx = x, by = y, bw = w, bh = h;
if (orientation == 0) { // Bottom row (Bar on top)
bh = bar_size;
content_y += bar_size; content_h -= bar_size;
} else if (orientation == 1) { // Left column (Bar on right)
bx = x + w - bar_size; bw = bar_size;
content_w -= bar_size;
} else if (orientation == 2) { // Top row (Bar on bottom)
by = y + h - bar_size; bh = bar_size;
content_h -= bar_size;
} else if (orientation == 3) { // Right column (Bar on left)
bw = bar_size;
content_x += bar_size; content_w -= bar_size;
}
if (isInverted) {
// Background is white, so bar is black
renderer->draw_filled_rectangle(bx, by, bw, bh, false, 0);
renderer->set_text_color(true); // White text
} else {
renderer->draw_filled_rectangle(bx, by, bw, bh, true, 1);
renderer->set_text_color(false); // Black text
}
// Group number
char gbuf[2] = { (char)('0' + tile.group[0]), '\0' };
renderer->draw_string_scaled(bx + (bw - 6) / 2, by + (bh - 8) / 2, gbuf, 1);
if (isInverted) renderer->set_text_color(false);
else renderer->set_text_color(true);
}
char short_name[10] = {0};
int s_ptr = 0;
bool isCurrentPos = (index == currentPlayerPos && observer_idx != -1);
if (isCurrentPos) short_name[s_ptr++] = '-';
// Add * if owned by someone else
if (owner_id != -1 && observer_idx != -1 && owner_id != observer_idx) {
short_name[s_ptr++] = '*';
}
const char* full_name = tile.name;
if (is_corner) {
int len = strlen(full_name);
if (len > 3) len = 3;
for(int i=0; i<len; i++) short_name[s_ptr++] = full_name[i];
} else {
short_name[s_ptr++] = full_name[0];
const char* space = strchr(full_name, ' ');
if (space && space[1] != '\0') short_name[s_ptr++] = space[1];
}
if (isCurrentPos) short_name[s_ptr++] = '-';
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;
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
int p_count = 0;
for (int i = 0; i < players_count; ++i) {
if (players[i].position == index) {
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);
p_count++;
}
}
if (isInverted) {
renderer->set_text_color(true);
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) {
if (index < 0 || index >= MONOPOLY_BOARD_SIZE)
return;
// Find owner
int owner_id = -1;
for (int i = 0; i < players_count; i++) {
for (int j = 0; j < players[i].property_count; j++) {
if (players[i].properties_owned[j] == index) {
owner_id = i;
break;
}
}
if (owner_id != -1)
break;
}
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 ch = height / 7; // Corner height
int rw = (width - 2 * cw) / 9; // Regular tile width
int rh = (height - 2 * ch) / 9; // Regular tile height
auto should_draw = [&](int index) {
if (limit_start == -1 || limit_end == -1) return true;
if (limit_start <= limit_end) {
return index >= limit_start && index <= limit_end;
} else {
return index >= limit_start || index <= limit_end;
}
};
// --- 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
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(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) ---
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);
}
// --- 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
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(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) ---
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);
}
bool isInverted = false;
if (observer_idx != -1) {
isInverted = (owner_id == observer_idx);
} else {
isInverted = (index == currentPlayerPos);
}
if (isInverted) {
renderer->draw_filled_rectangle(x, y, w, h, true, 1);
renderer->set_text_color(false); // Black text on white background
} else {
renderer->draw_rectangle(x, y, w, h, true, 1);
}
const BoardTile &tile = MONOPOLY_BOARD[index];
int content_x = x, content_y = y, content_w = w, content_h = h;
if (!is_corner && tile.type == TILE_PROPERTY) {
int bar_size = 10;
int bx = x, by = y, bw = w, bh = h;
if (orientation == 0) { // Bottom row (Bar on top)
bh = bar_size;
content_y += bar_size;
content_h -= bar_size;
} else if (orientation == 1) { // Left column (Bar on right)
bx = x + w - bar_size;
bw = bar_size;
content_w -= bar_size;
} else if (orientation == 2) { // Top row (Bar on bottom)
by = y + h - bar_size;
bh = bar_size;
content_h -= bar_size;
} else if (orientation == 3) { // Right column (Bar on left)
bw = bar_size;
content_x += bar_size;
content_w -= bar_size;
}
if (isInverted) {
// Background is white, so bar is black
renderer->draw_filled_rectangle(bx, by, bw, bh, false, 0);
renderer->set_text_color(true); // White text
} else {
renderer->draw_filled_rectangle(bx, by, bw, bh, true, 1);
renderer->set_text_color(false); // Black text
}
// Group number
char gbuf[2] = {(char)('0' + tile.group[0]), '\0'};
renderer->draw_string_scaled(bx + (bw - 6) / 2, by + (bh - 8) / 2, gbuf,
1);
if (isInverted)
renderer->set_text_color(false);
else
renderer->set_text_color(true);
}
char short_name[10] = {0};
int s_ptr = 0;
bool isCurrentPos = (index == currentPlayerPos && observer_idx != -1);
if (isCurrentPos)
short_name[s_ptr++] = '-';
// Add * if owned by someone else
if (owner_id != -1 && observer_idx != -1 && owner_id != observer_idx) {
short_name[s_ptr++] = '*';
}
const char *full_name = tile.name;
if (is_corner) {
int len = strlen(full_name);
if (len > 3)
len = 3;
for (int i = 0; i < len; i++)
short_name[s_ptr++] = full_name[i];
} else {
short_name[s_ptr++] = full_name[0];
const char *space = strchr(full_name, ' ');
if (space && space[1] != '\0')
short_name[s_ptr++] = space[1];
}
if (isCurrentPos)
short_name[s_ptr++] = '-';
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;
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
int p_count = 0;
for (int i = 0; i < players_count; ++i) {
if (players[i].position == index) {
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);
p_count++;
}
}
if (isInverted) {
renderer->set_text_color(true);
}
}
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 ch = height / 7; // Corner height
int rw = (width - 2 * cw) / 9; // Regular tile width
int rh = (height - 2 * ch) / 9; // Regular tile height
auto should_draw = [&](int index) {
if (limit_start == -1 || limit_end == -1)
return true;
if (limit_start <= limit_end) {
return index >= limit_start && index <= limit_end;
} else {
return index >= limit_start || index <= limit_end;
}
};
// --- 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
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(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) ---
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);
}
// --- 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
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(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) ---
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);
}
}
};

File diff suppressed because it is too large Load Diff

View File

@@ -5,72 +5,232 @@
#include <stdbool.h>
typedef enum {
TILE_GO,
TILE_PROPERTY,
TILE_COMMUNITY_CHEST,
TILE_TAX,
TILE_RAILROAD,
TILE_CHANCE,
TILE_JAIL,
TILE_UTILITY,
TILE_FREE_PARKING,
TILE_GO_TO_JAIL
TILE_GO,
TILE_PROPERTY,
TILE_COMMUNITY_CHEST,
TILE_TAX,
TILE_RAILROAD,
TILE_CHANCE,
TILE_JAIL,
TILE_UTILITY,
TILE_FREE_PARKING,
TILE_GO_TO_JAIL
} TileType;
typedef struct {
const char* name;
TileType type;
bool is_corner;
int cost; // 0 if not applicable
const char* color; // Hex string, NULL if not property
int rent[6]; // Base, 1H, 2H, 3H, 4H, Hotel
int group[3]; // Group ID, Position in group, Total in group
int house_cost; // Cost to build
const char *name;
TileType type;
bool is_corner;
int cost; // 0 if not applicable
const char *color; // Hex string, NULL if not property
int rent[6]; // Base, 1H, 2H, 3H, 4H, Hotel
int group[3]; // Group ID, Position in group, Total in group
int house_cost; // Cost to build
} 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},
{"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},
{"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},
{"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},
{"Vermont Avenue", 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},
{"Vermont Avenue",
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},
{"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},
{"States Avenue", TILE_PROPERTY, false, 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},
{"States Avenue",
TILE_PROPERTY,
false,
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},
{"Tennessee Avenue", 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},
{"Tennessee Avenue",
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},
{"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},
{"Indiana Avenue", 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},
{"Indiana Avenue",
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},
{"Atlantic Avenue", 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},
{"Atlantic Avenue",
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},
{"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},
{"Pacific Avenue", 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},
{"Pacific Avenue",
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},
{"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},
{"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},
{"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

File diff suppressed because it is too large Load Diff

View File

@@ -27,8 +27,9 @@ static bool ft6336u_read_reg(uint8_t reg, uint8_t *value) {
continue;
}
// Add delay after write, before read (like Arduino library does)
sleep_us(10000); // 10ms delay
// Short settle time between register address write and read.
// 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);
if (result == 1) {
@@ -50,8 +51,9 @@ static bool ft6336u_read_regs(uint8_t reg, uint8_t *buf, size_t len) {
continue;
}
// Add delay after write, before read (like Arduino library does)
sleep_us(10000); // 10ms delay
// Short settle time between register address write and read.
// 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);
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");
}
// 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
printf("[FT6336U] Configuring gesture detection parameters...\n");

View File

@@ -129,6 +129,7 @@ bool GameLauncher::update(const InputEvent& event) {
if (event.y >= y - 5 && event.y < y + MENU_ITEM_HEIGHT - 5) {
// Game selected - create instance
printf("Selected game: %s\n", games[i].name);
selected_index = i;
selected_game = games[i].factory(width, height, renderer, gui, input_manager);
if (selected_game) {
selected_game->init();
@@ -298,6 +299,30 @@ bool GameLauncher::select_game_by_name(const char* 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 {
if (games.empty()) return 1;
return (games.size() + GAMES_PER_PAGE - 1) / GAMES_PER_PAGE;

View File

@@ -97,6 +97,17 @@ public:
*/
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:
uint16_t width;
uint16_t height;

View File

@@ -10,7 +10,6 @@
// External interrupt flags from basic1.cpp
extern volatile bool touch_interrupt_flag;
extern volatile bool touch_event_down;
extern volatile bool button_key0_pressed;
extern volatile bool button_key1_pressed;
@@ -41,78 +40,87 @@ bool InputManager::has_buttons() const {
InputEvent InputManager::process_touch_input(uint32_t* last_time) {
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
if (!touch_interrupt_flag) {
// Process immediately on IRQ, and continue sampling while a touch is active.
// Some controllers only IRQ on edge transitions, so move events require polling.
if (!touch_interrupt_flag && *last_time == 0) {
return event; // No touch event
}
printf("Processing touch: flag=%d, event_down=%d\n", touch_interrupt_flag, touch_event_down);
// Don't clear the flag yet - we may still be processing continuous touch
// Check if touch is active
if (!touch_event_down) {
// Touch released - reset timing for next touch
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;
// 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;
}
}
// Touch is down - check debounce timing
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
// Always validate via controller state instead of relying on edge flag alone.
// Edge chatter can flip touch_event_down without a real touch transition.
TouchData touch_data;
if (!touch || !touch->read_touch(&touch_data)) {
// Clear flag even if read failed to prevent getting stuck
last_touch_sample_ms = now;
touch_interrupt_flag = false;
printf("Touch read FAILED\n");
return event; // Read failed
return event;
}
// Clear the interrupt flag after successfully reading touch data
// This allows the next touch interrupt to be detected
last_touch_sample_ms = now;
touch_interrupt_flag = false;
printf("Touch DOWN at (%d,%d)\n", touch_data.points[0].x, touch_data.points[0].y);
// Populate event structure
// 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;
// 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.y = touch_data.points[0].y;
event.pressure = touch_data.points[0].pressure;
event.gesture_code = touch_data.gesture;
event.valid = true;
// Determine event type
if (*last_time == 0) {
event.type = INPUT_TOUCH_DOWN;
// Check for virtual buttons
InputType virtual_type;
if (check_virtual_buttons(event.x, event.y, virtual_type)) {
event.type = virtual_type;
event.button_id = (virtual_type == INPUT_BUTTON_0) ? 0 : 1;
printf("Virtual button %d pressed via touch\n", event.button_id);
if (config->debug_verbose) {
printf("Virtual button %d pressed via touch\n", event.button_id);
}
}
} else {
event.type = INPUT_TOUCH_MOVE;
}
// Handle gesture events
if (config->enable_gestures && touch_data.gesture != 0) {
event.type = INPUT_GESTURE;
if (config->debug_verbose) {
printf("Gesture: 0x%02X (%s)\n", event.gesture_code, get_gesture_name(event.gesture_code));
}
}
*last_time = now;
return event;
}

View File

@@ -37,9 +37,9 @@ public:
InputManager(LowLevelTouch* touch, const GameConfig* config);
/**
* @brief Process touch input from controller
* @param last_time Pointer to last touch timestamp for debouncing
* @return InputEvent (valid=false if no valid input)
* @brief Process touch input using hybrid IRQ + active-session sampling.
* @param last_time Pointer to last touch timestamp (0 means no active touch session)
* @return InputEvent (valid=false if no state change/update is emitted)
*/
InputEvent process_touch_input(uint32_t* last_time);
@@ -97,6 +97,11 @@ public:
private:
LowLevelTouch* touch;
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
int v_button_a[4] = {0, 0, 0, 0}; // [x, y, w, h]

45
lib/scene_stack.h Normal file
View File

@@ -0,0 +1,45 @@
#ifndef SCENE_STACK_H
#define SCENE_STACK_H
#include <vector>
enum class SceneId {
LAUNCHER = 0,
GAME,
IN_GAME_MENU
};
class SceneStack {
public:
SceneStack() {
stack.push_back(SceneId::LAUNCHER);
}
SceneId current() const {
return stack.empty() ? SceneId::LAUNCHER : stack.back();
}
bool is(SceneId scene) const {
return current() == scene;
}
void push(SceneId scene) {
stack.push_back(scene);
}
void pop() {
if (stack.size() > 1) {
stack.pop_back();
}
}
void clear_to_launcher() {
stack.clear();
stack.push_back(SceneId::LAUNCHER);
}
private:
std::vector<SceneId> stack;
};
#endif // SCENE_STACK_H

View File

@@ -5,6 +5,7 @@
#include "sd_card.h"
#include "hardware/gpio.h"
#include "board_config.h"
#include "shared_spi_bus.h"
#include "ff.h" // FatFS
#include <string.h>
#include <stdio.h>
@@ -430,7 +431,10 @@ bool sd_card_init_with_board_config(void) {
uint sd_card_set_spi_speed(void) {
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
uint current_speed = spi_get_baudrate(g_config->spi);
spi_set_baudrate(g_config->spi, 12500 * 1000); // 12.5 MHz for SD card
@@ -438,8 +442,18 @@ uint sd_card_set_spi_speed(void) {
}
void sd_card_restore_spi_speed(uint baudrate) {
if (!g_config || baudrate == 0) return;
spi_set_baudrate(g_config->spi, baudrate);
if (!g_config) {
shared_spi_bus_unlock();
return;
}
if (baudrate != 0) {
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) {

View File

@@ -173,7 +173,7 @@ bool SerialUploader::complete_launch() {
return launched;
}
bool SerialUploader::process() {
bool SerialUploader::process(bool spi_busy) {
if (state == IDLE) {
// Check for "UPLOAD" command
int c = getchar_timeout_us(0);
@@ -311,6 +311,9 @@ bool SerialUploader::process() {
}
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

View File

@@ -12,7 +12,8 @@ public:
// Process incoming serial data (call this frequently in main loop)
// Returns true if a game was launched
bool process();
// 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; }

65
lib/shared_spi_bus.c Normal file
View File

@@ -0,0 +1,65 @@
#include "shared_spi_bus.h"
#include "pico/mutex.h"
#include "pico/multicore.h"
#include <stdbool.h>
static mutex_t g_spi_bus_mutex;
static bool g_spi_bus_initialized = false;
static uint32_t g_core_depth[2] = {0, 0};
void shared_spi_bus_init(void) {
if (g_spi_bus_initialized) {
return;
}
mutex_init(&g_spi_bus_mutex);
g_spi_bus_initialized = true;
}
void shared_spi_bus_lock(void) {
if (!g_spi_bus_initialized) {
shared_spi_bus_init();
}
int core = get_core_num();
if (core < 0 || core > 1) {
core = 0;
}
// Re-entrant lock on same core (needed for nested SD helpers).
if (g_core_depth[core] > 0) {
g_core_depth[core]++;
return;
}
mutex_enter_blocking(&g_spi_bus_mutex);
g_core_depth[core] = 1;
}
void shared_spi_bus_unlock(void) {
if (!g_spi_bus_initialized) {
return;
}
int core = get_core_num();
if (core < 0 || core > 1) {
core = 0;
}
if (g_core_depth[core] == 0) {
return;
}
g_core_depth[core]--;
if (g_core_depth[core] == 0) {
mutex_exit(&g_spi_bus_mutex);
}
}
void shared_spi_bus_force_recover(void) {
// Used after Core1 reset to avoid stale mutex/depth state.
mutex_init(&g_spi_bus_mutex);
g_core_depth[0] = 0;
g_core_depth[1] = 0;
g_spi_bus_initialized = true;
}

18
lib/shared_spi_bus.h Normal file
View File

@@ -0,0 +1,18 @@
#ifndef SHARED_SPI_BUS_H
#define SHARED_SPI_BUS_H
#ifdef __cplusplus
extern "C" {
#endif
// Cross-core SPI bus lock for shared SD/display SPI usage.
void shared_spi_bus_init(void);
void shared_spi_bus_lock(void);
void shared_spi_bus_unlock(void);
void shared_spi_bus_force_recover(void);
#ifdef __cplusplus
}
#endif
#endif // SHARED_SPI_BUS_H

View File

@@ -1,528 +0,0 @@
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (215,122)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (203,137)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (312,131)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (355,140)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (376,273)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (357,277)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (323,258)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (321,261)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (89,291)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (327,264)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (315,250)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (262,151)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (319,150)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
Long press detected - returning to launcher
Launcher reset - returning to menu
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (358,157)
Touch at (358,157) in launcher
Selected game: Snake Game
Lua bindings registered
LuaGame Error [load script]: [string "/games/SNAKE.LUA"]:113: unexpected symbol near ','
LuaGame: Failed to load /games/SNAKE.LUA: load script: [string "/games/SNAKE.LUA"]:113: unexpected symbol near ','
Game launched successfully
INT: RISE
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (164,97)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (194,215)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (288,289)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (285,169)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (255,117)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (168,100)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
Long press detected - returning to launcher
Launcher reset - returning to menu
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (424,264)
Touch at (424,264) in launcher
INT: RISE
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (389,237)
INT: RISE
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (392,247)
INT: RISE
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (399,249)
INT: RISE
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (138,49)
INT: RISE
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (125,111)
INT: RISE
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (80,146)
INT: RISE
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (125,37)
INT: RISE
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (100,73)
INT: RISE
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (82,154)
INT: RISE
INT: FALL
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
INT: RISE
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (120,255)
Touch at (120,255) in launcher
INT: RISE
INT: FALL
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
INT: RISE
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (191,125)
Touch at (191,125) in launcher
Selected game: Touch Counter
Lua bindings registered
LuaGame Error [load script]: [string "/games/COUNTER.LUA"]:36: unexpected symbol near ','
LuaGame: Failed to load /games/COUNTER.LUA: load script: [string "/games/COUNTER.LUA"]:36: unexpected symbol near ','
Game launched successfully
INT: RISE
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (165,106)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (121,224)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (253,160)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (258,158)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (256,156)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (255,154)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (295,161)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (296,157)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
INT: RISE
Touch DOWN at (477,202)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (236,221)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (338,182)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
TFT: Dimmed to 5%
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (306,162)
TFT: Restored brightness
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (338,116)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (208,258)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (232,55)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (37,160)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (224,208)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (53,150)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (225,239)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (247,231)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (152,187)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (426,157)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (198,272)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (384,191)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (44,195)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (283,266)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (50,215)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (287,265)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (189,275)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (421,142)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (43,192)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (390,155)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (48,204)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (258,58)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (275,284)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (23,202)
LuaGame Error [draw]: [string "/games/2048.LUA"]:145: bad argument #1 to 'rect' (number has no integer representation)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (173,173)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (121,151)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (71,119)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (220,109)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (313,124)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (235,140)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (131,185)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (269,54)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP
INT: FALL
Processing touch: flag=1, event_down=1
Touch DOWN at (227,165)
INT: RISE
Processing touch: flag=1, event_down=0
Touch UP