Máquina Expendedora Arcade
Autores: Stefan Rodolfo Gonzales Nagybabi, Rubén Vicente Benito, Francisco Gómez García
Introducción
Nuestro proyecto arduino para la asignatura de Diseño de Sistemas Empotrados consiste en una máquina expendedora arcade en donde el usuario:
- Puede jugar distintos juegos a su placer.
- Puede obtener créditos canjeables mediante puntuaciones objetivo dentro de los juegos.
- Obtener recompensas que pueden ser chicles, chocolatinas, etc (que sean esféricas), al canjear créditos.
Material utilizado
Material | Coste |
---|---|
Placa Arduino UNO R3 | Proporcionado por la universidad |
Protoboard | Proporcionado por la universidad |
Servomotor SG90 | Proporcionado por la universidad |
Módulo Pantalla LCD 1602 | Proporcionado por la universidad |
Módulo Receptor IR | Proporcionado por la universidad |
Mando a distancia | Proporcionado por la universidad |
Potenciómetro | Proporcionado por la universidad |
LED (x2) | Proporcionado por la universidad |
Resistencia 220Ω (x3) | Proporcionado por la universidad |
Cables Macho-Macho | Proporcionado por la universidad |
Cables Macho-Hembra | 5€ |
Madera de contrachapado | 12.30€ |
Listón de madera de pino | 2.40€ |
Tornillos, tuercas y clavos | 5.45€ |
Herramientas para trabajar con madera | 17€ |
Pintura | 5.75€ |
Botellas vacías | 0€ (En propiedad) |
Coste total | 47.9€ |
Esquema de conexiones
Proceso de construcción
Rueda de recompensa
La primera pieza a construir fue la rueda encargada de suministrar la recompensa. Para construirla, cortamos varias piezas redondas con la madera de contrachapado y las unimos con dos tornillos de madera para darle grosor. Le hicimos un agujero para transportar la recompensa hasta la salida, y pusimos dos planchas extra para dar sujeción al servo y al tornillo (que da sujeción a la estructura, quitándole peso al servomotor), uniéndolos con dos piezas del listón de madera para mantener firme la estructura.
Estructura
Tras esto, montamos las siguientes piezas que componen la estructura de la máquina con la madera de contrachapado y cerramos la estructura con piezas del listón de madera en las esquinas de la misma para unirlos entre sí con tornillos de madera. También construimos la bandeja donde aparecerá la recompensa cuando el usuario lo canjee, con la madera de contrachapado en la parte baja y con trozos del listón de madera en los laterales.
Montaje de componentes
Tras la realización de la estructura, procedimos al montaje de los componentes en físico y encajarlos dentro de la máquina de la forma más óptima posible para que los cables no sufran de doblarse o no lleguen adonde le corresponden. Hemos usado bridas para juntar algunos cables para que no estén enredadas entre ellos y no estén dispersas.
Pintura y dispensadora
Decidimos darle algo de color y poner una estructura dispensadora para almacenar las recompensas en la parte superior. Para el color, decidimos usar una paleta de colores típicas de las clásicas máquinas de chicles estadounidenses, rojo y gris plateado. Hemos usado pintura acrílica ya que es buena para pintar sobre madera. Para la dispensadora, hemos utilizado dos botellas vacías, una de té con limón y otra de coca-cola.
Resultado final
Problemas encontrados
Relacionado con la planificación:
- En la elección de juegos, la primera idea en mente era hacer un «Simón dice», pero con sonidos. Pero al ser muy usada en proyectos anteriores, se descartó.
- Después se tuvo la idea de una versión del Snake, pero al ser tan pequeña la pantalla LCD, no nos parecía muy cómodo de jugar. Una idea que pensó era utilizar una matriz de LEDs para reemplazar la LCD, pero como queriamos aprovechar al máximo el kit proporcionado, reduciendo gastos externos, se descartó también.
- Se acabó con la idea de implementar otro tipo de juegos donde sea cómodo jugar en la pantalla LCD, como ha sido el runner y el ahorcado.
Relacionado con el código:
- Los códigos de internet no se adaptan casi nada a nuestro código, por lo que tuvimos que realizar grandes modificaciones, sobre todo el ahorcado que lo cambiamos casi entero.
- Hacer el menú y los juegos, que gracias al struct y a que implementamos el programa lo más modular posible, se soluciono rápido.
Relacionado con la máquina:
- El mecanismo giratorio para expulsar la recompensa: La pieza de giro del servomotor no era lo suficientemente grande y se colocó la pieza un poco ladeada.
- El sensor IR: Se iba a dejar por dentro, pero la señal no se transmitía muy bien a través de la madera, así que se dejó por fuera.
- El sistema para almacenar las recompensas: Se iba a utilizar un recipiente esférico, pero al tener la pieza giratoria un poco ladeada no caían bien. Por lo que usamos las botellas como dispensadora.
Demostración
Código
// Librería para el control inalámbrico
#include <IRremote.h>
// Librería para la pantalla LCD 16x2
#include <LiquidCrystal.h>
// Librería para el servomotor
#include <Servo.h>
// Control de botones del mando
#define POWER 0xBA45FF00
#define VOLUP 0XB946FF00
#define FASTBACK 0XBB44FF00
#define PLAY 0XBF40FF00
#define FASTFORWARD 0XBC43FF00
#define VOLDOWN 0XEA15FF00
//Inicializo los elementos que utilizaremos
#define MENU 0
#define AHORCADO 1
#define RUNNER 2
// Nº de puntos para recompensa
#define REWARD 150
// Elementos del juego runner
#define SPRITE_RUN1 1
#define SPRITE_RUN2 2
#define SPRITE_JUMP 3
#define SPRITE_JUMP_UPPER '.' // Use the '.' character for the head
#define SPRITE_JUMP_LOWER 4
#define SPRITE_TERRAIN_EMPTY ' ' // User the ' ' character
#define SPRITE_TERRAIN_SOLID 5
#define SPRITE_TERRAIN_SOLID_RIGHT 6
#define SPRITE_TERRAIN_SOLID_LEFT 7
#define HERO_HORIZONTAL_POSITION 1 // Horizontal position of hero on screen
#define TERRAIN_WIDTH 16
#define TERRAIN_EMPTY 0
#define TERRAIN_LOWER_BLOCK 1
#define TERRAIN_UPPER_BLOCK 2
#define HERO_POSITION_OFF 0 // Hero is invisible
#define HERO_POSITION_RUN_LOWER_1 1 // Hero is running on lower row (pose 1)
#define HERO_POSITION_RUN_LOWER_2 2 // (pose 2)
#define HERO_POSITION_JUMP_1 3 // Starting a jump
#define HERO_POSITION_JUMP_2 4 // Half-way up
#define HERO_POSITION_JUMP_3 5 // Jump is on upper row
#define HERO_POSITION_JUMP_4 6 // Jump is on upper row
#define HERO_POSITION_JUMP_5 7 // Jump is on upper row
#define HERO_POSITION_JUMP_6 8 // Jump is on upper row
#define HERO_POSITION_JUMP_7 9 // Half-way down
#define HERO_POSITION_JUMP_8 10 // About to land
#define HERO_POSITION_RUN_UPPER_1 11 // Hero is running on upper row (pose 1)
#define HERO_POSITION_RUN_UPPER_2 12 // (pose 2)
// Para el tamaño del array utilizado en el juego del ahorcado
#define ARRAY_LENGTH(x) (sizeof(x) / sizeof((x)[0]))
LiquidCrystal lcd(7, 8, 9, 10, 11, 12); // LCD y los pines utilizados
const int RECV_PIN = 3; //Variable para el pin del sensor IR
const int servoPin = 5; // Pin del servo motor
const int redLED = 4; // LED rojo que indicará que no va salir recompensa
const int greenLED = 2; // LED verde para indicar que va a salir recompensa
const int N_GAMES = 2; // Constante para el número máximo de juegos.
static bool fin = false; // Variable para saber si hemos finalizado la partida del juego en el que estabamos.
static unsigned int score = 0; // Variable para guardar el score de los juegos
static unsigned int credits = 0; // Variable para guardar la cantidad de recompensas a canjear
static bool on = false; // Variable para saber que hemos salido de la pantalla de inicio
static unsigned int creditsTemp = 0; // Variable para guardar los creditos que se han conseguido en la sesión actual de juego.
static char terrainUpper[TERRAIN_WIDTH + 1]; // Variable para el terreno superior del runner
static char terrainLower[TERRAIN_WIDTH + 1]; // Variable para el terreno inferior del runner
bool buttonPushed = false; // Variable para saber si hemos pulsado el botón de saltar en el runner
Servo gumballServo; // El servo que dispensará la recompensa
/* ESTRUCTURA PARA GESTIONAR EL MENU EN EL QUE NOS SITUAMOS Y COMO SE DEBE COMPORTAR EL CONTROL
int state: Marca el estado en el que nos encontramos:
- 0 -> si nos encontramos en el menú de selección de juego.
- 1 -> si nos encontramos en el ahorcado.
- 2 -> si nos encontramos en el runner.
int mod_controll: Marca el modo de comportamiento que tiene que tener el mando, puede ser:
- 0 -> si nos encontramos en el menú de selección de juego.
- 1 -> si nos encontramos en el ahorcado.
- 2 -> si nos encontramos en el runner.
int movMenu: Es una variable utilizada para moverse por el menú de selección.
*/
struct Mod_state {
int state;
int mod_controll;
int movMenu;
};
Mod_state mS; // Declaro la variable que utilizará la estructura.
void setup() {
lcd.begin(16, 2); // Se define el LCD de 16x2
Serial.begin(9600); // Se inicia el monitor serie
Serial.println("IR Receiver Button Decode");
IrReceiver.begin(RECV_PIN, ENABLE_LED_FEEDBACK); // Se inicia el IR
mS.state = MENU; // Inicializo en 0, para empezar en el menú.
mS.mod_controll = MENU; // Inicializo en 0, para empezar en el menú.
mS.movMenu = 0; // Inicializo en 0, para empezar en el Ahorcado
randomSeed(analogRead(6));
pinMode(redLED, OUTPUT);
pinMode(greenLED, OUTPUT);
}
// Método encargado de definir en que estado se encuentra la máquina (Menú o Juegos)
void stateControl () {
if (!on) initScreen ();
score = 0;
creditState();
switch (mS.state) {
case 0: modMenu(); break;
case 1: modAhorcado(); break;
case 2: modRunner(); break;
}
}
/******************************************MENU-GENERAL*********************************************/
//Método que muestra una pantalla de inicio que no desaparecerá hasta pulsar un botón
void initScreen () {
while(!on){
digitalWrite(redLED, LOW);
digitalWrite(greenLED, LOW);
lcd.setCursor(2, 0);
lcd.print("ArdaneExpend");
lcd.setCursor(0, 1);
lcd.print("Press any button");
delay(1250);
lcd.clear();
delay(750);
if(IrReceiver.decode()) {
on = true;
IrReceiver.resume();
}
}
}
// Método encargado de mostrar en pantalla los creditos ganados en la sesión de juego, y los totales
void endGameScreen () {
lcd.clear();
lcd.setCursor(1, 0);
lcd.print("Credits earned");
lcd.setCursor(8, 1);
lcd.print(creditsTemp);
delay(1250);
lcd.setCursor(1, 0);
lcd.print("Total Credits:");
lcd.setCursor(8, 1);
lcd.print(credits);
delay(1250);
lcd.clear();
}
// Método que se encarga del menú.
void modMenu () {
creditsTemp = 0; // Como estamos en el menu, los créditos temporales desaparecen
mS.mod_controll = MENU; // Pone el mando en modo menú.
menuOptions();
if (IrReceiver.decode()) { // Si recibimos datos del mando
controllerControl();
lcd.clear(); // Limpiamos la LCD
IrReceiver.resume(); // Recibimos el siguiente valor
}
}
// Método encargado desumar un crédito
void creditsUp() {
credits++;
creditState();
}
// Método que evalua si hay creditos, si los hay ilumina el LED verde, si no, el LED rojo
void creditState() {
if(credits > 0) {
digitalWrite(redLED, LOW);
digitalWrite(greenLED, HIGH);
}
else {
digitalWrite(redLED, HIGH);
digitalWrite(greenLED, LOW);
}
}
// Método encargado de dar la recompensa al llegar a 50 puntos
void rewardTime(){
if (credits >= 1){
//Serial.println("SOLTANDO RECOMPENSA");
int i=0;
digitalWrite(redLED, LOW);
digitalWrite(greenLED, HIGH);
gumballServo.attach(servoPin); // Conectamos el servo
gumballServo.write(130);
delay(50);
int d=15;
for (int i=130;i>=15;i--){
gumballServo.write(i); // Rotamos el servo
delay(d);
}
delay(100);
for (int i=15;i<=130;i++){ // Devolvemos el servo a la posición inicial
gumballServo.write(i);
delay(d);
}
delay(50);
gumballServo.detach(); // Desconectamos el servo
delay(100);
credits--;
}
else {
lcd.setCursor(3, 0);
lcd.print("NO CREDITS");
lcd.setCursor(2, 1);
lcd.print("PLAY FOR WIN");
delay (1500);
}
}
/*********************************************RUNNER************************************************/
// Método que se encarga del runner
void modRunner () {
Serial.println("RUNNER");
fin = false;
lcd.clear();
initializeGraphics(); // Inicializan los gráficos del juego del runner
while (!fin){
playRunner();
}
endGameScreen();
}
// Método que inicializa los gráficos del juego runner
void initializeGraphics() {
static byte graphics[] = {
// Run position 1
B01100,
B01100,
B00000,
B01110,
B11100,
B01100,
B11010,
B10011,
// Run position 2
B01100,
B01100,
B00000,
B01100,
B01100,
B01100,
B01100,
B01110,
// Jump
B01100,
B01100,
B00000,
B11110,
B01101,
B11111,
B10000,
B00000,
// Jump lower
B11110,
B01101,
B11111,
B10000,
B00000,
B00000,
B00000,
B00000,
// Ground
B11111,
B11111,
B11111,
B11111,
B11111,
B11111,
B11111,
B11111,
// Ground right
B00011,
B00011,
B00011,
B00011,
B00011,
B00011,
B00011,
B00011,
// Ground left
B11000,
B11000,
B11000,
B11000,
B11000,
B11000,
B11000,
B11000,
};
int i;
for (i = 0; i < 7; ++i) {
lcd.createChar(i + 1, &graphics[i * 8]);
}
for (i = 0; i < TERRAIN_WIDTH; ++i) {
terrainUpper[i] = SPRITE_TERRAIN_EMPTY;
terrainLower[i] = SPRITE_TERRAIN_EMPTY;
}
}
// Método que se encarga de que el terreno avance
void advanceTerrain(char* terrain, byte newTerrain) {
for (int i = 0; i < TERRAIN_WIDTH; ++i) {
char current = terrain[i];
char next = (i == TERRAIN_WIDTH - 1) ? newTerrain : terrain[i + 1];
switch (current) {
case SPRITE_TERRAIN_EMPTY:
terrain[i] = (next == SPRITE_TERRAIN_SOLID) ? SPRITE_TERRAIN_SOLID_RIGHT : SPRITE_TERRAIN_EMPTY;
break;
case SPRITE_TERRAIN_SOLID:
terrain[i] = (next == SPRITE_TERRAIN_EMPTY) ? SPRITE_TERRAIN_SOLID_LEFT : SPRITE_TERRAIN_SOLID;
break;
case SPRITE_TERRAIN_SOLID_RIGHT:
terrain[i] = SPRITE_TERRAIN_SOLID;
break;
case SPRITE_TERRAIN_SOLID_LEFT:
terrain[i] = SPRITE_TERRAIN_EMPTY;
break;
}
}
}
// Método que se encarga de sibujar al personaje, la escena y los puntos, además, devuelve si se a producido una colisión.
bool drawHero(byte position, char* terrainUpper, char* terrainLower, unsigned int score) {
bool collide = false;
char upperSave = terrainUpper[HERO_HORIZONTAL_POSITION];
char lowerSave = terrainLower[HERO_HORIZONTAL_POSITION];
byte upper, lower;
switch (position) {
case HERO_POSITION_OFF:
upper = lower = SPRITE_TERRAIN_EMPTY;
break;
case HERO_POSITION_RUN_LOWER_1:
upper = SPRITE_TERRAIN_EMPTY;
lower = SPRITE_RUN1;
break;
case HERO_POSITION_RUN_LOWER_2:
upper = SPRITE_TERRAIN_EMPTY;
lower = SPRITE_RUN2;
break;
case HERO_POSITION_JUMP_1:
case HERO_POSITION_JUMP_8:
upper = SPRITE_TERRAIN_EMPTY;
lower = SPRITE_JUMP;
break;
case HERO_POSITION_JUMP_2:
case HERO_POSITION_JUMP_7:
upper = SPRITE_JUMP_UPPER;
lower = SPRITE_JUMP_LOWER;
break;
case HERO_POSITION_JUMP_3:
case HERO_POSITION_JUMP_4:
case HERO_POSITION_JUMP_5:
case HERO_POSITION_JUMP_6:
upper = SPRITE_JUMP;
lower = SPRITE_TERRAIN_EMPTY;
break;
case HERO_POSITION_RUN_UPPER_1:
upper = SPRITE_RUN1;
lower = SPRITE_TERRAIN_EMPTY;
break;
case HERO_POSITION_RUN_UPPER_2:
upper = SPRITE_RUN2;
lower = SPRITE_TERRAIN_EMPTY;
break;
}
if (upper != ' ') {
terrainUpper[HERO_HORIZONTAL_POSITION] = upper;
collide = (upperSave == SPRITE_TERRAIN_EMPTY) ? false : true;
}
if (lower != ' ') {
terrainLower[HERO_HORIZONTAL_POSITION] = lower;
collide |= (lowerSave == SPRITE_TERRAIN_EMPTY) ? false : true;
}
byte digits = (score > 9999) ? 5 : (score > 999) ? 4 : (score > 99) ? 3 : (score > 9) ? 2 : 1;
// Dibuja el escenario
terrainUpper[TERRAIN_WIDTH] = '\0';
terrainLower[TERRAIN_WIDTH] = '\0';
char temp = terrainUpper[16 - digits];
terrainUpper[16 - digits] = '\0';
lcd.setCursor(0, 0);
lcd.print(terrainUpper);
terrainUpper[16 - digits] = temp;
lcd.setCursor(0, 1);
lcd.print(terrainLower);
lcd.setCursor(16 - digits, 0);
lcd.print(score);
terrainUpper[HERO_HORIZONTAL_POSITION] = upperSave;
terrainLower[HERO_HORIZONTAL_POSITION] = lowerSave;
return collide;
}
// Método que permite jugar al runner (Se debe ejecutar en un bucle hasta que se pulse el botón POWER)
void playRunner() {
static byte heroPos = HERO_POSITION_RUN_LOWER_1;
static byte newTerrainType = TERRAIN_EMPTY;
static byte newTerrainDuration = 1;
static bool blink = false;
static unsigned int nRewards = 0;
static bool playing = false;
if (!playing) {
drawHero((blink) ? HERO_POSITION_OFF : heroPos, terrainUpper, terrainLower, score);
if (blink) {
lcd.setCursor(0, 0);
lcd.print("Press Start");
}
delay(250);
blink = !blink;
if (IrReceiver.decode()) {
if (IrReceiver.decodedIRData.decodedRawData == POWER) exitGame();
else {
initializeGraphics();
heroPos = HERO_POSITION_RUN_LOWER_1;
playing = true;
Serial.println("JUGAR = TRUE");
score = 0;
nRewards = 0;
}
IrReceiver.resume();
}
return;
}
// Desplazar el terreno a la izquierda
advanceTerrain(terrainLower, newTerrainType == TERRAIN_LOWER_BLOCK ? SPRITE_TERRAIN_SOLID : SPRITE_TERRAIN_EMPTY);
advanceTerrain(terrainUpper, newTerrainType == TERRAIN_UPPER_BLOCK ? SPRITE_TERRAIN_SOLID : SPRITE_TERRAIN_EMPTY);
// Se hace un terreno nuevo
if (--newTerrainDuration == 0) {
if (newTerrainType == TERRAIN_EMPTY) {
newTerrainType = (random(3) == 0) ? TERRAIN_UPPER_BLOCK : TERRAIN_LOWER_BLOCK;
newTerrainDuration = 2 + random(10);
} else {
newTerrainType = TERRAIN_EMPTY;
newTerrainDuration = 10 + random(10);
}
}
if (IrReceiver.decode()) {
controllerControl();
if (buttonPushed){
if (heroPos <= HERO_POSITION_RUN_LOWER_2) heroPos = HERO_POSITION_JUMP_1;
buttonPushed = false;
}
IrReceiver.resume();
}
if (drawHero(heroPos, terrainUpper, terrainLower, score)) {
playing = false;
} else {
if (heroPos == HERO_POSITION_RUN_LOWER_2 || heroPos == HERO_POSITION_JUMP_8) {
heroPos = HERO_POSITION_RUN_LOWER_1;
} else if ((heroPos >= HERO_POSITION_JUMP_3 && heroPos <= HERO_POSITION_JUMP_5) && terrainLower[HERO_HORIZONTAL_POSITION] != SPRITE_TERRAIN_EMPTY) {
heroPos = HERO_POSITION_RUN_UPPER_1;
} else if (heroPos >= HERO_POSITION_RUN_UPPER_1 && terrainLower[HERO_HORIZONTAL_POSITION] == SPRITE_TERRAIN_EMPTY) {
heroPos = HERO_POSITION_JUMP_5;
} else if (heroPos == HERO_POSITION_RUN_UPPER_2) {
heroPos = HERO_POSITION_RUN_UPPER_1;
} else {
++heroPos;
}
score++;
if ((score/(nRewards + 1)) == REWARD){
creditsTemp++;
creditsUp();
nRewards ++;
}
}
delay(100);
}
/**********************************************AHORCADO*********************************************/
/*
Ahorcado: lista de palabras
*/
const char* wordList[] = {
"arduino",
"proyecto",
"juego",
"mesa",
"resistencia",
"cable",
"pizarra"
};
/*
Ahorcado: dibujo de monigote
*/
void hangman_stick(int state)
{
#define vHead 1
#define vBody 2
#define vRightArm 3
#define vLeftArm 4
#define vRightLeg 5
#define vLeftLeg 6
#define vGameOver 7
byte head[] = { 0x1C, 0x04, 0x04, 0x0E, 0x0E, 0x00, 0x00, 0x00 };
byte topBody[] = { 0x1C, 0x04, 0x04, 0x0E, 0x0E, 0x04, 0x04, 0x04 };
byte bottomBody[] = { 0x04, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
byte rightArm[] = { 0x1C, 0x04, 0x04, 0x0E, 0x0E, 0x05, 0x06, 0x04 };
byte leftArm[] = { 0x1C, 0x04, 0x04, 0x0E, 0x0E, 0x15, 0x0E, 0x04 };
byte rightLeg[] = { 0x04, 0x04, 0x02, 0x02, 0x01, 0x00, 0x00, 0x00 };
byte leftLeg[] = { 0x04, 0x04, 0x0A, 0x0A, 0x11, 0x00, 0x00, 0x00 };
switch (state)
{
case vHead:
lcd.createChar(1, head);
break;
case vBody:
lcd.createChar(1, topBody);
lcd.createChar(3, bottomBody);
break;
case vRightArm:
lcd.createChar(1, rightArm);
break;
case vLeftArm:
lcd.createChar(1, leftArm);
break;
case vRightLeg:
lcd.createChar(3, rightLeg);
break;
case vLeftLeg:
lcd.createChar(3, leftLeg);
break;
default:
break;
};
}
/*
Ahorcado: impresión de letra
*/
void hangman_p(int pos, char n, int f)
{
lcd.setCursor(pos+3, f);
lcd.print(n);
lcd.setCursor(pos+3, 1);
}
// Comprueba si las letras estan en la palabra.
bool comprobarLetra(char word, const char* pick, int tam, char selected[]) {
bool flag = false;
if (word == '_' || word == 'X') flag = true;
else {
for(int i = 0; i < tam; i++) {
if(pick[i] == word){
selected[i] = word;
hangman_p(i, word, 0);
if(!flag) flag = true;
}
}
}
return flag;
}
bool compararPalabras (char* selected,const char* pick,int tam) {
bool flag = true;
int i = 0;
while (flag && i < tam){
if (selected[i] != pick[i]) flag = false;
i++;
}
return flag;
}
/*
Ahorcado: dibujo de menú de tablero
*/
void hangman_board(int pick)
{
byte topLeft[] = { 0x1F, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10 };
byte topRight[] = { 0x1C, 0x04, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00 };
byte bottomLeft[] = { 0x10, 0x10, 0x10, 0x10, 0x10, 0x1F, 0x1F, 0x1F };
byte bottomRight[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
// Establecer los caracteres personalizados
lcd.createChar(0, topLeft);
lcd.createChar(1, topRight);
lcd.createChar(2, bottomLeft);
lcd.createChar(3, bottomRight);
// Dibujar
lcd.clear();
lcd.home();
lcd.write((int) 0);
lcd.write((int) 1);
lcd.setCursor(0,1);
lcd.write((int) 2);
lcd.write((int) 3);
const char* word = strdup(wordList[pick]);
// Cargar las letras a adivinar
for (int i = 0; i < String(word).length(); i++) {
lcd.setCursor(i+3, 0);
lcd.print('_');
}
// Proceder a cargar menú
hangman_loop(word);
}
/*
Ahorcado: derrota
*/
void hangman_gameOver()
{
lcd.clear();
lcd.setCursor(6,0);
lcd.print("GAME");
lcd.setCursor(6,1);
lcd.print("OVER");
delay(1000);
}
/*
Ahorcado: victoria
*/
void hangman_victory()
{
lcd.clear();
lcd.setCursor(4,0);
lcd.print("GANASTE");
lcd.setCursor(4,1);
lcd.print("MAESTRO");
delay(1000);
}
/*
Ahorcado: función de bucle
*/
void hangman_loop(const char *pick)
{
int pos = 0;
int fails = 0;
int wordSize = String(pick).length();
char guess[] = "abcdefghijklmnopqrstuvwxyz";
char aciertos[wordSize];
// Cargar letras visibles
for (int i = 0; i < 13; i++) {
hangman_p(i,guess[i],1);
}
lcd.setCursor(3,1);
// Mostrar cursor
lcd.blink();
// Bucle de menú de selección
while (!fin) {
if (IrReceiver.decode()) {
switch(IrReceiver.decodedIRData.decodedRawData) {
// Menú: Cierre manual del juego
case POWER: fin = true; break;
// Menú: Selección de letra
case FASTBACK: {
// Seleccionar letra del alfabeto previa
if (pos == 0) pos = 25;
else pos --;
for (int i = 0; i < 13; i++)
hangman_p(i, pos + i < 26 ? guess[pos + i] : '_',1);
lcd.setCursor(3,1);
break;
}
case FASTFORWARD: {
// Seleccionar letra del alfabeto siguiente
if (pos == 25) pos = 0;
else pos ++;
for (int i = 0; i < 13; i++)
hangman_p(i, pos + i < 26 ? guess[pos + i] : '_',1);
lcd.setCursor(3,1);
break;
}
// Menú: Confirmación de letra
case PLAY: {
// Determinar si se ha escogido la letra correcta
bool guessed = comprobarLetra(guess[pos], pick, wordSize, aciertos);
if (guessed) {
// Desactivar selección si se ha acertado
guess[pos] = '_';
// Si están todas acertadas, cantar victoria
if (compararPalabras(aciertos, pick, wordSize)) {
creditsTemp ++;
creditsUp();
hangman_victory();
int newpick = random(ARRAY_LENGTH(wordList));
hangman_board(newpick);
}
} else {
guess[pos] = 'X';
// Contabilizar fallo
fails += 1;
// Si se han gastado todas las oportunidades, perder
if (fails >= vGameOver) {
fin = true;
hangman_gameOver();
}
// Si no, dibujar ahorcado
else {
hangman_stick(fails);
lcd.setCursor(pos+3, 1);
}
}
}; break;
};
IrReceiver.resume();
delay (100);
}
}
lcd.noBlink(); // Quitar el cursor
}
/*
Ahorcado: función de inicialización
*/
void modAhorcado()
{
Serial.println("AHORCADO");
fin = false;
int pick = random(ARRAY_LENGTH(wordList));
// Inicializar menú de tablero
hangman_board(pick);
exitGame();
endGameScreen();
}
/******************************************MENU-GENERAL*********************************************/
// Método para salir de los juegos
void exitGame () {
fin = true;
mS.state = MENU;
mS.mod_controll = MENU;
lcd.clear();
}
// Método que define el comportamiento del mando según el modo en el que esté.
void controllerControl () {
switch (mS.mod_controll) {
case 0: controlMenu(); break;
case 2: controlRunner();break;
}
}
// Método que define el funcionamiento del mando en el menú de selección.
void controlMenu () {
switch(IrReceiver.decodedIRData.decodedRawData) {
case FASTBACK: desplMenuDec(); break; // FAST BACK = Desplazarse a la izquierda
case PLAY: selectGame(); break; // PLAY/PAUSE = Deleccionar la opción que aparezca en pantalla.
case FASTFORWARD: desplMenuInc(); break; // FAST FORWARD = Desplazarse a la derecha
case POWER: on = false; break; //POWER = Devuelve la máquina a la pantalla de inicio
}
}
// Método que define a que juego o opción se entra al seleccionar una opción en el menú
void selectGame () {
if (mS.state == 0) {
switch (mS.movMenu) {
case 0: mS.state = 1; break;
case 1: mS.state = 2; mS.mod_controll = RUNNER; break;
case 2: rewardTime(); break;
}
}
}
// Método que incrementa el movimiento en el menu (simula desplazarse a la derecha)
void desplMenuInc () {
if (mS.movMenu < N_GAMES) mS.movMenu++;
else mS.movMenu = 0;
}
// Método que decrementa el movimiento en el menu (simula desplazarse a la izquierda)
void desplMenuDec () {
if (mS.movMenu > 0) mS.movMenu--;
else mS.movMenu = N_GAMES;
}
// Método que muestra las opciones del menú en la LCD
void menuOptions () {
switch (mS.movMenu){
case 0:
lcd.setCursor(4, 0);
lcd.print("Ahorcado!");
break;
case 1:
lcd.setCursor(5, 0);
lcd.print("Runner!");
break;
case 2:
lcd.setCursor(5, 0);
lcd.print("Reward!");
lcd.setCursor(3, 1);
lcd.print("Credits:");
lcd.setCursor(12, 1);
lcd.print(credits);
break;
}
}
/************************************************RUNNER*********************************************/
// Método que define el funcionamiento del mando en el menú de selección.
void controlRunner () {
switch(IrReceiver.decodedIRData.decodedRawData) {
case POWER: exitGame(); break; // POWER = Permite salir al meú principal
case PLAY: buttonPushed = true; break; // PLAY/PAUSE = Indica que el personaje debe saltar.
}
}
/*********************************************PRUEBAS***********************************************/
// Método que muestra por el monitor serial el botón pulsado. USADO PARA PRUEBAS
void bottonPress (){
if(IrReceiver.decode()) Serial.println(IrReceiver.decodedIRData.decodedRawData, HEX);
IrReceiver.resume();
delay(100);
}
void loop() {
stateControl();
}