Project: Mini Stacker

  1. Project: Bugtrakt
  2. Project: Imgalib
  3. Project: Go Fish!
  4. Project: trewest.dev
  5. Arduino Light effects
  6. Project: Mini Stacker
  7. Project: Arduino NES Controller
  8. Project: Metroidvania-Like
  9. Project: Susminer
  10. Project: Farming Demo
  11. Project: VRC Basement
  12. Project: Hash Clear
  13. Project: Titan Help
  14. Project: Computer Craft Scripts
  15. Project: ESP8266 Plant Watering
  16. Project: Golf Demo
  17. Project: Ghost Holic
  18. Project: Multi-Chat Viewer

Status: Complete
Type: Microcontroller
Technology: Arduino UNO, C++

Mini Stacker was a bit of a challenge I undertook to remake a favorite arcade game of mine as a project. This was one of my introductory projects into the world of Arduino and as such wasn’t be best planned out. It works simply with an array of light strips to form an image of the game. I also included a beeping speaker for the sake of having a little audio. The game itself is simple, you press a button to place a block, and if the block is not on another block it falls. If you lose all the blocks you lose the game. Since I originally made this in a Arduino simulator/emulator, you can play it now using the button above. Originally I was going to make this in real life but sadly soldering a lot of light strips together and in the right order is a much harder thing to pull off than I originally thought (Turns out pulling off the small soldering pads is very easy!) so I put that on hiatus until I rework the code a bit to render onto a large LED array that I can simply plug into a board instead of a bunch of light strips. Anyhow, code and screenshots of the emulator are below.

//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
}