MÁQUINA REPARTIDORA Y DEALER BLACKJACK


Índice


Visión general

La Máquina Dealer Blackjack es un sistema completamente autónomo capaz de replicar las funciones de un crupier en un juego de Blackjack: barajar, repartir, leer y gestionar las cartas, interactuar con varios jugadores y la banca, y mostrar en tiempo real los resultados de cada mano. El dispositivo integra un dealer mecánico precomprado (con dos motores y botón de barajado), un microcontrolador Arduino Uno, un lector NFC PN532, etiquetas NFC en cada carta de una baraja estándar de poker, una pantalla LCD 16×2 para la interfaz y cuatro botones para la navegación de menús y toma de decisiones (número de jugadores, pedir carta, plantarse, reiniciar).

El objetivo de este proyecto es hacer algo útil, es decir, algo que tiempo después de haberlo entregado siga siendo útil en un entorno de fiesta o con amigos en casa.


Proceso de desarrollo

El camino desde la idea inicial hasta el prototipo final siguió un esquema iterativo y disciplinado, con múltiples fases de prueba y ajuste:

1. Investigación y definición de objetivos

  • Realicé un análisis de los flujos de una partida de Blackjack real: lectura de reglas, tiempos entre reparto y toma de decisiones, y posibles errores humanos.
  • Elaboré un documento de requisitos donde definí aspectos como número máximo de jugadores (4), tiempo de respuesta de cada acción (< 1 s), y tolerancia a fallos de lectura (< 5 %).
  • Comparé diferentes tecnologías de lectura de cartas (sensores ópticos, códigos de barras, RFID y NFC) y seleccioné NFC con el lector PN532 por su fiabilidad y facilidad de etiquetado.

2. Prototipado aislado de módulos

  • Lectura NFC:
    • Etiquetado de un subconjunto de cartas para medir la distancia de lectura y la estabilidad en posiciones diversas (ángulo, velocidad de paso).
    • Implementación de un filtro básico para descartar lecturas erróneas cuando el mismo UID se reportaba varias veces en menos de 150 ms.
  • Control mecánico del dealer:
    • Desmontaje parcial del dealer comercial para identificar el funcionamiento del botón de inicio de barajado, los motores y los engranajes que empujan las cartas.
    • Separación de los motores para que se pudiesen accionar de forma separada, sin necesidad de que, por ejemplo, para soltar una carta hubiese que tener al motor de la derecha encendido de igual manera
    • Pruebas de duración de un ciclo completo de barajado, midiendo consumos y tiempos para garantizar consistencia. Esto lo hice ya que supuse que no tardaría lo mismo en barajar las cartas o soltar una sola con las pilas llenas que a la mitad o si quiera al 95 porciento.
  • Interfaz de usuario:
    • Prototipo de menús en una LCD 16×2, experimentando con textos de 16 caracteres y scroll para mensajes más largos.
    • Validación de la ergonomía de los cuatro botones: disposición en protoboard, distancia entre pulsadores y facilidad de uso.

3. Integración progresiva

Comencé uniendo el módulo NFC con la interfaz LCD: cada vez que una carta se leía, mostraba su valor en la pantalla. Una vez establecida esa comunicación, conecté el control del dealer para disparar el barajado desde el mismo menú de la LCD.

  1. Sincronización inicial: calibré un retardo tras activar los motores para que la primera carta no golpease el sensor óptico antes de tiempo.
  2. Secuenciación de reparto: diseñé una rutina que iteraba 2 veces por jugador y banca, intercambiando motor—sensor—lectura en bucle.
  3. Gestión de la banca: tras los turnos humanos, la máquina entraba en modo “banca automática” repitiendo el mismo flujo hasta alcanzar 17 puntos o más.

4. Pruebas de robustez y optimización

En entornos reales (con luz variable, barajado rápido, múltiples pases de carta) identifiqué escenarios de fallo:

  • Ruidos eléctricos en motores que ocasionaban falsos disparos del lector NFC.
  • Vibraciones en la mesa que desplazaban ligeramente las pegatinas NFC, afectando la distancia de lectura.
  • Interrupciones cuando se pulsaban botones mientras se leía una carta, bloqueando el flujo.

