//Mini Stacker is an attempt at cloning the Arcade game Stacker by LAI Games.
//Using LED Strips, a button, and optionally a Buzzer.
//Oh, and of course the Arduino Uno
//Animations and sounds aren't accurate to the machine/machines.
//Also my first time coding for Arduino.
#include <FastLED.h>
#define NUM_STRIPS 7
#define NUM_LEDS_PER_STRIP 12
#define BUTTON_PIN 4
CRGB leds[NUM_STRIPS][NUM_LEDS_PER_STRIP];
bool clicked = false;
bool goingLeft = true; // Going left if true, right if false.
const int buzzerPin = 6; //Is there a buzzer, set to -1 to disable.
const int top = NUM_LEDS_PER_STRIP; // Basically the height of the strips, this is adjustable if you have a larger matrix
const int bottom = top-1; // Subtract one from the top if using the whole strip.
const int maxHeight = bottom - top;
CRGB blockColor = CRGB::Blue;
unsigned long prevMillis = 0; // The previous time in milliseconds
unsigned long curMillis = 0; //Current time in milliseconds, to prevent multiple calls to millis()
unsigned long buttonwait =0;
const long animationDuration = 2000; // Animation for loss of block
const long defaultInterval = 200; // The starting time between updates
const int intervalModifier = 15; // How much the interval should change for every success.
//Your maximum interval modifier should equal defaultInterval / top
long maxSpeed = 10; // Maximum allowed speed for the blocks. Never set to negative
long interval = defaultInterval; // Time between updates. This shouldn't ever be made a const, as it will change.
int cursize = 3; //Size of the stacking block
int curpos = 2; // Position of the block
int currow = bottom;//What row of LEDS the block is in
// For mirroring strips, all the "special" stuff happens just in setup. We
// just addLeds multiple times, once for each strip
void setup() {
Serial.begin(115200);
//Initializing the FastLED/Neopixel Array
FastLED.addLeds<NEOPIXEL, 10>(leds[0], NUM_LEDS_PER_STRIP);
FastLED.addLeds<NEOPIXEL, 11>(leds[1], NUM_LEDS_PER_STRIP);
FastLED.addLeds<NEOPIXEL, 12>(leds[2], NUM_LEDS_PER_STRIP);
FastLED.addLeds<NEOPIXEL, 13>(leds[3], NUM_LEDS_PER_STRIP);
FastLED.addLeds<NEOPIXEL, 9>(leds[4], NUM_LEDS_PER_STRIP);
FastLED.addLeds<NEOPIXEL, 8>(leds[5], NUM_LEDS_PER_STRIP);
FastLED.addLeds<NEOPIXEL, 7>(leds[6], NUM_LEDS_PER_STRIP);
if (maxSpeed<0) {
maxSpeed = 0;
Serial.println("Error: Max speed was set to negative.");
}
pinMode(BUTTON_PIN, INPUT_PULLUP);
int value = digitalRead((BUTTON_PIN));
}
int lastState = HIGH;
int value = HIGH;
bool checkAir (); // Checking if blocks are on air or blocks
void drawBlock(); // Drawing call
void onUpdate(); // Update call
void resetGame(); // Function to reset the game
void failState(int echo); // Called when the player loses
void winState(); // Called when the player wins
void animatedrop(int pos, int width); //Drop animation
void animatedrop(int pos, int width, bool isFail); //Drop animation if the player loses
void buttonPressed(); //Called on button press.
void onUpdate()
{
if (goingLeft) { // If we're moving left
if (curpos > 0) { // Check if we can go left
curpos--; // If we can, let's go left
} else { //Otherwise...
curpos++; //We'll go right
goingLeft =false; //And make sure we're really going right.
}
} else { //If we're going right
if (curpos+cursize < 7) { //Check if we can go right
curpos++; //If we can, let's go right.
} else { //Otherwise...
curpos--; //Go left
goingLeft=true; //And really go left
}
}
drawBlock(); // Finally we may draw
prevMillis = curMillis;
}
void drawBlock()
{
leds[curpos][currow] = blockColor; // We always draw the leftmost block
if (cursize>1) //Then check if there's more
{
if (cursize==3) //If there's three we can draw the third
{
leds[curpos+2][currow] = blockColor; //Drawing the third
}
leds[curpos+1][currow] = blockColor; //If we're more than one, it should be at least two. So draw the second.
}
if (goingLeft) //If the movement is going left
{
leds[curpos+cursize][currow] = CRGB::Black; // Clear the previous rightmost
}
else
{
leds[curpos-1][currow] = CRGB::Black; //Otherwise we're going right, and only have to remove one to the left
}
FastLED.show(); // Then we can draw
//Serial.print("X: "); Serial.print(curpos); Serial.print(" Y: "); Serial.print(currow);Serial.print("\n");
}
bool checkAir ()
{
Serial.println("Checking Air");
if (leds[curpos][currow+1] == CRGB(0,0,0)) { // If the leftmost block has fallen
if (cursize>1) { // If there's more than one block
if (cursize==3) // if 3 wide
{
if (leds[curpos+2][currow+1] == CRGB(0,0,0)) { // If the bar isnt seated
//Failstate, the triple block has fallen
failState(1);
return true;
}
else if (leds[curpos+1][currow+1] == CRGB(0,0,0)) { //If this is call
cursize -= 2; //2 blocks have dropped
animatedrop(curpos,2);
}
else {
cursize--; // 1 block has dropped
animatedrop(curpos,1);
}
}
else if (leds[curpos+1][currow+1] == CRGB(0,0,0)) { // If its greater than one but less than three it's two wide
//Failstate, the double block has fallen
failState(2);
return true;
}
else {
cursize--; // one block has dropped
animatedrop(curpos,1);
}
}
else { // If there's only one block
//Failstate, the single block has fallen
failState(3);
return true;
}
}
else // If the leftmost block hasn't fallen
{
//Serial.println("Left didnt fall");
if (cursize>1) { // If there's more than one block
if (cursize==3) // if 3 wide
{
if (leds[curpos+1][currow+1] == CRGB(0,0,0)) { // If the second isn't seated
cursize -= 2; //2 blocks have dropped
animatedrop(curpos+1,2);
}
else if (leds[curpos+2][currow+1] == CRGB(0,0,0)) { //if the third isn't seated
cursize -= 1; //1 blocks have dropped
animatedrop(curpos+2,1);
}
else // Nothing fallen scenario
{
if (buzzerPin != -1)
{
tone(buzzerPin, 1000, 100);
}
}
}
else if (leds[curpos+1][currow+1] == CRGB(0,0,0)) {// if there's two blocks, check only the rightmost
cursize -= 1; //1 blocks have dropped
animatedrop(curpos+1,1);
}
else //Nothing fallen, with only two blocks
{
if (buzzerPin != -1) //If we can buzz
{
tone(buzzerPin, 1000, 100);//Buzz
}
}
}
else { // If there's only one block
//failState(4);
//Congrats! Nothing happens!
if (buzzerPin != -1)
{
tone(buzzerPin, 1000, 100);
}
}
}
return false;
}
void resetGame()
{
//Begin resetting the scenario
curpos=2;
currow = bottom;
cursize=3;
interval = defaultInterval;
// This outer loop will go over each strip, one at a time
for(int x = 0; x < NUM_STRIPS; x++) {
// This inner loop will go over each led in the current strip, one at a time
for(int i = 0; i < NUM_LEDS_PER_STRIP; i++) {
leds[x][i] = CRGB::Black;
}
}
FastLED.show();
curMillis = millis();
prevMillis = curMillis; // The previous time in milliseconds
}
void winState()
{
Serial.println("Winstate!");//Output success
if (buzzerPin != -1) //play a little song if we can
{
tone(buzzerPin, 1000, 100);
delay(100);
tone(buzzerPin, 1200, 100);
delay(100);
tone(buzzerPin, 1000, 100);
}
resetGame(); // Reset the game to play again
}
void failState(int echo) //Function for the ending of the game
{
/*Serial.print("\nFailstate! ");
Serial.print(echo);
Serial.print("\n");*/
//Animate the failure
animatedrop(curpos,cursize,true);
resetGame(); //Reset the game to play again
}
void animatedrop(int pos, int width) //This is the function that animates the loss of blocks when the player overshoots
{
//Serial.println("Animating");
unsigned long tillAnimation = curMillis + animationDuration;
unsigned long blinkTimer = curMillis + (animationDuration/8);
bool blink = false;
while(tillAnimation > curMillis)
{
curMillis = millis();
prevMillis = curMillis;
if (blinkTimer < curMillis)
{
if (blink) //if off
{
for (int i = 0; i<width; i++)
{
leds[pos+i][currow] = blockColor;
}
FastLED.show();
blink = false;
blinkTimer = curMillis + (animationDuration/8);
//Serial.println("Blinking on");
if (buzzerPin != -1)
{
tone(buzzerPin, 262, (animationDuration/8));
}
}
else
{
for (int i = 0; i<width; i++)
{
leds[pos+i][currow] = CRGB::Black;
}
FastLED.show();
//Serial.println("Blinking off");
if (buzzerPin != -1)
{
tone(buzzerPin, 288, (animationDuration/8));
}
blink = true;
blinkTimer = curMillis + (animationDuration/8);
}
FastLED.show();
}
}
}
void animatedrop(int pos, int width, bool isFail) // This one is only called if it's a fail
{ // This is done to make a different noise, can definitely be written more compactly, but I'm coding this lazily
//Serial.println("Animating");
unsigned long tillAnimation = curMillis + animationDuration;
unsigned long blinkTimer = curMillis + (animationDuration/8);
unsigned int buzz = 600;
unsigned int buzzI = 10;
bool blink = false;
while(tillAnimation > curMillis)
{
curMillis = millis();
prevMillis = curMillis;
if (blinkTimer < curMillis)
{
if (blink) //if off
{
for (int i = 0; i<width; i++)
{
leds[pos+i][currow] = blockColor;
}
FastLED.show();
blink = false;
blinkTimer = curMillis + (animationDuration/8);
//Serial.println("Blinking on");
}
else
{
for (int i = 0; i<width; i++)
{
leds[pos+i][currow] = CRGB::Black;
}
FastLED.show();
//Serial.println("Blinking off");
blink = true;
blinkTimer = curMillis + (animationDuration/8);
}
if (buzzerPin != -1)
{
tone(buzzerPin, buzz, (animationDuration/8));
}
buzz -= buzzI;
FastLED.show();
}
}
}
void buttonPressed()
{
if (value== HIGH)
{
//Serial.println("Button Released");
clicked = true;
buttonwait = curMillis;
}
else if (value == LOW)
{
//Serial.println("Button Pressed");
if (!clicked)
{
if (currow != bottom)
{
if (checkAir())
{
return 0;
}
}
else // Using an else statement here to play the sound that check air would play normally on success
{
if (buzzerPin != -1)
{
tone(buzzerPin, 1000, 100);
}
}
currow--; // Going upwards on the rows
if (currow <= maxHeight )
{
winState();
return 0;
}
if (currow < (bottom - (top / 4) ) && cursize == 3)
{
cursize--;
}
else if (currow < (bottom - (top / 2) ) && cursize == 2)
{
cursize--;
}
onUpdate();
interval -= intervalModifier;
if (interval < maxSpeed) // prevention from going over the max speed
{
interval = maxSpeed;
}
clicked = true;
buttonwait = curMillis;
}
}
}
void loop() {
curMillis = millis();
value = digitalRead((BUTTON_PIN));
if (value != lastState) {
lastState = value;
buttonPressed();
}
if (curMillis - prevMillis >= interval) //If the interval between updates has been reached
{
onUpdate(); // update
}
if (clicked)
{
if (curMillis - buttonwait >= 50)
{
clicked = false;
}
}
//delay(500); DELAY IS NASTY. DO NOT USE DELAY
}