MÁQUINA REPARTIDORA Y DEALER BLACKJACK

Índice
- Visión general
- Proceso de desarrollo
- Materiales y componentes
- Diagrama de actividad
- Arquitectura del código
- Casos de uso
- Retos técnicos y soluciones
- Aplicaciones y escenarios de uso
- Demostración en vídeo
- Autoría
- Conclusión y perspectivas
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.
- Sincronización inicial: calibré un retardo tras activar los motores para que la primera carta no golpease el sensor óptico antes de tiempo.
- Secuenciación de reparto: diseñé una rutina que iteraba 2 veces por jugador y banca, intercambiando motor—sensor—lectura en bucle.
- 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
Componente | Descripción | Precio (EUR) |
---|---|---|
Arduino Uno | MCU ATmega328P, 14 pines digitales, 6 analógicos, 32 KB Flash | 29.04 |
Keyestudio L298P Motor Driver Shield | Controlador de activación y velocidad de cada uno de los motores | 6,50 |
Dealer de cartas | Mecanismo con motores DC de 3 V, incluye engranajes para barajar cartas y soltarlas una a una | 12,00 |
Lector NFC PN532 | Módulo I²C/SPI con alcance de hasta 5 cm y soporte de etiquetas ISO14443A | 8,50 |
Pantalla LCD 16×2 (HD44780) | Interfaz básica de texto, 2 líneas de 16 caracteres cada una | 3,00 |
4 botones pulsadores | Para navegar menús y confirmar acciones (jugadores, pedir, plantarse, reiniciar) | 1,00 |
Sensor óptico infrarrojo | Detecta paso de carta entre dos guías, sincroniza expulsión con lectura NFC | 2,50 |
Pegatinas NFC (NTAG213, 100 unidades) | Adhesivas, cada una codificada con UID único | 2.14 |
Cables jumper | Para conexiones entre módulos | ≈ 2,00 en total todos los usados |
Placa protoboard | Para prototipado sin soldadura | 3,00 |
Fuente de alimentación 6 V / 2 A | Provee energía estable a los componentes | 5,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 deint
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.
- 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.
- 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.
Aplicaciones y escenarios de uso
La versatilidad de este sistema permite varios ámbitos de aplicación más allá de una simple partida casera:
Demostración en vídeo
El vídeo completo lo he alojado en Drive por una cuestión de peso:
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:
Este proyecto no solo consolida conocimientos de electrónica y programación embebida, sino que abre la puerta a futuras aplicaciones interactivas y educativas.