// Gestión de una matriz de pulsadores usando un escaneo.
//
// Este es un ejemplo para gestionar una matriz de pulsadores
// 5x5 usando un escaneo de barrido por filas y columnas.
//
// Este sistema resulta mucho más rápido en respuesta que su
// equivalente con multiplexor a costa de un mayor número de
// pines I/O.
//
// ESP32
// Pins to avoid: 34, 35, 36, 39
//
// NOTE:
// During compilation, console throws a warning with
// "No hardware SPI pins defined. All SPI access will default to bitbanged output"
// This warning can be ignored because FASTLED never use SPI to send the signal
// for clockless chips like the WS281x family
// INCLUDES
/////////////////////////////////////////////
#include <Button2.h>
#include <FastLED.h>
#include "levels.h"
#include "pitches.h"
#include "randomseed.h"
// DEFINES
/////////////////////////////////////////////
#define DEBUG 0
#define COUNT_ROWS 5
#define COUNT_COLS 5
#define TOTAL_LEDS (COUNT_ROWS * COUNT_COLS)
#define LEDS_PIN 13 // SPI Hardware
#define ATTEMPTS_LEDS 8
#define ATTEMPTS_LEDS_USED 5 // 5 used + 3 unused
#define ATTEMPTS_PIN 23 // SPI Hardware
// Controls (Dpad + Buttons)
#define BUTTON_SELECT_PIN 4
#define BUTTON_START_PIN 5
#define BUTTON_UP_PIN 16
#define BUTTON_RIGHT_PIN 17
#define BUTTON_DOWN_PIN 21
#define BUTTON_LEFT_PIN 22
#define BUTTON_GAME_MODE_PIN 34
#define SPEAKER_PIN 19
// TYPES
/////////////////////////////////////////////
typedef enum {
HISTORY_MODE,
RANDOM_MODE
} GAME_MODE;
typedef enum {
WELCOME_STATE, // Estado inicial
IDLE_STATE,
DEMO_STATE,
INIT_HISTORY_GAME_STATE,
INIT_RANDOM_GAME_STATE,
LOAD_LEVEL,
START_LEVEL,
IDLE_GAME_STATE,
FAILED_LEVEL,
GAME_OVER_STATE,
NUM_STATES // This is not a state, just an interesting counter
} STATES;
// STRUCTS
/////////////////////////////////////////////
typedef struct {
STATES State; // Estado miembro del conjunto enumerado STATES
void (*func)(); // Acciones asociadas al estado
} FSM;
// GLOBALS
/////////////////////////////////////////////
STATES currentState = WELCOME_STATE;
// Define the array of leds
CRGB leds[TOTAL_LEDS];
CRGB attempts[ATTEMPTS_LEDS];
CRGB attemptsColorCode[4] = {0, CRGB::Red, CRGB::Green, CRGB::Blue};
// Switches
const byte rowsPins[COUNT_ROWS] = {18, 12, 14, 27, 26};
const byte colsPins[COUNT_COLS] = {25, 33, 32, 15, 2};
// Game
byte currentLevelMap[TOTAL_LEDS];
const byte *movesNeededPtr;
byte currentLevelMovesNeeded;
byte inputValues[TOTAL_LEDS];
byte inputPreviousValues[TOTAL_LEDS];
byte playerMovesCounter = 0;
byte gameMode;
byte gameStateChanged = 0;
byte currentLevel = 0;
// Timers
const unsigned long keypadPeriod = 50;
unsigned long keypadPrevMillis = 0;
const unsigned long extraButtonsPeriod = 50;
unsigned long extraButtonsPrevMillis = 0;
Button2 buttonUp, buttonDown, buttonLeft, buttonRight, buttonStart, buttonSelect;
// PROTOTYPES
///////////////////////////////////////////////
void playWelcomeMelody();
void readKeypad();
void updateAttempts();
void buildLevel(byte level);
void attractMode();
bool checkStageClear();
void shuffleLevelMap();
void readExtraGameButtons();
void incrementPlayerMoves();
void playFailedLevelMelody();
void showFailedLevelAnimation();
// STATES
void WelcomeStateFn();
void IdleStateFn();
void DemoStateFn();
void InitHistoryGameStateFn();
void InitRandomGameStateFn();
void LoadLevelFn();
void StartLevelFn();
void FailedLevelFn();
void IdleGameStateFn();
void GameOverStateFn();
FSM StateMachine[] = {
{WELCOME_STATE, WelcomeStateFn},
{IDLE_STATE, IdleStateFn},
{DEMO_STATE, DemoStateFn},
{INIT_HISTORY_GAME_STATE, InitHistoryGameStateFn},
{INIT_RANDOM_GAME_STATE, InitRandomGameStateFn},
{LOAD_LEVEL, LoadLevelFn},
{START_LEVEL, StartLevelFn},
{IDLE_GAME_STATE, IdleGameStateFn},
{FAILED_LEVEL, FailedLevelFn},
{GAME_OVER_STATE, GameOverStateFn},
};
// FUNCTIONS
///////////////////////////////////////////////
void playWelcomeMelody() {
tone(SPEAKER_PIN, NOTE_C4, 200);
delay(400);
tone(SPEAKER_PIN, NOTE_C4, 90);
delay(200);
tone(SPEAKER_PIN, NOTE_G4, 140);
delay(400);
tone(SPEAKER_PIN, NOTE_G4, 140);
delay(200);
tone(SPEAKER_PIN, NOTE_C5, 450);
delay(600);
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(2000);
}
void readKeypad() {
byte currentInputPos = 0;
if (millis() - keypadPrevMillis > keypadPeriod) {
keypadPrevMillis = millis();
gameStateChanged = 0;
for (byte currentCol = 0; currentCol < COUNT_COLS; currentCol++) {
pinMode(colsPins[currentCol], OUTPUT);
digitalWrite(colsPins[currentCol], LOW);
for (byte currentRow = 0; currentRow < COUNT_ROWS; currentRow++) {
currentInputPos = (currentRow * COUNT_ROWS) + currentCol;
inputValues[currentInputPos] = !digitalRead(rowsPins[currentRow]);
if (inputPreviousValues[currentInputPos] != inputValues[currentInputPos]) {
gameStateChanged = 1;
}
}
digitalWrite(colsPins[currentCol], HIGH);
pinMode(colsPins[currentCol], INPUT);
}
}
}
void updateAttempts() {
byte attemptsMap[ATTEMPTS_LEDS_USED];
byte *attemptsMapPtr;
byte attemptsLeft;
// If player is out of moves, the attempts counter remains blank.
if (playerMovesCounter > currentLevelMovesNeeded) {
return;
}
attemptsLeft = currentLevelMovesNeeded - playerMovesCounter;
attemptsMapPtr = attemptsMap;
memcpy_P(attemptsMapPtr, attemptsMapLookup + attemptsLeft, ATTEMPTS_LEDS_USED);
// It does not works well. Emulation is buggy??
for (byte x = 0; x < ATTEMPTS_LEDS_USED; x++) {
attempts[x] = attemptsColorCode[attemptsMap[x]];
}
}
void buildLevel(byte level) {
const byte *selectedLevel;
const byte *movesNeeded;
byte *currentLevelPtr;
currentLevelPtr = currentLevelMap;
selectedLevel = levels[level];
memcpy_P(currentLevelPtr, selectedLevel, TOTAL_LEDS);
movesNeeded = movesNeededPerLevel;
currentLevelMovesNeeded = pgm_read_byte(movesNeeded + level);
}
void attractMode() {
// Dancing LEDS
}
bool checkStageClear() {
return (
leds[0] == 0 &&
memcmp(leds, leds + 1, (TOTAL_LEDS - 1) * sizeof(leds[0])) == 0);
}
void shuffleLevelMap() {
byte r, tmpSwap;
for (byte i = TOTAL_LEDS - 1; i > 0; --i) {
r = random(0, i + 1);
tmpSwap = currentLevelMap[i];
currentLevelMap[i] = currentLevelMap[r];
currentLevelMap[r] = tmpSwap;
}
}
void click(Button2 &btn) {
Serial.println("PULSADO BOTON: ");
if (btn == buttonUp) {
Serial.println("UP");
}
if (btn == buttonDown) {
Serial.println("DOWN");
}
if (btn == buttonLeft) {
Serial.println("LEFT");
}
if (btn == buttonRight) {
Serial.println("RIGHT");
}
if (btn == buttonStart) {
Serial.println("START");
}
if (btn == buttonSelect) {
Serial.println("SELECT");
}
}
void readExtraGameButtons() {
buttonUp.loop();
buttonDown.loop();
buttonLeft.loop();
buttonRight.loop();
buttonStart.loop();
buttonSelect.loop();
// Reading switch for gameMode
if (digitalRead(BUTTON_GAME_MODE_PIN) == LOW) {
if (gameMode != RANDOM_MODE) {
gameMode = RANDOM_MODE;
currentState = INIT_RANDOM_GAME_STATE;
}
} else {
if (gameMode != HISTORY_MODE) {
gameMode = HISTORY_MODE;
currentState = INIT_HISTORY_GAME_STATE;
}
}
}
void incrementPlayerMoves() {
playerMovesCounter++;
updateAttempts();
}
// STATE FUNCTIONS
/////////////////////////////////////////////
void WelcomeStateFn() {
// playWelcomeMelody();
currentState = IDLE_STATE;
Serial.println("Welcome state");
}
void IdleStateFn() {
Serial.println("Idle state");
if (gameMode == RANDOM_MODE) {
currentState = INIT_RANDOM_GAME_STATE;
} else {
currentState = INIT_HISTORY_GAME_STATE;
}
}
void DemoStateFn() {
Serial.println("Demo state");
}
void InitHistoryGameStateFn() {
Serial.println("Init history game state");
return;
currentLevel = 0; // TODO: Load last level from EEPROM
currentState = LOAD_LEVEL;
}
void LoadLevelFn() {
buildLevel(currentLevel);
updateAttempts();
// Forcing a first 'render' out of the main loop
FastLED.show();
currentState = START_LEVEL;
}
void InitRandomGameStateFn() {
Serial.println("Init random game state");
byte *currentLevelMapPtr;
currentLevelMapPtr = currentLevelMap;
memcpy_P(currentLevelMapPtr, defaultLevel, TOTAL_LEDS);
currentLevelMovesNeeded = 0xff; // Virtually unlimited
shuffleLevelMap();
currentState = START_LEVEL;
Serial.print("Nuevo nivel aleatorio!\n");
}
void StartLevelFn() {
byte currentLedValue;
playerMovesCounter = 0;
for (byte x = 0; x < COUNT_COLS * COUNT_ROWS; x++) {
currentLedValue = currentLevelMap[x];
leds[x] = currentLedValue ? CRGB::Blue : 0;
#ifdef DEBUG
/*Serial.print(currentLedValue);
if ((x + 1) % 5 == 0) {
Serial.print("\n");
} else {
Serial.print(", ");
}*/
#endif
}
//Serial.println("Idle game state...");
//Serial.println("Waiting for input...");
currentState = IDLE_GAME_STATE;
}
void IdleGameStateFn() { // running game state
Serial.print("Idle game state: ");
Serial.print("gameStateChanged: ");
Serial.println(gameStateChanged);
return;
if (gameStateChanged) {
Serial.println("Game change");
// Toggle LEDS:
for (byte x = 0; x < 25; x++) {
if (inputValues[x] == 1) {
inputPreviousValues[x] = 1;
incrementPlayerMoves();
// Current LED
leds[x] = leds[x] ? 0 : CRGB::Blue;
// Right LED
if (x != 0 && x % COUNT_COLS != 0) { // Its not on the right edge
leds[x - 1] = leds[x - 1] ? 0 : CRGB::Blue;
}
// Left LED
if ((x + 1) % COUNT_COLS != 0) { // Its not on the left edge
leds[x + 1] = leds[x + 1] ? 0 : CRGB::Blue;
}
// Top LED
if (x > (COUNT_COLS - 1)) { // Its not on the top row
leds[x - COUNT_COLS] = leds[x - COUNT_COLS] ? 0 : CRGB::Blue;
}
// Bottom LED
if (x < ((COUNT_ROWS - 1) * COUNT_COLS)) { // Its not on the bottom row
leds[x + COUNT_COLS] = leds[x + COUNT_COLS] ? 0 : CRGB::Blue;
}
} else {
inputPreviousValues[x] = 0;
}
}
gameStateChanged = 0;
if (checkStageClear()) {
Serial.print("Genial, toda la pantalla despejada. Utilizados ");
Serial.print(playerMovesCounter);
Serial.print(" movimentos de ");
Serial.print(currentLevelMovesNeeded);
Serial.print(" permitidos.\n");
if (playerMovesCounter <= currentLevelMovesNeeded) {
Serial.print("Pasamos de nivel!\n");
if (gameMode == RANDOM_MODE) {
currentState = INIT_RANDOM_GAME_STATE;
} else {
currentLevel++;
currentState = LOAD_LEVEL;
}
} else {
Serial.print("Reiniciamos nivel :(\n");
//currentState = FAILED_LEVEL;
}
} else {
Serial.print("Continúa el juego: ");
Serial.print(playerMovesCounter);
Serial.print("\n");
}
FastLED.show();
}
}
void FailedLevelFn() {
Serial.println("Failed level state");
return;
playFailedLevelMelody();
// showFailedLevelAnimation();
currentState = LOAD_LEVEL;
}
void playFailedLevelMelody() {
tone(SPEAKER_PIN, NOTE_E5, 200);
delay(200);
tone(SPEAKER_PIN, NOTE_B4, 200);
delay(200);
tone(SPEAKER_PIN, NOTE_G4, 200);
delay(200);
tone(SPEAKER_PIN, NOTE_E4, 200);
delay(200);
}
void GameOverStateFn() {
Serial.println("Game over state");
}
// INIT FUNCTIONS
/////////////////////////////////////////////
void setup() {
#ifdef DEBUG
Serial.begin(115200);
#endif
randomSeed(seedOut(31, RANDOM_SEED_PORT));
for (byte x = 0; x < COUNT_ROWS; x++) {
pinMode(colsPins[x], INPUT);
digitalWrite(colsPins[x], HIGH);
pinMode(rowsPins[x], INPUT_PULLUP);
}
// Init neoxpixels
FastLED.addLeds<NEOPIXEL, LEDS_PIN>(leds, TOTAL_LEDS);
FastLED.addLeds<NEOPIXEL, ATTEMPTS_PIN>(attempts, ATTEMPTS_LEDS);
buttonUp.begin(BUTTON_UP_PIN);
buttonDown.begin(BUTTON_DOWN_PIN);
buttonLeft.begin(BUTTON_LEFT_PIN);
buttonRight.begin(BUTTON_RIGHT_PIN);
buttonStart.begin(BUTTON_START_PIN);
buttonSelect.begin(BUTTON_SELECT_PIN);
buttonUp.setClickHandler(click);
buttonDown.setClickHandler(click);
buttonLeft.setClickHandler(click);
buttonRight.setClickHandler(click);
buttonStart.setClickHandler(click);
buttonSelect.setClickHandler(click);
pinMode(BUTTON_GAME_MODE_PIN, INPUT_PULLUP);
// gameMode = digitalRead(BUTTON_GAME_MODE_PIN) == LOW ? RANDOM_MODE : HISTORY_MODE;
Serial.println("SISTEMA INICIALIZADO");
}
void loop() {
readKeypad();
readExtraGameButtons();
StateMachine[currentState].func();
delay(30);
}