diff --git a/usermods/TetrisAI_v2/TetrisAI_v2.cpp b/usermods/TetrisAI_v2/TetrisAI_v2.cpp index d4fefe104..f9a86f304 100644 --- a/usermods/TetrisAI_v2/TetrisAI_v2.cpp +++ b/usermods/TetrisAI_v2/TetrisAI_v2.cpp @@ -5,9 +5,12 @@ #include "tetrisaigame.h" // By: muebau +bool noFlashOnClear = false; + typedef struct TetrisAI_data { unsigned long lastTime = 0; + unsigned long clearingStartTime = 0; TetrisAIGame tetris; uint8_t intelligence; uint8_t rotate; @@ -31,16 +34,27 @@ void drawGrid(TetrisAIGame* tetris, TetrisAI_data* tetrisai_data) //GRID for (auto index_y = 4; index_y < tetris->grid.height; index_y++) { + bool isRowClearing = tetris->grid.gridBW.clearingRows[index_y]; for (auto index_x = 0; index_x < tetris->grid.width; index_x++) { CRGB color; - if (*tetris->grid.getPixel(index_x, index_y) == 0) - { + uint8_t gridPixel = *tetris->grid.getPixel(index_x, index_y); + if (isRowClearing) { + if (noFlashOnClear) { + color = CRGB::Gray; + } else { + //flash color white and black every 200ms + color = (strip.now % 200) < 150 + ? CRGB::Gray + : CRGB::Black; + } + } + else if (gridPixel == 0) { //BG color color = SEGCOLOR(1); } //game over animation - else if(*tetris->grid.getPixel(index_x, index_y) == 254) + else if (gridPixel == 254) { //use fg color = SEGCOLOR(0); @@ -48,7 +62,7 @@ void drawGrid(TetrisAIGame* tetris, TetrisAI_data* tetrisai_data) else { //spread the color over the whole palette - uint8_t colorIndex = *tetris->grid.getPixel(index_x, index_y) * 32; + uint8_t colorIndex = gridPixel * 32; colorIndex += tetrisai_data->colorOffset; color = ColorFromPalette(SEGPALETTE, colorIndex, 255, NOBLEND); } @@ -170,6 +184,7 @@ void mode_2DTetrisAI() tetrisai_data->tetris = TetrisAIGame(gridWidth, gridHeight, nLookAhead, piecesData, numPieces); tetrisai_data->tetris.state = TetrisAIGame::States::INIT; + tetrisai_data->clearingStartTime = 0; SEGMENT.fill(SEGCOLOR(1)); } @@ -184,7 +199,21 @@ void mode_2DTetrisAI() tetrisai_data->tetris.ai.bumpiness = -0.184483f + dui; } - if (tetrisai_data->tetris.state == TetrisAIGame::ANIMATE_MOVE) + //end line clearing flashing effect if needed + if (tetrisai_data->tetris.grid.gridBW.hasClearingRows()) + { + if (tetrisai_data->clearingStartTime == 0) { + tetrisai_data->clearingStartTime = strip.now; + } + if (strip.now - tetrisai_data->clearingStartTime > 750) + { + tetrisai_data->tetris.grid.gridBW.clearedLinesReadyForRemoval = true; + tetrisai_data->tetris.grid.cleanupFullLines(); + tetrisai_data->clearingStartTime = 0; + } + drawGrid(&tetrisai_data->tetris, tetrisai_data); + } + else if (tetrisai_data->tetris.state == TetrisAIGame::ANIMATE_MOVE) { if (strip.now - tetrisai_data->lastTime > msDelayMove) @@ -229,6 +258,7 @@ class TetrisAIUsermod : public Usermod { private: + static const char _name[]; public: void setup() @@ -236,6 +266,20 @@ public: strip.addEffect(255, &mode_2DTetrisAI, _data_FX_MODE_2DTETRISAI); } + void addToConfig(JsonObject& root) override + { + JsonObject top = root.createNestedObject(FPSTR(_name)); + top["noFlashOnClear"] = noFlashOnClear; + } + + bool readFromConfig(JsonObject& root) override + { + JsonObject top = root[FPSTR(_name)]; + bool configComplete = !top.isNull(); + configComplete &= getJsonValue(top["noFlashOnClear"], noFlashOnClear); + return configComplete; + } + void loop() { @@ -247,6 +291,7 @@ public: } }; +const char TetrisAIUsermod::_name[] PROGMEM = "TetrisAI_v2"; static TetrisAIUsermod tetrisai_v2; REGISTER_USERMOD(tetrisai_v2); \ No newline at end of file diff --git a/usermods/TetrisAI_v2/gridbw.h b/usermods/TetrisAI_v2/gridbw.h index a96351749..5db6e25a9 100644 --- a/usermods/TetrisAI_v2/gridbw.h +++ b/usermods/TetrisAI_v2/gridbw.h @@ -25,11 +25,18 @@ public: uint8_t width; uint8_t height; std::vector pixels; + // When a row fills, we mark it here first so it can flash before being + // fully removed. + std::vector clearingRows; + // True when a line clearing flashing effect is over and we're ready to + // fully clean up the lines + bool clearedLinesReadyForRemoval = false; GridBW(uint8_t width, uint8_t height): width(width), height(height), - pixels(height) + pixels(height), + clearingRows(height) { if (width > 32) { @@ -84,9 +91,26 @@ public: piece->landingY = piece->landingY > 0 ? piece->landingY - 1 : 0; } + bool hasClearingRows() + { + for (bool rowClearing : clearingRows) + { + if (rowClearing) + { + return true; + } + } + return false; + } + void cleanupFullLines() { + // Skip cleanup if there are rows clearing + if (hasClearingRows() && !clearedLinesReadyForRemoval) { + return; + } uint8_t offset = 0; + bool doneRemovingClearedLines = false; //from "height - 1" to "0", so from bottom row to top for (uint8_t row = height; row-- > 0; ) @@ -94,8 +118,13 @@ public: //full line? if (isLineFull(row)) { - offset++; - pixels[row] = 0x0; + if (clearedLinesReadyForRemoval) { + offset++; + pixels[row] = 0x0; + doneRemovingClearedLines = true; + } else { + clearingRows[row] = true; + } continue; } @@ -105,11 +134,20 @@ public: pixels[row] = 0x0; } } + if (doneRemovingClearedLines) { + clearingRows.assign(height, false); + clearedLinesReadyForRemoval = false; + } } bool isLineFull(uint8_t y) { - return pixels[y] == (uint32_t)((1 << width) - 1); + return pixels[y] == (width >= 32 ? UINT32_MAX : (1U << width) - 1); + } + + bool isLineReadyForRemoval(uint8_t y) + { + return clearedLinesReadyForRemoval && isLineFull(y); } void reset() @@ -121,6 +159,8 @@ public: pixels.clear(); pixels.resize(height); + clearingRows.assign(height, false); + clearedLinesReadyForRemoval = false; } }; diff --git a/usermods/TetrisAI_v2/gridcolor.h b/usermods/TetrisAI_v2/gridcolor.h index 815c2a560..5f6902796 100644 --- a/usermods/TetrisAI_v2/gridcolor.h +++ b/usermods/TetrisAI_v2/gridcolor.h @@ -82,7 +82,7 @@ public: //from "height - 1" to "0", so from bottom row to top for (uint8_t y = height; y-- > 0; ) { - if (gridBW.isLineFull(y)) + if (gridBW.isLineReadyForRemoval(y)) { offset++; for (uint8_t x = 0; x < width; x++) diff --git a/usermods/TetrisAI_v2/readme.md b/usermods/TetrisAI_v2/readme.md index 5ac802896..7962a9010 100644 --- a/usermods/TetrisAI_v2/readme.md +++ b/usermods/TetrisAI_v2/readme.md @@ -2,13 +2,16 @@ This usermod adds a self-playing Tetris game as an 'effect'. The mod requires version 0.14 or higher as it relies on matrix support. The effect was tested on an ESP32 4MB with a WS2812B 16x16 matrix. -Version 1.0 +PHOTOSENSITIVE EPILEPSY WARNING: By default the effect features a flashing animation on line clear. This can be disabled +from the usermod settings page in WLED. ## Installation -Just activate the usermod with `-D USERMOD_TETRISAI` and the effect will become available under the name 'Tetris AI'. If you are running out of flash memory, use a different memory layout (e.g. [WLED_ESP32_4MB_256KB_FS.csv](https://github.com/wled-dev/WLED/blob/main/tools/WLED_ESP32_4MB_256KB_FS.csv)). +To activate the usermod, add the following line to your platformio_override.ini +`custom_usermods = tetrisai_v2` +The effect will then become available under the name 'Tetris AI'. If you are running out of flash memory, use a different memory layout (e.g. [WLED_ESP32_4MB_256KB_FS.csv](https://github.com/wled-dev/WLED/blob/main/tools/WLED_ESP32_4MB_256KB_FS.csv)). -If needed simply add to `platformio_override.ini` (or `platformio_override.ini`): +If needed simply add to `platformio_override.ini`: ```ini board_build.partitions = tools/WLED_ESP32_4MB_256KB_FS.csv diff --git a/usermods/TetrisAI_v2/tetrisai.h b/usermods/TetrisAI_v2/tetrisai.h index ba4fe60e4..7bb1d9321 100644 --- a/usermods/TetrisAI_v2/tetrisai.h +++ b/usermods/TetrisAI_v2/tetrisai.h @@ -68,7 +68,7 @@ public: } //line full if all ones in mask :-) - if (grid.isLineFull(row)) + if (grid.isLineReadyForRemoval(row)) { rating->fullLines++; }