refactored to multiple games implementation
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
321
TESTING_CHECKLIST.md
Normal 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
|
||||||
555
basic1.cpp
555
basic1.cpp
@@ -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);
|
||||||
|
|||||||
@@ -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
304
games/tic_tac_toe.cpp
Normal 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
78
games/tic_tac_toe.h
Normal 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
82
lib/game.h
Normal 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
34
lib/input_event.h
Normal 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
154
lib/input_manager.cpp
Normal 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
64
lib/input_manager.h
Normal 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
|
||||||
Reference in New Issue
Block a user