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
@@ -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.
|
||||
|
||||
  
|
||||
 
|
||||
@@ -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?)
|
||||
|
||||
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.8 KiB |
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||