Para cada uno, implementé mecanizados sencillos (estructuras de sujeción), filtrados adicionales en software y bloqueos de estado que garantizan que no se pueden pulsar botones durante una lectura crítica.


Materiales y componentes

ComponenteDescripciónPrecio (EUR)
Arduino UnoMCU ATmega328P, 14 pines digitales, 6 analógicos, 32 KB Flash29.04
Keyestudio L298P Motor Driver ShieldControlador de activación y velocidad de cada uno de los motores6,50
Dealer de cartasMecanismo con motores DC de 3 V, incluye engranajes para barajar cartas y soltarlas una a una12,00
Lector NFC PN532Módulo I²C/SPI con alcance de hasta 5 cm y soporte de etiquetas ISO14443A8,50
Pantalla LCD 16×2 (HD44780)Interfaz básica de texto, 2 líneas de 16 caracteres cada una3,00
4 botones pulsadoresPara navegar menús y confirmar acciones (jugadores, pedir, plantarse, reiniciar)1,00
Sensor óptico infrarrojoDetecta paso de carta entre dos guías, sincroniza expulsión con lectura NFC2,50
Pegatinas NFC (NTAG213, 100 unidades)Adhesivas, cada una codificada con UID único2.14
Cables jumperPara conexiones entre módulos≈ 2,00 en total todos los usados
Placa protoboardPara prototipado sin soldadura3,00
Fuente de alimentación 6 V / 2 AProvee energía estable a los componentes5,00

Diagrama de actividad


Arquitectura del código

El código está organizado en módulos y funciones bien separadas, siguiendo una máquina de estados principal y capas de abstracción para facilitar el mantenimiento.

#include <Wire.h>
#include <LiquidCrystal.h>
#include <Adafruit_PN532.h>

// Pines LCD
const int rs = 8, en = 9, d4 = 5, d5 = 4, d6 = 2, d7 = 7;
LiquidCrystal lcd(rs, en, d4, d5, d6, d7);

// Pines motor
#define ENA 3
#define IN1 12
#define IN2 13
#define ENB 11

// Botones
const int btnJugador1 = A0; // Pedir carta
const int btnJugador2 = A1; // Plantarse
const int btnJugador3 = 6;
const int btnJugador4 = 10;

// NFC PN532 por I2C
Adafruit_PN532 nfc(A4, A5);

// Constantes
#define VELOCIDAD_MOTOR 200
#define TIEMPO_BARAJAR 1000
#define VELOCIDAD_MOTOR_REPARTO 175
#define TIEMPO_ACTIVACION_REPARTO 200
#define MAX_CARTAS 10
#define RETARDO_DEBOUNCE 100
#define LIMITE_BANCA 17
#define BLACKJACK 21

// Estados
enum Estado : uint8_t { INICIO, BARAJANDO, SELECCION_JUGADORES, REPARTO, JUEGO, FIN_JUEGO };
Estado estadoActual = INICIO;

// Resultados del juego
enum Resultado : uint8_t { JUGANDO, GANA, PIERDE, EMPATE };

// Jugadores (optimizado para reducir RAM)
struct Jugador {
  char cartas[MAX_CARTAS][3]; // Solo almacenamos hasta 2 caracteres + terminador nulo
  uint8_t total;
  uint8_t cantidadCartas;
  Resultado resultado;
};
Jugador jugadores[5]; // Hasta 4 jugadores + banca
uint8_t numJugadores = 0;
uint8_t jugadorActual = 0;

void mostrarMensaje(const char* linea1, const char* linea2 = "") {
  lcd.begin(16, 2);
  delay(50);
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print(linea1);
  if (*linea2) {
    lcd.setCursor(0, 1);
    lcd.print(linea2);
  }
}

bool leerBotonFiltrado(int pin) {
  bool lectura = digitalRead(pin) == LOW;
  if (lectura) {
    delay(10);
    return digitalRead(pin) == LOW;
  }
  return false;
}

void esperarBoton(int pin) {
  while (!leerBotonFiltrado(pin)) {
    delay(10);
  }
  delay(250);
}

