Mystify Demo, TwoEleven updates & Snake (#21)

* **New Features**
  * Added Mystify screensaver demo with animated polygons and trails
  * Added Snake game with multiple difficulties, high-score persistence, and multi-input (touch/keyboard) support
  * Added CLI tool for building, packaging, and deploying apps (end-to-end build/install/run workflow)
  * Per-grid-size high-score persistence added to 2048 app; expanded keyboard controls (WASD and device-specific mappings)

* **Documentation**
  * Added Snake README with gameplay, controls, and usage instructions
This commit is contained in:
Shadowtrance
2026-02-07 08:35:13 +10:00
committed by GitHub
parent d31b6b48a4
commit 46cf00d92e
41 changed files with 4025 additions and 131 deletions
+7 -8
View File
@@ -13,13 +13,14 @@ TwoEleven is a faithful implementation of the popular 2048 game, where you slide
- **Intuitive Controls**: Swipe gestures on touchscreens or use arrow keys for keyboard input.
- **Visual Feedback**: Color-coded tiles with smooth animations.
- **Score Tracking**: Real-time score display with win/lose detection.
- **High Score Persistence**: Saves best scores for each grid size.
- **Responsive UI**: Optimized for small screens with clean, modern design.
- **Thread-Safe**: Proper handling of UI updates to prevent crashes.
## Screenshots
Screenshots taken directly from my Lilygo T-Deck Plus.
Which is also the only device it has been tested on so far.
Tested on Lilygo T-Deck Plus and M5Stack Cardputer.
![alt text](images/3x3.png) ![alt text](images/4x4.png) ![alt text](images/5x5.png)
![alt text](images/6x6.png) ![alt text](images/selection.png)
@@ -31,7 +32,7 @@ Which is also the only device it has been tested on so far.
## Usage
1. Launch the TwoEleven app.
1. Launch the 2048 app.
2. Select your preferred grid size (3x3 to 6x6).
3. Swipe tiles in any direction to move and combine them.
4. Reach the 2048 tile to win, or get stuck to lose.
@@ -40,8 +41,10 @@ Which is also the only device it has been tested on so far.
## Controls
- **Touchscreen**: Swipe up, down, left, or right to move tiles.
- **Keyboard**: Use arrow keys (↑, ↓, ←, →) for movement.
- **New Game**: Press the "New" button to reset the board.
- **Keyboard (Arrow Keys)**: Use arrow keys (Up, Down, Left, Right) for movement.
- **Keyboard (WASD)**: Use W, A, S, D keys for movement.
- **Keyboard (Cardputer)**: Use semicolon (;), comma (,), period (.), slash (/) for up, left, down, right.
- **New Game**: Press the refresh button in the toolbar to reset the board.
## Game Rules
@@ -51,7 +54,3 @@ Which is also the only device it has been tested on so far.
- New tiles appear after each move.
- Game ends when you reach 2048 (win) or no moves are possible (lose).
## TODO
- Maybe trackball one day?
- Maybe other keys rather being limited to arrow directions. (why so limited lvgl?)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

+246 -115
View File
@@ -1,58 +1,190 @@
/**
* @file TwoEleven.cpp
* @brief 2048 game app implementation for Tactility
*/
#include "TwoEleven.h"
#include <inttypes.h>
#include <tt_hal.h>
#include <tt_lvgl_toolbar.h>
#include <tt_app_alertdialog.h>
#include <tt_app_selectiondialog.h>
#include <tt_preferences.h>
#include <TactilityCpp/LvglLock.h>
constexpr auto* TAG = "TwoEleven";
static lv_obj_t* scoreLabel = nullptr;
static lv_obj_t* scoreWrapper = nullptr;
static lv_obj_t* toolbar = nullptr;
static lv_obj_t* mainWrapper = nullptr;
static lv_obj_t* newGameWrapper = nullptr;
static lv_obj_t* gameObject = nullptr;
// Preferences keys for high scores (one per grid size)
static constexpr const char* PREF_NAMESPACE = "TwoEleven";
static constexpr const char* PREF_HIGH_3X3 = "high_3x3";
static constexpr const char* PREF_HIGH_4X4 = "high_4x4";
static constexpr const char* PREF_HIGH_5X5 = "high_5x5";
static constexpr const char* PREF_HIGH_6X6 = "high_6x6";
static uint16_t selectedSize = 4;
// High scores for each grid size (loaded from preferences)
static int32_t highScore3x3 = 0;
static int32_t highScore4x4 = 0;
static int32_t highScore5x5 = 0;
static int32_t highScore6x6 = 0;
static constexpr size_t SIZE_COUNT = 4;
// Selection dialog indices (0 = How to Play, 1-4 = grid sizes)
static constexpr int32_t SELECTION_HOW_TO_PLAY = 0;
static constexpr int32_t SELECTION_3X3 = 1;
static constexpr int32_t SELECTION_4X4 = 2;
static constexpr int32_t SELECTION_5X5 = 3;
static constexpr int32_t SELECTION_6X6 = 4;
// Grid size options (index matches selection - 1)
static const uint16_t gridSizes[SIZE_COUNT] = { 3, 4, 5, 6 };
static int getToolbarHeight(UiScale uiScale) {
if (uiScale == UiScale::UiScaleSmallest) {
return 22;
} else {
return 40;
}
}
static void loadHighScores() {
PreferencesHandle prefs = tt_preferences_alloc(PREF_NAMESPACE);
if (prefs) {
tt_preferences_opt_int32(prefs, PREF_HIGH_3X3, &highScore3x3);
tt_preferences_opt_int32(prefs, PREF_HIGH_4X4, &highScore4x4);
tt_preferences_opt_int32(prefs, PREF_HIGH_5X5, &highScore5x5);
tt_preferences_opt_int32(prefs, PREF_HIGH_6X6, &highScore6x6);
tt_preferences_free(prefs);
}
}
static void saveHighScore(int32_t gridSize, int32_t score) {
PreferencesHandle prefs = tt_preferences_alloc(PREF_NAMESPACE);
if (prefs) {
switch (gridSize) {
case SELECTION_3X3:
highScore3x3 = score;
tt_preferences_put_int32(prefs, PREF_HIGH_3X3, score);
break;
case SELECTION_4X4:
highScore4x4 = score;
tt_preferences_put_int32(prefs, PREF_HIGH_4X4, score);
break;
case SELECTION_5X5:
highScore5x5 = score;
tt_preferences_put_int32(prefs, PREF_HIGH_5X5, score);
break;
case SELECTION_6X6:
highScore6x6 = score;
tt_preferences_put_int32(prefs, PREF_HIGH_6X6, score);
break;
}
tt_preferences_free(prefs);
}
}
static int32_t getHighScore(int32_t gridSize) {
switch (gridSize) {
case SELECTION_3X3: return highScore3x3;
case SELECTION_4X4: return highScore4x4;
case SELECTION_5X5: return highScore5x5;
case SELECTION_6X6: return highScore6x6;
default: return 0;
}
}
void TwoEleven::showSelectionDialog() {
const char* items[] = { "How to Play", "3x3", "4x4", "5x5", "6x6" };
selectionDialogId = tt_app_selectiondialog_start("2048", 5, items);
}
void TwoEleven::showHelpDialog() {
const char* buttons[] = { "OK" };
helpDialogId = tt_app_alertdialog_start(
"How to Play",
"Swipe or use arrow keys to move tiles.\n"
"Tiles with the same number merge.\n"
"Reach 2048 to win!",
buttons, 1);
}
void TwoEleven::twoElevenEventCb(lv_event_t* e) {
TwoEleven* self = (TwoEleven*)lv_event_get_user_data(e);
if (self == nullptr) {
return;
}
lv_event_code_t code = lv_event_get_code(e);
lv_obj_t* obj_2048 = lv_event_get_target_obj(e);
lv_obj_t* scoreLabel = (lv_obj_t *)lv_event_get_user_data(e);
const char* alertDialogLabels[] = { "OK" };
if (code == LV_EVENT_VALUE_CHANGED) {
if (twoeleven_get_best_tile(obj_2048) >= 2048) {
char message[64];
sprintf(message, "YOU WIN!\n\nSCORE: %d", twoeleven_get_score(obj_2048));
tt_app_alertdialog_start("YOU WIN!", message, alertDialogLabels, 1);
} else if (twoeleven_get_status(obj_2048)) {
char message[64];
sprintf(message, "GAME OVER!\n\nSCORE: %d", twoeleven_get_score(obj_2048));
tt_app_alertdialog_start("GAME OVER!", message, alertDialogLabels, 1);
int32_t score = twoeleven_get_score(self->gameObject);
if (self->gameOverDialogId == 0 && twoeleven_get_best_tile(self->gameObject) >= 2048) {
int32_t prevHighScore = getHighScore(self->currentGridSize);
bool isNewHighScore = score > prevHighScore;
// Save high score if it's a new record
if (isNewHighScore) {
saveHighScore(self->currentGridSize, score);
}
const char* alertDialogLabels[] = { "OK" };
char message[100];
if (isNewHighScore) {
snprintf(message, sizeof(message), "NEW HIGH SCORE!\n\nSCORE: %" PRId32, score);
self->gameOverDialogId = tt_app_alertdialog_start("YOU WIN!", message, alertDialogLabels, 1);
} else {
snprintf(message, sizeof(message), "YOU WIN!\n\nSCORE: %" PRId32 "\nBEST: %" PRId32, score, getHighScore(self->currentGridSize));
self->gameOverDialogId = tt_app_alertdialog_start("YOU WIN!", message, alertDialogLabels, 1);
}
} else if (self->gameOverDialogId == 0 && twoeleven_get_status(self->gameObject)) {
int32_t prevHighScore = getHighScore(self->currentGridSize);
bool isNewHighScore = score > prevHighScore;
// Save high score if it's a new record
if (isNewHighScore) {
saveHighScore(self->currentGridSize, score);
}
const char* alertDialogLabels[] = { "OK" };
char message[100];
if (isNewHighScore && score > 0) {
snprintf(message, sizeof(message), "NEW HIGH SCORE!\n\nSCORE: %" PRId32, score);
self->gameOverDialogId = tt_app_alertdialog_start("NEW HIGH SCORE!", message, alertDialogLabels, 1);
} else {
snprintf(message, sizeof(message), "GAME OVER!\n\nSCORE: %" PRId32 "\nBEST: %" PRId32, score, getHighScore(self->currentGridSize));
self->gameOverDialogId = tt_app_alertdialog_start("GAME OVER!", message, alertDialogLabels, 1);
}
} else {
lv_label_set_text_fmt(scoreLabel, "SCORE: %d", twoeleven_get_score(obj_2048));
// Update score display
lv_label_set_text_fmt(self->scoreLabel, "SCORE: %" PRId32, score);
}
}
}
void TwoEleven::newGameBtnEvent(lv_event_t* e) {
lv_obj_t* obj_2048 = (lv_obj_t *)lv_event_get_user_data(e);
twoeleven_set_new_game(obj_2048);
TwoEleven* self = (TwoEleven*)lv_event_get_user_data(e);
if (self == nullptr) {
return;
}
twoeleven_set_new_game(self->gameObject);
// Update score label
if (self->scoreLabel) {
lv_label_set_text_fmt(self->scoreLabel, "SCORE: %" PRId32, twoeleven_get_score(self->gameObject));
}
}
void TwoEleven::create_game(lv_obj_t* parent, uint16_t size, lv_obj_t* toolbar) {
void TwoEleven::createGame(lv_obj_t* parent, uint16_t size, lv_obj_t* tb) {
lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
//game...
// Create game widget
gameObject = twoeleven_create(parent, size);
lv_obj_set_style_text_font(gameObject, lv_font_get_default(), 0);
lv_obj_set_size(gameObject, LV_PCT(100), LV_PCT(100));
lv_obj_set_flex_grow(gameObject, 1);
scoreWrapper = lv_obj_create(toolbar);
// Create score wrapper in toolbar
scoreWrapper = lv_obj_create(tb);
lv_obj_set_size(scoreWrapper, LV_SIZE_CONTENT, LV_PCT(100));
lv_obj_set_style_pad_top(scoreWrapper, 4, LV_STATE_DEFAULT);
lv_obj_set_style_pad_bottom(scoreWrapper, 0, LV_STATE_DEFAULT);
@@ -64,119 +196,69 @@ void TwoEleven::create_game(lv_obj_t* parent, uint16_t size, lv_obj_t* toolbar)
lv_obj_set_style_bg_opa(scoreWrapper, 0, LV_STATE_DEFAULT);
lv_obj_remove_flag(scoreWrapper, LV_OBJ_FLAG_SCROLLABLE);
//toolbar new score
// Create score label
scoreLabel = lv_label_create(scoreWrapper);
lv_label_set_text_fmt(scoreLabel, "SCORE: %d", twoeleven_get_score(gameObject));
lv_label_set_text_fmt(scoreLabel, "SCORE: %" PRId32, twoeleven_get_score(gameObject));
lv_obj_set_style_text_align(scoreLabel, LV_TEXT_ALIGN_LEFT, LV_STATE_DEFAULT);
lv_obj_align(scoreLabel, LV_ALIGN_CENTER, 0, 0);
lv_obj_set_size(scoreLabel, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_set_style_text_font(scoreLabel, lv_font_get_default(), 0);
lv_obj_set_style_text_color(scoreLabel, lv_palette_main(LV_PALETTE_AMBER), LV_PART_MAIN);
lv_obj_add_event_cb(gameObject, twoElevenEventCb, LV_EVENT_ALL, scoreLabel);
lv_obj_add_event_cb(gameObject, twoElevenEventCb, LV_EVENT_VALUE_CHANGED, this);
newGameWrapper = lv_obj_create(toolbar);
// Create new game button wrapper
newGameWrapper = lv_obj_create(tb);
lv_obj_set_width(newGameWrapper, LV_SIZE_CONTENT);
lv_obj_set_flex_flow(newGameWrapper, LV_FLEX_FLOW_ROW);
lv_obj_set_style_pad_all(newGameWrapper, 2, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(newGameWrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_style_bg_opa(newGameWrapper, 0, LV_STATE_DEFAULT);
//toolbar reset
// Create new game button
auto ui_scale = tt_hal_configuration_get_ui_scale();
auto toolbar_height = getToolbarHeight(ui_scale);
lv_obj_t* newGameBtn = lv_btn_create(newGameWrapper);
lv_obj_set_size(newGameBtn, 34, 34);
if (ui_scale == UiScale::UiScaleSmallest) {
lv_obj_set_size(newGameBtn, toolbar_height - 8, toolbar_height - 8);
} else {
lv_obj_set_size(newGameBtn, toolbar_height - 6, toolbar_height - 6);
}
lv_obj_set_style_pad_all(newGameBtn, 0, LV_STATE_DEFAULT);
lv_obj_align(newGameBtn, LV_ALIGN_CENTER, 0, 0);
lv_obj_add_event_cb(newGameBtn, newGameBtnEvent, LV_EVENT_CLICKED, gameObject);
lv_obj_add_event_cb(newGameBtn, newGameBtnEvent, LV_EVENT_CLICKED, this);
lv_obj_t* btnLabel = lv_image_create(newGameBtn);
lv_image_set_src(btnLabel, LV_SYMBOL_REFRESH);
lv_obj_align(btnLabel, LV_ALIGN_CENTER, 0, 0);
lv_obj_t* btnIcon = lv_image_create(newGameBtn);
lv_image_set_src(btnIcon, LV_SYMBOL_REFRESH);
lv_obj_align(btnIcon, LV_ALIGN_CENTER, 0, 0);
}
void TwoEleven::create_selection(lv_obj_t* parent, lv_obj_t* toolbar) {
lv_obj_t* selection = lv_obj_create(parent);
lv_obj_set_size(selection, LV_PCT(100), LV_PCT(100));
lv_obj_set_flex_flow(selection, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_grow(selection, 1);
lv_obj_remove_flag(selection, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_style_pad_all(selection, 0, LV_PART_MAIN);
lv_obj_set_style_border_width(selection, 0, LV_STATE_DEFAULT);
lv_obj_t* titleWrapper = lv_obj_create(selection);
lv_obj_set_size(titleWrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(titleWrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(titleWrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_style_bg_opa(titleWrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_flex_flow(titleWrapper, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(titleWrapper, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_remove_flag(titleWrapper, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* titleLabel = lv_label_create(titleWrapper);
lv_label_set_text(titleLabel, "Select Matrix Size");
lv_obj_align(titleLabel, LV_ALIGN_CENTER, 0, 0);
lv_obj_set_size(titleLabel, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_t* controlsWrapper = lv_obj_create(titleWrapper);
lv_obj_set_size(controlsWrapper, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_all(controlsWrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(controlsWrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_style_bg_opa(controlsWrapper, 0, LV_STATE_DEFAULT);
lv_obj_set_flex_flow(controlsWrapper, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(controlsWrapper, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_remove_flag(controlsWrapper, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* touchControlsLabel = lv_label_create(controlsWrapper);
lv_label_set_text(touchControlsLabel, "Touchscreen:\nSwipe up, down, left, right to move tiles.");
lv_obj_set_style_text_font(touchControlsLabel, lv_font_get_default(), 0);
lv_obj_set_style_text_align(touchControlsLabel, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_t* keyControlsLabel = lv_label_create(controlsWrapper);
lv_label_set_text_fmt(keyControlsLabel, "Keyboard:\nUse arrow keys (%s, %s, %s, %s) to move tiles.", LV_SYMBOL_UP, LV_SYMBOL_DOWN, LV_SYMBOL_LEFT, LV_SYMBOL_RIGHT);
lv_obj_set_style_text_font(keyControlsLabel, lv_font_get_default(), 0);
lv_obj_set_style_text_align(keyControlsLabel, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_t* buttonContainer = lv_obj_create(selection);
lv_obj_set_flex_flow(buttonContainer, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(buttonContainer, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_remove_flag(buttonContainer, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_size(buttonContainer, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_bg_opa(buttonContainer, 0, LV_STATE_DEFAULT);
lv_obj_set_style_border_width(buttonContainer, 0, LV_STATE_DEFAULT);
for(int s = 3; s <= 6; s++) {
lv_obj_t* btn = lv_btn_create(buttonContainer);
lv_obj_set_size(btn, 60, 40);
lv_obj_t* lbl = lv_label_create(btn);
char txt[10];
sprintf(txt, "%dx%d", s, s);
lv_label_set_text(lbl, txt);
lv_obj_center(lbl);
lv_obj_add_event_cb(btn, size_select_cb, LV_EVENT_CLICKED, (void*)s);
}
}
void TwoEleven::size_select_cb(lv_event_t* e) {
selectedSize = (uint16_t)(uintptr_t)lv_event_get_user_data(e);
lv_obj_t* selection = lv_obj_get_parent(lv_event_get_target_obj(e));
lv_obj_t* selectionWrapper = lv_obj_get_parent(selection);
lv_obj_clean(selectionWrapper);
void TwoEleven::onHide(AppHandle appHandle) {
scoreLabel = nullptr;
scoreWrapper = nullptr;
toolbar = nullptr;
mainWrapper = nullptr;
newGameWrapper = nullptr;
gameObject = nullptr;
create_game(selectionWrapper, selectedSize, toolbar);
}
void TwoEleven::onShow(AppHandle appHandle, lv_obj_t* parent) {
// Check if we should exit (user closed selection dialog)
if (shouldExit) {
shouldExit = false;
tt_app_stop();
return;
}
lv_obj_remove_flag(parent, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_flex_flow(parent, LV_FLEX_FLOW_COLUMN);
// Create toolbar
toolbar = tt_lvgl_toolbar_create_for_app(parent, appHandle);
lv_obj_align(toolbar, LV_ALIGN_TOP_MID, 0, 0);
// Create main wrapper
mainWrapper = lv_obj_create(parent);
lv_obj_set_width(mainWrapper, LV_PCT(100));
lv_obj_set_height(mainWrapper, LV_PCT(100));
lv_obj_set_flex_grow(mainWrapper, 1);
lv_obj_set_style_pad_all(mainWrapper, 2, LV_PART_MAIN);
lv_obj_set_style_pad_row(mainWrapper, 2, LV_PART_MAIN);
@@ -184,19 +266,68 @@ void TwoEleven::onShow(AppHandle appHandle, lv_obj_t* parent) {
lv_obj_set_style_border_width(mainWrapper, 0, LV_PART_MAIN);
lv_obj_remove_flag(mainWrapper, LV_OBJ_FLAG_SCROLLABLE);
create_selection(mainWrapper, toolbar);
// Load high scores on first show
if (!highScoresLoaded) {
loadHighScores();
highScoresLoaded = true;
}
// Check if we need to show the help dialog
if (showHelpOnShow) {
showHelpOnShow = false;
showHelpDialog();
// Check if we have a pending size selection from onResult
} else if (pendingSelection >= SELECTION_3X3 && pendingSelection <= SELECTION_6X6) {
// Force layout update before creating game so dimensions are computed
lv_obj_update_layout(parent);
// Track which grid size we're playing for high score saving
currentGridSize = pendingSelection;
// Start game with selected size (convert selection index to size index)
int32_t sizeIndex = pendingSelection - SELECTION_3X3;
createGame(mainWrapper, gridSizes[sizeIndex], toolbar);
pendingSelection = -1;
} else {
// Show selection dialog
showSelectionDialog();
}
}
void TwoEleven::onResult(AppHandle appHandle, void* _Nullable data, AppLaunchId launchId, AppResult result, BundleHandle resultData) {
if (result == APP_RESULT_OK && resultData != nullptr) {
// Dialog closed with OK, go back to selection
tt_lvgl_lock(TT_LVGL_DEFAULT_LOCK_TIME);
lv_obj_clean(mainWrapper);
scoreLabel = nullptr;
scoreWrapper = nullptr;
newGameWrapper = nullptr;
gameObject = nullptr;
create_selection(mainWrapper, toolbar);
tt_lvgl_unlock();
// Don't manipulate LVGL objects here - they may be invalid
// Just store state for onShow to handle
if (launchId == selectionDialogId && selectionDialogId != 0) {
selectionDialogId = 0;
int32_t selection = -1;
if (resultData != nullptr) {
selection = tt_app_selectiondialog_get_result_index(resultData);
}
if (selection == SELECTION_HOW_TO_PLAY) {
// Mark to show help dialog in onShow
showHelpOnShow = true;
} else if (selection >= SELECTION_3X3 && selection <= SELECTION_6X6) {
// Store selection for onShow to handle
pendingSelection = selection;
} else {
// User closed dialog without selecting - mark for exit
shouldExit = true;
}
} else if (launchId == helpDialogId && helpDialogId != 0) {
helpDialogId = 0;
// Return to selection dialog
pendingSelection = -1;
} else if (launchId == gameOverDialogId && gameOverDialogId != 0) {
gameOverDialogId = 0;
// Mark to show selection dialog in onShow
pendingSelection = -1;
} else if (launchId == winDialogId && winDialogId != 0) {
winDialogId = 0;
// Mark to show selection dialog in onShow
pendingSelection = -1;
}
}
+26 -3
View File
@@ -11,14 +11,37 @@
class TwoEleven final : public App {
private:
// UI element pointers (invalidated on hide, recreated on show)
lv_obj_t* scoreLabel = nullptr;
lv_obj_t* scoreWrapper = nullptr;
lv_obj_t* toolbar = nullptr;
lv_obj_t* mainWrapper = nullptr;
lv_obj_t* newGameWrapper = nullptr;
lv_obj_t* gameObject = nullptr;
// State tracking (persists across hide/show cycles)
int32_t pendingSelection = -1;
bool shouldExit = false;
bool showHelpOnShow = false;
int32_t currentGridSize = -1;
bool highScoresLoaded = false;
// Dialog launch IDs
AppLaunchId selectionDialogId = 0;
AppLaunchId gameOverDialogId = 0;
AppLaunchId winDialogId = 0;
AppLaunchId helpDialogId = 0;
static void twoElevenEventCb(lv_event_t* e);
static void newGameBtnEvent(lv_event_t* e);
static void create_game(lv_obj_t* parent, uint16_t size, lv_obj_t* toolbar);
static void create_selection(lv_obj_t* parent, lv_obj_t* toolbar);
static void size_select_cb(lv_event_t* e);
void createGame(lv_obj_t* parent, uint16_t size, lv_obj_t* toolbar);
void showSelectionDialog();
void showHelpDialog();
public:
void onShow(AppHandle context, lv_obj_t* parent) override;
void onHide(AppHandle context) override;
void onResult(AppHandle appHandle, void* _Nullable data, AppLaunchId launchId, AppResult result, BundleHandle resultData) override;
};
+1 -1
View File
@@ -126,7 +126,7 @@ bool game_over(uint16_t matrix_size, const uint16_t **matrix) {
/**
* @brief Get the current score
*/
uint16_t twoeleven_get_score(lv_obj_t * obj)
uint32_t twoeleven_get_score(lv_obj_t * obj)
{
const twoeleven_t * game_2048 = (const twoeleven_t *)lv_obj_get_user_data(obj);
if (!game_2048) return 0;
+1 -1
View File
@@ -52,7 +52,7 @@ bool game_over(uint16_t matrix_size, const uint16_t **matrix);
/**
* @brief Get the current score
*/
uint16_t twoeleven_get_score(lv_obj_t * obj);
uint32_t twoeleven_get_score(lv_obj_t * obj);
/**
* @brief Get the game over status
+56 -2
View File
@@ -3,9 +3,11 @@
#include "TwoElevenHelpers.h"
#include <stdlib.h>
#include <string.h>
#include <tt_lvgl_keyboard.h>
static void game_play_event(lv_event_t * e);
static void btnm_event_cb(lv_event_t * e);
static void focus_event(lv_event_t* e);
/**
* @brief Free all resources for the 2048 game object
@@ -15,6 +17,13 @@ static void delete_event(lv_event_t * e)
lv_obj_t * obj = lv_event_get_target_obj(e);
twoeleven_t * game_2048 = (twoeleven_t *)lv_obj_get_user_data(obj);
if (game_2048) {
// Reset group editing mode if we enabled it
if (tt_lvgl_hardware_keyboard_is_available()) {
lv_group_t* group = lv_group_get_default();
if (group) {
lv_group_set_editing(group, false);
}
}
for (uint16_t index = 0; index < game_2048->map_count; index++) {
if (game_2048->btnm_map[index]) {
lv_free(game_2048->btnm_map[index]);
@@ -32,6 +41,24 @@ static void delete_event(lv_event_t * e)
}
}
/**
* @brief Handle focus/defocus to manage edit mode for keyboard input
*/
static void focus_event(lv_event_t* e) {
lv_event_code_t code = lv_event_get_code(e);
lv_group_t* group = lv_group_get_default();
if (!group) return;
if (code == LV_EVENT_FOCUSED) {
// Enable edit mode so arrow keys control the game
lv_group_set_editing(group, true);
} else if (code == LV_EVENT_DEFOCUSED) {
// Restore normal focus navigation
lv_group_set_editing(group, false);
}
}
/**
* @brief Create a new 2048 game object
*/
@@ -113,10 +140,23 @@ lv_obj_t * twoeleven_create(lv_obj_t * parent, uint16_t matrix_size)
lv_btnmatrix_set_map(game_2048->btnm, (const char **)game_2048->btnm_map);
lv_btnmatrix_set_btn_ctrl_all(game_2048->btnm, LV_BTNMATRIX_CTRL_DISABLED);
lv_obj_add_event_cb(game_2048->btnm, game_play_event, LV_EVENT_ALL, obj);
lv_obj_add_event_cb(game_2048->btnm, game_play_event, LV_EVENT_GESTURE, obj);
lv_obj_add_event_cb(game_2048->btnm, game_play_event, LV_EVENT_KEY, obj);
lv_obj_add_event_cb(game_2048->btnm, btnm_event_cb, LV_EVENT_DRAW_TASK_ADDED, NULL);
lv_obj_add_event_cb(obj, delete_event, LV_EVENT_DELETE, NULL);
if (tt_lvgl_hardware_keyboard_is_available()) {
lv_group_t* group = lv_group_get_default();
if (group) {
lv_group_add_obj(group, game_2048->btnm);
// Register focus handlers to manage edit mode lifecycle
lv_obj_add_event_cb(game_2048->btnm, focus_event, LV_EVENT_FOCUSED, NULL);
lv_obj_add_event_cb(game_2048->btnm, focus_event, LV_EVENT_DEFOCUSED, NULL);
// Focus the container (will trigger FOCUSED event and enable edit mode)
lv_group_focus_obj(game_2048->btnm);
}
}
return obj;
}
@@ -175,17 +215,31 @@ static void game_play_event(lv_event_t * e)
} else if (code == LV_EVENT_KEY) {
game_2048->game_over = game_over(game_2048->matrix_size, (const uint16_t **)game_2048->matrix);
if (!game_2048->game_over) {
switch (*((const uint8_t *) lv_event_get_param(e))) {
uint32_t key = lv_event_get_key(e);
switch (key) {
// Arrow keys, WASD, and punctuation keys for cardputer
case LV_KEY_UP:
case 'w':
case 'W':
case ';':
success = move_right(&(game_2048->score), game_2048->matrix_size, game_2048->matrix);
break;
case LV_KEY_DOWN:
case 's':
case 'S':
case '.':
success = move_left(&(game_2048->score), game_2048->matrix_size, game_2048->matrix);
break;
case LV_KEY_LEFT:
case 'a':
case 'A':
case ',':
success = move_up(&(game_2048->score), game_2048->matrix_size, game_2048->matrix);
break;
case LV_KEY_RIGHT:
case 'd':
case 'D':
case '/':
success = move_down(&(game_2048->score), game_2048->matrix_size, game_2048->matrix);
break;
default: