From 28aca0372b14501985ec70c49fbd846eb87365e1 Mon Sep 17 00:00:00 2001 From: xhon-pelushi Date: Mon, 17 Aug 2026 08:56:08 -0400 Subject: [PATCH] Move Twos grid state and rules into a TwosGrid class The Twos screen held both the game state and the rules that manipulate it alongside its LVGL objects. Move the grid, the score and the functions that act on them into a TwosGrid class, leaving the screen responsible only for drawing. The four swipe branches of OnTouchEvent were the same algorithm written out once per direction. They are replaced by TwosGrid::Slide, which takes the direction as a row/column step, so the sliding and merging rules now exist in one place. Fixes #1668 --- src/displayapp/screens/Twos.cpp | 309 ++++++++++++++------------------ src/displayapp/screens/Twos.h | 47 ++++- 2 files changed, 175 insertions(+), 181 deletions(-) diff --git a/src/displayapp/screens/Twos.cpp b/src/displayapp/screens/Twos.cpp index 6f2eff40c4..e27f945a6c 100644 --- a/src/displayapp/screens/Twos.cpp +++ b/src/displayapp/screens/Twos.cpp @@ -5,6 +5,103 @@ using namespace Pinetime::Applications::Screens; +bool TwosGrid::PlaceNewTile() { + unsigned int emptyCells[nCells]; + unsigned int nEmpty = 0; + for (unsigned int i = 0; i < nCells; i++) { + const unsigned int row = i / nCols; + const unsigned int col = i % nCols; + if (grid[row][col].value == 0) { + emptyCells[nEmpty] = i; + nEmpty++; + } + } + + if (nEmpty == 0) { + return false; // game lost + } + + int random = rand() % nEmpty; + + if ((rand() % 100) < 90) { + grid[emptyCells[random] / nCols][emptyCells[random] % nCols].value = 2; + } else { + grid[emptyCells[random] / nCols][emptyCells[random] % nCols].value = 4; + } + return true; +} + +void TwosGrid::ResetMergeState() { + for (auto& gridRow : grid) { + for (TwosTile& tile : gridRow) { + tile.merged = false; + } + } +} + +bool TwosGrid::TryMerge(int newRow, int newCol, int oldRow, int oldCol) { + if (grid[newRow][newCol].value == grid[oldRow][oldCol].value) { + if ((newCol != oldCol) || (newRow != oldRow)) { + if (!grid[newRow][newCol].merged) { + grid[newRow][newCol].value *= 2; + score += grid[newRow][newCol].value; + grid[oldRow][oldCol].value = 0; + grid[newRow][newCol].merged = true; + return true; + } + } + } + return false; +} + +bool TwosGrid::TryMove(int newRow, int newCol, int oldRow, int oldCol) { + if ((newRow == oldRow) && (newCol == oldCol)) { + return false; + } + grid[newRow][newCol].value = grid[oldRow][oldCol].value; + grid[oldRow][oldCol].value = 0; + return true; +} + +bool TwosGrid::Slide(int rowStep, int colStep) { + ResetMergeState(); + + bool validMove = false; + // Tiles closest to the edge they are sliding towards have to be resolved + // first, so that the ones behind them can use the space they free up. + for (int i = 0; i < nRows; i++) { + const int row = rowStep > 0 ? nRows - 1 - i : i; + for (int j = 0; j < nCols; j++) { + const int col = colStep > 0 ? nCols - 1 - j : j; + if (grid[row][col].value == 0) { + continue; + } + + // Look for the furthest empty cell in this direction, stopping at the + // first tile in the way and merging with it if possible. + int newRow = row; + int newCol = col; + for (int r = row + rowStep, c = col + colStep; (r >= 0) && (r < nRows) && (c >= 0) && (c < nCols); r += rowStep, c += colStep) { + if (grid[r][c].value == 0) { + newRow = r; + newCol = c; + } else { // blocked by another tile + if (TryMerge(r, c, row, col)) { + validMove = true; + } + break; + } + } + + if (TryMove(newRow, newCol, row, col)) { + validMove = true; + } + } + } + + return validMove; +} + Twos::Twos() { struct colorPair { @@ -35,13 +132,12 @@ Twos::Twos() { lv_obj_add_style(gridDisplay, LV_TABLE_PART_CELL1 + i, &cellStyles[i]); } - lv_table_set_col_cnt(gridDisplay, nCols); - lv_table_set_row_cnt(gridDisplay, nRows); - for (int col = 0; col < nCols; col++) { - static constexpr int colWidth = LV_HOR_RES_MAX / nCols; + lv_table_set_col_cnt(gridDisplay, TwosGrid::nCols); + lv_table_set_row_cnt(gridDisplay, TwosGrid::nRows); + for (int col = 0; col < TwosGrid::nCols; col++) { + static constexpr int colWidth = LV_HOR_RES_MAX / TwosGrid::nCols; lv_table_set_col_width(gridDisplay, col, colWidth); - for (int row = 0; row < nRows; row++) { - grid[row][col].value = 0; + for (int row = 0; row < TwosGrid::nRows; row++) { lv_table_set_cell_type(gridDisplay, row, col, 1); lv_table_set_cell_align(gridDisplay, row, col, LV_LABEL_ALIGN_CENTER); } @@ -51,8 +147,9 @@ Twos::Twos() { lv_obj_clean_style_list(gridDisplay, LV_TABLE_PART_BG); - placeNewTile(); - placeNewTile(); + grid.PlaceNewTile(); + grid.PlaceNewTile(); + UpdateGridDisplay(); // format score text scoreText = lv_label_create(lv_scr_act(), nullptr); @@ -60,7 +157,7 @@ Twos::Twos() { lv_label_set_align(scoreText, LV_ALIGN_IN_LEFT_MID); lv_obj_align(scoreText, nullptr, LV_ALIGN_IN_TOP_LEFT, 0, 0); lv_label_set_recolor(scoreText, true); - lv_label_set_text_fmt(scoreText, "Score #FFFF00 %i#", score); + UpdateScoreDisplay(); } Twos::~Twos() { @@ -70,184 +167,52 @@ Twos::~Twos() { lv_obj_clean(lv_scr_act()); } -bool Twos::placeNewTile() { - unsigned int emptyCells[nCells]; - unsigned int nEmpty = 0; - for (unsigned int i = 0; i < nCells; i++) { - const unsigned int row = i / nCols; - const unsigned int col = i % nCols; - if (grid[row][col].value == 0) { - emptyCells[nEmpty] = i; - nEmpty++; - } - } - - if (nEmpty == 0) { - return false; // game lost - } - - int random = rand() % nEmpty; - - if ((rand() % 100) < 90) { - grid[emptyCells[random] / nCols][emptyCells[random] % nCols].value = 2; - } else { - grid[emptyCells[random] / nCols][emptyCells[random] % nCols].value = 4; - } - updateGridDisplay(); - return true; -} - -bool Twos::tryMerge(int newRow, int newCol, int oldRow, int oldCol) { - if (grid[newRow][newCol].value == grid[oldRow][oldCol].value) { - if ((newCol != oldCol) || (newRow != oldRow)) { - if (!grid[newRow][newCol].merged) { - grid[newRow][newCol].value *= 2; - score += grid[newRow][newCol].value; - lv_label_set_text_fmt(scoreText, "Score #FFFF00 %i#", score); - grid[oldRow][oldCol].value = 0; - grid[newRow][newCol].merged = true; - return true; - } - } - } - return false; -} - -bool Twos::tryMove(int newRow, int newCol, int oldRow, int oldCol) { - if (((newCol >= 0) && (newCol != oldCol)) || ((newRow >= 0) && (newRow != oldRow))) { - grid[newRow][newCol].value = grid[oldRow][oldCol].value; - grid[oldRow][oldCol].value = 0; - return true; - } - return false; -} - bool Twos::OnTouchEvent(Pinetime::Applications::TouchEvents event) { - bool validMove = false; - for (unsigned int i = 0; i < nCells; i++) { - const unsigned int row = i / nCols; - const unsigned int col = i % nCols; - grid[row][col].merged = false; // reinitialize merge state - } + int rowStep = 0; + int colStep = 0; switch (event) { case TouchEvents::SwipeLeft: - for (int col = 1; col < nCols; col++) { // ignore tiles already on far left - for (int row = 0; row < nRows; row++) { - if (grid[row][col].value > 0) { - int newCol = -1; - for (int potentialNewCol = col - 1; potentialNewCol >= 0; potentialNewCol--) { - if (grid[row][potentialNewCol].value == 0) { - newCol = potentialNewCol; - } else { // blocked by another tile - if (tryMerge(row, potentialNewCol, row, col)) { - validMove = true; - } - break; - } - } - if (tryMove(row, newCol, row, col)) { - validMove = true; - } - } - } - } - if (validMove) { - placeNewTile(); - } - return true; + colStep = -1; + break; case TouchEvents::SwipeRight: - for (int col = nCols - 2; col >= 0; col--) { // ignore tiles already on far right - for (int row = 0; row < nRows; row++) { - if (grid[row][col].value > 0) { - int newCol = -1; - for (int potentialNewCol = col + 1; potentialNewCol < nCols; potentialNewCol++) { - if (grid[row][potentialNewCol].value == 0) { - newCol = potentialNewCol; - } else { // blocked by another tile - if (tryMerge(row, potentialNewCol, row, col)) { - validMove = true; - } - break; - } - } - if (tryMove(row, newCol, row, col)) { - validMove = true; - } - } - } - } - if (validMove) { - placeNewTile(); - } - return true; + colStep = 1; + break; case TouchEvents::SwipeUp: - for (int row = 1; row < nRows; row++) { // ignore tiles already on top - for (int col = 0; col < nCols; col++) { - if (grid[row][col].value > 0) { - int newRow = -1; - for (int potentialNewRow = row - 1; potentialNewRow >= 0; potentialNewRow--) { - if (grid[potentialNewRow][col].value == 0) { - newRow = potentialNewRow; - } else { // blocked by another tile - if (tryMerge(potentialNewRow, col, row, col)) { - validMove = true; - } - break; - } - } - if (tryMove(newRow, col, row, col)) { - validMove = true; - } - } - } - } - if (validMove) { - placeNewTile(); - } - return true; + rowStep = -1; + break; case TouchEvents::SwipeDown: - for (int row = nRows - 2; row >= 0; row--) { // ignore tiles already on bottom - for (int col = 0; col < nCols; col++) { - if (grid[row][col].value > 0) { - int newRow = -1; - for (int potentialNewRow = row + 1; potentialNewRow < nRows; potentialNewRow++) { - if (grid[potentialNewRow][col].value == 0) { - newRow = potentialNewRow; - } else { // blocked by another tile - if (tryMerge(potentialNewRow, col, row, col)) { - validMove = true; - } - break; - } - } - if (tryMove(newRow, col, row, col)) { - validMove = true; - } - } - } - } - if (validMove) { - placeNewTile(); - } - return true; + rowStep = 1; + break; default: return false; } - return false; + + if (grid.Slide(rowStep, colStep)) { + grid.PlaceNewTile(); + UpdateGridDisplay(); + UpdateScoreDisplay(); + } + return true; } -void Twos::updateGridDisplay() { - for (unsigned int i = 0; i < nCells; i++) { - const unsigned int row = i / nCols; - const unsigned int col = i % nCols; - if (grid[row][col].value > 0) { - char buffer[7]; - snprintf(buffer, sizeof(buffer), "%u", grid[row][col].value); +void Twos::UpdateScoreDisplay() { + lv_label_set_text_fmt(scoreText, "Score #FFFF00 %i#", grid.GetScore()); +} + +void Twos::UpdateGridDisplay() { + for (unsigned int i = 0; i < TwosGrid::nCells; i++) { + const unsigned int row = i / TwosGrid::nCols; + const unsigned int col = i % TwosGrid::nCols; + const unsigned int value = grid.GetTileValue(row, col); + if (value > 0) { + // Large enough for any unsigned int, so the value can never be truncated + char buffer[11]; + snprintf(buffer, sizeof(buffer), "%u", value); lv_table_set_cell_value(gridDisplay, row, col, buffer); } else { lv_table_set_cell_value(gridDisplay, row, col, ""); } - switch (grid[row][col].value) { + switch (value) { case 0: lv_table_set_cell_type(gridDisplay, row, col, 1); break; diff --git a/src/displayapp/screens/Twos.h b/src/displayapp/screens/Twos.h index 9ebd7f2e44..a96b9c7007 100644 --- a/src/displayapp/screens/Twos.h +++ b/src/displayapp/screens/Twos.h @@ -12,6 +12,41 @@ namespace Pinetime { }; namespace Screens { + // Holds the state of a game of Twos and the rules that manipulate it. + // Knows nothing about how the game is drawn. + class TwosGrid { + public: + static constexpr int nCols = 4; + static constexpr int nRows = 4; + static constexpr int nCells = nCols * nRows; + + // Puts a new tile on a randomly chosen empty cell. + // Returns false when there is no empty cell left, which means the game is lost. + bool PlaceNewTile(); + + // Slides every tile as far as it goes in the given direction, merging equal + // tiles it runs into. rowStep and colStep give the direction, for example + // {0, -1} to slide left and {1, 0} to slide down. + // Returns true if any tile moved or merged. + bool Slide(int rowStep, int colStep); + + unsigned int GetTileValue(int row, int col) const { + return grid[row][col].value; + } + + unsigned int GetScore() const { + return score; + } + + private: + bool TryMerge(int newRow, int newCol, int oldRow, int oldCol); + bool TryMove(int newRow, int newCol, int oldRow, int oldCol); + void ResetMergeState(); + + TwosTile grid[nRows][nCols]; + unsigned int score = 0; + }; + class Twos : public Screen { public: Twos(); @@ -25,15 +60,9 @@ namespace Pinetime { lv_obj_t* scoreText; lv_obj_t* gridDisplay; - static constexpr int nCols = 4; - static constexpr int nRows = 4; - static constexpr int nCells = nCols * nRows; - TwosTile grid[nRows][nCols]; - unsigned int score = 0; - void updateGridDisplay(); - bool tryMerge(int newRow, int newCol, int oldRow, int oldCol); - bool tryMove(int newRow, int newCol, int oldRow, int oldCol); - bool placeNewTile(); + TwosGrid grid; + void UpdateGridDisplay(); + void UpdateScoreDisplay(); }; }