uint8_t valorCarta(const char* valor) {
  char letra = valor[0];
  if (letra == 'A') return 1;
  if (letra == 'J' || letra == 'Q' || letra == 'K') return 10;
  if (letra >= '2' && letra <= '9') return letra - '0';
  return 0; // Error
}

bool leerCarta(char* cartaResultado) {
  for (uint8_t intento = 0; intento < 2; intento++) {
    uint8_t uid[7] = {0};
    uint8_t uidLength;

    unsigned long start = millis();
    while (millis() - start < 1000) {
      if (nfc.readPassiveTargetID(PN532_MIFARE_ISO14443A, uid, &uidLength)) {
        uint8_t datos[4];
        if (nfc.mifareultralight_ReadPage(4, datos)) {
          // Copiar solo los dos primeros caracteres válidos
          uint8_t idx = 0;
          for (uint8_t i = 0; i < 4 && idx < 2; i++) {
            if (datos[i] >= 32 && datos[i] <= 126) {
              cartaResultado[idx++] = (char)datos[i];
            }
          }
          cartaResultado[idx] = '\0'; // Terminar la cadena
          
          return true;
        }
      }
      delay(10);
    }
  }

  strcpy(cartaResultado, "Er");
  return false;
}

void barajar() {
  mostrarMensaje("Barajando...", "Espere");
  digitalWrite(IN1, HIGH);
  digitalWrite(IN2, LOW);
  analogWrite(ENA, VELOCIDAD_MOTOR);
  analogWrite(ENB, VELOCIDAD_MOTOR);
  delay(TIEMPO_BARAJAR);
  analogWrite(ENA, 0);
  analogWrite(ENB, 0);
  mostrarMensaje("Barajado OK", "");
  delay(1500);
}

void seleccionarJugadores() {
  mostrarMensaje("Elige jugadores", "Btn 1-4");
  while (true) {
    if (leerBotonFiltrado(btnJugador1)) { numJugadores = 1; break; }
    if (leerBotonFiltrado(btnJugador2)) { numJugadores = 2; break; }
    if (leerBotonFiltrado(btnJugador3)) { numJugadores = 3; break; }
    if (leerBotonFiltrado(btnJugador4)) { numJugadores = 4; break; }
    delay(10);
  }
  
  char buffer[10];
  itoa(numJugadores, buffer, 10);
  mostrarMensaje("Jugadores:", buffer);
  delay(1000);
  
  numJugadores += 1; // +1 para incluir la banca
  for (uint8_t i = 0; i < numJugadores; i++) {
    jugadores[i].total = 0;
    jugadores[i].cantidadCartas = 0;
    jugadores[i].resultado = JUGANDO;
  }
}

void repartirCartas() {
  mostrarMensaje("Cartas listas", "Pulsa Btn1");
  esperarBoton(btnJugador1);
  bool cartaOcultaBanca = true;
  char buffer[17]; // Buffer para mensajes LCD
  char cartaLeida[3]; // Buffer para la carta leída

  for (uint8_t ronda = 0; ronda < 2; ronda++) {
    for (uint8_t i = 0; i < numJugadores; i++) {
      // Preparar mensaje de turno
      if (i == numJugadores - 1) {
        strcpy(buffer, "Turno Banca");
      } else {
        strcpy(buffer, "Turno J");
        buffer[strlen(buffer) + 1] = '\0';
        buffer[strlen(buffer)] = '1' + i;
      }
      
      mostrarMensaje(buffer, "Pulsa Btn1");
      esperarBoton(btnJugador1);

      digitalWrite(IN1, HIGH);
      digitalWrite(IN2, LOW);
      analogWrite(ENA, VELOCIDAD_MOTOR_REPARTO);
      delay(TIEMPO_ACTIVACION_REPARTO);
      analogWrite(ENA, 0);

      delay(800);
      if (leerCarta(cartaLeida)) {
        // Guardar carta
        strcpy(jugadores[i].cartas[jugadores[i].cantidadCartas], cartaLeida);
        jugadores[i].total += valorCarta(cartaLeida);
        jugadores[i].cantidadCartas++;

        if (i == numJugadores - 1) {  // Banca
          if (jugadores[i].cantidadCartas == 1) {
            mostrarMensaje("Banca:", cartaLeida);
          } else if (jugadores[i].cantidadCartas == 2 && cartaOcultaBanca) {
            mostrarMensaje("Banca:", "???");
          }
        } else {
          // Mostrar información del jugador
          if (i == numJugadores - 1) {
            strcpy(buffer, "Banca: ");
          } else {
            strcpy(buffer, "J");
            buffer[1] = '1' + i;
            buffer[2] = ':';
            buffer[3] = ' ';
            buffer[4] = '\0';
          }
          strcat(buffer, cartaLeida);
          
          // Preparar línea 2 con el total
          itoa(jugadores[i].total, cartaLeida, 10);
          strcpy(buffer + strlen(buffer), " Tot:");
          strcat(buffer, cartaLeida);
          
          mostrarMensaje(buffer, "");
        }
        delay(1500);
      } else {
        mostrarMensaje("Error NFC", "Reintente");
        i--; // Repetir turno
      }
    }
  }
}

