Llegados a este punto, vamos a poner a prueba nuestras habilidades de programación para crear un juego con lo que hemos aprendido. Si hemos seguido los tutoriales para el desarrollo y el uso de una pantalla TFT con teclado, ahora seremos capaces de reutilizar este conocimiento para aplicar en nuestros modelos de juego.
En este tutorial vamos a crear un juego que se base en la generación de contraseñas aleatorias.
Por una parte, vamos a depurar esas zonas de nuestro anterior código susceptibles a “bugs” para reconocer algunos de los errores que pueden ejecutarse.
RandomPass
Para ejecutar la construcción de una contraseña de 4 dígitos, podriamos crear un numero aleatorio sencillamente con la función random ( 1000,9999); pero debemos recordar que nuestro juego no puede repetir ninguno de los dígitos en la contraseña.
Aunque pueden existir otras soluciones a nuestro problema, nosotros vamos a desarrollar el siguiente formato usando Strings y su capacidad para acceder a la posición de un caracter y eliminarlo.
String pass = "0123"; void setup() { Serial.begin(9600); } void loop() { if( Serial.available() ){ Serial.readString(); GeneratePassword(); } } void GeneratePassword(){ String str_pass = "0123456789"; for(int i=0; i < 4;i++){ int n_option = random ( 0,str_pass.length()-1 ); pass[i] = str_pass[n_option]; str_pass.remove(n_option, 1); } Serial.println( pass ); }
Para obtener una contraseña nueva, solo tendremos que acceder a nuestro puerto serie y escribir un valor para que genere una nueva contraseña.
Este modelo es conveniente para otros casos en los que la contraseña pueda definirse como un conjunto de caracteres. Recordemos que nuestro teclado también contiene los caracteres ‘*’ y ‘#’ y las letras ‘A’, ‘B’, ‘C’ y ‘D’.
Por lo que utilizar solamente formatos de número nos restringiría si nuestras lecturas son carácter a carácter.
Un aspecto curioso es que la ejecución de números aleatorios, quizás no sea tan aleatoria como nosotros creíamos. Si ejecutamos el anterior código repetidas veces comprobaremos que nos aparecen las mismas contraseñas una y otra vez, por lo que nuestro juego contiene un pequeño bug de jugabilidad. No es que esté mal diseñado el programa, pero nuestra placa no tiene capacidad de discernir entre un estado aleatorio en distintas ejecuciones del juego, por lo que vamos a añadir la función randomSeed().
String pass = "0123"; void setup() { Serial.begin(9600); } void loop() { if( Serial.available() ){ Serial.readString(); GeneratePassword(); } } void GeneratePassword(){ randomSeed(analogRead(5)); String str_pass = "0123456789"; for(int i=0; i < 4;i++){ int n_option = random ( 0,str_pass.length()-1 ); pass[i] = str_pass[n_option]; str_pass.remove(n_option, 1); } Serial.println( pass ); }
Posibles bugs de nuestro anterior código
Vamos a fijarnos en el siguiente apartado de nuestro código. Estábamos asociando un valor numérico a cada letra, pero en el código ASCII los números se pueden interpretar como la resta por el carácter ‘0’ que en ese caso sería el número 48.
Pero qué pasa si pulsamos la tecla A, B, C, D o el símbolo * o el #? Estos números corresponderían con el 65, 66, 67, 68 respectivamente para las letras y el número 42 y 35 para los símbolos.
Si restáramos el valor númerico del carácter 0, nos quedaría 17, 18, 19, 20 para las letras y el -6 y el -13 que son números negativos para los símbolos.
Es decir que estaríamos accediendo a componentes del vector de botones que no existen. Y esto es un problema de acceso a memoría que en algunas ocasiones puede producir errores sin que nos demos cuenta.
Por lo que deberemos de realizar una conversión de carácter a número de una forma más adecuada en el que solo registren datos núméricos del 0 al 9 y evitar cualquier acceso al vector en caso de no apretar ningún símbolo numérico.
Aunque pueda haber otras soluciones, nosotros solamente vamos a programar un condicional que deje procesar desde el valor 0 al 9.
if( key - '0' >= 0 && key - '0' <= 9 ){ btn_list[ key - '0' ].draw(false); counter++; Serial.print("Digit ");Serial.print(key);Serial.print(" in position ");Serial.print( counter );Serial.println( " is correct!!" ); }
Juego con contador de fallos
Ahora vamos a introducir todos estos pequeños cambios añadiendo también un contador de fallos que aparezca en la escena final y conocer nuestra habilidad de predicción de números aleatorios.
#include <SPI.h> #include "Keypad.h" #define BLACK 0x0000 #define BLUE 0x001F #define RED 0xF800 #define GREEN 0x07E0 #define CYAN 0x07FF #define MAGENTA 0xF81F #define YELLOW 0xFFE0 #define ROSE 0xF81F #define WHITE 0xFFFF #include "Adafruit_GFX.h" #include "Adafruit_ILI9341.h" #define TFT_DC 8 #define TFT_CS 9 Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC); const byte ROWS = 4; //four rows const byte COLS = 4; //three columns char keys[ROWS][COLS] = { {'1','2','3','A'}, {'4','5','6','B'}, {'7','8','9','C'}, {'*','0','#','D'} }; byte rowPins[ROWS] = {4,5,6,7}; //connect to the row pinouts of the keypad byte colPins[COLS] = {A3, A2, A1, A0}; Keypad keypad = Keypad( makeKeymap(keys), rowPins, colPins, ROWS, COLS ); class TXTButton: public Adafruit_GFX_Button{ public: uint16_t _x, _y, _w, _h; uint8_t _marginx, _marginy, _txtsize; uint16_t _c, _c_txt, _c_border; char* _msg; String _str; TXTButton::TXTButton(void):Adafruit_GFX_Button(),_marginx(10), _marginy(5), _c( 0xFFFF ), _c_txt( 0x0000 ), _c_border( 0xFFFF ), _txtsize( 4 ) {}; TXTButton( char* msg, uint16_t x, uint16_t y, uint16_t c = 0xFFFF , uint16_t txtc = 0x0000,uint16_t borderc = 0xFFFF, uint8_t textsize = 4, uint8_t mgx = 10, uint8_t mgy = 10 ) :Adafruit_GFX_Button( ),_str( msg ), _x( x ), _y( y ),_marginx(mgx), _marginy(mgy), _c( c ), _c_txt( txtc ), _c_border( borderc ), _txtsize( textsize ) { _msg = msg; } TXTButton( String msg, uint16_t x, uint16_t y, uint16_t c = 0xFFFF , uint16_t txtc = 0x0000, uint16_t borderc = 0xFFFF, uint8_t textsize = 4, uint8_t mgx = 10, uint8_t mgy = 5 ) :Adafruit_GFX_Button( ),_str( msg ), _x( x ), _y( y ),_marginx(mgx), _marginy(mgy), _c( c ), _c_txt( txtc ), _c_border( borderc ), _txtsize( textsize ) { } void init (Adafruit_GFX& _tft ){ uint16_t msg_w = _str.length()*_txtsize*5+ (_str.length()-1)*_txtsize; uint16_t msg_h = _txtsize*8; //Tamanio real del boton uint16_t btn_w = msg_w+2*_marginx; uint16_t btn_h = msg_h+2*_marginy; // Center of Button uint16_t btn_x = _x ; uint16_t btn_y = _y ; char buf[ 10 ]; _str.toCharArray( buf, _str.length()+1 ); initButton( &_tft ,btn_x, btn_y, btn_w, btn_h, _c_border , _c, _c_txt, buf, _txtsize); } void setStr( char* msg ){ _str = msg; }; void setStr( String str ){ _str = str; }; void setTextSize( uint16_t size ){ _txtsize = size; }; void setMarginSize( uint16_t mx,uint16_t my ){ _marginx =mx; _marginy =my; }; void setXY( uint16_t x,uint16_t y ){ setX(x); setY(y); }; void setX( uint16_t x ){ _x =x; }; void setY( uint16_t y ){ _y =y; }; void setColor( uint16_t c, uint16_t ctxt, uint16_t cBorder ){ setFillColor(c); setTextColor( ctxt ); setBorderColor (cBorder); }; void setTextColor( uint16_t c ){ _c_txt = c; }; void setBorderColor( uint16_t c ){ _c_border = c; }; void setFillColor( uint16_t c ){ _c = c; }; void draw(bool inv = true){ drawButton(inv); } void clear(Adafruit_GFX& _tft, uint16_t c ){ setColor( c, c, c ); init ( _tft ); draw(true); } }; #define nrows 3 #define ncols 4 #define n nrows*ncols TXTButton btn_list[n]; String pass= "2019"; char str_array[4]; int counter = 0; int failures = 0; void setup() { Serial.begin(9600); tft.begin(); // Begin ILI9341 tft.setRotation(3); tft.fillScreen ( WHITE ); Serial.println( pass ); keypad.addEventListener(keypadPressed); createPanel(); GeneratePassword(); } void loop() { keypad.getKey(); } void keypadPressed(KeypadEvent key) { if( keypad.getState( ) == PRESSED ){ if ( key == pass[counter] ){ if( key - '0' >= 0 && key - '0' <= 9 ){ btn_list[ key - '0' ].draw(false); counter++; Serial.print("Digit ");Serial.print(key);Serial.print(" in position ");Serial.print( counter );Serial.println( " is correct!!" ); } }else{ failures++; counter = 0; Serial.print("Digit ");Serial.print(key); Serial.println(" Failed. Try again"); createPanel(); } if (counter == pass.length() ){ Serial.println("Pass Unlocked"); counter = 0; unlocked(); failures = 0; GeneratePassword(); createPanel(); } } if( keypad.getState( ) == RELEASED ){ //btn_list[ key - '0' ].clear( tft, WHITE); } } void createPanel(){ randomSeed(analogRead(5)); int w = tft.width() ; int h = tft.height() ; int l = h/6; int lx = w/(2*ncols); int ly = h/(2*nrows); String strlist[n] = { "0","1","2","3","4","5","6","7","8","9","10","11"}; //Grid of numbers in list for(int i=0; i< n;i++){ int my = (2*(i/ncols)+1); int mx = (2*(i%ncols)+1); int row = my*ly; int col = mx*lx; btn_list[i].setXY(w/2-ncols*lx+col, h/2-nrows*ly+row); btn_list[i].setStr( String(strlist[i]) ); btn_list[i].setColor( BLACK, YELLOW, CYAN ); btn_list[i].init(tft); btn_list[i].draw(); } } void GeneratePassword(){ String str_pass = "0123456789"; for(int i=0; i< 4;i++){ int n_option = random ( 0,str_pass.length()-1 ); pass[i] = str_pass[n_option]; str_pass.remove(n_option, 1); } Serial.println( pass ); } void unlocked(){ tft.fillScreen( CYAN ); int nr = 100; for (int i=0; i < nr; i++ ){ tft.drawCircle (tft.width()/2, tft.height()/2, 200*i/nr, tft.color565( random(0,255), random(0,255), random(0,255) ) ); } Serial.print( "Number of fails: " );Serial.println( failures ); TXTButton unlock; unlock.setXY ( tft.width()/2, tft.height()/2 ); unlock.setStr( "UNLOCK "+ String(failures) ); unlock.setTextSize( 5 ); unlock.setColor( tft.color565( random(0,255), random(0,255), random(0,255) ), YELLOW, tft.color565( random(0,255), random(0,255), random(0,255) ) ); unlock.init( tft ); unlock.draw(); delay(10000); tft.fillScreen(WHITE); }
RandomPanel
Vamos a aumentar el nivel de dificultad. Antes hemos trabajado asociando cada una de las opciones de teclado con el valor que aparece en la pantalla TFT. Pero ahora, vamos a ejecutar la creación del panel en formato también aleatorio.
Para ello, vamos a usar un modelo similar al anterior para generar una contraseña en una nueva función que hemos llamado randomPanel() . Pero este panel, deberá de ser modificado cada vez que se complete una escena del juego. Esto quiere decir que se habrá de mantener la creacion de un panel aleatorio en una variable que vamos a llamar static_panel.
Esta variable es a la que accedera desde la función createPanel() para crear de nuevo el mapa de teclas manteniendose fijo, hasta que lo hayamos acertado.
Hay que darse cuenta ahora que ya no existe ninguna relación con la que se asocia el número de teclado con el valor que aparece en la pantalla. Solamente se asocia a la posición.
Finalmente nuestro juego queda de la siguiente manera:
#include <SPI.h> #include "Keypad.h" #define BLACK 0x0000 #define BLUE 0x001F #define RED 0xF800 #define GREEN 0x07E0 #define CYAN 0x07FF #define MAGENTA 0xF81F #define YELLOW 0xFFE0 #define ROSE 0xF81F #define WHITE 0xFFFF #include "Adafruit_GFX.h" #include "Adafruit_ILI9341.h" #define TFT_DC 8 #define TFT_CS 9 Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC); const byte ROWS = 4; //four rows const byte COLS = 4; //three columns char keys[ROWS][COLS] = { {'1','2','3','A'}, {'4','5','6','B'}, {'7','8','9','C'}, {'*','0','#','D'} }; byte rowPins[ROWS] = {4,5,6,7}; //connect to the row pinouts of the keypad byte colPins[COLS] = {A3, A2, A1, A0}; Keypad keypad = Keypad( makeKeymap(keys), rowPins, colPins, ROWS, COLS ); class TXTButton: public Adafruit_GFX_Button{ public: uint16_t _x, _y, _w, _h; uint8_t _marginx, _marginy, _txtsize; uint16_t _c, _c_txt, _c_border; char* _msg; String _str; TXTButton::TXTButton(void):Adafruit_GFX_Button(),_marginx(10), _marginy(5), _c( 0xFFFF ), _c_txt( 0x0000 ), _c_border( 0xFFFF ), _txtsize( 4 ) {}; TXTButton( char* msg, uint16_t x, uint16_t y, uint16_t c = 0xFFFF , uint16_t txtc = 0x0000,uint16_t borderc = 0xFFFF, uint8_t textsize = 4, uint8_t mgx = 10, uint8_t mgy = 10 ) :Adafruit_GFX_Button( ),_str( msg ), _x( x ), _y( y ),_marginx(mgx), _marginy(mgy), _c( c ), _c_txt( txtc ), _c_border( borderc ), _txtsize( textsize ) { _msg = msg; } TXTButton( String msg, uint16_t x, uint16_t y, uint16_t c = 0xFFFF , uint16_t txtc = 0x0000, uint16_t borderc = 0xFFFF, uint8_t textsize = 4, uint8_t mgx = 10, uint8_t mgy = 5 ) :Adafruit_GFX_Button( ),_str( msg ), _x( x ), _y( y ),_marginx(mgx), _marginy(mgy), _c( c ), _c_txt( txtc ), _c_border( borderc ), _txtsize( textsize ) { } void init (Adafruit_GFX& _tft ){ uint16_t msg_w = _str.length()*_txtsize*5+ (_str.length()-1)*_txtsize; uint16_t msg_h = _txtsize*8; //Tamanio real del boton uint16_t btn_w = msg_w+2*_marginx; uint16_t btn_h = msg_h+2*_marginy; // Center of Button uint16_t btn_x = _x ; uint16_t btn_y = _y ; char buf[ 10 ]; _str.toCharArray( buf, _str.length()+1 ); initButton( &_tft ,btn_x, btn_y, btn_w, btn_h, _c_border , _c, _c_txt, buf, _txtsize); } void setStr( char* msg ){ _str = msg; }; void setStr( String str ){ _str = str; }; void setTextSize( uint16_t size ){ _txtsize = size; }; void setMarginSize( uint16_t mx,uint16_t my ){ _marginx =mx; _marginy =my; }; void setXY( uint16_t x,uint16_t y ){ setX(x); setY(y); }; void setX( uint16_t x ){ _x =x; }; void setY( uint16_t y ){ _y =y; }; void setColor( uint16_t c, uint16_t ctxt, uint16_t cBorder ){ setFillColor(c); setTextColor( ctxt ); setBorderColor (cBorder); }; void setTextColor( uint16_t c ){ _c_txt = c; }; void setBorderColor( uint16_t c ){ _c_border = c; }; void setFillColor( uint16_t c ){ _c = c; }; void draw(bool inv = true){ drawButton(inv); } void clear(Adafruit_GFX& _tft, uint16_t c ){ setColor( c, c, c ); init ( _tft ); draw(true); } }; #define nrows 3 #define ncols 4 #define n nrows*ncols TXTButton btn_list[n]; String pass= "2019"; char str_array[4]; int counter = 0; String static_panel = "0123456789ABCD"; int failures = 0; void setup() { Serial.begin(9600); tft.begin(); // Begin ILI9341 tft.setRotation(3); tft.fillScreen ( WHITE ); keypad.addEventListener(keypadPressed); GeneratePassword(); randomPanel(); } void loop() { keypad.getKey(); } void keypadPressed(KeypadEvent key) { if( keypad.getState( ) == PRESSED ){ if ( key == pass[counter] ){ if( key - '0' >= 0 && key - '0' <= 9 ){ btn_list[ key - '0' ].draw(false); counter++; Serial.print("Digit ");Serial.print(key);Serial.print(" in position ");Serial.print( counter );Serial.println( " is correct!!" ); } }else{ failures++; counter = 0; Serial.print("Digit ");Serial.print(key); Serial.println(" Failed. Try again"); createPanel(); } if (counter == pass.length() ){ Serial.println("Pass Unlocked"); counter = 0; unlocked(); failures = 0; GeneratePassword(); randomPanel(); } } if( keypad.getState( ) == RELEASED ){ //btn_list[ key - '0' ].clear( tft, WHITE); } } void createPanel(){ randomSeed(analogRead(5)); int w = tft.width() ; int h = tft.height() ; int l = h/6; int lx = w/(2*ncols); int ly = h/(2*nrows); //Grid of numbers in list for(int i=0; i< n;i++){ int my = (2*(i/ncols)+1); int mx = (2*(i%ncols)+1); int row = my*ly; int col = mx*lx; btn_list[i].setXY(w/2-ncols*lx+col, h/2-nrows*ly+row); btn_list[i].setStr( String(static_panel[i]) ); btn_list[i].setColor( BLACK, YELLOW, CYAN ); btn_list[i].init(tft); btn_list[i].draw(); } } void randomPanel(){ randomSeed(analogRead(5)); int w = tft.width() ; int h = tft.height() ; int l = h/6; int lx = w/(2*ncols); int ly = h/(2*nrows); //Random Panel String str_options = "0123456789ABCD"; //Grid of numbers in list for(int i=0; i< n;i++){ int my = (2*(i/ncols)+1); int mx = (2*(i%ncols)+1); int row = my*ly; int col = mx*lx; int n_option = random ( 0,str_options.length()-1 ); char opt = str_options[ n_option ]; static_panel[i] = opt; str_options.remove(n_option, 1); btn_list[i].setXY(w/2-ncols*lx+col, h/2-nrows*ly+row); btn_list[i].setStr( String(opt) ); btn_list[i].setColor( BLACK, YELLOW, CYAN ); btn_list[i].init(tft); btn_list[i].draw(); } } void GeneratePassword(){ String str_pass = "0123456789"; for(int i=0; i< 4;i++){ int n_option = random ( 0,str_pass.length()-1 ); pass[i] = str_pass[n_option]; str_pass.remove(n_option, 1); } Serial.println( pass ); } void unlocked(){ tft.fillScreen( CYAN ); int nr = 100; for (int i=0; i < nr; i++ ){ tft.drawCircle (tft.width()/2, tft.height()/2, 200*i/nr, tft.color565( random(0,255), random(0,255), random(0,255) ) ); } Serial.print( "Number of fails: " );Serial.println( failures ); TXTButton unlock; unlock.setXY ( tft.width()/2, tft.height()/2 ); unlock.setStr( "UNLOCK "+ String(failures) ); unlock.setTextSize( 5 ); unlock.setColor( tft.color565( random(0,255), random(0,255), random(0,255) ), YELLOW, tft.color565( random(0,255), random(0,255), random(0,255) ) ); unlock.init( tft ); unlock.draw(); delay(10000); tft.fillScreen(WHITE); }
Con este tutorial, podemos dar por terminada nuestra sección de juegos en modelos Vista controlador, en el que hemos aprendido cómo crear de forma artesanal una comunicación con teclados y el modelo táctil de nuestras pantallas TFT.