Nuestro proyecto Ping Pong con Arduino empieza a tomar forma. En esta cuarta parte vamos a incluir un poco más de lógica para completar el control del juego.
Sketch
Este es el sketch que tendremos al completar esta cuarta parte del proyecto:
/*********************
DECLARACIONES
**********************/
//LEDS:
const int LEDS [] = {0,1,4,5,6,7,8,9,10,11};
int time_led =1000;
//BOTONES:
const byte BUTTON_1 = 3;
const byte BUTTON_2 = 2;
const byte BUTTON_START = 12;
bool btn1_pushed = false;
bool btn2_pushed = false;
//CONTROL DEL JUEGO:
byte turn = 1;
byte last_led = 0;
bool end_play = false;
int score [] = {0,0};
const int LED_PLY1 = 8;
const int LED_PLY2 = 1;
/*********************
CONFIGURACIÓN
**********************/
void setup() {
Serial.begin(9600);
//LEDS:
for(int i=0; i<10; i++){
pinMode(LEDS[i], OUTPUT);
}
//BOTONES:
pinMode(BUTTON_1, INPUT);
pinMode(BUTTON_2, INPUT);
pinMode(BUTTON_START, INPUT);
attachInterrupt(digitalPinToInterrupt(BUTTON_1), interrBtn1, RISING);
attachInterrupt(digitalPinToInterrupt(BUTTON_2), interrBtn2, RISING);
//CONTROL DEL JUEGO
randomSeed(analogRead(0));
}
/*********************
FUNCIONES
**********************/
void clean (){
//LEDS:
for(int i=0; i<10; i++){
digitalWrite(LEDS[i], LOW);
}
//BOTONES:
btn1_pushed = false;
btn2_pushed = false;
}
//LEDS
void led(int i){
digitalWrite(LEDS[i], HIGH);
delay(time_led);
digitalWrite(LEDS[i], LOW);
}
void sequence(){
int result = 0;
int led_fail = 0;
clean ();
if(turn == 1){
for(int i=last_led; i<10 && !btn1_pushed; i++){
led(i);
last_led = i;
}
led_fail = LED_PLY1+1;
result = check(btn1_pushed, last_led, LED_PLY1, led_fail, turn);
turn = 2;
}else{
for(int i=last_led; i>=0 && !btn2_pushed; i--){
led(i);
last_led = i;
}
led_fail = LED_PLY2-1;
result = check(btn2_pushed, last_led, LED_PLY2, led_fail, turn);
turn = 1;
}
if(result == 1){
showScore(score[0], score[1]);
}else if(result == 2){ //led rojo
digitalWrite(LEDS[led_fail], HIGH);
end_play = true;
}
}
//BOTONES
void interrBtn1 (){
Serial.println("Btn1 pulsado");
btn1_pushed = true;
}
void interrBtn2 (){
Serial.println("Btn2 pulsado");
btn2_pushed = true;
}
//CONTROL DEL JUEGO
void start(){
clean ();
Serial.println("Pulsa START para empezar");
while(digitalRead(BUTTON_START)==LOW);
turn = random(2)+1;
if(turn==1){
last_led = 2;
}else{
last_led = 7;
}
end_play = false;
time_led = 1000;
score[0] = 0;
score[1] = 0;
}
int check(bool btn_pushed, int last_led, int led_player, int led_fail, int player){
if(time_led > 50){
time_led = time_led - 50;
}
if(btn_pushed && last_led == led_player){
Serial.print("Jugador ");
Serial.print(player);
Serial.print(" suma un punto! ");
score[player-1]++;
return 1; //el jugador suma punto
}else if(last_led == led_fail){
Serial.println("Fin del juego");
if(score[player-1]>0){
score[player-1]--;
}
return 2; //se termina el juego
}
return 3; //el jugador ha presionado antes de tiempo
}
void showScore(int score_ply1, int score_ply2){
Serial.print("JUGADOR 1: ");
Serial.print(score_ply1);
Serial.print(" - JUGADOR 2: ");
Serial.println(score_ply2);
}
void showFinalScore(int score_ply1, int score_ply2){
showScore(score_ply1, score_ply2);
if(score_ply1 > score_ply2){
Serial.println("Gana: Jugador 1");
}else if(score_ply1 < score_ply2){
Serial.println("Gana: Jugador 2");
}else{
Serial.println("Empate");
}
}
/*********************
FUNCIÓN LOOP
**********************/
void loop() {
start();
showScore(score[0], score[1]);
while(!end_play){
sequence();
}
showFinalScore(score[0], score[1]);
}
Declaraciones
Vamos a incluir 3 elementos nuevos en las declaraciones:
int score [] = {0,0};
const int LED_PLY1 = 8;
const int LED_PLY2 = 1;
- score: Es un array de enteros de 2 posiciones. Lo usaremos para almacenar los puntos que va sumando cada jugador. Recuerda, suma un punto si presiona su pulsador cuando esté su led naranja encendido y, pierde un punto si se enciende su led rojo. Los leds verdes ni suman ni restan.
- LED_PLY1: Constante entera con el índice del led naranja del jugador 1.
- LED_PLY2: Constante entera con el índice del led naranja del jugador 2.
Estas dos últimas constantes nos serán muy útiles para determinar si el jugador suma un punto o no.
Funciones
Vamos a necesitar 3 nuevas funciones: check(), showScore() y showFinalScore().
También vamos a realizar algunos cambios en las funciones start() y sequence() que ya teníamos de las partes anteriores.
Empecemos por las nuevas funciones.
Función check()
La función check() es la encargada de comprobar si el jugador presionó su botón y cuál fue el último led que se iluminó en su ronda. En función de estos datos determinará si suma un punto, resta un punto o permanece con la puntuación actual.
int check(bool btn_pushed, int last_led, int led_player, int led_fail, int player){
if(time_led > 50){
time_led = time_led - 50;
}
if(btn_pushed && last_led == led_player){
Serial.print("Jugador ");
Serial.print(player);
Serial.print(" suma un punto! ");
score[player-1]++;
return 1; //el jugador suma punto
}else if(last_led == led_fail){
Serial.println("Fin del juego");
if(score[player-1]>0){
score[player-1]--;
}
return 2; //se termina el juego
}
return 3; //el jugador ha presionado antes de tiempo
}
Vamos a empezar por los parámetros de la función. Como puedes ver recibe 5:
- btn_pushed: Un bool que tendrá valor true si el botón del jugador que tiene el turno ha sido presionado y valor false en caso contrario.
- last_led: Un int con el índice del último led iluminado.
- led_player: Un int con el índice del led naranja del jugador que tiene el turno.
- led_fail: Un int con el índice del led rojo del jugador que tiene el turno.
- player: Un int con el número del jugador que tiene el turno (1 ó 2).
Ahora revisemos el código.
El primer if es para decrementar el tiempo que permanecen los leds encendidos siempre que el tiempo actual supere los 50ms:
if(time_led > 50){
time_led = time_led - 50;
}
Si la condición se cumple se decrementa la función time_led en 50ms, sino se deja como está.
¿Por qué hacemos esta comprobación? Simplemente para evitar que el tiempo que están los leds encendidos sea tan pequeño que los jugadores no puedan reaccionar. Si lo prefieres, puedes poner un valor límite más alto. Es una cuestión de probar hasta que veas qué velocidad te parece lo suficientemente rápida pero que permita reaccionar a los jugadores.
Recuerda que time_led comienza en 1000ms, y esta función se ejecutará por cada turno, por lo que los leds permanecerán menos tiempo encendidos haciendo que la secuencia sea cada vez más rápida.
Si quieres que el juego sea mucho más rápido, resta un numero mayor que 50. Te digo lo mismo de antes, haz pruebas y valora qué velocidad te interesa.
A continuación tenemos otro if un poco más largo, pero no te asustes porque la gran mayoría de las instrucciones son Serial.print():
if(btn_pushed && last_led == led_player){
Serial.print("Jugador ");
Serial.print(player);
Serial.print(" suma un punto! ");
score[player-1]++;
return 1; //el jugador suma punto
}else if(last_led == led_fail){
Serial.println("Fin del juego");
if(score[player-1]>0){
score[player-1]--;
}
return 2; //se termina el juego
}
Este if sirve para registrar los puntos del jugador.
Fíjate en la primera condición:
if(btn_pushed && last_led == led_player)
Comprueba si el parámetro btn_pushed es true y si el último led iluminado (last_led) es igual al led naranja del jugador (led_player).
Si esta condición se cumple, significa que el jugador presionó el botón justo cundo se iluminó el led naranja, por lo que suma un punto:
score[player-1]++;
Las instrucciones anteriores a esta simplemente muestran un mensaje en el monitor serie indicando que el jugador 1 suma 1 punto. Nos ayudará a depurar, realmente no sirve para nada más.
Justo después de sumar un punto al jugador hay un return:
return 1;
Devolvemos un 1 como resultado final de la función para que el módulo que la ejecute posteriormente sepa que se ha sumado un punto.
Si el if no se cumple, la ejecución salta a la instrucción else, donde nos encontramos de nuevo otro if:
else if(last_led == led_fail){
Serial.println("Fin del juego");
if(score[player-1]>0){
score[player-1]--;
}
return 2; //se termina el juego
}
En este caso evaluamos si el último led encendido (last_led) es el led rojo (led_fail). Si es así el jugador pierde un punto, pero solo si su marcador no estaba a 0 para evitar puntos negativos:
if(score[player-1]>0){
score[player-1]--;
}
Y el juego termina. Para indicar esto al módulo llamador devolvemos un 2:
return 2;
Si el jugador presiona el pulsador con un led verde encendido no debería ocurrir nada. Si te fijas, en las condiciones de los if no se contempla este caso, por eso al final de la función devolvemos un 3, para que el módulo llamador conozca esta circunstancia:
return 3;
Función showScore()
Esta función muestra el estado actual del marcador:
void showScore(int score_ply1, int score_ply2){
Serial.print("JUGADOR 1: ");
Serial.print(score_ply1);
Serial.print(" - JUGADOR 2: ");
Serial.println(score_ply2);
}
Recibe dos parámetros de tipo int:
- score_ply1: Puntos del jugador 1.
- score_ply2: Puntos del jugador 2.
La función showScore() simplemente muestra los puntos de ambos jugadores en el monitor serie en este formato: «JUGADOR 1: 3 – JUGADOR 2: 4».
Se debe ejecutar después de cada turno. Luego veremos cómo y dónde.
Función showFinalScore()
Esta función es parecida a la anterior, pero ademas, muestra un mensaje indicando quién ha sido el ganador:
void showFinalScore(int score_ply1, int score_ply2){
showScore(score_ply1, score_ply2);
if(score_ply1 > score_ply2){
Serial.println("Gana: Jugador 1");
}else if(score_ply1 < score_ply2){
Serial.println("Gana: Jugador 2");
}else{
Serial.println("Empate");
}
}
De nuevo recibe 2 parámetros. Son los mismo que la función showScore().
Verás que la primera instrucción es la ejecución de showScore(). Como puedes comprobar hay que pasarle los puntos de los jugadores:
showScore(score_ply1, score_ply2);
Con esto ya estaríamos mostrando los puntos de cada jugador tal y como vimos anteriormente. Lo siguiente es comprobar, a partir de esos puntos, quién es el ganador. De eso se encarga el siguiente if else:
if(score_ply1 > score_ply2){
Serial.println("Gana: Jugador 1");
}else if(score_ply1 < score_ply2){
Serial.println("Gana: Jugador 2");
}else{
Serial.println("Empate");
}
Es sencillo, si score_ply1 es mayor que score_ply2 (score_ply1 > score_ply2), entonces gana el jugador 1.
Si esto no se cumple saltamos al primer else y evaluamos de nuevo. Si score_ply1 es menor que scrore_ply2 (score_ply1 < score_ply2) entonces el que gana es el jugador 2.
Si tampoco se cumple esa condición saltamos directamente al último else. Esto significa que ambos jugadores tienen la misma puntuación, por lo que se trata de un empate.
Hemos terminado con las nuevas funciones. Vamos a revisar ahora los cambios que hemos hecho sobre las funciones anteriores.
Función start()
El único cambio que debemos hacer en la función start() es incluir las 3 últimas instrucciones:
void start(){
clean ();
Serial.println("Pulsa START para empezar");
while(digitalRead(BUTTON_START)==LOW);
turn = random(2)+1;
if(turn==1){
last_led = 2;
}else{
last_led = 7;
}
end_play = false;
time_led = 1000;
score[0] = 0;
score[1] = 0;
}
Inicializamos time_led a 1000 ms y ponemos el marcador (score) a 0 para ambos jugadores.
Recuerda que la función start() es la primera que se llama dentro de la función loop(), por lo que tiene sentido que sea la responsable de inicializar todo para comenzar el juego.
Función sequence()
Vamos a incluir alguna instrucción nueva aquí, pero la lógica principal no la tocaremos.
void sequence(){
int result = 0;
int led_fail = 0;
clean ();
if(turn == 1){
for(int i=last_led; i<10 && !btn1_pushed; i++){
led(i);
last_led = i;
}
led_fail = LED_PLY1+1;
result = check(btn1_pushed, last_led, LED_PLY1, led_fail, turn);
turn = 2;
}else{
for(int i=last_led; i>=0 && !btn2_pushed; i--){
led(i);
last_led = i;
}
led_fail = LED_PLY2-1;
result = check(btn2_pushed, last_led, LED_PLY2, led_fail, turn);
turn = 1;
}
if(result == 1){
showScore(score[0], score[1]);
}else if(result == 2){ //led rojo
digitalWrite(LEDS[led_fail], HIGH);
end_play = true;
}
}
Necesitamos dos variables locales nuevas:
int result = 0;
int led_fail = 0;
La primera la usaremos para guardar el resultado de la función check() y la segunda para calcular el led rojo del jugador que tiene el turno. Ambas enteras con valor 0 inicialmente.
En la línea 12 (línea 72 del skeetch total) hay una nueva instrucción:
led_fail = LED_PLY1+1;
Con ella calculamos el led rojo del jugador 1. Recuerda que la constante LED_PLY1 es el índice del led naranja y el led rojo está justo a continuación porque es el jugador 1. Sumando una unidad a la constante obtenemos el índice del led rojo.
Una vez hecho este cálculo ya tenemos toda la información necesaria para llamar a la función check(), que lo hacemos justo a continuación:
result = check(btn1_pushed, last_led, LED_PLY1, led_fail, turn);
Le tenemos que pasar como parámetros y en ese orden:
- btn1_pushed: el botón pulsado del jugador.
- last_led: el último led encendido.
- LED_PLY1: el índice del led naranja.
- led_fail: el índice del led rojo.
- turn: el número del jugador que tiene el turno.
Con esta información, la función check() nos devolverá 1 si ha sumado un punto, 2 si ha perdido un punto y 3 si se queda como está. El resultado lo guardamos en la variable result que declaramos anteriormente.
Si ahora te fijas en las líneas 20 y 21 (líneas 80 y 81 del sketch total) verás que repetimos lo mismo para el jugador 2:
led_fail = LED_PLY2-1;
result = check(btn2_pushed, last_led, LED_PLY2, led_fail, turn);
Ahora fíjate en el final de la función sequence(), ahí tenemos un bloque de código nuevo:
if(result == 1){
showScore(score[0], score[1]);
}else if(result == 2){ //led rojo
digitalWrite(LEDS[led_fail], HIGH);
end_play = true;
}
No es más que un if else que evalúa el resultado devuelto por la función check().
Si el resultado es 1 significa que el jugador ha sumado un punto. Por eso mostramos el marcador:
showScore(score[0], score[1]);
Si el resultado es 2, entones el juego termina porque se ha iluminado un led rojo. Dejamos ese led encendido y cambiamos el valor de la variable end_play a false:
digitalWrite(LEDS[led_fail], HIGH);
end_play = true;
Y con esto terminaríamos con los cambios de las funciones.
Función loop
La función loop tambén tiene un pequeño cambio:
void loop() {
start();
showScore(score[0], score[1]);
while(!end_play){
sequence();
}
showFinalScore(score[0], score[1]);
}
Tenemos que llamar a la función showScore() justo después de llamar a start() para que se muestre el marcador inicializado a 0.
El otro cambio está justo al final. Llamamos a la función showFinalScore() para que muestre el resultado final del juego una vez termine de ejecutarse el bucle while.