// Pedir una carta adicional para el jugador actual
bool pedirCarta(uint8_t jugadorIdx) {
  char buffer[17];
  char cartaLeida[3];
  
  if (jugadorIdx == numJugadores - 1) {
    strcpy(buffer, "Banca pide");
  } else {
    strcpy(buffer, "J");
    buffer[1] = '1' + jugadorIdx;
    buffer[2] = '\0';
    strcat(buffer, " pide carta");
  }
  
  mostrarMensaje(buffer, "Pulsa Btn1");
  esperarBoton(btnJugador1);
  
  digitalWrite(IN1, HIGH);
  digitalWrite(IN2, LOW);
  analogWrite(ENA, VELOCIDAD_MOTOR_REPARTO);
  delay(TIEMPO_ACTIVACION_REPARTO);
  analogWrite(ENA, 0);
  
  delay(800);
  if (leerCarta(cartaLeida)) {
    strcpy(jugadores[jugadorIdx].cartas[jugadores[jugadorIdx].cantidadCartas], cartaLeida);
    jugadores[jugadorIdx].total += valorCarta(cartaLeida);
    jugadores[jugadorIdx].cantidadCartas++;
    
    // Mostrar la carta
    if (jugadorIdx == numJugadores - 1) {
      strcpy(buffer, "B:");
    } else {
      strcpy(buffer, "J");
      buffer[1] = '1' + jugadorIdx;
      buffer[2] = ':';
      buffer[3] = ' ';
      buffer[4] = '\0';
    }
    strcat(buffer, cartaLeida);
    
    char totalStr[4];
    itoa(jugadores[jugadorIdx].total, totalStr, 10);
    strcpy(buffer + strlen(buffer), " Tot:");
    strcat(buffer, totalStr);
    
    mostrarMensaje(buffer, "");
    delay(1500);
    
    // Verificar si se pasó de 21
    if (jugadores[jugadorIdx].total > BLACKJACK) {
      jugadores[jugadorIdx].resultado = PIERDE;
      
      if (jugadorIdx == numJugadores - 1) {
        mostrarMensaje("Banca > 21", "Pierde!");
      } else {
        strcpy(buffer, "J");
        buffer[1] = '1' + jugadorIdx;
        buffer[2] = '\0';
        strcat(buffer, " > 21");
        mostrarMensaje(buffer, "Pierde!");
      }
      delay(2000);
      return false;
    }
    return true;
  } else {
    mostrarMensaje("Error NFC", "Reintente");
    return false;
  }
}

void mostrarEstadoJugador(uint8_t jugadorIdx) {
  char buffer[17];
  char totalStr[4];
  
  if (jugadorIdx == numJugadores - 1) {
    strcpy(buffer, "B:");
  } else {
    strcpy(buffer, "J");
    buffer[1] = '1' + jugadorIdx;
    buffer[2] = ':';
    buffer[3] = ' ';
    buffer[4] = '\0';
  }
  
  // Mostrar primera carta + total
  strcat(buffer, jugadores[jugadorIdx].cartas[0]);
  
  if (jugadores[jugadorIdx].cantidadCartas > 1) {
    strcat(buffer, "+");
    
    // Si hay más de 2 cartas, mostrar signo +
    if (jugadores[jugadorIdx].cantidadCartas > 2) {
      strcat(buffer, "..");
    } else {
      strcat(buffer, jugadores[jugadorIdx].cartas[1]);
    }
  }
  
  itoa(jugadores[jugadorIdx].total, totalStr, 10);
  strcpy(buffer + strlen(buffer), "=");
  strcat(buffer, totalStr);
  
  // Solo mostrar opciones si el jugador aún está jugando
  if (jugadorIdx < numJugadores - 1 && jugadores[jugadorIdx].resultado == JUGANDO) {
    mostrarMensaje(buffer, "1:Carta 2:Pasar");
  } else {
    mostrarMensaje(buffer, "");
  }
}

