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(); }; }