refactored to multiple games implementation

This commit is contained in:
Adolfo Reyna
2026-01-30 21:33:42 -05:00
parent d81d547983
commit 2a6861fdf5
11 changed files with 1216 additions and 539 deletions

View File

@@ -41,6 +41,8 @@ pico_sdk_init()
add_executable(basic1 add_executable(basic1
basic1.cpp basic1.cpp
lib/input_manager.cpp
games/tic_tac_toe.cpp
lib/st7796/st7796.c lib/st7796/st7796.c
lib/ft6336u/ft6336u.c lib/ft6336u/ft6336u.c
lib/sd_card/sd_card.c lib/sd_card/sd_card.c
@@ -75,6 +77,8 @@ target_link_libraries(basic1
# Add the standard include files to the build # Add the standard include files to the build
target_include_directories(basic1 PRIVATE target_include_directories(basic1 PRIVATE
${CMAKE_CURRENT_LIST_DIR} ${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/fatfs/source
${CMAKE_CURRENT_LIST_DIR}/lib/st7796 ${CMAKE_CURRENT_LIST_DIR}/lib/st7796
${CMAKE_CURRENT_LIST_DIR}/lib/ft6336u ${CMAKE_CURRENT_LIST_DIR}/lib/ft6336u

View File

@@ -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

321
TESTING_CHECKLIST.md Normal file
View File

@@ -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

View File

@@ -44,10 +44,12 @@
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
#include "display/low_level_render.h" #include "display/low_level_render.h"
#include "display/low_level_gui.h"
#include "display/low_level_display.h" #include "display/low_level_display.h"
#include "display/low_level_display_epaper.h" #include "display/low_level_display_epaper.h"
#include "display/low_level_touch.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 // 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 // Game configuration - adjust these for your game
struct GameConfig { struct GameConfig {
uint32_t touch_debounce_ms; // Touch polling rate uint32_t touch_debounce_ms; // Touch polling rate
@@ -282,488 +241,9 @@ void refresh_screen(const uint8_t *buffer, LowLevelDisplay* display) {
// INPUT PROCESSING // 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 // MAIN PROGRAM
@@ -835,16 +315,17 @@ int main()
.debug_verbose = false .debug_verbose = false
}; };
// Initialize game state // Create InputManager for processing inputs
GameState game_state; InputManager input_manager(touch, &config);
// Initialize statistics (persists across game restarts)
game_state.x_wins = 0; // Create game instance (polymorphic - can swap for other games later)
game_state.o_wins = 0; Game* current_game = new TicTacToeGame(V_WIDTH, V_HEIGHT, &renderer, &gui);
game_state.ties = 0;
game_init(&game_state); // Initialize game
current_game->init();
// Draw initial game graphics // 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 the screen with the rendered GUI (async on Core 1)
refresh_screen_async(bit_buffer, display); refresh_screen_async(bit_buffer, display);
@@ -935,16 +416,16 @@ int main()
bool needs_refresh = false; bool needs_refresh = false;
// 1. Process button input first (higher priority) // 1. Process button input first (higher priority)
input = process_button_input(config); input = input_manager.process_button_input();
if (input.valid) { 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) // 2. Process touch input (if no button was pressed)
if (!input.valid) { if (!input.valid) {
input = process_touch_input(config, &last_touch_time); input = input_manager.process_touch_input(&last_touch_time);
if (input.valid) { 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) { if (needs_refresh || pending_refresh) {
// Clear buffer and redraw entire UI with updated state // Clear buffer and redraw entire UI with updated state
memset(bit_buffer, 0, V_WIDTH * V_HEIGHT / 8); memset(bit_buffer, 0, V_WIDTH * V_HEIGHT / 8);
game_draw(&game_state, &renderer, &gui); current_game->draw();
// Request async refresh (non-blocking - handled by Core 1) // Request async refresh (non-blocking - handled by Core 1)
bool refresh_started = refresh_screen_async(bit_buffer, display); bool refresh_started = refresh_screen_async(bit_buffer, display);

View File

@@ -16,9 +16,9 @@
// ============================================================================ // ============================================================================
// ---- SELECT YOUR BOARD HERE ---- // ---- 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_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
// -------------------------------- // --------------------------------
// ============================================================================ // ============================================================================

304
games/tic_tac_toe.cpp Normal file
View File

@@ -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 <stdio.h>
#include <string.h>
// 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);
}

78
games/tic_tac_toe.h Normal file
View File

@@ -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

82
lib/game.h Normal file
View File

@@ -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 <stdint.h>
#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

34
lib/input_event.h Normal file
View File

@@ -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 <stdint.h>
// 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

154
lib/input_manager.cpp Normal file
View File

@@ -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 <stdio.h>
// 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";
}
}

64
lib/input_manager.h Normal file
View File

@@ -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 <stdint.h>
#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