void jugarTurnos() {
  // Iniciar con el jugador 1
  jugadorActual = 0;
  
  // Turnos para cada jugador
  while (jugadorActual < numJugadores - 1) { // Todos menos la banca
    // Saltar jugadores que ya han perdido
    if (jugadores[jugadorActual].resultado != JUGANDO) {
      jugadorActual++;
      continue;
    }
    
    mostrarEstadoJugador(jugadorActual);
    
    // Esperar decisión del jugador
    bool decisionTomada = false;
    while (!decisionTomada) {
      if (leerBotonFiltrado(btnJugador1)) { // Pedir carta
        decisionTomada = true;
        if (pedirCarta(jugadorActual)) {
          if (jugadores[jugadorActual].total == BLACKJACK) {
            mostrarMensaje("BlackJack!", "");
            jugadores[jugadorActual].resultado = GANA;
            delay(1500);
            decisionTomada = true;
            jugadorActual++;
          } else {
            // Volver a mostrar el estado si no perdió ni ganó
            if (jugadores[jugadorActual].resultado == JUGANDO) {
              decisionTomada = false; // Para permitir otra decisión
              mostrarEstadoJugador(jugadorActual);
            } else {
              // Si perdió, avanzar al siguiente jugador
              jugadorActual++;
            }
          }
        }
      } 
      else if (leerBotonFiltrado(btnJugador2)) { // Plantarse
        char buffer[17];
        if (jugadorActual == numJugadores - 1) {
          strcpy(buffer, "Banca se planta");
        } else {
          strcpy(buffer, "J");
          buffer[1] = '1' + jugadorActual;
          buffer[2] = '\0';
          strcat(buffer, " se planta");
        }
        mostrarMensaje(buffer, "");
        delay(1500);
        decisionTomada = true;
        jugadorActual++;
      }
      delay(10);
    }
  }
  
  // Turno de la banca (automático)
  uint8_t idxBanca = numJugadores - 1;
  mostrarMensaje("Turno Banca", "");
  delay(1500);
  
  while (jugadores[idxBanca].total < LIMITE_BANCA && jugadores[idxBanca].resultado == JUGANDO) {
    mostrarEstadoJugador(idxBanca);
    delay(1500);
    pedirCarta(idxBanca);
  }
  
  if (jugadores[idxBanca].total <= BLACKJACK) {
    char buffer[17];
    strcpy(buffer, "B planta: ");
    char totalStr[4];
    itoa(jugadores[idxBanca].total, totalStr, 10);
    strcat(buffer, totalStr);
    mostrarMensaje(buffer, "");
    delay(1500);
  }
}

void determinarResultados() {
  uint8_t idxBanca = numJugadores - 1;
  bool bancaPerdio = (jugadores[idxBanca].resultado == PIERDE);
  uint8_t totalBanca = jugadores[idxBanca].total;
  
  // Si la banca no se pasó, comparamos totales
  for (uint8_t i = 0; i < numJugadores - 1; i++) {
    // Si el jugador ya perdió, no hacemos nada
    if (jugadores[i].resultado == PIERDE) continue;
    
    // Si la banca se pasó, los jugadores que no se pasaron ganan
    if (bancaPerdio) {
      jugadores[i].resultado = GANA;
    } 
    // Si no, comparamos los totales
    else {
      if (jugadores[i].total > totalBanca) {
        jugadores[i].resultado = GANA;
      } else if (jugadores[i].total < totalBanca) {
        jugadores[i].resultado = PIERDE;
      } else {
        jugadores[i].resultado = EMPATE;
      }
    }
  }
}

