#include "SPI.h"
#include "Adafruit_GFX.h"
#include "Adafruit_ILI9341.h"
#define TFT_DC 9
#define TFT_CS 10
#define VERT_PIN A0
#define HORZ_PIN A1
#define SEL_PIN 2 // Botão do Joystick
// Instancia o objeto da tela TFT
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC);
int const blockSize = 15;
int const displayHeight = 20; // Altura do grid em blocos
int const displayWidth = 10; // Largura do grid em blocos
/*
The actual grid is represented by the even coordinates,
while the middle points are represented by the odd coordinates.
*/
class Point {
public:
int x;
int y;
Point(int x = 0, int y = 0) {
this->x = x;
this->y = y;
}
Point rotate90ccw() {
return Point(-y, x);
}
Point rotate90cw() const {
return Point(y, -x);
}
Point operator + (Point const oth) const {
return Point(x + oth.x, y + oth.y);
}
Point operator - (Point const oth) const {
return Point(x - oth.x, y - oth.y);
}
Point operator * (int scale) const {
return Point(x * scale, y * scale);
}
// CORREÇÃO: Lógica de validação de limites ajustada para X
bool gameValid(uint16_t (&board)[displayWidth][displayHeight]) {
// Garante que a coordenada X esteja dentro do limite lógico (0 a 18)
return (x % 2 == 0) && (y % 2 == 0) &&
0 <= x && x < displayWidth * 2 &&
0 <= y && (y / 2) < displayHeight &&
board[x / 2][y / 2] == 0;
}
void debug() {
Serial.print("Point: ");
Serial.print(x);
Serial.print(" ");
Serial.println(y);
}
};
// Darken 565 color was generated by ChatGPT, so no clu
uint16_t darken_565_color(uint16_t color, float factor = 0.1) {
// Extract the red, green, and blue components
uint16_t red = (color >> 11) & 0x1F;
uint16_t green = (color >> 5) & 0x3F;
uint16_t blue = color & 0x1F;
// Darken each component
red = (uint16_t)(red * (1.0f - factor));
green = (uint16_t)(green * (1.0f - factor));
blue = (uint16_t)(blue * (1.0f - factor));
// Clamp values to ensure they are within valid range
red = (red > 0x1F) ? 0x1F : red;
green = (green > 0x3F) ? 0x3F : green;
blue = (blue > 0x1F) ? 0x1F : blue;
// Reassemble the color
return (red << 11) | (green << 5) | blue;
}
void displayBlock(int x, int y, int color) {
tft.fillRect(x * blockSize, y * blockSize,
blockSize, blockSize,
darken_565_color(color, 0.3));
tft.fillRect(x * blockSize + 1, y * blockSize + 1,
blockSize - 2, blockSize - 2,
color);
}
class Tetromino {
public:
Point center;
int blockCount;
Point* blocks;
uint16_t color;
Tetromino(Point center, int blockCount, Point* blocks, uint16_t color){
this->center = center;
this->blockCount = blockCount;
this->blocks = blocks;
this->color = color;
}
Tetromino(int type) {
if (type == 0)
(*this) = Tetromino({1, 1}, 4, new Point[4]{Point(1, 1), Point(-1, 1), Point(1, -1), Point(-1, -1)}, ILI9341_YELLOW);
else if (type == 1) {
(*this) = Tetromino({2, 2}, 4, new Point[4]{Point(0, 0), Point(-2, 0), Point(2, 0), Point(2, -2)}, ILI9341_ORANGE);
} else if(type == 2) {
(*this) = Tetromino({2, 2}, 4, new Point[4]{Point(0, 0), Point(-2, 0), Point(2, 0), Point(-2, -2)}, ILI9341_BLUE);
} else if(type == 3) {
(*this) = Tetromino({2, 2}, 4, new Point[4]{Point(0, 0), Point(-2, 0), Point(2, 0), Point(4, 0)}, ILI9341_CYAN);
} else if(type == 4){
(*this) = Tetromino({2, 2}, 4, new Point[4]{Point(0, 0), Point(-2, 0), Point(0, -2), Point(2, -2)}, ILI9341_GREEN);
} else if (type == 5) {
(*this) = Tetromino({2, 2}, 4, new Point[4]{Point(0, 0), Point(-2, 0), Point(2, 0), Point(0, -2)}, ILI9341_PURPLE);
} else if (type == 6) {
(*this) = Tetromino({2, 2}, 4, new Point[4]{Point(0, 0), Point(-2, -2), Point(2, 0), Point(0, -2)}, ILI9341_RED);
}
}
void display(uint16_t (&board)[displayWidth][displayHeight]) {
for (int i = 0; i < blockCount; i++) {
Point blockPosition = blocks[i] + center;
if (blockPosition.x >= 0 && blockPosition.x < displayWidth * 2 &&
blockPosition.y >= 0 && blockPosition.y < displayHeight * 2) {
displayBlock(blockPosition.x / 2 , blockPosition.y / 2, color);
}
}
}
void undisplay(uint16_t (&board)[displayWidth][displayHeight]) {
for(int i = 0; i < blockCount; i++) {
Point blockPosition = blocks[i] + center;
if (blockPosition.x >= 0 && blockPosition.x < displayWidth * 2 &&
blockPosition.y >= 0 && blockPosition.y < displayHeight * 2) {
displayBlock(blockPosition.x / 2 , blockPosition.y / 2, 0); // Cor 0 (fundo)
}
}
}
bool checkObject(uint16_t (&board)[displayWidth][displayHeight]) {
for(int i = 0; i < blockCount; i++ ) {
if((blocks[i] + center).gameValid(board) == false)
return false;
}
return true;
}
void transform(Point oth) {
this->center.x += oth.x;
this->center.y += oth.y;
}
//Transform restricted based on game rules.
bool gameTransform(Point oth, uint16_t (&board)[displayWidth][displayHeight]) {
this->undisplay(board);
this->transform(oth);
bool verdict = true;
if(!this->checkObject(board)) {
this->transform(Point(0, 0)-oth);
verdict = false;
}
this->display(board);
return verdict;
}
void rotate90ccw() {
for(int i = 0; i < blockCount; i++) {
blocks[i] = blocks[i].rotate90ccw();
}
}
void rotate90cw() {
for(int i = 0; i < blockCount; i++) {
blocks[i] = blocks[i].rotate90cw();
}
}
void gameRotate(uint16_t (&board)[displayWidth][displayHeight]) {
this->undisplay(board);
this->rotate90ccw();
if (!this->checkObject(board)) {
this->rotate90cw();
}
this->display(board);
}
void solidify(uint16_t (&board)[displayWidth][displayHeight]) {
for (int i = 0; i < blockCount; i++) {
Point block = blocks[i] + center;
if ((block.x % 2 == 0) && (block.y % 2 == 0) &&
0 <= block.x && block.x < displayWidth * 2 &&
0 <= block.y && (block.y / 2) < displayHeight) {
board[block.x / 2][block.y / 2] = color;
}
}
}
void debug() {
Serial.println("Debug state:");
center.debug();
Serial.print("blockCount: ");
Serial.println(blockCount);
for(int i = 0; i < blockCount; i++) {
blocks[i].debug();
}
Serial.print("color: ");
Serial.println(color);
}
};
void PrintTestPieces() {
uint16_t board[displayWidth][displayHeight];
for(int i = 0; i < 7; i++) {
Tetromino tromino = Tetromino(i);
tromino.transform({4,6 + 2 + 6 * i});
for(int j = 0;j < 4; j++) {
tromino.display(board);
tromino.transform(Point(8, 0));
tromino.rotate90ccw();
}
}
}
const unsigned long gameStepMillis = 1000;
const unsigned long joystickStepMillis = 50;
class Game {
public:
uint16_t board[displayWidth][displayHeight] = {0};
Tetromino* activeObject;
bool gameOver = false;
Game() {
activeObject = nullptr;
}
void spawnObject() {
int type = random(0, 7); // Alterado para 0, 7 para incluir todos os 7 tipos
activeObject = new Tetromino(type);
// Spawna no centro superior
activeObject->transform(Point(displayWidth, 0));
// Checa se a peça pode ser spawnada (se o topo já não está cheio)
if (!activeObject->checkObject(board)) {
gameOver = true;
// Lógica de Game Over pode ser adicionada aqui
}
activeObject->display(board);
}
//
void solidify() {
activeObject->solidify(board);
delete activeObject; // Libera a memória alocada
activeObject = nullptr;
}
// Função básica para desenhar o tabuleiro estático
void drawBoard() {
for (int y = 0; y < displayHeight; y++) {
for (int x = 0; x < displayWidth; x++) {
if (board[x][y] != 0) {
displayBlock(x, y, board[x][y]);
} else {
displayBlock(x, y, ILI9341_BLACK); // Desenha fundo preto se vazio
}
}
}
}
// A função advanceIteration original não estava completa no seu código,
// mas aqui está a base para o loop do jogo:
void advanceGameLogic() {
if (activeObject == nullptr) {
spawnObject();
if (gameOver) return;
}
// Tenta mover a peça para baixo. Se não conseguir, solidifica e spawna a próxima.
if (!activeObject->gameTransform(Point(0, 2), board)) {
solidify();
spawnObject();
}
}
};
// FIM DO CÓDIGO DA LÓGICA
// =====================================
// SETUP E LOOP OBRIGATÓRIOS DO ARDUINO
// =====================================
Game myGame;
unsigned long lastGameStepTime = 0;
unsigned long lastInputTime = 0; // Para controlar a sensibilidade do joystick
void setup() {
Serial.begin(9600);
tft.begin();
tft.setRotation(3); // Ajuste a rotação conforme sua montagem
tft.fillScreen(ILI9341_BLACK);
// Define o pino do botão como INPUT_PULLUP
pinMode(SEL_PIN, INPUT_PULLUP);
// Inicializa o gerador de números aleatórios (usa um pino analógico não usado)
randomSeed(analogRead(A2));
lastGameStepTime = millis();
// Desenha a borda do jogo
// A largura total da tela é 320, o grid ocupa 10*15 = 150 pixels.
tft.drawRect(0, 0, displayWidth * blockSize + 1, displayHeight * blockSize + 1, ILI9341_WHITE);
tft.setCursor(160, 10);
tft.setTextColor(ILI9341_WHITE);
tft.setTextSize(2);
tft.println("TETRIS");
}
void loop() {
if (myGame.gameOver) {
tft.setCursor(160, 50);
tft.setTextColor(ILI9341_RED);
tft.setTextSize(3);
tft.println("GAME OVER");
while(1); // Trava o loop em caso de game over
}
unsigned long currentTime = millis();
// --- 1. Lógica de queda automática (a cada segundo) ---
if (currentTime - lastGameStepTime >= gameStepMillis) {
myGame.advanceGameLogic();
lastGameStepTime = currentTime;
}
// --- 2. Lógica de Input do Joystick (controle de velocidade) ---
if (currentTime - lastInputTime >= joystickStepMillis) {
int vertValue = analogRead(VERT_PIN);
int horzValue = analogRead(HORZ_PIN);
bool selectPressed = (digitalRead(SEL_PIN) == LOW);
// Movimento Horizontal (Esquerda/Direita)
if (horzValue < 300) { // Joystick para a esquerda
myGame.activeObject->gameTransform(Point(-2, 0), myGame.board);
lastInputTime = currentTime;
} else if (horzValue > 700) { // Joystick para a direita
myGame.activeObject->gameTransform(Point(2, 0), myGame.board);
lastInputTime = currentTime;
}
// Rotação (Botão pressionado)
if (selectPressed) {
myGame.activeObject->gameRotate(myGame.board);
lastInputTime = currentTime;
// Pequeno delay para evitar múltiplas rotações com um único clique
delay(200);
}
// Queda rápida (Joystick para baixo)
if (vertValue > 700) {
// gameTransform para baixo move de 2 em 2 unidades lógicas (1 bloco)
myGame.activeObject->gameTransform(Point(0, 2), myGame.board);
lastInputTime = currentTime;
}
}
// Desenha o tabuleiro estático atualizado
myGame.drawBoard();
}