MÁQUINA REPARTIDORA Y DEALER BLACKJACK

Índice
- Visión general
- Proceso de desarrollo
- Materiales y componentes
- Arquitectura del código
- 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).
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 botón de inicio de barajado.
- Diseño de un circuito con transistor y diodo de protección para que el Arduino accionase los motores sin interferir en su electrónica interna.
- Pruebas de duración de un ciclo completo de barajado, midiendo consumos y tiempos para garantizar consistencia.
- 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
1. Iniciar el Juego
Actor: Jugador
Descripción: El jugador inicia una partida de Blackjack pulsando el botón 1 cuando la pantalla LCD muestra el mensaje inicial.
- Flujo: El sistema detecta la pulsación del botón 1, cambia al estado de barajado y ejecuta el proceso correspondiente.
- Resultado: El sistema prepara el mazo y avanza al estado de selección de jugadores.
2. Barajar las Cartas
Actor: Sistema (automático)
Descripción: El sistema activa un motor para barajar las cartas físicamente, preparando el mazo para la partida.
- Flujo: Muestra «Barajando… Espere» en la pantalla LCD, activa los motores a una velocidad predefinida durante un tiempo fijo, detiene los motores y muestra «Barajado OK».
- Resultado: Las cartas están barajadas, y el sistema pasa al estado de selección de jugadores.
3. Seleccionar el Número de Jugadores
Actor: Jugador
Descripción: El jugador selecciona el número de participantes (de 1 a 4) pulsando uno de los botones asignados.
- Flujo: La pantalla LCD muestra «Elige jugadores Btn 1-4». El sistema espera la pulsación de un botón, asigna el número de jugadores (más la banca) y muestra la selección. Reinicia los datos de los jugadores.
- Resultado: El número de jugadores queda configurado, y el sistema avanza al estado de reparto.
4. Repartir Cartas Iniciales
Actor: Jugador y Sistema
Descripción: El sistema reparte dos cartas a cada jugador y a la banca, utilizando un lector NFC para identificarlas.
- Flujo: El jugador pulsa el botón 1 para iniciar. Por cada turno, el sistema muestra el jugador actual, activa el motor para repartir una carta, lee la carta con NFC, actualiza el total y muestra el resultado. La segunda carta de la banca se oculta. Si hay errores NFC, se reintenta.
- Resultado: Cada jugador y la banca tienen dos cartas, y el sistema pasa al estado de juego.
5. Jugar un Turno de Jugador
Actor: Jugador
Descripción: El jugador decide si pide una carta adicional o se planta durante su turno.
- Flujo: La pantalla LCD muestra las cartas, el total y las opciones (pedir carta o plantarse). Si pide carta, el sistema reparte una, actualiza el total y verifica si supera 21 o alcanza Blackjack. Si se planta, avanza al siguiente jugador.
- Resultado: El jugador completa su turno, y el sistema avanza al siguiente jugador o a la banca.
6. Jugar el Turno de la Banca
Actor: Sistema (automático)
Descripción: La banca juega automáticamente, pidiendo cartas hasta alcanzar al menos 17.
- Flujo: Muestra «Turno Banca». Mientras el total sea menor a 17, pide cartas (activa motor, lee NFC, actualiza total). Si supera 21, marca como pérdida. Si no, muestra el total final.
- Resultado: La banca completa su turno, y el sistema pasa a determinar resultados.
7. Determinar Resultados
Actor: Sistema
Descripción: El sistema compara los totales de los jugadores con el de la banca para asignar resultados (gana, pierde, empata).
- Flujo: Si la banca supera 21, los jugadores no eliminados ganan. Si no, compara totales: mayor que la banca gana, menor pierde, igual empata.
- Resultado: Cada jugador tiene un resultado asignado.
8. Mostrar Resultados Finales
Actor: Sistema
Descripción: El sistema muestra los resultados de cada jugador y la banca en la pantalla LCD.
- Flujo: Para cada jugador, muestra «Jx: [GANA/PIERDE/EMPATE] ([total])». Para la banca, muestra «Banca: [PIERDE/OK] ([total])». Finaliza con «Fin del juego Btn1:Reiniciar».
- Resultado: Los jugadores ven los resultados, y el sistema espera la orden de reinicio.
9. Reiniciar el Juego
Actor: Jugador
Descripción: El jugador reinicia el juego para comenzar una nueva partida.
- Flujo: La pantalla LCD muestra «Fin del juego Btn1:Reiniciar». Al pulsar el botón 1, muestra «Reiniciando» y retorna al estado inicial con el mensaje «BlackJack Btn1: Barajar».
- 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 deslizar la baraja 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 altura de la guía de cartas y añadí una lente plástica que canaliza el campo NFC justo en la trayectoria del tag.
Temporización y coordinación con el dealer
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:
- Registro de tiempos de inicio y fin de cada barajado, calculando un promedio.
- Ajuste en software de un delay variable que espera a que el sensor óptico confirme paso de carta.
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 y vibraciones
En sesiones largas la fricción entre cartas provocaba atascos. La mejora incluyó:
- Inserción de una lámina guía de acetato para separar ligeramente las cartas.
- Montaje de muelles tensores que devuelven la baraja a posición inicial tras cada ciclo.
Limitaciones de memoria y consumo
Al añadir funcionalidades de historial y menús complejos, el Arduino se quedaba sin memoria SRAM. Para optimizar:
- Reducir el tamaño de cadenas de texto en flash (
const char[] PROGMEM
). - Usar tipos de datos más pequeños (
uint8_t
en lugar deint
cuando solo cabe 0–255). - Eliminar
Serial.print
de depuración una vez estabilizado el flujo.
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:
https://drive.google.com/file/d/1z3AZJMvEebal3uZ1viPEdo12XwCld4b8/view?usp=sharing
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.