void mostrarResultadosFinal() {
  char buffer[17];
  for (uint8_t i = 0; i < numJugadores - 1; i++) {
    strcpy(buffer, "J");
    buffer[1] = '1' + i;
    buffer[2] = ':';
    buffer[3] = ' ';
    buffer[4] = '\0';
    
    // Añadir resultado
    switch (jugadores[i].resultado) {
      case GANA:
        strcat(buffer, "GANA");
        break;
      case PIERDE:
        strcat(buffer, "PIERDE");
        break;
      case EMPATE:
        strcat(buffer, "EMPATE");
        break;
      default:
        strcat(buffer, "ERROR");
    }
    
    // Añadir total
    char totalStr[4];
    itoa(jugadores[i].total, totalStr, 10);
    strcat(buffer, " (");
    strcat(buffer, totalStr);
    strcat(buffer, ")");
    
    mostrarMensaje(buffer, "");
    delay(2000);
  }
  
  // Mostrar resultado banca
  uint8_t idxBanca = numJugadores - 1;
  strcpy(buffer, "Banca: ");
  if (jugadores[idxBanca].resultado == PIERDE) {
    strcat(buffer, "PIERDE");
  } else {
    strcat(buffer, "OK");
  }
  
  char totalStr[4];
  itoa(jugadores[idxBanca].total, totalStr, 10);
  strcat(buffer, " (");
  strcat(buffer, totalStr);
  strcat(buffer, ")");
  
  mostrarMensaje(buffer, "");
  delay(2000);
}

void setup() {
  lcd.begin(16, 2);

  pinMode(ENA, OUTPUT); pinMode(IN1, OUTPUT); pinMode(IN2, OUTPUT);
  pinMode(ENB, OUTPUT);
  analogWrite(ENA, 0); analogWrite(ENB, 0);

  pinMode(btnJugador1, INPUT_PULLUP);
  pinMode(btnJugador2, INPUT_PULLUP);
  pinMode(btnJugador3, INPUT_PULLUP);
  pinMode(btnJugador4, INPUT_PULLUP);

  mostrarMensaje("BlackJack", "Bienvenido!");
  delay(2000);
  mostrarMensaje("Btn1: Barajar", "");

  nfc.begin();
  uint32_t versiondata = nfc.getFirmwareVersion();
  if (!versiondata) {
    mostrarMensaje("No NFC", "ERROR");
    while (1);
  }
  nfc.SAMConfig();
}

void loop() {
  switch (estadoActual) {
    case INICIO:
      if (leerBotonFiltrado(btnJugador1)) {
        estadoActual = BARAJANDO;
        barajar();
        estadoActual = SELECCION_JUGADORES;
      }
      break;
      
    case SELECCION_JUGADORES:
      seleccionarJugadores();
      estadoActual = REPARTO;
      break;
      
    case REPARTO:
      repartirCartas();
      estadoActual = JUEGO;
      break;
      
    case JUEGO:
      jugarTurnos();
      determinarResultados();
      estadoActual = FIN_JUEGO;
      break;
      
    case FIN_JUEGO:
      mostrarResultadosFinal();
      mostrarMensaje("Fin del juego", "Btn1:Reiniciar");
      while (!leerBotonFiltrado(btnJugador1)) {
        delay(100);
      }
      // Reiniciar el juego
      mostrarMensaje("Reiniciando", "");
      delay(1000);
      estadoActual = INICIO;
      mostrarMensaje("BlackJack", "Btn1: Barajar");
      break;
  }
}

Casos de uso

CU 01: Barajar las Cartas
Actor: Sistema
Descripción: El sistema activa un motor para barajar las cartas físicamente.
Flujo: En primer lugar muestra en el LCD «Pulsa Btn 1 para barajar», cuando dicho evento ocurre, enseña el mensaje «Barajando… Espere», activa los motores, espera y muestra «Barajado OK».
Resultado: Las cartas están barajadas, y el sistema pasa al estado de selección de jugadores.

