From 2a6861fdf5b896102e768fa70396478a391d97b2 Mon Sep 17 00:00:00 2001 From: Adolfo Reyna Date: Fri, 30 Jan 2026 21:33:42 -0500 Subject: [PATCH] refactored to multiple games implementation --- CMakeLists.txt | 4 + MultigameRefactorPlan.md | 155 +++++++++++ TESTING_CHECKLIST.md | 321 ++++++++++++++++++++++ basic1.cpp | 555 ++------------------------------------- board_config.h | 4 +- games/tic_tac_toe.cpp | 304 +++++++++++++++++++++ games/tic_tac_toe.h | 78 ++++++ lib/game.h | 82 ++++++ lib/input_event.h | 34 +++ lib/input_manager.cpp | 154 +++++++++++ lib/input_manager.h | 64 +++++ 11 files changed, 1216 insertions(+), 539 deletions(-) create mode 100644 TESTING_CHECKLIST.md create mode 100644 games/tic_tac_toe.cpp create mode 100644 games/tic_tac_toe.h create mode 100644 lib/game.h create mode 100644 lib/input_event.h create mode 100644 lib/input_manager.cpp create mode 100644 lib/input_manager.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 827d1b9..628df30 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,6 +41,8 @@ pico_sdk_init() add_executable(basic1 basic1.cpp + lib/input_manager.cpp + games/tic_tac_toe.cpp lib/st7796/st7796.c lib/ft6336u/ft6336u.c lib/sd_card/sd_card.c @@ -75,6 +77,8 @@ target_link_libraries(basic1 # Add the standard include files to the build target_include_directories(basic1 PRIVATE ${CMAKE_CURRENT_LIST_DIR} + ${CMAKE_CURRENT_LIST_DIR}/games + ${CMAKE_CURRENT_LIST_DIR}/lib ${CMAKE_CURRENT_LIST_DIR}/lib/fatfs/source ${CMAKE_CURRENT_LIST_DIR}/lib/st7796 ${CMAKE_CURRENT_LIST_DIR}/lib/ft6336u diff --git a/MultigameRefactorPlan.md b/MultigameRefactorPlan.md index e69de29..df4681c 100644 --- a/MultigameRefactorPlan.md +++ b/MultigameRefactorPlan.md @@ -0,0 +1,155 @@ +Multigame Migration Plan +Overview +Refactor the existing monolithic tic-tac-toe implementation into a modular game architecture that allows multiple games to run on the RP2350 platform while preserving the interrupt-driven, dual-core reactive architecture. + +Goals +Abstract game logic from hardware/display management +Create reusable input processing system +Enable multiple games to coexist in the codebase +Maintain the efficient reactive architecture (event-driven, dual-core) +Keep hardware-specific code isolated in basic1.cpp +Architecture Components +1. Input Event System +Files: lib/input_event.h + +Defines shared input event structures used by both InputManager and Game classes: + +InputType enum (NONE, TOUCH_DOWN, TOUCH_MOVE, TOUCH_UP, BUTTON_0, BUTTON_1, GESTURE) +InputEvent struct with coordinates, gesture codes, button IDs, pressure, validity flag +2. Input Manager +Files: lib/input_manager.h, lib/input_manager.cpp + +Handles all input processing and debouncing: + +Constructor: InputManager(LowLevelTouch* touch, GameConfig* config) +Methods: process_touch_input(uint32_t* last_time), process_button_input() +Returns InputEvent objects to be passed to games +Manages touch debouncing and gesture recognition +Does NOT handle button GPIO setup (stays in basic1.cpp) +3. Abstract Game Base Class +Files: lib/game.h + +Defines the interface all games must implement: + +Constructor: Game(uint16_t width, uint16_t height, LowLevelRenderer* renderer, LowLevelGUI* gui) +Protected members: width, height, renderer, gui +Pure virtual methods: +virtual void init() = 0 - Initialize game state +virtual bool update(const InputEvent& event) = 0 - Process input, return true if redraw needed +virtual void draw() = 0 - Render game to buffer +Virtual destructor: virtual ~Game() {} +4. Tic-Tac-Toe Game Implementation +Files: games/tic_tac_toe.h, games/tic_tac_toe.cpp + +Concrete implementation of the Game interface: + +Private GameState struct with board, current player, winner, statistics +Private method: check_winner() +Private constants: BOARD_SIZE, CELL_SIZE, BOARD_Y, LINE_WIDTH +Overrides: init(), update(const InputEvent& event), draw() +Encapsulates all tic-tac-toe logic extracted from basic1.cpp lines 162-770 +5. Main Loop Refactor +File: basic1.cpp + +Simplified main program focusing on hardware management: + +Keep: Display initialization, dual-core setup, interrupt handlers, GPIO configuration +Remove: Game-specific logic (moves to TicTacToeGame) +Add: InputManager instantiation, Game* pointer instantiation +Modify main loop: +Migration Steps +Step 1: Create Input Event Header +Extract input structures from basic1.cpp (lines 134-156) to new file lib/input_event.h. + +Validation: + +Compile successfully +No duplicate definitions +Both basic1.cpp and new files include this header +Step 2: Create Input Manager +Move input processing functions (lines 313-442) to new InputManager class. + +Validation: + +Constructor properly stores touch pointer and config +process_touch_input() returns valid InputEvent on touch +process_button_input() returns valid InputEvent on button press +Debouncing still works correctly +Compile and run - verify touch/button detection unchanged +Step 3: Create Abstract Game Class +Create lib/game.h with base class definition. + +Validation: + +Header compiles successfully +Pure virtual methods defined correctly +Protected members accessible to derived classes +Step 4: Extract Tic-Tac-Toe Game +Create TicTacToeGame class inheriting from Game. + +Validation: + +Move GameState struct (lines 162-176) - verify all members present +Move check_winner() (lines 447-495) - verify logic identical +Move game_init() to init() override (lines 505-522) +Move game_update() to update() override (lines 536-668) +Move game_draw() to draw() override (lines 671-770) +Move constants: BOARD_SIZE, CELL_SIZE, BOARD_Y +Compile successfully +Step 5: Refactor Main Loop +Update basic1.cpp to use new architecture. + +Validation: + +Remove old GameState state variable +Remove old game functions +Instantiate InputManager after hardware init +Instantiate TicTacToeGame as Game* +Update main loop to use polymorphic calls +Compile successfully +Run and verify: Touch input works, buttons work, game plays correctly +Verify dual-core refresh still works (Core 1 handles display) +Test full game: place pieces, win conditions, restart +Step 6: Update Build System +Modify CMakeLists.txt to include new source files. + +Validation: + +Add lib/input_manager.cpp +Add games/tic_tac_toe.cpp +Clean build succeeds +Binary size reasonable (no major bloat) +Step 7: Final Testing +Comprehensive system test. + +Validation: + +Touch detection works (tap cells) +Button navigation works (KEY0/KEY1) +Game logic correct (wins, ties, restart) +Display refreshes properly (no artifacts) +Statistics persist across games +Debug output shows correct events +System remains responsive during e-ink refresh +Future Extensions +Adding New Games +To add a new game (e.g., Snake, Pong): + +Create games/snake.h and games/snake.cpp +Inherit from Game base class +Implement init(), update(), draw() +Define game-specific state as private members +Add to CMakeLists.txt +Change game selection in basic1.cpp: Game* current_game = new SnakeGame(...) +Game Launcher (Future) +Create GameLauncher class to display menu +Allow runtime game switching +Handle cleanup: delete current_game; current_game = new SnakeGame(...) +Store game selection preference in flash +Key Design Principles +Hardware Isolation: All GPIO, interrupts, hardware config stay in basic1.cpp +State Encapsulation: Each game owns its state completely +Polymorphism: Use Game* pointer for runtime flexibility +Event-Driven: Games respond to InputEvent objects only +Reactive Rendering: Only redraw when update() returns true +Dual-Core Efficiency: Core 0 handles logic, Core 1 handles display refresh \ No newline at end of file diff --git a/TESTING_CHECKLIST.md b/TESTING_CHECKLIST.md new file mode 100644 index 0000000..2afa65b --- /dev/null +++ b/TESTING_CHECKLIST.md @@ -0,0 +1,321 @@ +# Step 7: Final Testing Checklist +## Refactored Tic-Tac-Toe Game - Hardware Validation + +**Date:** January 30, 2026 +**Status:** Compilation successful ✓ + +--- + +## Pre-Test Verification + +### Build Artifacts +- [x] Compiles without errors +- [x] Compiles without warnings +- [x] basic1.uf2 generated successfully +- [x] All new modules included in build + +### Architecture Components +- [x] InputManager class (lib/input_manager.cpp) +- [x] Game base class (lib/game.h) +- [x] TicTacToeGame class (games/tic_tac_toe.cpp) +- [x] Input events (lib/input_event.h) +- [x] Main loop refactored (basic1.cpp) + +--- + +## Hardware Testing Plan + +### Test 1: Basic System Startup +**Expected Behavior:** +- USB serial connection establishes +- Debug messages appear: + - "=== [BOARD_NAME] Demo ===" + - "Core 1 started - handling display refreshes" + - "Touch initialized successfully" + - Initial game board renders + +**Validation:** +- [ ] Serial output shows startup messages +- [ ] Both cores start successfully +- [ ] Display shows Tic-Tac-Toe board +- [ ] No system hangs or crashes + +--- + +### Test 2: Touch Input Detection +**Test Steps:** +1. Tap empty cell in top-left corner +2. Tap empty cell in center +3. Tap empty cell in bottom-right corner +4. Tap already occupied cell +5. Tap outside board area + +**Expected Behavior:** +- Debug output: "Processing touch: flag=1" +- Debug output: "Touch DOWN at (x,y)" +- X or O appears in tapped cell +- Occupied cells reject new pieces +- Touches outside board are ignored +- Turn indicator updates on left side + +**Validation:** +- [ ] Touch interrupts fire correctly +- [ ] Coordinates map to correct cells +- [ ] Pieces appear in touched cells +- [ ] Turn switches between X and O +- [ ] Invalid moves rejected +- [ ] Debug messages show correct coordinates + +--- + +### Test 3: Button Navigation (e-ink boards) +**Test Steps:** +1. Press KEY0 to move selection +2. Press KEY0 multiple times to cycle cells +3. Press KEY1 to place piece in selected cell +4. Verify selection skips occupied cells + +**Expected Behavior:** +- KEY0: Selection highlight moves to next cell +- KEY1: Piece placed at highlighted cell +- Selection automatically skips occupied cells +- Debug output: "Selection moved to [row,col]" + +**Validation:** +- [ ] KEY0 moves selection correctly +- [ ] KEY1 places piece at selection +- [ ] Selection visual feedback works +- [ ] Button debouncing functions +- [ ] Debug messages show correct events + +--- + +### Test 4: Win Condition Detection +**Test Scenarios:** + +#### Row Win (X) +- Tap cells: [0,0], [1,0], [0,1], [1,1], [0,2] +- Expected: "X WINS!" message + +#### Column Win (O) +- Tap cells: [0,0], [0,1], [1,0], [0,2], [2,1], [0,3] +- Expected: "O WINS!" message (after X's move) + +#### Diagonal Win (X) +- Tap cells: [0,0], [0,1], [1,1], [0,2], [2,2] +- Expected: "X WINS!" message + +#### Tie Game +- Fill all cells with no winner +- Expected: "TIE GAME!" message + +**Validation:** +- [ ] Row wins detected correctly +- [ ] Column wins detected correctly +- [ ] Diagonal wins detected correctly +- [ ] Tie games detected correctly +- [ ] Game over message displays +- [ ] No further moves accepted after win + +--- + +### Test 5: Game Restart +**Test Steps:** +1. Complete a game (win or tie) +2. Touch screen or press KEY0 +3. Verify board clears +4. Verify X starts first +5. Play another game + +**Expected Behavior:** +- Board clears to empty state +- X always starts after restart +- Turn indicator resets +- Previous game statistics preserved +- New game begins immediately + +**Validation:** +- [ ] Touch restarts game +- [ ] KEY0 restarts game +- [ ] Board completely clears +- [ ] X starts correctly +- [ ] Game flows normally after restart + +--- + +### Test 6: Statistics Tracking +**Test Steps:** +1. Play game where X wins +2. Check statistics: "X:1 O:0 Tie:0" +3. Restart and let O win +4. Check statistics: "X:1 O:1 Tie:0" +5. Play to tie +6. Check statistics: "X:1 O:1 Tie:1" +7. Verify move counter increments + +**Expected Behavior:** +- Win counts persist across restarts +- Tie count tracks correctly +- Move counter shows total moves +- Statistics display at bottom + +**Validation:** +- [ ] X wins increment correctly +- [ ] O wins increment correctly +- [ ] Ties increment correctly +- [ ] Move counter accurate +- [ ] Stats persist across restarts +- [ ] Display shows current stats + +--- + +### Test 7: Display Refresh (Dual-Core) +**Test Steps:** +1. Make move and observe refresh +2. Make rapid successive moves +3. During e-ink refresh, attempt another move +4. Verify Core 0 stays responsive + +**Expected Behavior:** +- Display refreshes after each move +- Core 1 handles refresh asynchronously +- Core 0 processes inputs during refresh +- Pending refresh queues if Core 1 busy +- Debug message: "Refresh pending - Core 1 still busy" (if applicable) + +**Validation:** +- [ ] Display updates after moves +- [ ] No screen artifacts +- [ ] System responsive during refresh +- [ ] E-ink refresh completes fully +- [ ] Dual-core architecture working +- [ ] No refresh race conditions + +--- + +### Test 8: Edge Cases & Error Handling +**Test Scenarios:** + +#### Rapid Touch Events +- Tap multiple cells rapidly +- Expected: All valid moves registered + +#### Touch During Transition +- Touch during display refresh +- Expected: Input queued, processed after refresh + +#### Multiple Button Presses +- Press both KEY0 and KEY1 simultaneously +- Expected: One action processed (KEY0 priority) + +#### Touch Release Events +- Touch and hold, then release +- Expected: Debug shows "Touch UP" + +**Validation:** +- [ ] Rapid touches handled correctly +- [ ] No missed inputs +- [ ] No duplicate moves +- [ ] Touch up events processed +- [ ] System remains stable + +--- + +### Test 9: Memory & Performance +**Observations:** +- Monitor for memory leaks (play 10+ games) +- Check for display artifacts over time +- Verify system doesn't slow down +- Confirm no crashes after extended use + +**Validation:** +- [ ] No memory leaks observed +- [ ] Consistent performance over time +- [ ] No progressive slowdown +- [ ] Display quality maintained +- [ ] System stable after 10+ games + +--- + +### Test 10: Code Architecture Validation +**Verification:** +- InputManager handles all input processing +- TicTacToeGame encapsulates game logic +- basic1.cpp only handles hardware/main loop +- Polymorphic Game pointer works correctly +- All extern variables resolve correctly + +**Validation:** +- [ ] InputManager processes touch correctly +- [ ] InputManager processes buttons correctly +- [ ] Game class polymorphism works +- [ ] Clean separation of concerns +- [ ] No tight coupling between modules + +--- + +## Success Criteria + +### Must Pass (Critical) +1. System boots and initializes +2. Touch input registers moves +3. Win conditions detect correctly +4. Game restarts properly +5. Display refreshes without artifacts + +### Should Pass (Important) +1. Button navigation works (if board has buttons) +2. Statistics track accurately +3. Debug output shows correct events +4. Dual-core refresh is non-blocking +5. Edge cases handled gracefully + +### Nice to Have (Enhancement) +1. Gesture recognition works +2. No performance degradation over time +3. Clean debug output formatting +4. Smooth user experience + +--- + +## Issue Tracking + +### Found Issues +| Issue | Severity | Description | Status | +|-------|----------|-------------|--------| +| | | | | + +### Notes +- +- +- + +--- + +## Test Results Summary + +**Date Tested:** _________________ +**Tester:** _________________ +**Hardware:** _________________ + +**Overall Status:** ⬜ PASS / ⬜ FAIL / ⬜ PARTIAL + +**Critical Issues:** _________________ + +**Recommendations:** _________________ + +--- + +## Next Steps + +If all tests pass: +- ✅ Architecture successfully refactored +- ✅ Ready to add new games (Snake, Pong, etc.) +- ✅ Can implement GameLauncher for game selection + +If issues found: +1. Document specific failing test +2. Check relevant module (InputManager, TicTacToeGame, or main loop) +3. Verify hardware connections (touch INT pin, button pins) +4. Review debug serial output for clues +5. Test with minimal configuration first diff --git a/basic1.cpp b/basic1.cpp index 507b045..df70f56 100644 --- a/basic1.cpp +++ b/basic1.cpp @@ -44,10 +44,12 @@ #include #include #include "display/low_level_render.h" -#include "display/low_level_gui.h" #include "display/low_level_display.h" #include "display/low_level_display_epaper.h" #include "display/low_level_touch.h" +#include "input_manager.h" +#include "game.h" +#include "tic_tac_toe.h" // Binary info for RP2350 - ensures proper boot image structure @@ -130,52 +132,9 @@ bool is_refresh_in_progress() { } // ============================================================================ -// INPUT EVENT STRUCTURES +// GAME CONFIGURATION // ============================================================================ -// Input event types -enum InputType { - INPUT_NONE = 0, - INPUT_TOUCH_DOWN, - INPUT_TOUCH_MOVE, - INPUT_TOUCH_UP, - INPUT_BUTTON_0, - INPUT_BUTTON_1, - INPUT_GESTURE -}; - -// Unified input event structure -struct InputEvent { - InputType type; - int16_t x; - int16_t y; - uint8_t gesture_code; // For gesture events - uint8_t button_id; // For button events - uint8_t pressure; // Touch pressure/weight - bool valid; // Set to true if event is valid -}; - -// ============================================================================ -// GAME STATE AND CONFIGURATION -// ============================================================================ - -// Game state - customize this for your game -struct GameState { - // Tic-Tac-Toe game state - uint8_t board[3][3]; // 0=empty, 1=X, 2=O - uint8_t current_player; // 1=X, 2=O - uint8_t winner; // 0=none, 1=X wins, 2=O wins, 3=tie - uint8_t selected_row; // Currently selected cell - uint8_t selected_col; - bool game_over; - - // Game statistics - uint32_t x_wins; - uint32_t o_wins; - uint32_t ties; - uint32_t total_moves; -}; - // Game configuration - adjust these for your game struct GameConfig { uint32_t touch_debounce_ms; // Touch polling rate @@ -282,488 +241,9 @@ void refresh_screen(const uint8_t *buffer, LowLevelDisplay* display) { // INPUT PROCESSING // ============================================================================ -/** - * @brief Get human-readable gesture name - * - * @param gesture_code Gesture code from touch controller - * @return Constant string with gesture name - */ -const char* get_gesture_name(uint8_t gesture_code) { - switch(gesture_code) { - case 0x10: return "Move Up"; - case 0x14: return "Move Right"; - case 0x18: return "Move Down"; - case 0x1C: return "Move Left"; - case 0x48: return "Zoom In"; - case 0x49: return "Zoom Out"; - default: return "Unknown"; - } -} -/** - * @brief Process touch input and convert to InputEvent - * - * Reads touch data from controller and creates appropriate InputEvent. - * Handles debouncing and filtering internally. - * - * @param config Game configuration - * @param last_time Pointer to last touch time for debouncing - * @return InputEvent structure (valid=false if no valid input) - */ -InputEvent process_touch_input(const GameConfig& config, uint32_t* last_time) { - InputEvent event = {INPUT_NONE, 0, 0, 0, 0, 0, false}; - - // Check if touch interrupt flag is set - if (!touch_interrupt_flag) { - 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; - } - - // 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 - TouchData touch_data; - if (!touch || !touch->read_touch(&touch_data)) { - // Clear flag even if read failed to prevent getting stuck - touch_interrupt_flag = false; - printf("Touch read FAILED\n"); - return event; // Read failed - } - - // Clear the interrupt flag after successfully reading touch data - // This allows the next touch interrupt to be detected - touch_interrupt_flag = false; - - printf("Touch DOWN at (%d,%d)\n", touch_data.points[0].x, touch_data.points[0].y); - - // Populate event structure - 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; - } 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; -} -/** - * @brief Process button input and convert to InputEvent - * - * Checks button flags and verifies button state with debouncing. - * Clears flags after processing. - * - * @param config Game configuration - * @return InputEvent structure (valid=false if no valid input) - */ -InputEvent process_button_input(const GameConfig& config) { - InputEvent event = {INPUT_NONE, 0, 0, 0, 0, 0, false}; - -#ifdef BUTTON_KEY0_PIN - // Check KEY0 - if (button_key0_pressed) { - button_key0_pressed = false; - sleep_ms(config.button_debounce_ms); - - if (gpio_get(BUTTON_KEY0_PIN) == 0) { // Verify still pressed - event.type = INPUT_BUTTON_0; - event.button_id = 0; - event.valid = true; - - if (config.debug_verbose) { - printf("Button KEY0 action triggered\n"); - } - return event; - } - } - -#ifdef BUTTON_KEY1_PIN - // Check KEY1 - if (button_key1_pressed) { - button_key1_pressed = false; - sleep_ms(config.button_debounce_ms); - - if (gpio_get(BUTTON_KEY1_PIN) == 0) { // Verify still pressed - event.type = INPUT_BUTTON_1; - event.button_id = 1; - event.valid = true; - - if (config.debug_verbose) { - printf("Button KEY1 action triggered\n"); - } - return event; - } - } -#endif -#endif - - return event; -} -// ============================================================================ -// GAME LOGIC (Customize this section for your game!) -// ============================================================================ - -// Game board dimensions (used for both drawing and touch detection) -const int BOARD_SIZE = 200; -const int CELL_SIZE = BOARD_SIZE / 3; -const int BOARD_Y = 80; // Y position below title - -/** - * @brief Check if there's a winner on the board - * @return 0=no winner, 1=X wins, 2=O wins, 3=tie - */ -uint8_t check_winner(const GameState* state) { - // Check rows - for (int row = 0; row < 3; row++) { - if (state->board[row][0] != 0 && - state->board[row][0] == state->board[row][1] && - state->board[row][1] == state->board[row][2]) { - return state->board[row][0]; - } - } - - // Check columns - for (int col = 0; col < 3; col++) { - if (state->board[0][col] != 0 && - state->board[0][col] == state->board[1][col] && - state->board[1][col] == state->board[2][col]) { - return state->board[0][col]; - } - } - - // Check diagonals - if (state->board[0][0] != 0 && - state->board[0][0] == state->board[1][1] && - state->board[1][1] == state->board[2][2]) { - return state->board[0][0]; - } - - if (state->board[0][2] != 0 && - state->board[0][2] == state->board[1][1] && - state->board[1][1] == state->board[2][0]) { - return state->board[0][2]; - } - - // Check for tie (board full) - bool board_full = true; - for (int row = 0; row < 3; row++) { - for (int col = 0; col < 3; col++) { - if (state->board[row][col] == 0) { - board_full = false; - break; - } - } - if (!board_full) break; - } - - if (board_full) return 3; // Tie - return 0; // No winner yet -} - -/** - * @brief Initialize game state - * - * Called once at startup to set initial game values. - * Customize this for your game. - * - * @param state Pointer to GameState to initialize - */ -void game_init(GameState* state) { - // Clear the board - for (int row = 0; row < 3; row++) { - for (int col = 0; col < 3; col++) { - state->board[row][col] = 0; - } - } - - state->current_player = 1; // X starts - state->winner = 0; - state->selected_row = 1; // Start in center - state->selected_col = 1; - state->game_over = false; - state->total_moves = 0; - - // Keep win statistics across games - // state->x_wins, state->o_wins, state->ties remain unchanged -} - -/** - * @brief Update game state based on input event - * - * This is where your game logic goes. - * Called whenever an input event occurs. - * - * @param state Pointer to GameState to update - * @param input Input event to process - * @param config Game configuration - * @param renderer Renderer for drawing operations - * @return true if screen needs refresh (drawing occurred) - */ -bool game_update(GameState* state, const InputEvent& input, const GameConfig& config, LowLevelRenderer* renderer) { - bool needs_refresh = false; - - switch (input.type) { - case INPUT_TOUCH_DOWN: { - // If game is over, restart on touch - if (state->game_over) { - game_init(state); - needs_refresh = true; - break; - } - - printf("Touch down at (%d,%d)\n", input.x, input.y); - - // Calculate board position (must match game_draw!) - int board_x = V_WIDTH - BOARD_SIZE - 20; - - // Check if touch is within board - if (input.x >= board_x && input.x < board_x + BOARD_SIZE && - input.y >= BOARD_Y && input.y < BOARD_Y + BOARD_SIZE) { - - int touched_col = (input.x - board_x) / CELL_SIZE; - int touched_row = (input.y - BOARD_Y) / CELL_SIZE; - - // Clamp to valid range (safety check) - if (touched_row >= 0 && touched_row < 3 && touched_col >= 0 && touched_col < 3) { - // Place piece if cell is empty - if (state->board[touched_row][touched_col] == 0) { - state->board[touched_row][touched_col] = state->current_player; - state->total_moves++; - - // Check for winner - state->winner = check_winner(state); - if (state->winner != 0) { - state->game_over = true; - if (state->winner == 1) state->x_wins++; - else if (state->winner == 2) state->o_wins++; - else if (state->winner == 3) state->ties++; - } else { - // Switch player - state->current_player = (state->current_player == 1) ? 2 : 1; - } - - needs_refresh = true; - - if (config.debug_verbose) { - printf("Touch at [%d,%d] (pixel %d,%d) by player %d\n", - touched_row, touched_col, input.x, input.y, - state->current_player == 1 ? 2 : 1); - } - } else if (config.debug_verbose) { - printf("Cell [%d,%d] already occupied\n", touched_row, touched_col); - } - } - } else if (config.debug_verbose) { - printf("Touch outside board: %d,%d\n", input.x, input.y); - } - break; - } - - case INPUT_BUTTON_0: - // KEY0: Move selection (for button-only boards) - if (!state->game_over) { - // Move to next cell, skipping occupied ones - int attempts = 0; - do { - state->selected_col++; - if (state->selected_col > 2) { - state->selected_col = 0; - state->selected_row++; - if (state->selected_row > 2) { - state->selected_row = 0; - } - } - attempts++; - // If we've tried all 9 cells, stop (game might be full) - if (attempts >= 9) break; - } while (state->board[state->selected_row][state->selected_col] != 0); - - needs_refresh = true; - if (config.debug_verbose) { - printf("Selection moved to [%d,%d]\n", state->selected_row, state->selected_col); - } - } else { - // Restart game - game_init(state); - needs_refresh = true; - } - break; - - case INPUT_BUTTON_1: - // KEY1: Place piece at selected position - if (!state->game_over) { - if (state->board[state->selected_row][state->selected_col] == 0) { - state->board[state->selected_row][state->selected_col] = state->current_player; - state->total_moves++; - - // Check for winner - state->winner = check_winner(state); - if (state->winner != 0) { - state->game_over = true; - if (state->winner == 1) state->x_wins++; - else if (state->winner == 2) state->o_wins++; - else if (state->winner == 3) state->ties++; - } else { - // Switch player - state->current_player = (state->current_player == 1) ? 2 : 1; - } - - needs_refresh = true; - - if (config.debug_verbose) { - printf("Piece placed at [%d,%d]\n", state->selected_row, state->selected_col); - } - } - } - break; - - default: - break; - } - - return needs_refresh; -} - -/** - * @brief Draw game graphics to screen buffer - * - * All initial UI drawing operations go here. - * Called once at startup to create the initial screen. - * - * @param state Pointer to current GameState - * @param renderer Renderer for drawing primitives - * @param gui GUI system for widgets (optional) - */ -void game_draw(const GameState* state, LowLevelRenderer* renderer, LowLevelGUI* gui) { - // Draw main window - LowLevelWindow *w1 = gui->draw_new_window(10, 10, V_WIDTH - 20, V_HEIGHT - 20, "Tic-Tac-Toe"); - - renderer->set_font(&font_5x5_obj); - - // Draw current player or game result - if (state->game_over) { - if (state->winner == 1) { - renderer->draw_string(20, 40, "X WINS!", true); - } else if (state->winner == 2) { - renderer->draw_string(20, 40, "O WINS!", true); - } else { - renderer->draw_string(20, 40, "TIE GAME!", true); - } - renderer->draw_string(20, 55, "Touch or KEY0 to restart", true); - } else { - char turn_text[30]; - snprintf(turn_text, sizeof(turn_text), "Turn: %s", state->current_player == 1 ? "X" : "O"); - renderer->draw_string(20, 40, turn_text, true); - renderer->draw_string(20, 55, "Touch cell or use keys", true); - } - - // Draw game board (use same layout as touch detection!) - int board_x = V_WIDTH - BOARD_SIZE - 20; - - // Draw current turn indicator (large, on left side) - if (!state->game_over) { - int indicator_x = 60; - int indicator_y = V_HEIGHT / 2; - int piece_offset = CELL_SIZE / 4; - - if (state->current_player == 1) { - // Draw large X - renderer->draw_line(indicator_x - piece_offset, indicator_y - piece_offset, - indicator_x + piece_offset, indicator_y + piece_offset, true, 4); - renderer->draw_line(indicator_x + piece_offset, indicator_y - piece_offset, - indicator_x - piece_offset, indicator_y + piece_offset, true, 4); - } else { - // Draw large O - int piece_radius = CELL_SIZE / 4; - renderer->draw_circle(indicator_x, indicator_y, piece_radius, true); - renderer->draw_circle(indicator_x, indicator_y, piece_radius - 1, true); - renderer->draw_circle(indicator_x, indicator_y, piece_radius - 2, true); - renderer->draw_circle(indicator_x, indicator_y, piece_radius - 3, true); - } - } - // Draw grid lines - for (int i = 1; i < 3; i++) { - // Vertical lines - renderer->draw_line(board_x + i * CELL_SIZE, BOARD_Y, - board_x + i * CELL_SIZE, BOARD_Y + BOARD_SIZE, true, 2); - // Horizontal lines - renderer->draw_line(board_x, BOARD_Y + i * CELL_SIZE, - board_x + BOARD_SIZE, BOARD_Y + i * CELL_SIZE, true, 2); - } - - // Draw outer border - renderer->draw_rectangle(board_x, BOARD_Y, BOARD_SIZE, BOARD_SIZE, true, 3); - - // Draw X's and O's - for (int row = 0; row < 3; row++) { - for (int col = 0; col < 3; col++) { - int cell_x = board_x + col * CELL_SIZE; - int cell_y = BOARD_Y + row * CELL_SIZE; - int center_x = cell_x + CELL_SIZE / 2; - int center_y = cell_y + CELL_SIZE / 2; - - // Highlight selected cell (for button navigation) - if (!state->game_over && row == state->selected_row && col == state->selected_col) { - renderer->draw_rectangle(cell_x + 5, cell_y + 5, CELL_SIZE - 10, CELL_SIZE - 10, true, 1); - } - - if (state->board[row][col] == 1) { - // Draw X - int offset = CELL_SIZE / 4; - renderer->draw_line(center_x - offset, center_y - offset, - center_x + offset, center_y + offset, true, 3); - renderer->draw_line(center_x + offset, center_y - offset, - center_x - offset, center_y + offset, true, 3); - } else if (state->board[row][col] == 2) { - // Draw O - int radius = CELL_SIZE / 4; - renderer->draw_circle(center_x, center_y, radius, true); - renderer->draw_circle(center_x, center_y, radius - 1, true); - renderer->draw_circle(center_x, center_y, radius - 2, true); - } - } - } - - // Draw statistics at bottom - char stats[60]; - snprintf(stats, sizeof(stats), "X:%d O:%d Tie:%d Moves:%d", - state->x_wins, state->o_wins, state->ties, state->total_moves); - renderer->draw_string(20, V_HEIGHT - 40, stats, true); -} // ============================================================================ // MAIN PROGRAM @@ -835,16 +315,17 @@ int main() .debug_verbose = false }; - // Initialize game state - GameState game_state; - // Initialize statistics (persists across game restarts) - game_state.x_wins = 0; - game_state.o_wins = 0; - game_state.ties = 0; - game_init(&game_state); + // Create InputManager for processing inputs + InputManager input_manager(touch, &config); + + // Create game instance (polymorphic - can swap for other games later) + Game* current_game = new TicTacToeGame(V_WIDTH, V_HEIGHT, &renderer, &gui); + + // Initialize game + current_game->init(); // Draw initial game graphics - game_draw(&game_state, &renderer, &gui); + current_game->draw(); // Refresh the screen with the rendered GUI (async on Core 1) refresh_screen_async(bit_buffer, display); @@ -935,16 +416,16 @@ int main() bool needs_refresh = false; // 1. Process button input first (higher priority) - input = process_button_input(config); + input = input_manager.process_button_input(); if (input.valid) { - needs_refresh = game_update(&game_state, input, config, &renderer); + needs_refresh = current_game->update(input); } // 2. Process touch input (if no button was pressed) if (!input.valid) { - input = process_touch_input(config, &last_touch_time); + input = input_manager.process_touch_input(&last_touch_time); if (input.valid) { - needs_refresh = game_update(&game_state, input, config, &renderer); + needs_refresh = current_game->update(input); } } @@ -952,7 +433,7 @@ int main() if (needs_refresh || pending_refresh) { // Clear buffer and redraw entire UI with updated state memset(bit_buffer, 0, V_WIDTH * V_HEIGHT / 8); - game_draw(&game_state, &renderer, &gui); + current_game->draw(); // Request async refresh (non-blocking - handled by Core 1) bool refresh_started = refresh_screen_async(bit_buffer, display); diff --git a/board_config.h b/board_config.h index 3875fae..b725ad3 100644 --- a/board_config.h +++ b/board_config.h @@ -16,9 +16,9 @@ // ============================================================================ // ---- SELECT YOUR BOARD HERE ---- -#define BOARD_FEATHER_TFT // Feather RP2350 + 4.0" TFT ST7796 +// #define BOARD_FEATHER_TFT // Feather RP2350 + 4.0" TFT ST7796 // #define BOARD_PICO2_TFT // Pico 2 + 4.0" TFT ST7796 -//#define BOARD_PICO2_EINK // Pico 2 + E-Ink Display +#define BOARD_PICO2_EINK // Pico 2 + E-Ink Display // -------------------------------- // ============================================================================ diff --git a/games/tic_tac_toe.cpp b/games/tic_tac_toe.cpp new file mode 100644 index 0000000..5e5b77f --- /dev/null +++ b/games/tic_tac_toe.cpp @@ -0,0 +1,304 @@ +// ============================================================================ +// TIC-TAC-TOE GAME IMPLEMENTATION +// ============================================================================ +// Game logic, input handling, and rendering for Tic-Tac-Toe + +#include "tic_tac_toe.h" +#include "board_config.h" +#include +#include + +// Font reference from display system +extern Font font_5x5_obj; + +TicTacToeGame::TicTacToeGame(uint16_t width, uint16_t height, LowLevelRenderer* renderer, LowLevelGUI* gui) + : Game(width, height, renderer, gui) { + // Initialize statistics to zero + state.x_wins = 0; + state.o_wins = 0; + state.ties = 0; +} + +void TicTacToeGame::init() { + // Clear the board + for (int row = 0; row < 3; row++) { + for (int col = 0; col < 3; col++) { + state.board[row][col] = 0; + } + } + + state.current_player = 1; // X starts + state.winner = 0; + state.selected_row = 1; // Start in center + state.selected_col = 1; + state.game_over = false; + state.total_moves = 0; + + // Keep win statistics across games + // state.x_wins, state.o_wins, state.ties remain unchanged +} + +uint8_t TicTacToeGame::check_winner() { + // Check rows + for (int row = 0; row < 3; row++) { + if (state.board[row][0] != 0 && + state.board[row][0] == state.board[row][1] && + state.board[row][1] == state.board[row][2]) { + return state.board[row][0]; + } + } + + // Check columns + for (int col = 0; col < 3; col++) { + if (state.board[0][col] != 0 && + state.board[0][col] == state.board[1][col] && + state.board[1][col] == state.board[2][col]) { + return state.board[0][col]; + } + } + + // Check diagonals + if (state.board[0][0] != 0 && + state.board[0][0] == state.board[1][1] && + state.board[1][1] == state.board[2][2]) { + return state.board[0][0]; + } + + if (state.board[0][2] != 0 && + state.board[0][2] == state.board[1][1] && + state.board[1][1] == state.board[2][0]) { + return state.board[0][2]; + } + + // Check for tie (board full) + bool board_full = true; + for (int row = 0; row < 3; row++) { + for (int col = 0; col < 3; col++) { + if (state.board[row][col] == 0) { + board_full = false; + break; + } + } + if (!board_full) break; + } + + if (board_full) return 3; // Tie + return 0; // No winner yet +} + +bool TicTacToeGame::update(const InputEvent& event) { + bool needs_refresh = false; + + switch (event.type) { + case INPUT_TOUCH_DOWN: { + // If game is over, restart on touch + if (state.game_over) { + init(); + needs_refresh = true; + break; + } + + printf("Touch down at (%d,%d)\n", event.x, event.y); + + // Calculate board position (must match draw()!) + int board_x = width - BOARD_SIZE - 20; + + // Check if touch is within board + if (event.x >= board_x && event.x < board_x + BOARD_SIZE && + event.y >= BOARD_Y && event.y < BOARD_Y + BOARD_SIZE) { + + int touched_col = (event.x - board_x) / CELL_SIZE; + int touched_row = (event.y - BOARD_Y) / CELL_SIZE; + + // Clamp to valid range (safety check) + if (touched_row >= 0 && touched_row < 3 && touched_col >= 0 && touched_col < 3) { + // Place piece if cell is empty + if (state.board[touched_row][touched_col] == 0) { + state.board[touched_row][touched_col] = state.current_player; + state.total_moves++; + + // Check for winner + state.winner = check_winner(); + if (state.winner != 0) { + state.game_over = true; + if (state.winner == 1) state.x_wins++; + else if (state.winner == 2) state.o_wins++; + else if (state.winner == 3) state.ties++; + } else { + // Switch player + state.current_player = (state.current_player == 1) ? 2 : 1; + } + + needs_refresh = true; + + printf("Touch at [%d,%d] (pixel %d,%d) by player %d\n", + touched_row, touched_col, event.x, event.y, + state.current_player == 1 ? 2 : 1); + } else { + printf("Cell [%d,%d] already occupied\n", touched_row, touched_col); + } + } + } else { + printf("Touch outside board: %d,%d\n", event.x, event.y); + } + break; + } + + case INPUT_BUTTON_0: + // KEY0: Move selection (for button-only boards) + if (!state.game_over) { + // Move to next cell, skipping occupied ones + int attempts = 0; + do { + state.selected_col++; + if (state.selected_col > 2) { + state.selected_col = 0; + state.selected_row++; + if (state.selected_row > 2) { + state.selected_row = 0; + } + } + attempts++; + // If we've tried all 9 cells, stop (game might be full) + if (attempts >= 9) break; + } while (state.board[state.selected_row][state.selected_col] != 0); + + needs_refresh = true; + printf("Selection moved to [%d,%d]\n", state.selected_row, state.selected_col); + } else { + // Restart game + init(); + needs_refresh = true; + } + break; + + case INPUT_BUTTON_1: + // KEY1: Place piece at selected position + if (!state.game_over) { + if (state.board[state.selected_row][state.selected_col] == 0) { + state.board[state.selected_row][state.selected_col] = state.current_player; + state.total_moves++; + + // Check for winner + state.winner = check_winner(); + if (state.winner != 0) { + state.game_over = true; + if (state.winner == 1) state.x_wins++; + else if (state.winner == 2) state.o_wins++; + else if (state.winner == 3) state.ties++; + } else { + // Switch player + state.current_player = (state.current_player == 1) ? 2 : 1; + } + + needs_refresh = true; + + printf("Piece placed at [%d,%d]\n", state.selected_row, state.selected_col); + } + } + break; + + default: + break; + } + + return needs_refresh; +} + +void TicTacToeGame::draw() { + // Draw main window + LowLevelWindow *w1 = gui->draw_new_window(10, 10, width - 20, height - 20, "Tic-Tac-Toe"); + + renderer->set_font(&font_5x5_obj); + + // Draw current player or game result + if (state.game_over) { + if (state.winner == 1) { + renderer->draw_string(20, 40, "X WINS!", true); + } else if (state.winner == 2) { + renderer->draw_string(20, 40, "O WINS!", true); + } else { + renderer->draw_string(20, 40, "TIE GAME!", true); + } + renderer->draw_string(20, 55, "Touch or KEY0 to restart", true); + } else { + char turn_text[30]; + snprintf(turn_text, sizeof(turn_text), "Turn: %s", state.current_player == 1 ? "X" : "O"); + renderer->draw_string(20, 40, turn_text, true); + renderer->draw_string(20, 55, "Touch cell or use keys", true); + } + + // Draw game board (use same layout as touch detection!) + int board_x = width - BOARD_SIZE - 20; + + // Draw current turn indicator (large, on left side) + if (!state.game_over) { + int indicator_x = 60; + int indicator_y = height / 2; + int piece_offset = CELL_SIZE / 4; + + if (state.current_player == 1) { + // Draw large X + renderer->draw_line(indicator_x - piece_offset, indicator_y - piece_offset, + indicator_x + piece_offset, indicator_y + piece_offset, true, 4); + renderer->draw_line(indicator_x + piece_offset, indicator_y - piece_offset, + indicator_x - piece_offset, indicator_y + piece_offset, true, 4); + } else { + // Draw large O + int piece_radius = CELL_SIZE / 4; + renderer->draw_circle(indicator_x, indicator_y, piece_radius, true); + renderer->draw_circle(indicator_x, indicator_y, piece_radius - 1, true); + renderer->draw_circle(indicator_x, indicator_y, piece_radius - 2, true); + renderer->draw_circle(indicator_x, indicator_y, piece_radius - 3, true); + } + } + + // Draw grid lines + for (int i = 1; i < 3; i++) { + // Vertical lines + renderer->draw_line(board_x + i * CELL_SIZE, BOARD_Y, + board_x + i * CELL_SIZE, BOARD_Y + BOARD_SIZE, true, 2); + // Horizontal lines + renderer->draw_line(board_x, BOARD_Y + i * CELL_SIZE, + board_x + BOARD_SIZE, BOARD_Y + i * CELL_SIZE, true, 2); + } + + // Draw outer border + renderer->draw_rectangle(board_x, BOARD_Y, BOARD_SIZE, BOARD_SIZE, true, 3); + + // Draw X's and O's + for (int row = 0; row < 3; row++) { + for (int col = 0; col < 3; col++) { + int cell_x = board_x + col * CELL_SIZE; + int cell_y = BOARD_Y + row * CELL_SIZE; + int center_x = cell_x + CELL_SIZE / 2; + int center_y = cell_y + CELL_SIZE / 2; + + // Highlight selected cell (for button navigation) + if (!state.game_over && row == state.selected_row && col == state.selected_col) { + renderer->draw_rectangle(cell_x + 5, cell_y + 5, CELL_SIZE - 10, CELL_SIZE - 10, true, 1); + } + + if (state.board[row][col] == 1) { + // Draw X + int offset = CELL_SIZE / 4; + renderer->draw_line(center_x - offset, center_y - offset, + center_x + offset, center_y + offset, true, 3); + renderer->draw_line(center_x + offset, center_y - offset, + center_x - offset, center_y + offset, true, 3); + } else if (state.board[row][col] == 2) { + // Draw O + int radius = CELL_SIZE / 4; + renderer->draw_circle(center_x, center_y, radius, true); + renderer->draw_circle(center_x, center_y, radius - 1, true); + renderer->draw_circle(center_x, center_y, radius - 2, true); + } + } + } + + // Draw statistics at bottom + char stats[60]; + snprintf(stats, sizeof(stats), "X:%d O:%d Tie:%d Moves:%d", + state.x_wins, state.o_wins, state.ties, state.total_moves); + renderer->draw_string(20, height - 40, stats, true); +} diff --git a/games/tic_tac_toe.h b/games/tic_tac_toe.h new file mode 100644 index 0000000..866dece --- /dev/null +++ b/games/tic_tac_toe.h @@ -0,0 +1,78 @@ +// ============================================================================ +// TIC-TAC-TOE GAME HEADER +// ============================================================================ +// Concrete implementation of the Game interface for Tic-Tac-Toe + +#ifndef TIC_TAC_TOE_H +#define TIC_TAC_TOE_H + +#include "game.h" + +/** + * @brief Tic-Tac-Toe game implementation + * + * Classic two-player Tic-Tac-Toe game with: + * - Touch input: Tap cells to place pieces + * - Button input: KEY0 moves selection, KEY1 places piece + * - Win detection: Rows, columns, and diagonals + * - Statistics tracking: Wins, ties, total moves + * - Visual feedback: Selection highlight, turn indicator + */ +class TicTacToeGame : public Game { +public: + /** + * @brief Construct a new Tic-Tac-Toe game + * @param width Display width in pixels + * @param height Display height in pixels + * @param renderer Pointer to low-level rendering interface + * @param gui Pointer to GUI drawing primitives + */ + TicTacToeGame(uint16_t width, uint16_t height, LowLevelRenderer* renderer, LowLevelGUI* gui); + + /** + * @brief Initialize game state (reset board, keep statistics) + */ + void init() override; + + /** + * @brief Update game state based on input event + * @param event Input event from InputManager + * @return true if screen redraw is needed + */ + bool update(const InputEvent& event) override; + + /** + * @brief Draw the game to the display buffer + */ + void draw() override; + +private: + // Game state + struct GameState { + uint8_t board[3][3]; // 0=empty, 1=X, 2=O + uint8_t current_player; // 1=X, 2=O + uint8_t winner; // 0=none, 1=X wins, 2=O wins, 3=tie + uint8_t selected_row; // Currently selected cell (for button navigation) + uint8_t selected_col; + bool game_over; + + // Game statistics + uint32_t x_wins; + uint32_t o_wins; + uint32_t ties; + uint32_t total_moves; + } state; + + /** + * @brief Check if there's a winner on the board + * @return 0=no winner, 1=X wins, 2=O wins, 3=tie + */ + uint8_t check_winner(); + + // Board layout constants + static const int BOARD_SIZE = 200; + static const int CELL_SIZE = BOARD_SIZE / 3; + static const int BOARD_Y = 80; // Y position below title +}; + +#endif // TIC_TAC_TOE_H diff --git a/lib/game.h b/lib/game.h new file mode 100644 index 0000000..4fcbf53 --- /dev/null +++ b/lib/game.h @@ -0,0 +1,82 @@ +// ============================================================================ +// ABSTRACT GAME BASE CLASS +// ============================================================================ +// Defines the interface all games must implement +// Provides polymorphic game architecture for modular design + +#ifndef GAME_H +#define GAME_H + +#include +#include "input_event.h" +#include "display/low_level_render.h" +#include "display/low_level_gui.h" + +/** + * @brief Abstract base class for all games + * + * Games inherit from this class and implement the three core methods: + * - init(): Set up initial game state + * - update(): Process input events and update game logic + * - draw(): Render the game to the display buffer + * + * The main loop in basic1.cpp calls these methods polymorphically, + * allowing different games to run with the same infrastructure. + */ +class Game { +public: + /** + * @brief Construct a new Game + * @param width Display width in pixels + * @param height Display height in pixels + * @param renderer Pointer to low-level rendering interface + * @param gui Pointer to GUI drawing primitives + */ + Game(uint16_t width, uint16_t height, LowLevelRenderer* renderer, LowLevelGUI* gui) + : width(width), height(height), renderer(renderer), gui(gui) { + } + + /** + * @brief Virtual destructor for proper cleanup of derived classes + */ + virtual ~Game() {} + + /** + * @brief Initialize game state + * + * Called once when the game starts. Set up initial values, + * reset statistics, prepare the game board, etc. + */ + virtual void init() = 0; + + /** + * @brief Update game state based on input event + * + * Process the input event and update game logic accordingly. + * This is where game rules, win conditions, state transitions happen. + * + * @param event Input event from InputManager + * @return true if screen redraw is needed, false otherwise + */ + virtual bool update(const InputEvent& event) = 0; + + /** + * @brief Draw the game to the display buffer + * + * Render all game graphics using the renderer and gui objects. + * This is called after update() returns true. + * The actual screen refresh happens in the main loop on Core 1. + */ + virtual void draw() = 0; + +protected: + // Display dimensions + uint16_t width; + uint16_t height; + + // Rendering interfaces (provided by basic1.cpp) + LowLevelRenderer* renderer; + LowLevelGUI* gui; +}; + +#endif // GAME_H diff --git a/lib/input_event.h b/lib/input_event.h new file mode 100644 index 0000000..b24d7ef --- /dev/null +++ b/lib/input_event.h @@ -0,0 +1,34 @@ +// ============================================================================ +// INPUT EVENT HEADER +// ============================================================================ +// Shared input event structures used by InputManager and Game classes +// Extracted from basic1.cpp for modular game architecture + +#ifndef INPUT_EVENT_H +#define INPUT_EVENT_H + +#include + +// Input event types +enum InputType { + INPUT_NONE = 0, + INPUT_TOUCH_DOWN, + INPUT_TOUCH_MOVE, + INPUT_TOUCH_UP, + INPUT_BUTTON_0, + INPUT_BUTTON_1, + INPUT_GESTURE +}; + +// Unified input event structure +struct InputEvent { + InputType type; + int16_t x; + int16_t y; + uint8_t gesture_code; // For gesture events + uint8_t button_id; // For button events + uint8_t pressure; // Touch pressure/weight + bool valid; // Set to true if event is valid +}; + +#endif // INPUT_EVENT_H diff --git a/lib/input_manager.cpp b/lib/input_manager.cpp new file mode 100644 index 0000000..c3de55d --- /dev/null +++ b/lib/input_manager.cpp @@ -0,0 +1,154 @@ +// ============================================================================ +// INPUT MANAGER IMPLEMENTATION +// ============================================================================ +// Processes touch and button inputs into InputEvent objects + +#include "input_manager.h" +#include "pico/stdlib.h" +#include "board_config.h" +#include + +// 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; + +// GameConfig struct definition (matches basic1.cpp) +struct GameConfig { + uint32_t touch_debounce_ms; + uint32_t button_debounce_ms; + bool enable_gestures; + bool enable_continuous_draw; + bool debug_verbose; +}; + +InputManager::InputManager(LowLevelTouch* touch, const GameConfig* config) + : touch(touch), config(config) { +} + +InputEvent InputManager::process_touch_input(uint32_t* last_time) { + InputEvent event = {INPUT_NONE, 0, 0, 0, 0, 0, false}; + + // Check if touch interrupt flag is set + if (!touch_interrupt_flag) { + 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; + } + + // 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 + TouchData touch_data; + if (!touch || !touch->read_touch(&touch_data)) { + // Clear flag even if read failed to prevent getting stuck + touch_interrupt_flag = false; + printf("Touch read FAILED\n"); + return event; // Read failed + } + + // Clear the interrupt flag after successfully reading touch data + // This allows the next touch interrupt to be detected + touch_interrupt_flag = false; + + printf("Touch DOWN at (%d,%d)\n", touch_data.points[0].x, touch_data.points[0].y); + + // Populate event structure + 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; + } 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; +} + +InputEvent InputManager::process_button_input() { + InputEvent event = {INPUT_NONE, 0, 0, 0, 0, 0, false}; + +#ifdef BUTTON_KEY0_PIN + // Check KEY0 + if (button_key0_pressed) { + button_key0_pressed = false; + sleep_ms(config->button_debounce_ms); + + if (gpio_get(BUTTON_KEY0_PIN) == 0) { // Verify still pressed + event.type = INPUT_BUTTON_0; + event.button_id = 0; + event.valid = true; + + if (config->debug_verbose) { + printf("Button KEY0 action triggered\n"); + } + return event; + } + } + +#ifdef BUTTON_KEY1_PIN + // Check KEY1 + if (button_key1_pressed) { + button_key1_pressed = false; + sleep_ms(config->button_debounce_ms); + + if (gpio_get(BUTTON_KEY1_PIN) == 0) { // Verify still pressed + event.type = INPUT_BUTTON_1; + event.button_id = 1; + event.valid = true; + + if (config->debug_verbose) { + printf("Button KEY1 action triggered\n"); + } + return event; + } + } +#endif +#endif + + return event; +} + +const char* InputManager::get_gesture_name(uint8_t gesture_code) { + switch(gesture_code) { + case 0x10: return "Move Up"; + case 0x14: return "Move Right"; + case 0x18: return "Move Down"; + case 0x1C: return "Move Left"; + case 0x48: return "Zoom In"; + case 0x49: return "Zoom Out"; + default: return "Unknown"; + } +} diff --git a/lib/input_manager.h b/lib/input_manager.h new file mode 100644 index 0000000..4d7bfb5 --- /dev/null +++ b/lib/input_manager.h @@ -0,0 +1,64 @@ +// ============================================================================ +// INPUT MANAGER HEADER +// ============================================================================ +// Handles all input processing and debouncing for touch and button inputs +// Converts hardware events into InputEvent objects for games to consume + +#ifndef INPUT_MANAGER_H +#define INPUT_MANAGER_H + +#include +#include "input_event.h" +#include "display/low_level_touch.h" + +// Forward declaration - avoid pulling in full basic1.cpp +struct GameConfig; + +/** + * @brief Input Manager - Processes touch and button inputs + * + * Responsibilities: + * - Reading touch controller data + * - Debouncing touch and button events + * - Converting raw inputs to InputEvent objects + * - Gesture recognition + * + * Does NOT handle: + * - Button GPIO initialization (stays in basic1.cpp) + * - Interrupt handler registration (stays in basic1.cpp) + */ +class InputManager { +public: + /** + * @brief Construct InputManager with hardware references + * @param touch Pointer to touch controller interface + * @param config Pointer to game configuration + */ + 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) + */ + InputEvent process_touch_input(uint32_t* last_time); + + /** + * @brief Process button input from GPIO flags + * @return InputEvent (valid=false if no valid input) + */ + InputEvent process_button_input(); + + /** + * @brief Get human-readable gesture name + * @param gesture_code Gesture code from touch controller + * @return Constant string with gesture name + */ + static const char* get_gesture_name(uint8_t gesture_code); + +private: + LowLevelTouch* touch; + const GameConfig* config; +}; + +#endif // INPUT_MANAGER_H