// Binary Hex Game
// Version BETA del prototipo.
//
// Hay dos modos de juego: DEMO y DESAFÍO.
// En el DEMO, aparece en pantalla el número
// introducido con los interruptores, ya sea en binario o decimal según el modo escogido.
// En el DESAFÍO, aparece un número al azar (de entre 4 y 8 bits según el NIVEL del desafío)
// que debe introducirse correctamente mediante los interruptores antes de que se cumpla el
// tiempo. Tras cada acierto, se suma un punto, se reduce el tiempo de juego y aparece otro
// número al azar.
//
// Se puede escoger mediante un interruptor si el formato de los números en el display son
// en base decimal o hexadecimal.
//
// Se puede escoger mediante un interruptor si el tamaño de los números a acertar durante el
// desafío son de 4 o 8 bits.
//
// Un sonido marca el tiempo restante para completar cada desafío.
//
// El juego guarda en la EEPROM el número más altos de aciertos obtenido durante el desafío.
//
// INCLUDES
/////////////////////////////////////////////
#include <Arduino.h>
#include <EEPROM.h>
#include <avr/pgmspace.h>
//#include <avr/sleep.h>
#include "pitches.h"
// DEFINES
/////////////////////////////////////////////
#define DEBUG 1 // 0 = OFF, 1 = ON
#if DEBUG == 1
# define PRINT(x) Serial.print(x)
# define PRINTLN(x) Serial.println(x)
#else
# define PRINT(x)
# define PRINTLN(x)
#endif
#define SPEAKER_PIN A0
#define RAMDOM_SEED_PIN A1
#define FPS 60
// STRUCTURES
/////////////////////////////////////////////
typedef enum {
DEMO_MODE,
GAME_MODE
} APP_MODE;
typedef enum {
BASE_DEC,
BASE_HEX
} NUMBER_BASE;
typedef enum {
NIBBLE,
BYTE
} BITS_SIZE;
typedef enum {
HIGH_SCORE,
RECORD
} DISPLAY_RECORD_MODE;
typedef enum {
WELCOME_STATE, // Estado inicial
IDLE_STATE,
DEMO_STATE,
INIT_GAME_STATE,
DISPLAY_RECORD_STATE,
INIT_GAME_ROUND_STATE,
RUN_GAME_STATE,
GAME_OVER_STATE,
NUM_STATES // Forma muy interesante de contar el número de estados
} STATES;
typedef struct {
STATES Estado; // Estado miembro del conjunto enumerado STATES
void (*func)(); // Acciones asociadas al estado
} FSM;
// GLOBALS
/////////////////////////////////////////////
// Display and LEDs
byte data = 7; // DS
byte latch = 8; // STCP / PL
byte clock = 9; // SHCP / CP
// Switches
byte swData = 10; // Q7
byte swLatch = 11; // PL
// System / Timers
byte eeAddress = 0; // EEPROM address to start reading from
word frameCount = 0;
byte eachFrameMillis = 16;
byte thisFrameStart;
byte lastFrameDurationMs;
bool justRendered = false;
byte dpPos = 0;
// Audio
boolean musicOn = true;
byte audio = SPEAKER_PIN;
byte melodyNoteLength = 0;
byte melodyHalfLength = 0;
byte melodyNotePointer = 0;
const int *melodyTrack;
int melodyTrackLength;
byte tetrisMusicLength = 234;
const int tetrisMusic[] PROGMEM = {
NOTE_E4, 4,
NOTE_B3, 2,
NOTE_C4, 2,
NOTE_D4, 2,
NOTE_E4, 1,
NOTE_D4, 1,
NOTE_C4, 2,
NOTE_B3, 2,
NOTE_A3, 4,
NOTE_A3, 2,
NOTE_C4, 2,
NOTE_E4, 4,
NOTE_D4, 2,
NOTE_C4, 2,
NOTE_B3, 4,
NOTE_B3, 1,
NOTE_B3, 1,
NOTE_C4, 2,
NOTE_D4, 4,
NOTE_E4, 4,
NOTE_C4, 4,
NOTE_A3, 4,
NOTE_A3, 8,
NOTE_D4, 4,
NOTE_D4, 2,
NOTE_F4, 2,
NOTE_A4, 4,
NOTE_G4, 2,
NOTE_F4, 2,
NOTE_E4, 4,
NOTE_E4, 2,
NOTE_C4, 2,
NOTE_E4, 4,
NOTE_D4, 2,
NOTE_C4, 2,
NOTE_B3, 4,
NOTE_B3, 1,
NOTE_B3, 1,
NOTE_C4, 2,
NOTE_D4, 4,
NOTE_E4, 4,
NOTE_C4, 4,
NOTE_A3, 4,
NOTE_A3, 8,
NOTE_E4, 4,
NOTE_B3, 2,
NOTE_C4, 2,
NOTE_D4, 2,
NOTE_E4, 1,
NOTE_D4, 1,
NOTE_C4, 2,
NOTE_B3, 2,
NOTE_A3, 4,
NOTE_A3, 2,
NOTE_C4, 2,
NOTE_E4, 4,
NOTE_D4, 2,
NOTE_C4, 2,
NOTE_B3, 4,
NOTE_B3, 2,
NOTE_C4, 2,
NOTE_D4, 4,
NOTE_E4, 4,
NOTE_C4, 4,
NOTE_A3, 4,
NOTE_A3, 8,
NOTE_D4, 4,
NOTE_D4, 2,
NOTE_F4, 2,
NOTE_A4, 4,
NOTE_G4, 2,
NOTE_F4, 2,
NOTE_E4, 4,
NOTE_E4, 2,
NOTE_C4, 2,
NOTE_E4, 4,
NOTE_D4, 2,
NOTE_C4, 2,
NOTE_B3, 4,
NOTE_B3, 2,
NOTE_C4, 2,
NOTE_D4, 4,
NOTE_E4, 4,
NOTE_C4, 4,
NOTE_A3, 4,
NOTE_A3, 8,
NOTE_C4, 8,
NOTE_A3, 8,
NOTE_B3, 8,
NOTE_GS3, 8,
NOTE_A3, 8,
NOTE_E3, 8,
NOTE_E3, 8,
NOTE_GS3, 8,
NOTE_C4, 8,
NOTE_A3, 8,
NOTE_B3, 8,
NOTE_GS3, 8,
NOTE_A3, 4,
NOTE_C4, 4,
NOTE_E4, 4,
NOTE_E4, 4,
NOTE_E4, 16,
NOTE_B3, 4,
NOTE_GS3, 2,
NOTE_A3, 2,
NOTE_B3, 2,
NOTE_E4, 1,
NOTE_D4, 1,
NOTE_A3, 2,
NOTE_GS3, 2,
NOTE_E3, 4,
NOTE_E3, 2,
NOTE_A3, 2,
NOTE_C4, 4,
NOTE_B3, 2,
NOTE_A3, 2};
// Game vars
STATES currentState = WELCOME_STATE;
byte appMode = DEMO_MODE; // 0: Demo, 1: Game
byte numberBase = BASE_DEC;
byte bitsSizeButtonState = NIBBLE;
byte numberBaseTable[2] = {10, 16};
byte actionButtonState = 0;
byte numberSelected = 0;
byte numberToGuess = 0;
byte hits = 0;
byte currentRecord = 0;
int initialTimeLeft = 18000; // 18 seconds
byte timeDiscountPerHit = 100; // 100 milliseconds
int minimumTimeLeft = 3000; // 3 second
int timeLeft = 0;
const byte numberOfDigits = 3;
unsigned char table[] = {
0xc0, // 0b11000000 => 0
0xf9, // 0b11111001 => 1
0xa4, // 0b10100100 => 2
0xb0, // 0b10110000 => 3
0x99, // 0b10011001 => 4
0x92, // 0b10010010 => 5
0x82, // 0b10000010 => 6
0xf8, // 0b11111000 => 7
0x80, // 0b10000000 => 8
0x90, // 0b10010000 => 9
0x88, // 0b10001000 => A
0x83, // 0b10000011 => B
0xc6, // 0b11000110 => C
0xa1, // 0b10100001 => D
0x86, // 0b10000110 => E
0x8e, // 0b10001110 => F
0xff, // 0b11111111 => Nothing
0x7f, // 0b01111111 => DP (decimal point)
0x8b, // 0b10001011 => h
0xfb, // 0b11111011 => i
};
// PROTOTYPES
/////////////////////////////////////////////
// State functions
void WelcomeStateFn();
void IdleStateFn();
void DemoStateFn();
void InitGameStateFn();
void DisplayRecordStateFn();
void InitGameRoundStateFn();
void RunGameStateFn();
void GameOverStateFn();
void idle();
void setFrameRate(byte rate);
void setFrameDuration(byte duration);
bool nextFrame();
bool everyXFrames(byte frames);
void resetFrameCount();
void readSwitches();
void displayNumber(byte number);
void displayNumberDEC(byte number);
void displayNumberHEX(byte number);
void displayNumberByBase(byte number, byte base);
void displayDigit(byte number, byte digit);
void playMelody();
void playWelcomeMelody();
void loadRecord();
void saveRecord();
void clearDisplay();
byte getNumberToGuess();
// Building the FSM structure
FSM Maquina_de_Estados[] = {
{WELCOME_STATE, WelcomeStateFn},
{IDLE_STATE, IdleStateFn},
{DEMO_STATE, DemoStateFn},
{INIT_GAME_STATE, InitGameStateFn},
{DISPLAY_RECORD_STATE, DisplayRecordStateFn},
{INIT_GAME_ROUND_STATE, InitGameRoundStateFn},
{RUN_GAME_STATE, RunGameStateFn},
{GAME_OVER_STATE, GameOverStateFn},
};
// FUNCTIONS
/////////////////////////////////////////////
void idle() {
//SMCR = _BV(SE); // select idle mode and enable sleeping
//sleep_cpu(); // Function from <avr/sleep.h>
//SMCR = 0; // disable sleeping
}
void setFrameRate(byte rate) {
eachFrameMillis = 1000 / rate;
}
void setFrameDuration(byte duration) {
eachFrameMillis = duration;
}
bool everyXFrames(byte frames) {
return frameCount % frames == 0;
}
void resetFrameCount() {
frameCount = 0;
}
bool nextFrame() {
byte now = millis();
byte frameDurationMs = now - thisFrameStart;
if (justRendered) {
lastFrameDurationMs = frameDurationMs;
justRendered = false;
return false;
} else if (frameDurationMs < eachFrameMillis) {
// Only idle if at least a full millisecond remains, since idle() may
// sleep the processor until the next millisecond timer interrupt.
if (eachFrameMillis > ++frameDurationMs) {
idle();
}
return false;
}
// pre-render
justRendered = true;
thisFrameStart = now;
frameCount++;
return true;
}
void readSwitches() {
// Step 1: Sample
digitalWrite(swLatch, LOW);
digitalWrite(swLatch, HIGH);
// Step 2: Shift
numberSelected = 0;
byte bit = 0;
// Loop through the 8 LED switches
for (byte x = 0; x < 8; x++) {
bit = digitalRead(swData);
// PRINT(bit);
numberSelected |= (bit << x);
digitalWrite(clock, HIGH); // Shift out the next bit
digitalWrite(clock, LOW);
}
// Go through the 2 mode switches (Demo/Game). No loop needed.
bit = digitalRead(swData);
appMode = bit;
digitalWrite(clock, HIGH); // Shift out the next bit
digitalWrite(clock, LOW);
bit = digitalRead(swData);
numberBase = bit;
digitalWrite(clock, HIGH); // Shift out the next bit
digitalWrite(clock, LOW);
// Go through the Action button. No loop needed.
bit = digitalRead(swData);
actionButtonState = bit;
digitalWrite(clock, HIGH); // Shift out the next bit
digitalWrite(clock, LOW);
// Go through the Bits Size button (8/4 bits). No loop needed.
bit = digitalRead(swData);
bitsSizeButtonState = bit;
digitalWrite(clock, HIGH); // Shift out the next bit
digitalWrite(clock, LOW);
}
void displayNumber(byte number) {
if (number == 0) {
displayDigit(0, 0); // display 0 in the rightmost digit
return;
}
if (numberBase == BASE_DEC) {
displayNumberDEC(number);
return;
}
displayNumberHEX(number);
}
void displayNumberDEC(byte number) {
byte base = numberBaseTable[BASE_DEC];
displayNumberByBase(number, base);
}
void displayNumberHEX(byte number) {
byte base = numberBaseTable[BASE_HEX];
displayNumberByBase(number, base);
}
void displayNumberByBase(byte number, byte base) {
for (byte digit = 0; digit <= numberOfDigits; digit++) {
if (number > 0) {
displayDigit(number % base, digit);
number = number / base;
}
}
}
void displayDigit(byte num, byte digit) {
byte display = _BV(digit);
shiftOut(data, clock, MSBFIRST, table[num]);
shiftOut(data, clock, MSBFIRST, display);
digitalWrite(latch, HIGH);
digitalWrite(latch, LOW);
delay(4);
}
void playMelody() {
if (musicOn) {
int currentNote = pgm_read_word(melodyTrack + melodyNotePointer);
int nextNote = pgm_read_word(melodyTrack + melodyNotePointer + 1);
int duration = (nextNote << 5) - (nextNote << 2) - (nextNote << 1) - nextNote; // nextNote * 25
if (melodyNoteLength == 0) {
tone(audio, currentNote, duration);
melodyNoteLength = nextNote << 1; // melodyNoteLength = nextNote * 2
melodyHalfLength = melodyNoteLength >> 1; // melodyHalfLength = melodyNoteLength / 2
}
if (melodyNoteLength == melodyHalfLength) {
tone(audio, currentNote >> 1, duration); // currentNote / 2
melodyNotePointer = melodyNotePointer + 2;
if (melodyNotePointer >= melodyTrackLength) {
melodyNotePointer = 0;
}
}
melodyNoteLength--;
}
}
void playWelcomeMelody() {
tone(SPEAKER_PIN, NOTE_C4, 200);
delay(200);
delay(200);
tone(SPEAKER_PIN, NOTE_C4, 90);
delay(200);
tone(SPEAKER_PIN, NOTE_G4, 140);
delay(200);
delay(200);
tone(SPEAKER_PIN, NOTE_G4, 140);
delay(200);
tone(SPEAKER_PIN, NOTE_C5, 450);
delay(200);
delay(200);
delay(200);
tone(SPEAKER_PIN, NOTE_AS4, 140);
delay(200);
tone(SPEAKER_PIN, NOTE_A4, 130);
delay(200);
tone(SPEAKER_PIN, NOTE_F4, 120);
delay(200);
tone(SPEAKER_PIN, NOTE_G4, 1000);
delay(1000);
}
// STATE FUNCTIONS
/////////////////////////////////////////////
void WelcomeStateFn() {
// playWelcomeMelody();
currentState = IDLE_STATE;
}
void IdleStateFn() {
if (appMode == 0) {
currentState = DEMO_STATE;
return;
}
currentState = INIT_GAME_STATE;
}
void DemoStateFn() {
if (appMode == 1) {
currentState = INIT_GAME_STATE;
return;
}
// Demo mode logic:
// - Selected number is displayed on the screen
displayNumber(numberSelected);
}
void InitGameStateFn() {
// Return to DEMO_STATE when appMode == 0
if (appMode == 0) {
currentState = DEMO_STATE;
return;
}
// Init game logic:
// - Hits counter is reset
// - Record is loaded/updated from EEPROM
hits = 0;
dpPos = numberOfDigits;
loadRecord();
melodyNotePointer = 0;
// Loading the melody track
melodyTrack = tetrisMusic; // Setting the melody track
melodyTrackLength = tetrisMusicLength; // Setting the melody track length
// Going to next state
currentState = DISPLAY_RECORD_STATE; // Going to next state
}
void DisplayRecordStateFn() {
// Return to DEMO_STATE when appMode == 0
if (appMode == 0) {
currentState = DEMO_STATE;
return;
}
// Lógica de pintado del record en la pantalla
// - En pantalla se pinta primero 'hi' (High Score)
// - Se espera un tiempo (2.5 segundos)
// - Se pinta el record
static byte currentData = HIGH_SCORE;
if (currentData == HIGH_SCORE) {
displayDigit(17, dpPos - 1); // Dot is iterating from right to left
displayDigit(18, 2); // h on display 2
displayDigit(19, 1); // i on display 3
} else {
displayNumberDEC(currentRecord);
}
if (everyXFrames(3)) { // 3 frames is a good timing for the melody
playMelody();
}
if (everyXFrames(50)) { // 50 * 3 = 150 FPS (2.5 seconds on 60 FPS)
dpPos--; // Moving the dot to the right
if (dpPos == 0) { // When the dot reaches the rightmost digit
dpPos = numberOfDigits; // We reset the dot position
currentData = !currentData; // We change the data to display
}
}
// Changing the state when the user presses the action button
if (actionButtonState == 1) {
// Clearing the screen
clearDisplay();
// Playing first beat
for (byte j = 0; j < 2; j++) {
for (byte i = 0; i < 10; i++) {
tone(audio, (i + 1) * 100, 10);
delay(20);
}
}
currentState = INIT_GAME_ROUND_STATE;
}
}
void InitGameRoundStateFn() {
// Return to DEMO_STATE when appMode == 0
if (appMode == 0) {
currentState = DEMO_STATE;
return;
}
byte nextNumberToGuess;
// Set the time left for the round. Less time each round (based on the number of hits).
timeLeft = initialTimeLeft - (hits * timeDiscountPerHit);
// We set a minimum time so that it does not decrease at that point:
timeLeft = max(timeLeft, minimumTimeLeft);
dpPos = numberOfDigits;
// We set the number to guess for the round
// We make sure that the number to guess is different from the previous one
do {
nextNumberToGuess = getNumberToGuess();
} while (nextNumberToGuess == numberToGuess);
numberToGuess = nextNumberToGuess;
// Playing first beat
for (byte i = 0; i < 10; i++) {
tone(audio, (i + 1) * 100, 14);
delay(20);
}
// Reset the frame count for better timing
resetFrameCount();
// Changing the state after the round preparations.
currentState = RUN_GAME_STATE;
}
void RunGameStateFn() {
// Return to DEMO_STATE when appMode == 0
if (appMode == 0) {
currentState = DEMO_STATE;
return;
}
static byte beatTime = FPS;
// Displaying the number to guess and the dot for time left
displayNumber(numberToGuess);
displayDigit(17, dpPos - 1);
if (everyXFrames(beatTime)) {
timeLeft -= (beatTime * 17);
if (timeLeft <= 6000) {
// beatTime -= 2;
tone(SPEAKER_PIN, NOTE_A4, 300);
beatTime -= 2;
} else if (timeLeft <= 10000) {
tone(SPEAKER_PIN, NOTE_A3, 300);
} else {
tone(SPEAKER_PIN, NOTE_C3, 300);
}
delay(20);
dpPos--;
if (dpPos == 0) {
dpPos = numberOfDigits;
}
}
// If the number entered is correct:
if (numberSelected == numberToGuess) {
hits++; // The number of hits is increased
// Playing a hit melody
// for (byte i = 0; i < 10; i++) {
// tone(audio, (i + 1) * 100, 10);
// delay(20);
// }
beatTime = FPS;
currentState = INIT_GAME_ROUND_STATE;
return;
}
// If time is over or we have reached the maximum number of hits:
if (timeLeft <= 0 || hits == 0xff) {
beatTime = FPS;
currentState = GAME_OVER_STATE;
}
}
void GameOverStateFn() {
PRINTLN("GAME OVER");
clearDisplay();
for (int i = 100; i > 0; i--) {
tone(audio, i * 5, 10);
delay(10);
}
// Se actualiza el record si es necesario
if (hits > currentRecord) {
currentRecord = hits; // Se actualiza el record
saveRecord(); // Se guarda el record en la EEPROM
}
// Playing Game Over melody
currentState = IDLE_STATE;
}
void clearDisplay() {
// Displaying nothing on the screen
displayDigit(16, 0);
displayDigit(16, 1);
displayDigit(16, 2);
}
void loadRecord() {
// Loading record from ATMega Nano EEPROM
EEPROM.get(eeAddress, currentRecord);
}
void saveRecord() {
EEPROM.put(eeAddress, currentRecord);
}
byte getNumberToGuess() {
return random(1, bitsSizeButtonState == NIBBLE ? 0x0f : 0xff);
}
void setup() {
#if DEBUG == 1
Serial.begin(115200);
#endif
// Configuring output pins for the 74HC595
pinMode(latch, OUTPUT);
pinMode(clock, OUTPUT);
pinMode(data, OUTPUT);
// Configuring output pins for the 74HC165
pinMode(swData, INPUT);
pinMode(swLatch, OUTPUT);
// Configuring the speaker pin
pinMode(SPEAKER_PIN, OUTPUT);
// Configuring timers
setFrameRate(FPS);
// Configuring the random seed pin
pinMode(RAMDOM_SEED_PIN, INPUT);
randomSeed(analogRead(RAMDOM_SEED_PIN));
}
void loop() {
if (!nextFrame()) {
return;
}
// Testing an async routine with the speaker
// if (everyXFrames(20)) {
// tone(SPEAKER_PIN, NOTE_C4, 200);
// }
//-- FSM ---
readSwitches(); // A cada iteración, se lee el número introducido por el usuario
(*Maquina_de_Estados[currentState].func)();
}