CU 02: Seleccionar el Número de Jugadores
Actor: Jugador
Descripción: Selecciona de 1 a 4 jugadores pulsando botones asignados.
Flujo: Muestra mensaje en pantalla, espera pulsación y asigna jugadores.
Resultado: El número queda configurado, el sistema pasa al estado de reparto.

CU 03: Repartir Cartas Iniciales
Actor: Jugador y Sistema
Descripción: El sistema reparte dos cartas a cada jugador y la banca, usando lector NFC.
Flujo: El jugador pulsa botón, el sistema reparte y registra las cartas, ocultando una de la banca.
Resultado: Todos tienen dos cartas, y el sistema pasa al estado de juego.

CU 04: Jugar un Turno de Jugador
Actor: Jugador
Descripción: Decide si pedir carta o plantarse.
Flujo: Muestra opciones, el jugador elige y el sistema actualiza el estado del turno.
Resultado: Se avanza al siguiente jugador o la banca.

CU 05: Jugar el Turno de la Banca
Actor: Sistema (automático)
Descripción: La banca pide cartas hasta alcanzar 17.
Flujo: Muestra mensaje, reparte cartas, lee NFC y actualiza el total.
Resultado: Finaliza el turno de la banca y pasa a resultados.

CU 06: Determinar Resultados
Actor: Sistema
Descripción: Compara totales de jugadores y banca.
Flujo: Se evalúa si la banca pierde, gana o empata contra cada jugador.
Resultado: Se asigna un resultado a cada jugador.

CU 07: Mostrar Resultados Finales
Actor: Sistema
Descripción: Muestra en pantalla los resultados de cada jugador y la banca.
Flujo: Muestra los totales y resultados individuales y de la banca.
Resultado: El sistema queda en espera del reinicio.

CU 08: Reiniciar el Juego
Actor: Jugador
Descripción: Reinicia para comenzar una nueva partida.
Flujo: Pulsa botón, se muestra «Reiniciando» y vuelve al estado inicial.
Resultado: El sistema se reinicia, listo para una nueva partida.


Retos técnicos y soluciones

