diff --git a/usermods/TetrisAI_v2/gridbw.h b/usermods/TetrisAI_v2/gridbw.h new file mode 100644 index 0000000000..af3f5bcf02 --- /dev/null +++ b/usermods/TetrisAI_v2/gridbw.h @@ -0,0 +1,117 @@ +/****************************************************************************** + * @file : gridbw.h + * @brief : contains the tetris grid as binary so black and white version + ****************************************************************************** + * @attention + * + * Copyright (c) muebau 2023 + * All rights reserved. + * + ****************************************************************************** +*/ + +#ifndef __GRIDBW_H__ +#define __GRIDBW_H__ + +#include +#include +#include "pieces.h" + +using namespace std; + +class GridBW +{ +private: +public: + uint8_t width; + uint8_t height; + std::vector pixels; + + GridBW(uint8_t width, uint8_t height): + width(width), + height(height), + pixels(height) + { + if (width > 32) + { + throw std::invalid_argument("maximal width is 32"); + } + } + + void placePiece(Piece* piece, uint8_t x, uint8_t y) + { + for (uint8_t row = 4 - piece->getRotation().height; row < 4; row++) + { + pixels[y + (row - (4 - piece->getRotation().height))] |= piece->getGridRow(x, row, width); + } + } + + void erasePiece(Piece* piece, uint8_t x, uint8_t y) + { + for (uint8_t row = 4 - piece->getRotation().height; row < 4; row++) + { + pixels[y + (row - (4 - piece->getRotation().height))] &= ~piece->getGridRow(x, row, width); + } + } + + bool noCollision(Piece* piece, uint8_t x, uint8_t y) + { + //if it touches a wall it is a collision + if (x > (this->width - piece->getRotation().width) || y > this->height - piece->getRotation().height) + { + return false; + } + + for (uint8_t row = 4 - piece->getRotation().height; row < 4; row++) + { + if (piece->getGridRow(x, row, width) & pixels[y + (row - (4 - piece->getRotation().height))]) + { + return false; + } + } + return true; + } + + void findLandingPosition(Piece* piece) + { + // move down until the piece bumps into some occupied pixels or the 'wall' + while (noCollision(piece, piece->x, piece->landingY)) + { + piece->landingY++; + } + + //at this point the positon is 'in the wall' or 'over some occupied pixel' + //so the previous position was the last correct one (clamped to 0 as minimum). + piece->landingY = piece->landingY > 0 ? piece->landingY - 1 : 0; + } + + void cleanupFullLines() + { + uint8_t offset = 0; + + //from "height - 1" to "0", so from bottom row to top + for (uint8_t row = height; row-- > 0; ) + { + //full line? + if (isLineFull(row)) + { + offset++; + pixels[row] = 0x0; + continue; + } + + if (offset > 0) + { + pixels[row + offset] = pixels[row]; + pixels[row] = 0x0; + } + } + } + + bool isLineFull(uint8_t y) + { + return pixels[y] == (uint32_t)((1 << width) - 1); + } +}; + +#endif /* __GRIDBW_H__ */ \ No newline at end of file diff --git a/usermods/TetrisAI_v2/gridcolor.h b/usermods/TetrisAI_v2/gridcolor.h new file mode 100644 index 0000000000..5c5ce7e422 --- /dev/null +++ b/usermods/TetrisAI_v2/gridcolor.h @@ -0,0 +1,132 @@ +/****************************************************************************** + * @file : gridcolor.h + * @brief : contains the tetris grid as 8bit indexed color version + ****************************************************************************** + * @attention + * + * Copyright (c) muebau 2023 + * All rights reserved. + * + ****************************************************************************** +*/ + +#ifndef __GRIDCOLOR_H__ +#define __GRIDCOLOR_H__ +#include +#include +#include +#include "gridbw.h" +#include "gridcolor.h" + +using namespace std; + +class GridColor +{ +private: +public: + uint8_t width; + uint8_t height; + GridBW gridBW; + std::vector pixels; + + GridColor(uint8_t width, uint8_t height): + width(width), + height(height), + gridBW(width, height), + pixels(width* height) + {} + + void clear() + { + for (uint8_t y = 0; y < height; y++) + { + gridBW.pixels[y] = 0x0; + for (int8_t x = 0; x < width; x++) + { + *getPixel(x, y) = 0; + } + } + } + + void placePiece(Piece* piece, uint8_t x, uint8_t y) + { + for (uint8_t pieceY = 0; pieceY < piece->getRotation().height; pieceY++) + { + for (uint8_t pieceX = 0; pieceX < piece->getRotation().width; pieceX++) + { + if (piece->getPixel(pieceX, pieceY)) + { + *getPixel(x + pieceX, y + pieceY) = piece->pieceData->colorIndex; + } + } + } + } + + void erasePiece(Piece* piece, uint8_t x, uint8_t y) + { + for (uint8_t pieceY = 0; pieceY < piece->getRotation().height; pieceY++) + { + for (uint8_t pieceX = 0; pieceX < piece->getRotation().width; pieceX++) + { + if (piece->getPixel(pieceX, pieceY)) + { + *getPixel(x + pieceX, y + pieceY) = 0; + } + } + } + } + + void cleanupFullLines() + { + uint8_t offset = 0; + //from "height - 1" to "0", so from bottom row to top + for (uint8_t y = height; y-- > 0; ) + { + if (gridBW.isLineFull(y)) + { + offset++; + for (uint8_t x = 0; x < width; x++) + { + pixels[y * width + x] = 0; + } + continue; + } + + if (offset > 0) + { + if (gridBW.pixels[y]) + { + for (uint8_t x = 0; x < width; x++) + { + pixels[(y + offset) * width + x] = pixels[y * width + x]; + pixels[y * width + x] = 0; + } + } + } + } + gridBW.cleanupFullLines(); + } + + uint8_t* getPixel(uint8_t x, uint8_t y) + { + return &pixels[y * width + x]; + } + + void sync() + { + for (uint8_t y = 0; y < height; y++) + { + gridBW.pixels[y] = 0x0; + for (int8_t x = 0; x < width; x++) + { + gridBW.pixels[y] <<= 1; + if (*getPixel(x, y) != 0) + { + gridBW.pixels[y] |= 0x1; + } + } + } + } +}; + +#endif /* __GRIDCOLOR_H__ */ \ No newline at end of file diff --git a/usermods/TetrisAI_v2/pieces.h b/usermods/TetrisAI_v2/pieces.h new file mode 100644 index 0000000000..5d461615ae --- /dev/null +++ b/usermods/TetrisAI_v2/pieces.h @@ -0,0 +1,184 @@ +/****************************************************************************** + * @file : pieces.h + * @brief : contains the tetris pieces with their colors indecies + ****************************************************************************** + * @attention + * + * Copyright (c) muebau 2022 + * All rights reserved. + * + ****************************************************************************** +*/ + +#ifndef __PIECES_H__ +#define __PIECES_H__ + +#include +#include + +#include +#include +#include +#include + +#define numPieces 7 + +struct PieceRotation +{ + uint8_t width; + uint8_t height; + uint16_t rows; +}; + +struct PieceData +{ + uint8_t rotCount; + uint8_t colorIndex; + PieceRotation rotations[4]; +}; + +PieceData piecesData[numPieces] = { + // I + { + 2, + 1, + { + { 1, 4, 0b0001000100010001}, + { 4, 1, 0b0000000000001111} + } + }, + // O + { + 1, + 2, + { + { 2, 2, 0b0000000000110011} + } + }, + // Z + { + 2, + 3, + { + { 3, 2, 0b0000000001100011}, + { 2, 3, 0b0000000100110010} + } + }, + // S + { + 2, + 4, + { + { 3, 2, 0b0000000000110110}, + { 2, 3, 0b0000001000110001} + } + }, + // L + { + 4, + 5, + { + { 2, 3, 0b0000001000100011}, + { 3, 2, 0b0000000001110100}, + { 2, 3, 0b0000001100010001}, + { 3, 2, 0b0000000000010111} + } + }, + // J + { + 4, + 6, + { + { 2, 3, 0b0000000100010011}, + { 3, 2, 0b0000000001000111}, + { 2, 3, 0b0000001100100010}, + { 3, 2, 0b0000000001110001} + } + }, + // T + { + 4, + 7, + { + { 3, 2, 0b0000000001110010}, + { 2, 3, 0b0000000100110001}, + { 3, 2, 0b0000000000100111}, + { 2, 3, 0b0000001000110010} + } + }, +}; + +class Piece +{ +private: +public: + uint8_t x; + uint8_t y; + PieceData* pieceData; + uint8_t rotation; + uint8_t landingY; + + Piece(uint8_t pieceIndex = 0): + x(0), + y(0), + rotation(0), + landingY(0) + { + this->pieceData = &piecesData[pieceIndex]; + } + + void reset() + { + this->rotation = 0; + this->x = 0; + this->y = 0; + this->landingY = 0; + } + + uint32_t getGridRow(uint8_t x, uint8_t y, uint8_t width) + { + if (x < width) + { + //shift the row with the "top-left" position to the "x" position + auto shiftx = (width - 1) - x; + auto topleftx = (getRotation().width - 1); + + auto finalShift = shiftx - topleftx; + auto row = getRow(y); + auto finalResult = row << finalShift; + + return finalResult; + } + return 0xffffffff; + } + + uint8_t getRow(uint8_t y) + { + if (y < 4) + { + return (getRotation().rows >> (12 - (4 * y))) & 0xf; + } + return 0xf; + } + + bool getPixel(uint8_t x, uint8_t y) + { + if(x > getRotation().width - 1 || y > getRotation().height - 1 ) + { + return false; + } + + if (x < 4 && y < 4) + { + return (getRow((4 - getRotation().height) + y) >> (3 - ((4 - getRotation().width) + x))) & 0x1; + } + return false; + } + + PieceRotation getRotation() + { + return this->pieceData->rotations[rotation]; + } +}; + +#endif /* __PIECES_H__ */ diff --git a/usermods/TetrisAI_v2/rating.h b/usermods/TetrisAI_v2/rating.h new file mode 100644 index 0000000000..4504ff468b --- /dev/null +++ b/usermods/TetrisAI_v2/rating.h @@ -0,0 +1,64 @@ +/****************************************************************************** + * @file : rating.h + * @brief : contains the tetris rating of a grid + ****************************************************************************** + * @attention + * + * Copyright (c) muebau 2022 + * All rights reserved. + * + ****************************************************************************** +*/ + +#ifndef __RATING_H__ +#define __RATING_H__ + +#include +#include +#include +#include +#include +#include "rating.h" + +using namespace std; + +class Rating +{ +private: +public: + uint8_t minHeight; + uint8_t maxHeight; + uint16_t holes; + uint8_t fullLines; + uint16_t bumpiness; + uint16_t aggregatedHeight; + double score; + uint8_t width; + std::vector lineHights; + + Rating(uint8_t width): + width(width), + lineHights(width) + { + reset(); + } + + void reset() + { + this->minHeight = 0; + this->maxHeight = 0; + + for (uint8_t line = 0; line < this->width; line++) + { + this->lineHights[line] = 0; + } + + this->holes = 0; + this->fullLines = 0; + this->bumpiness = 0; + this->aggregatedHeight = 0; + this->score = -DBL_MAX; + } +}; + +#endif /* __RATING_H__ */ diff --git a/usermods/TetrisAI_v2/readme.md b/usermods/TetrisAI_v2/readme.md new file mode 100644 index 0000000000..b21bc5fde1 --- /dev/null +++ b/usermods/TetrisAI_v2/readme.md @@ -0,0 +1,33 @@ +# Tetris AI effect usermod + +This usermod brings you a effect brings a self playing Tetris game. The mod needs version 0.14 or above as it is based on matrix support. The effect was tested on an ESP32 with a WS2812B 16x16 matrix. + +Version 1.0 + +## Installation + +Just activate the usermod with `-D USERMOD_TETRISAI` and the effect will become available under the name 'Tetris AI'. + +## Usage + +It is best to set the background color to black, the border color to light grey and the game over color (foreground) to dark grey. + +### Sliders and boxes + +#### Sliders + +* speed: speed the game plays +* look ahead: how many pieces is the AI allowed to know the next pieces (0 - 2) +* intelligence: how good the AI will play +* Rotate color: make the colors shift (rotate) every few cicles +* Mistakes free: how many good moves between mistakes (if activated) + +#### Checkboxes + +* show next: if true a space of 5 pixels from the right is used to show the next pieces. The whole segment is used for the grid otherwise. +* show border: if true an additional column of 1 pixel is used to draw a border between the grid and the next pieces +* mistakes: if true the worst instead of the best move is choosen every few moves (read above) + +## Best results + + If the speed is set to be a little bit faster than a good human could play with maximal intelligence and very few mistakes it makes people furious/happy at a party. \ No newline at end of file diff --git a/usermods/TetrisAI_v2/tetrisai.h b/usermods/TetrisAI_v2/tetrisai.h new file mode 100644 index 0000000000..ef7868a47c --- /dev/null +++ b/usermods/TetrisAI_v2/tetrisai.h @@ -0,0 +1,302 @@ +/****************************************************************************** + * @file : ai.h + * @brief : contains the heuristic + ****************************************************************************** + * @attention + * + * Copyright (c) muebau 2023 + * All rights reserved. + * + ****************************************************************************** +*/ + +#ifndef __AI_H__ +#define __AI_H__ + +#include "gridbw.h" +#include "rating.h" + +using namespace std; + +class TetrisAI +{ +private: +public: + double aHeight; + double fullLines; + double holes; + double bumpiness; + bool findWorstMove = false; + + uint8_t countOnes(uint32_t vector) + { + uint8_t count = 0; + while (vector) + { + vector &= (vector - 1); + count++; + } + return count; + } + + void updateRating(GridBW grid, Rating* rating) + { + rating->minHeight = 0; + rating->maxHeight = 0; + rating->holes = 0; + rating->fullLines = 0; + rating->bumpiness = 0; + rating->aggregatedHeight = 0; + fill(rating->lineHights.begin(), rating->lineHights.end(), 0); + + uint32_t columnvector = 0x0; + uint32_t lastcolumnvector = 0x0; + for (uint8_t row = 0; row < grid.height; row++) + { + columnvector |= grid.pixels[row]; + + //first (highest) column makes it + if (rating->maxHeight == 0 && columnvector) + { + rating->maxHeight = grid.height - row; + } + + //if column vector is full we found the minimal height (or it stays zero) + if (rating->minHeight == 0 && (columnvector == (uint32_t)((1 << grid.width) - 1))) + { + rating->minHeight = grid.height - row; + } + + //line full if all ones in mask :-) + if (grid.isLineFull(row)) + { + rating->fullLines++; + } + + //holes are basically a XOR with the "full" columns + rating->holes += countOnes(columnvector ^ grid.pixels[row]); + + //calculate the difference (XOR) between the current column vector and the last one + uint32_t columnDelta = columnvector ^ lastcolumnvector; + + //process every new column + uint8_t index = 0; + while (columnDelta) + { + //if this is a new column + if (columnDelta & 0x1) + { + //update hight of this column + rating->lineHights[(grid.width - 1) - index] = grid.height - row; + + // update aggregatedHeight + rating->aggregatedHeight += grid.height - row; + } + index++; + columnDelta >>= 1; + } + lastcolumnvector = columnvector; + } + + //compare every two columns to get the difference and add them up + for (uint8_t column = 1; column < grid.width; column++) + { + rating->bumpiness += abs(rating->lineHights[column - 1] - rating->lineHights[column]); + } + + rating->score = (aHeight * (rating->aggregatedHeight)) + (fullLines * (rating->fullLines)) + (holes * (rating->holes)) + (bumpiness * (rating->bumpiness)); + } + + TetrisAI(): TetrisAI(-0.510066, 0.760666, -0.35663, -0.184483) + {} + + TetrisAI(double aHeight, double fullLines, double holes, double bumpiness): + aHeight(aHeight), + fullLines(fullLines), + holes(holes), + bumpiness(bumpiness) + {} + + void findBestMove(GridBW grid, Piece *piece) + { + vector pieces = {*piece}; + findBestMove(grid, &pieces); + *piece = pieces[0]; + } + + void findBestMove(GridBW grid, std::vector *pieces) + { + findBestMove(grid, pieces->begin(), pieces->end()); + } + + void findBestMove(GridBW grid, std::vector::iterator start, std::vector::iterator end) + { + Rating bestRating(grid.width); + findBestMove(grid, start, end, &bestRating); + } + + void findBestMove(GridBW grid, std::vector::iterator start, std::vector::iterator end, Rating* bestRating) + { + grid.cleanupFullLines(); + Rating curRating(grid.width); + Rating deeperRating(grid.width); + Piece piece = *start; + + // for every rotation of the piece + for (piece.rotation = 0; piece.rotation < piece.pieceData->rotCount; piece.rotation++) + { + // put piece to top left corner + piece.x = 0; + piece.y = 0; + + //test for every column + for (piece.x = 0; piece.x <= grid.width - piece.getRotation().width; piece.x++) + { + //todo optimise by the use of the previous grids height + piece.landingY = 0; + //will set landingY to final position + grid.findLandingPosition(&piece); + + // draw piece + grid.placePiece(&piece, piece.x, piece.landingY); + + if(start == end - 1) + { + //at the deepest level + updateRating(grid, &curRating); + } + else + { + //go deeper to take another piece into account + findBestMove(grid, start + 1, end, &deeperRating); + curRating = deeperRating; + } + + // eraese piece + grid.erasePiece(&piece, piece.x, piece.landingY); + + if(findWorstMove) + { + //init rating for worst + if(bestRating->score == -DBL_MAX) + { + bestRating->score = DBL_MAX; + } + + // update if we found a worse one + if (bestRating->score > curRating.score) + { + *bestRating = curRating; + (*start) = piece; + } + } + else + { + // update if we found a better one + if (bestRating->score < curRating.score) + { + *bestRating = curRating; + (*start) = piece; + } + } + } + } + } + + bool findBestMoveNonBlocking(GridBW grid, std::vector::iterator start, std::vector::iterator end, Rating* bestRating) + { + //vector with pieces + //for every piece + //for every + switch (expression) + { + case INIT: + break; + + default: + break; + } + } + + bool findBestMoveNonBlocking(GridBW grid, std::vector::iterator start, std::vector::iterator end, Rating* bestRating) + { + //INIT + grid.cleanupFullLines(); + Rating curRating(grid.width); + Rating deeperRating(grid.width); + Piece piece = *start; + + // for every rotation of the piece + piece.rotation = 0; + + //HANDLE + while (piece.rotation < piece.pieceData->rotCount) + { + // put piece to top left corner + piece.x = 0; + piece.y = 0; + + //test for every column + piece.x = 0; + while (piece.x <= grid.width - piece.getRotation().width) + { + + //todo optimise by the use of the previous grids height + piece.landingY = 0; + //will set landingY to final position + grid.findLandingPosition(&piece); + + // draw piece + grid.placePiece(&piece, piece.x, piece.landingY); + + if(start == end - 1) + { + //at the deepest level + updateRating(grid, &curRating); + } + else + { + //go deeper to take another piece into account + findBestMove(grid, start + 1, end, &deeperRating); + curRating = deeperRating; + } + + // eraese piece + grid.erasePiece(&piece, piece.x, piece.landingY); + + if(findWorstMove) + { + //init rating for worst + if(bestRating->score == -DBL_MAX) + { + bestRating->score = DBL_MAX; + } + + // update if we found a worse one + if (bestRating->score > curRating.score) + { + *bestRating = curRating; + (*start) = piece; + } + } + else + { + // update if we found a better one + if (bestRating->score < curRating.score) + { + *bestRating = curRating; + (*start) = piece; + } + } + piece.x++; + } + piece.rotation++; + } + + //EXIT + + return true; + } +}; + +#endif /* __AI_H__ */ \ No newline at end of file diff --git a/usermods/TetrisAI_v2/tetrisaigame.h b/usermods/TetrisAI_v2/tetrisaigame.h new file mode 100644 index 0000000000..de3c86e7e3 --- /dev/null +++ b/usermods/TetrisAI_v2/tetrisaigame.h @@ -0,0 +1,150 @@ +/****************************************************************************** + * @file : tetrisaigame.h + * @brief : main tetris functions + ****************************************************************************** + * @attention + * + * Copyright (c) muebau 2022 + * All rights reserved. + * + ****************************************************************************** +*/ + +#ifndef __TETRISAIGAME_H__ +#define __TETRISAIGAME_H__ + +#include +#include +#include +#include "pieces.h" +#include "gridcolor.h" +#include "tetrisbag.h" +#include "tetrisai.h" + +using namespace std; + +class TetrisAIGame +{ +private: + bool animateFallOfPiece(Piece* piece, bool skip) + { + if (skip || piece->y >= piece->landingY) + { + piece->y = piece->landingY; + grid.gridBW.placePiece(piece, piece->x, piece->landingY); + grid.placePiece(piece, piece->x, piece->y); + return false; + } + else + { + // eraese last drawing + grid.erasePiece(piece, piece->x, piece->y); + + //move piece down + piece->y++; + + // draw piece + grid.placePiece(piece, piece->x, piece->y); + + return true; + } + } + +public: + uint8_t width; + uint8_t height; + uint8_t nLookAhead; + TetrisBag bag; + GridColor grid; + TetrisAI ai; + Piece curPiece; + PieceData* piecesData; + enum States { INIT, TEST_GAME_OVER, GET_NEXT_PIECE, FIND_BEST_MOVE, ANIMATE_MOVE, ANIMATE_GAME_OVER } state = INIT; + + TetrisAIGame(uint8_t width, uint8_t height, uint8_t nLookAhead, PieceData* piecesData, uint8_t nPieces): + width(width), + height(height), + nLookAhead(nLookAhead), + bag(nPieces, 1, nLookAhead), + grid(width, height + 4), + ai(), + piecesData(piecesData) + { + } + + void nextPiece() + { + grid.cleanupFullLines(); + bag.queuePiece(); + } + + void findBestMove() + { + ai.findBestMove(grid.gridBW, &bag.piecesQueue); + } + + bool animateFall(bool skip) + { + return animateFallOfPiece(&(bag.piecesQueue[0]), skip); + } + + bool isGameOver() + { + //if there is something in the 4 lines of the hidden area the game is over + return grid.gridBW.pixels[0] || grid.gridBW.pixels[1] || grid.gridBW.pixels[2] || grid.gridBW.pixels[3]; + } + + void poll() + { + switch (state) + { + case INIT: + reset(); + state = TEST_GAME_OVER; + break; + case TEST_GAME_OVER: + if (isGameOver()) + { + state = ANIMATE_GAME_OVER; + } + else + { + state = GET_NEXT_PIECE; + } + break; + case GET_NEXT_PIECE: + nextPiece(); + state = FIND_BEST_MOVE; + break; + case FIND_BEST_MOVE: + findBestMove(); + state = ANIMATE_MOVE; + break; + case ANIMATE_MOVE: + if (!animateFall(false)) + { + state = TEST_GAME_OVER; + } + break; + case ANIMATE_GAME_OVER: + static auto curPixel = grid.pixels.size(); + grid.pixels[curPixel] = 254; + + if (curPixel == 0) + { + state = INIT; + curPixel = grid.pixels.size(); + } + curPixel--; + break; + } + } + + void reset() + { + grid.clear(); + bag.init(); + } +}; + +#endif /* __TETRISAIGAME_H__ */ diff --git a/usermods/TetrisAI_v2/tetrisbag.h b/usermods/TetrisAI_v2/tetrisbag.h new file mode 100644 index 0000000000..3ecadbc0be --- /dev/null +++ b/usermods/TetrisAI_v2/tetrisbag.h @@ -0,0 +1,100 @@ +/****************************************************************************** + * @file : tetrisbag.h + * @brief : the tetris implementation of a random piece generator + ****************************************************************************** + * @attention + * + * Copyright (c) muebau 2022 + * All rights reserved. + * + ****************************************************************************** +*/ + +#ifndef __TETRISBAG_H__ +#define __TETRISBAG_H__ + +#include +#include +#include + +#include "tetrisbag.h" + +class TetrisBag +{ +private: +public: + uint8_t nPieces; + uint8_t nBagLength; + uint8_t bagIdx; + std::vector bag; + std::vector piecesQueue; + + TetrisBag(uint8_t nPieces, uint8_t nBagLength, uint8_t queueLength): + nPieces(nPieces), + nBagLength(nBagLength), + bag(nPieces * nBagLength), + piecesQueue(queueLength) + { + init(); + } + + void init() + { + //will shuffle the bag at first use + bagIdx = nPieces - 1; + + for (uint8_t bagIndex = 0; bagIndex < nPieces * nBagLength; bagIndex++) + { + bag[bagIndex] = bagIndex % nPieces; + } + + //will init the queue + for (uint8_t index = 0; index < piecesQueue.size(); index++) + { + queuePiece(); + } + } + + void shuffleBag() + { + uint8_t temp; + uint8_t swapIdx; + for (int index = nPieces - 1; index > 0; index--) + { + //get candidate to swap + swapIdx = rand() % index; + + //swap it! + temp = bag[swapIdx]; + bag[swapIdx] = bag[index]; + bag[index] = temp; + } + } + + Piece getNextPiece() + { + bagIdx++; + if (bagIdx >= nPieces) + { + shuffleBag(); + bagIdx = 0; + } + return Piece(bag[bagIdx]); + } + + void queuePiece() + { + //move vector to left + std::rotate(piecesQueue.begin(), piecesQueue.begin() + 1, piecesQueue.end()); + piecesQueue[piecesQueue.size() - 1] = getNextPiece(); + } + + void queuePiece(uint8_t idx) + { + //move vector to left + std::rotate(piecesQueue.begin(), piecesQueue.begin() + 1, piecesQueue.end()); + piecesQueue[piecesQueue.size() - 1] = Piece(idx % nPieces); + } +}; + +#endif /* __TETRISBAG_H__ */ diff --git a/usermods/TetrisAI_v2/usermod_v2_tetrisai.h b/usermods/TetrisAI_v2/usermod_v2_tetrisai.h new file mode 100644 index 0000000000..1c077d0485 --- /dev/null +++ b/usermods/TetrisAI_v2/usermod_v2_tetrisai.h @@ -0,0 +1,222 @@ +#pragma once + +#include "wled.h" +#include "FX.h" +#include "fcn_declare.h" + +#include "tetrisaigame.h" +// By: muebau + +typedef struct TetrisAI_data +{ + unsigned long lastTime = 0; + TetrisAIGame tetris; + uint8_t intelligence; + uint8_t rotate; + bool showNext; + bool showBorder; + uint8_t colorOffset; + uint8_t colorInc; + uint8_t mistaceCountdown; +} tetrisai_data; + +void drawGrid(TetrisAIGame* tetris, TetrisAI_data* tetrisai_data) +{ + SEGMENT.fill(SEGCOLOR(1)); + + //GRID + for (auto index_y = 4; index_y < tetris->grid.height; 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) + { + //BG color + color = SEGCOLOR(1); + } + //game over animation + else if(*tetris->grid.getPixel(index_x, index_y) == 254) + { + //use fg + color = SEGCOLOR(0); + } + else + { + //spread the color over the whole palette + uint8_t colorIndex = *tetris->grid.getPixel(index_x, index_y) * 32; + colorIndex += tetrisai_data->colorOffset; + color = ColorFromPalette(SEGPALETTE, colorIndex, 255, NOBLEND); + } + + SEGMENT.setPixelColorXY(index_x, index_y - 4, color); + } + } + tetrisai_data->colorOffset += tetrisai_data->colorInc; + + //NEXT PIECE AREA + if (tetrisai_data->showNext) + { + //BORDER + if (tetrisai_data->showBorder) + { + //draw a line 6 pixels from right with the border color + for (auto index_y = 0; index_y < SEGMENT.virtualHeight(); index_y++) + { + SEGMENT.setPixelColorXY(SEGMENT.virtualWidth() - 6, index_y, SEGCOLOR(2)); + } + } + + //NEXT PIECE + int piecesOffsetX = SEGMENT.virtualWidth() - 4; + int piecesOffsetY = 1; + for (uint8_t nextPieceIdx = 1; nextPieceIdx < tetris->nLookAhead; nextPieceIdx++) + { + uint8_t pieceNbrOffsetY = (nextPieceIdx - 1) * 5; + + Piece piece(tetris->bag.piecesQueue[nextPieceIdx]); + + for (uint8_t pieceY = 0; pieceY < piece.getRotation().height; pieceY++) + { + for (uint8_t pieceX = 0; pieceX < piece.getRotation().width; pieceX++) + { + if (piece.getPixel(pieceX, pieceY)) + { + uint8_t colIdx = ((piece.pieceData->colorIndex * 32) + tetrisai_data->colorOffset); + SEGMENT.setPixelColorXY(piecesOffsetX + pieceX, piecesOffsetY + pieceNbrOffsetY + pieceY, ColorFromPalette(SEGPALETTE, colIdx, 255, NOBLEND)); + } + } + } + } + } +} + +//////////////////////////// +// 2D Tetris AI // +//////////////////////////// +uint16_t mode_2DTetrisAI() +{ + if (!strip.isMatrix || !SEGENV.allocateData(sizeof(tetrisai_data))) + { + // not a 2D set-up + SEGMENT.fill(SEGCOLOR(0)); + return 350; + } + TetrisAI_data* tetrisai_data = reinterpret_cast(SEGENV.data); + + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + + //range 0 - 1024ms => 1024/255 ~ 4 + uint16_t msDelayMove = 1024 - (4 * SEGMENT.speed); + int16_t msDelayGameOver = msDelayMove / 4; + + //range 0 - 2 (not including current) + uint8_t nLookAhead = SEGMENT.intensity ? (SEGMENT.intensity >> 7) + 2 : 1; + //range 0 - 16 + tetrisai_data->colorInc = SEGMENT.custom2 >> 4; + + if (!tetrisai_data->tetris || (tetrisai_data->tetris.nLookAhead != nLookAhead + || tetrisai_data->showNext != SEGMENT.check1 + || tetrisai_data->showBorder != SEGMENT.check2 + ) + ) + { + tetrisai_data->showNext = SEGMENT.check1; + tetrisai_data->showBorder = SEGMENT.check2; + + //not more than 32 as this is the limit of this implementation + uint8_t gridWidth = cols < 32 ? cols : 32; + uint8_t gridHeight = rows; + + // do we need space for the 'next' section? + if (tetrisai_data->showNext) + { + // make space for the piece and one pixel of space + gridWidth = gridWidth - 5; + + // do we need space for a border? + if (tetrisai_data->showBorder) + { + gridWidth = gridWidth - 1; + } + } + + tetrisai_data->tetris = TetrisAIGame(gridWidth, gridHeight, nLookAhead, piecesData, numPieces); + SEGMENT.fill(SEGCOLOR(1)); + } + + if (tetrisai_data->intelligence != SEGMENT.custom1) + { + tetrisai_data->intelligence = SEGMENT.custom1; + double dui = 0.2 - (0.2 * (tetrisai_data->intelligence / 255.0)); + + tetrisai_data->tetris.ai.aHeight = -0.510066 + dui; + tetrisai_data->tetris.ai.fullLines = 0.760666 - dui; + tetrisai_data->tetris.ai.holes = -0.35663 + dui; + tetrisai_data->tetris.ai.bumpiness = -0.184483 + dui; + } + + if (tetrisai_data->tetris.state == TetrisAIGame::ANIMATE_MOVE) + { + if (millis() - tetrisai_data->lastTime > msDelayMove) + { + drawGrid(&tetrisai_data->tetris, tetrisai_data); + tetrisai_data->lastTime = millis(); + tetrisai_data->tetris.poll(); + } + } + else if (tetrisai_data->tetris.state == TetrisAIGame::ANIMATE_GAME_OVER) + { + if (millis() - tetrisai_data->lastTime > msDelayGameOver) + { + drawGrid(&tetrisai_data->tetris, tetrisai_data); + tetrisai_data->lastTime = millis(); + tetrisai_data->tetris.poll(); + } + } + else if (tetrisai_data->tetris.state == TetrisAIGame::FIND_BEST_MOVE) + { + if (SEGMENT.check3) + { + if(tetrisai_data->mistaceCountdown == 0) + { + tetrisai_data->tetris.ai.findWorstMove = true; + tetrisai_data->tetris.poll(); + tetrisai_data->tetris.ai.findWorstMove = false; + tetrisai_data->mistaceCountdown = SEGMENT.custom3; + } + tetrisai_data->mistaceCountdown--; + } + tetrisai_data->tetris.poll(); + } + else + { + tetrisai_data->tetris.poll(); + } + + return FRAMETIME; +} // mode_2DTetrisAI() +static const char _data_FX_MODE_2DTETRISAI[] PROGMEM = "Tetris AI@!,Look ahead,Intelligence,Rotate color,Mistake free,Show next,Border,Mistakes;Game Over,!,Border;!;2;sx=127,ix=64,c1=255,c2=0,c3=31,o1=1,o2=1,o3=0,pal=11"; + +class TetrisAIUsermod : public Usermod +{ + +private: + +public: + void setup() + { + strip.addEffect(255, &mode_2DTetrisAI, _data_FX_MODE_2DTETRISAI); + } + + void loop() + { + + } + + uint16_t getId() + { + return USERMOD_ID_TETRISAI; + } +}; diff --git a/wled00/const.h b/wled00/const.h index dd965bc40c..611230ea24 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -180,6 +180,7 @@ #define USERMOD_ID_STAIRWAY_WIPE 44 //Usermod "stairway-wipe-usermod-v2.h" #define USERMOD_ID_ANIMARTRIX 45 //Usermod "usermod_v2_animartrix.h" #define USERMOD_ID_HTTP_PULL_LIGHT_CONTROL 46 //usermod "usermod_v2_HttpPullLightControl.h" +#define USERMOD_ID_TETRISAI 47 //Usermod "usermod_v2_tetris.h" //Access point behavior #define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot diff --git a/wled00/usermods_list.cpp b/wled00/usermods_list.cpp index bcfb41fa3b..f76606aae6 100644 --- a/wled00/usermods_list.cpp +++ b/wled00/usermods_list.cpp @@ -209,6 +209,10 @@ #include "../usermods/stairway_wipe_basic/stairway-wipe-usermod-v2.h" #endif +#ifdef USERMOD_TETRISAI +#include "../usermods/TetrisAI_v2/usermod_v2_tetrisai.h" +#endif + void registerUsermods() { /* @@ -405,4 +409,8 @@ void registerUsermods() #ifdef USERMOD_STAIRCASE_WIPE usermods.add(new StairwayWipeUsermod()); #endif + + #ifdef USERMOD_TETRISAI + usermods.add(new TetrisAIUsermod()); + #endif }