#include <Adafruit_GFX.h>
#include <Adafruit_NeoMatrix.h>
#include <Adafruit_NeoPixel.h>
#include <Fonts/FreeSansBold9pt7b.h>
#include "animationFrames.h"
#define PIN 4 // Pin de la tira de LED
#define XPIN A0 // Pin x del joystick
#define YPIN A1 // Pin y del joystick
#define BUTTON_PIN 3 // Pin del botón de cambio de pantalla
#define TOGGLE_BUTTON 6 // Pin para encender o apagar los leds
#define JOY_BUTTON_PIN 2 // Pin del botón del joystick
#define POT_PIN A2 // Pin del potenciómetro
#define LED_PIN 7 // Pin del led
#define NUM_ROWS 16
#define NUM_COLS 16
#define NUM_LEDS (NUM_ROWS * NUM_COLS)
Adafruit_NeoMatrix matrix = Adafruit_NeoMatrix(NUM_ROWS, NUM_COLS, PIN,
NEO_MATRIX_TOP + NEO_MATRIX_LEFT +
NEO_MATRIX_ROWS + NEO_MATRIX_PROGRESSIVE,
NEO_GRB + NEO_KHZ800);
enum { PLAYER_X, PLAYER_O }; // Jugadores
int currentPlayer = PLAYER_X; // Jugador actual
bool ledsOn = false; // Estado de los leds (encendidos o apagados)
// Comienza el cursor en el centro
int cursorX = 1, cursorY = 1;
bool gameBoard[3][3] = {0}; // Tablero de juego
bool xPositions[3][3] = {0};
bool oPositions[3][3] = {0};
// Controla si el juego está activo o en estado de fin/ganar
bool winnerAnnounced = false;
bool gameActive = true;
static bool winnerAnimationActive = false;
/**
* tiempo de rebote en milisegundos. Sirve para evitar
* que se detecten múltiples pulsaciones de botón.
*/
unsigned long debounceDelay = 500;
// Pantallas disponibles: UTEC, COUNTER, MARIO, TICTACTOE, NUM_SCREENS
enum Screen { UTEC, COUNTER, MARIO, TICTACTOE, NUM_SCREENS };
// La pantalla inicial
Screen currentScreen = UTEC;
int brightness = 20;
void setup() {
pinMode(TOGGLE_BUTTON, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
pinMode(XPIN, INPUT);
pinMode(YPIN, INPUT);
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(JOY_BUTTON_PIN, INPUT_PULLUP);
pinMode(POT_PIN, INPUT);
matrix.begin();
matrix.setBrightness(brightness);
matrix.setTextWrap(false);
matrix.setFont(&FreeSansBold9pt7b);
digitalWrite(LED_PIN, HIGH);
}
// Limpia la animación de Mario
void clearMarioAnimData() {
matrix.fillScreen(0);
matrix.show();
}
// Limpia los datos del juego TicTacToe
void clearTicTacToeData() {
memset(gameBoard, 0, sizeof(gameBoard));
memset(xPositions, 0, sizeof(xPositions));
memset(oPositions, 0, sizeof(oPositions));
winnerAnnounced = false;
gameActive = true;
winnerAnimationActive = false;
cursorX = 1;
cursorY = 1;
}
/**
* Cambiar el brillo de la pantalla led con un potenciómetro.
*/
void changeBrightness() {
int potValue = analogRead(POT_PIN);
int newBrightness = map(potValue, 0, 1023, 0, 255);
if (newBrightness != brightness) {
brightness = newBrightness;
matrix.setBrightness(brightness);
}
}
/**
* Función para mostrar una letra desplazándose de derecha a izquierda.
* @param letter: Caracter a mostrar.
*/
const char* text = "UTEC";
const int numLetters = 4; // Number of letters in the text
void displayLetter(char letter) {
int charWidth = 11;
int startX = (matrix.width() - charWidth) / 2;
int position = matrix.width();
// Desplaza la letra hacia la vista
while (position >= startX) {
matrix.fillScreen(0); // Limpia la pantalla
matrix.setCursor(position, 14); // Establece el cursor en la posición actual
matrix.print(letter);
matrix.setTextColor(matrix.Color(255,0,0));
matrix.show(); // Actualiza la pantalla con los nuevos datos
delay(40); // Controla la velocidad de la animación
position--; // Mueve la posición hacia la izquierda
if (digitalRead(BUTTON_PIN) == LOW) {
return; // Salir de la función si el botón está presionado
}
}
// Mantiene la letra en vista por 1 segundo
delay(300);
// Desplaza la letra fuera de la vista
while (position >= -charWidth) {
matrix.fillScreen(0); // Limpia la pantalla
matrix.setCursor(position, 14); // Establece el cursor en la posición actual
matrix.print(letter);
matrix.show(); // Actualiza la pantalla con los nuevos datos
delay(40); // Controla la velocidad de la animación
position--; // Mueve la posición hacia la izquierda
if (digitalRead(BUTTON_PIN) == LOW) {
return; // Salir de la función si el botón está presionado
}
}
}
/**
* Función para mostrar la animación de UTEC.
*/
void displayUTEC() {
static int letterIndex = 0; // Índice de la letra actual
if (digitalRead(BUTTON_PIN) == LOW) { // Verifica si el botón está presionado
letterIndex = 0; // Reinicia el índice de la letra para la próxima vez
return; // Salir de la función
}
displayLetter(text[letterIndex]); // Muestra la letra actual
letterIndex++; // Mueve al siguiente letra
if (letterIndex >= numLetters) {
letterIndex = 0; // Reinicia al primer letra si hemos llegado al final
}
}
/**
* Pantalla: ANIMACION
* Dibuja una animación simple que muestra los números del 0 al 9.
*/
void displayCounter() {
static unsigned long lastUpdate = 0;
static int i = 0;
if (millis() - lastUpdate > 1000) {
// Esta variable se usa para controlar el tiempo de la animación
lastUpdate = millis();
/* Esta condición se usa para salir de la animación, de
lo contrario hay que esperar a que termine para poder
cambiar de pantalla */
if (digitalRead(BUTTON_PIN) == LOW) { // Check if button is pressed
i = 0; // Reset counter for next time
return; // Exit the function
}
matrix.fillScreen(0);
matrix.setCursor(3, 14);
matrix.setTextColor(matrix.Color(255, 0, 0));
matrix.print(i++);
matrix.show();
delay(500);
if (i > 9) {
i = 0;
}
}
}
/**
* Muestra un frame en la tira de LED.
*/
void displayFrame(const Frame* currentFrame, int size) {
matrix.clear(); // Clear all LEDs
for (int i = 0; i < size; i++) {
Frame f;
// Read each frame component from PROGMEM
memcpy_P(&f, ¤tFrame[i], sizeof(Frame));
matrix.setPixelColor(f.index, f.r, f.g, f.b);
}
matrix.show();
delay(150);
}
/**
* Pantalla: Mario animation
*/
void displayMarioAnim() {
static unsigned long lastUpdate = 0;
static int frameIndex = 0;
static bool reverse = false;
int frameCount = sizeof(frames) / sizeof(frames[0]);
if (millis() - lastUpdate > 100) {
lastUpdate = millis();
if (digitalRead(BUTTON_PIN) == LOW) { // Check if button is pressed
frameIndex = 0; // Reset counter for next time
return; // Exit the function
}
displayFrame(frames[frameIndex], sizeof(frames[frameIndex]) / sizeof(frames[0][0]));
if (!reverse) {
frameIndex++;
if (frameIndex >= frameCount) {
frameIndex = frameCount - 2; // Adjust to avoid repeating the last frame
reverse = true; // Change direction
}
} else {
frameIndex--;
if (frameIndex < 0) {
frameIndex = 1; // Adjust to avoid repeating the first frame
reverse = false; // Change direction
}
}
}
}
/**
* Pantalla: TICTACTOE
* Muestra el tablero de juego TicTacToe y permite a los jugadores interactuar con él.
*/
void displayTicTacToe() {
if(ledsOn) {
drawGrid();
}
for (int y = 0; y < 3; y++) {
for (int x = 0; x < 3; x++) {
int cellX = x * 5 + 2; // Posición X de la celda
int cellY = y * 5 + 2; // Posición Y de la celda
if (xPositions[y][x]) {
drawX(cellX, cellY);
}
if (oPositions[y][x]) {
drawO(cellX, cellY);
}
}
}
// Dibujar el cursor en la posición actual
if ((millis() / 200) % 2) {
int cursorPixelX = cursorX * 5 + 2;
int cursorPixelY = cursorY * 5 + 2;
if (currentPlayer == 0) {
drawX(cursorPixelX, cursorPixelY);
} else {
drawO(cursorPixelX, cursorPixelY);
}
}
}
// Apaga la matriz de leds
void turnOffMatrix() {
bool toggleState = digitalRead(TOGGLE_BUTTON) == HIGH;
if (!toggleState) {
matrix.clear();
matrix.show();
}
ledsOn = toggleState;
}
/**
* Verifica si el botón de cambio de pantalla se ha presionado y cambia la pantalla actual.
*/
void checkScreenChangeButton() {
static unsigned long lastButtonPressTime = 0;
static bool lastButtonState = HIGH;
bool buttonState = digitalRead(BUTTON_PIN);
/**
* Verifica si el botón se ha presionado y cambia la pantalla actual
* solo si el juego está activo y no se está mostrando la animación del ganador.
*/
if (buttonState == LOW && lastButtonState == HIGH && millis() - lastButtonPressTime > debounceDelay) {
if (gameActive && !winnerAnimationActive) {
switch (currentScreen) {
case MARIO:
clearMarioAnimData();
break;
case TICTACTOE:
clearTicTacToeData();
break;
}
currentScreen = Screen((currentScreen + 1) % NUM_SCREENS); // Cambia a la siguiente pantalla
lastButtonPressTime = millis();
}
}
lastButtonState = buttonState;
}
/**
* Verifica si el botón del joystick se ha presionado y reinicia el juego si es así.
*/
void checkJoystickButton() {
static unsigned long lastJoyPressTime = 0;
static bool lastJoyButtonState = HIGH; // El botón está configurado como PULLUP
bool joyButtonState = digitalRead(JOY_BUTTON_PIN);
if (joyButtonState == LOW && lastJoyButtonState == HIGH && millis() - lastJoyPressTime > debounceDelay) {
if (!gameActive) {
resetGame(); // Reinicia el juego
winnerAnimationActive = false; // Termina la animación del ganador
currentScreen = TICTACTOE; // Nos aseguramos de que la pantalla sea TICTACTOE
lastJoyPressTime = millis();
}
}
lastJoyButtonState = joyButtonState;
}
/**
* Reinicia el juego a su estado inicial.
*/
void resetGame() {
memset(gameBoard, 0, sizeof(gameBoard));
memset(xPositions, 0, sizeof(xPositions));
memset(oPositions, 0, sizeof(oPositions));
currentPlayer = PLAYER_X;
gameActive = true; // Marca el juego como activo
winnerAnimationActive = false; // Termina la animación del ganador
matrix.clear();
matrix.show();
}
/**
* Maneja la entrada del joystick para mover el cursor y colocar marcadores en el tablero.
*/
void handleJoystick() {
/**
* Esta función solo se ejecuta si la pantalla actual es TICTACTOE,
* el juego está activo y no se está mostrando la animación del ganador.
*/
if (currentScreen != TICTACTOE || !gameActive || winnerAnimationActive) return;
static unsigned long lastMoveTime = 0;
int xValue = analogRead(XPIN);
int yValue = analogRead(YPIN);
bool buttonState = digitalRead(JOY_BUTTON_PIN);
/**
* Condición para mover el cursor solo después de un cierto tiempo,
* esto es como un retraso (delay) para evitar que el cursor se mueva demasiado rápido.
*/
if (millis() - lastMoveTime > 100) {
updateCursorPosition(xValue, yValue);
lastMoveTime = millis();
}
/**
* Verifica si el botón del joystick se ha presionado y
* coloca un marcador en la posición actual.
*/
static bool lastJoyButtonState = HIGH; // El botón está configurado como PULLUP
if (buttonState == LOW && lastJoyButtonState == HIGH) {
placeMarker();
lastJoyButtonState = LOW;
} else if (buttonState == HIGH) {
lastJoyButtonState = HIGH;
}
}
/**
* Dibuja la pantalla actual en función de la pantalla actual seleccionada.
*/
void drawCurrentScreen() {
matrix.fillScreen(0);
/**
* Aquí se llama a la función de visualización correspondiente
* según la pantalla actual seleccionada.
*/
switch (currentScreen) {
case UTEC:
displayUTEC();
break;
case COUNTER:
displayCounter();
break;
case MARIO:
displayMarioAnim();
break;
case TICTACTOE:
displayTicTacToe();
break;
default:
Serial.print('error de pantalla');
break;
}
matrix.show();
}
/**
* Actualiza la posición del cursor en función de los valores X e Y del joystick.
*/
void updateCursorPosition(int xValue, int yValue) {
if (xValue < 400) cursorX = max(0, cursorX - 1);
else if (xValue > 600) cursorX = min(2, cursorX + 1);
if (yValue < 400) cursorY = max(0, cursorY - 1);
else if (yValue > 600) cursorY = min(2, cursorY + 1);
}
/**
* Coloca un marcador en la posición actual del cursor si la celda está vacía.
*/
void placeMarker() {
if (!gameActive || winnerAnimationActive) return; // Ignore inputs if the game is not active or animation is active
/**
* Verifica si la celda actual está vacía y coloca el marcador del jugador actual.
*/
if (xPositions[cursorY][cursorX] == 0 && oPositions[cursorY][cursorX] == 0) {
// Coloca el marcador del jugador actual en la celda actual
if (currentPlayer == PLAYER_X) {
xPositions[cursorY][cursorX] = 1; // X
} else {
oPositions[cursorY][cursorX] = 1; // O
}
/**
* Verifica si hay un ganador después de colocar el marcador.
*/
if (checkForWinner()) {
// Marca el juego como inactivo y activa la animación del ganador
gameActive = false;
winnerAnimationActive = true;
} else {
// Cambia al siguiente jugador si no hay ganador
currentPlayer = (currentPlayer + 1) % 2;
}
}
}
/**
* Dibuja el tablero de juego TicTacToe en la pantalla.
*/
void drawGrid() {
// Color un poco más tenue de las líneas de la cuadrícula
uint16_t lineColor = matrix.Color(255 * 0.4, 255 * 0.3, 255 * 0.4);
// Dibujar las líneas de la cuadrícula
matrix.drawLine(5, 0, 5, 15, lineColor);
matrix.drawLine(10, 0, 10, 15, lineColor);
matrix.drawLine(0, 5, 15, 5, lineColor);
matrix.drawLine(0, 10, 15, 10, lineColor);
// Dibujar los bordes del tablero
matrix.drawLine(0, 0, 15, 0, lineColor); // Borde superior
matrix.drawLine(0, 15, 15, 15, lineColor); // Borde inferior
matrix.drawLine(0, 0, 0, 15, lineColor); // Borde izquierdo
matrix.drawLine(15, 0, 15, 15, lineColor); // Borde derecho
}
/**
* Verifica si hay un ganador en el juego TicTacToe.
*/
bool checkForWinner() {
/**
* Se verifica si hay un ganador para ambos jugadores.
*/
for (int player = 0; player < 2; player++) {
bool (*positions)[3][3] = (player == PLAYER_X) ? &xPositions : &oPositions;
// Verifica las filas y columnas para encontrar un ganador.
for (int i = 0; i < 3; i++) {
if ((*positions)[i][0] && (*positions)[i][0] == (*positions)[i][1] && (*positions)[i][1] == (*positions)[i][2]) {
return true; // Ganador encontrado en fila
}
if ((*positions)[0][i] && (*positions)[0][i] == (*positions)[1][i] && (*positions)[1][i] == (*positions)[2][i]) {
return true; // Ganador encontrado en columna
}
}
/**
* Verifica las diagonales para encontrar un ganador.
*/
if ((*positions)[0][0] && (*positions)[0][0] == (*positions)[1][1] && (*positions)[1][1] == (*positions)[2][2]) {
return true; // Ganador encontrado en la diagonal principal
}
if ((*positions)[0][2] && (*positions)[0][2] == (*positions)[1][1] && (*positions)[1][1] == (*positions)[2][0]) {
return true; // Ganador encontrado en la diagonal secundaria
}
}
return false; // No se encontró un ganador
}
/**
* Efecto de arco iris continuo cuando se anuncia un ganador.
*/
void displayContinuousRainbow() {
static unsigned long lastUpdate = 0;
static int shift = 0;
if (millis() - lastUpdate > 50) {
for (int y = 0; y < matrix.height(); y++) {
for (int x = 0; x < matrix.width(); x++) {
uint16_t color = matrix.Color(
(255 * (x + shift) / 16) % 255,
(255 * (y + shift) / 16) % 255,
(255 * (x + y + shift) / 16) % 255
);
matrix.drawPixel(x, y, color);
}
}
// Dibuja el símbolo del ganador en el centro
int centerX = matrix.width() / 2;
int centerY = matrix.height() / 2;
if (currentPlayer == PLAYER_X) {
drawX(centerX, centerY);
} else {
drawO(centerX, centerY);
}
// Desplaza el arco iris para un efecto continuo
shift = (shift + 1) % 256;
matrix.show();
lastUpdate = millis();
}
}
/**
* Dibuja un marcador 'X' en la posición especificada.
*/
void drawX(int centerX, int centerY) {
uint16_t xColor = matrix.Color(255, 0, 0); // Red color for X
// De arriba izquierda a abajo derecha
matrix.drawLine(centerX - 1, centerY - 1, centerX + 2, centerY + 2, xColor);
// De arriba derecha a abajo izquierda
matrix.drawLine(centerX + 2, centerY - 1, centerX - 1, centerY + 2, xColor);
}
/**
* Dibuja un marcador 'O' en la posición especificada.
*/
void drawO(int centerX, int centerY) {
uint16_t oColor = matrix.Color(0, 255, 0);
// Dibuja 'O' dentro de una celda de 4x4 ajustando los puntos finales para no tocar las líneas de la cuadrícula
matrix.drawPixel(centerX - 1, centerY, oColor);
matrix.drawPixel(centerX, centerY - 1, oColor);
matrix.drawPixel(centerX + 1, centerY - 1, oColor);
matrix.drawPixel(centerX + 2, centerY, oColor);
matrix.drawPixel(centerX + 2, centerY + 1, oColor);
matrix.drawPixel(centerX + 1, centerY + 2, oColor);
matrix.drawPixel(centerX, centerY + 2, oColor);
matrix.drawPixel(centerX -1, centerY + 1, oColor);
}
void loop() {
turnOffMatrix();
if (ledsOn) {
checkScreenChangeButton();
checkJoystickButton();
changeBrightness();
/**
* Esta condición se usa para evitar que el juego se actualice
* mientras se muestra la animación de ganador.
*/
if (!winnerAnimationActive) {
handleJoystick();
/**
* Solo se dibuja la pantalla actual si el juego está activo.
* Esto evita interactuar con el juego si se està en otra pantalla.
*/
if (gameActive) {
drawCurrentScreen();
}
} else {
displayContinuousRainbow();
}
}
delay(10);
}