A lo largo del desarrollo surgieron diversos desafíos que pusieron a prueba tanto el diseño mecánico como la programación embebida:

  • Lecturas erráticas del PN532: En las primeras pruebas, al dejar caer cartas y recogerlas a distinta velocidad, el lector capturaba múltiples lecturas o ninguna en algunos casos. Para solucionarlo:
    • Implementé un temporizador que, tras detectar un UID, bloquea nuevas lecturas durante 200 ms.
    • Ajusté la posición del lector al centro perfecto de donde caen las cartas.
    • Intenté pegar las pegatinas de nuevo para que estén lo más alineadas al caer con el lector.
  • Temporización y coordinación entre el dealer y lector NFC: El dealer expulsaba la carta antes de que el PN532 estuviera activado. Tras varios tests, ajusté un retardo dinámico basado en la duración real de cada ciclo de motor.
  • Rebotes y bloqueos en los pulsadores: Los botones generaban lecturas múltiples por rebote, y a veces interrumpían tareas críticas. La estrategia fue:
    • Debounce de 50 ms por software con lectura en bucle cerrado.
    • Deshabilitar la lectura de botones mientras el sistema está en modo de “reparto” o “lectura de carta”.
  • Atascos mecánicos: En sesiones largas de prueba del sistema, la fricción entre cartas y el desgaste provocaba atascos en la caída de una carta. La mejora incluyó:
    • Inserción de una lámina en el dealer para que solo pudiese pasar una carta en el modo de reparto solo.
    • Ajustar los tiempos y velocidades a valores prácticamente idóneos para que, aproximadamente 9 de cada 10 veces suelte solo una carta sin trabarse.
  • Limitaciones de memoria y consumo: Al añadir funcionalidades de historial y menús complejos, el Arduino se quedaba sin memoria SRAM, lo cual provocaba bloqueos intermitentes y reinicios inesperados. Para optimizar el uso de memoria se implementaron varias estrategias:
    • Reducir el tamaño de cadenas de texto y optimizar al máximo el cómo se muestran, utilizando frases más cortas y abreviaturas donde fuera posible. Se eliminaron mensajes redundantes y se reutilizaron buffers de forma eficiente.
    • Almacenar cadenas de texto constantes en memoria Flash (PROGMEM), evitando que ocupen espacio en la SRAM durante la ejecución.
    • Sustituir tipos de datos grandes por equivalentes más ligeros: por ejemplo, usar uint8_t en lugar de int cuando el valor esperado estaba en el rango de 0 a 255.
    • Eliminar todo el código de depuración (como Serial.print) una vez estabilizado el comportamiento general del sistema, para liberar tanto espacio de programa como de RAM.
    • Reorganizar las estructuras de datos, reduciendo el número de elementos preasignados y utilizando estructuras más compactas para representar cartas, jugadores y estados de juego.
  • Ruido electromagnético en entornos reales: En ciertos entornos como aulas o mesas metálicas, el sistema experimentaba interferencias que afectaban la lectura del PN532 y el comportamiento de los motores. Solución: separar físicamente los cables de alimentación de los de señal.
  • Gestión de energía y estabilidad: El uso simultáneo de motor, pantalla, NFC y botones causaba caídas de tensión breves que reiniciaban el Arduino o provocaban fallos intermitentes. Se solucionó usando una fuente de 6V/2A con regulación más estable.

  • Aplicaciones y escenarios de uso

    La versatilidad de este sistema permite varios ámbitos de aplicación más allá de una simple partida casera:

    • Formación de crupieres: Academias de hostelería y casinos pueden usarlo como simulador: los estudiantes practican control de baraja y atención al cliente sin supervisión constante.
    • Permite medir tiempos de reacción, precisión en el reparto y gestión de errores en un entorno controlado.
    • Demostraciones y ferias tecnológicas: Exhibición de integración entre componentes mecánicos, NFC y microcontroladores en eventos de electrónica y robótica.
    • Funciona como un atractivo “gadget” interactivo que muestra en directo la comunicación I²C, la lectura de tags y la automatización de procesos.
    • Ocio interactivo en bares y cafeterías: Implementar mesas temáticas donde los clientes jueguen Blackjack sin la necesidad de un crupier humano.
    • Posibilidad de añadir un módulo de pagos por uso, haciendo del sistema un servicio autónomo de entretenimiento.
    • Investigación en sistemas embebidos: Base para proyectos de fin de grado o máster en los que se estudie el control de motores, la sincronización de sensores y la gestión de interfaces de usuario en tiempo real.
    • Puede ampliarse con algoritmos de inteligencia artificial que analicen estrategias de juego o predigan cartas restantes.
    • Adaptación a otros juegos de cartas: Con cambios mínimos en la lógica, puede servir para distribuir cartas de Poker, Baccarat, UNO o cualquier baraja personalizada.
    • La arquitectura modular permite reemplazar fácilmente la lógica de reparto y las reglas de puntuación.

    Demostración en vídeo

    El vídeo completo lo he alojado en Drive por una cuestión de peso:

    Ver vídeo en Google Drive


    Autoría

    Este proyecto ha sido diseñado, ensamblado y programado íntegramente por:

    Gabriel Serrano Díaz


    Conclusión y perspectivas

    La Máquina Dealer Blackjack ha supuesto un reto multidisciplinar: desde el control preciso de motores y sensores hasta la programación de una interfaz clara y la implementación de la lógica de juego. Cada fase de depuración reforzó la necesidad de una arquitectura modular y bien documentada. El resultado es un sistema estable, capaz de gestionar partidas completas de hasta 4 jugadores más banca, con una experiencia de usuario transparente gracias a la pantalla LCD y los botones físicos.

    De cara al futuro, algunas mejoras posibles incluyen:

    • Conectividad inalámbrica: Integrar Wi‑Fi o Bluetooth para registrar estadísticas en la nube.
    • Interfaz gráfica avanzada: Sustituir la LCD por pantalla TFT a color para mostrar animaciones y cartas reales.
    • Estadísticas y análisis: Guardar historial de partidas en tarjeta SD para evaluar estrategias de juego.
    • Soporte multi-juego: Permitir configurar nuevos juegos de cartas cambiando solo la lógica de reglas.

    Este proyecto no solo consolida conocimientos de electrónica y programación embebida, sino que abre la puerta a futuras aplicaciones interactivas y educativas.

También te podría gustar...

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *