Cyclone - das Spiel

In the cold and wet season you look for something more to do for your child (ren) or want to prove your nerd skills to your buddies. I came across the game Cyclone, which means Tornado in German, on the Internet. This game fascinated me and my children. However, I didn't like any of the variants shown, so a separate variant was quickly developed.

What is the game Cyclone about

If you watch the video (https://www.youtube.com/watch?v=5HFTi8qphDA&feature=emb_logo), the principle is quickly explained. They have an LED ring with a running LED point and an LED target marker. The player point runs at a defined speed across the ring and you must try to press a button as soon as the player point is congruent with the target marking. Unlike in the video, I didn't develop a level, but a continuous game that ends when the target marker is not hit. In addition, an LCD display is installed, which shows the high score, the current score and the current lap. The speed is chosen randomly with each new lap.

The hardware

With the hardware you need, you only need a few parts. Since the code is written in such a way that you can replace the WS2812B LED ring from Arduino with a WS2812B strip, two parts lists are kept here.

For the variant presented in this blog post, you need the components that are listed in Table 1 .

Pos Number Component
1 1 Nano V3.0 with Atmega328 CH340
or
Nano V3.0 with FT232RL Chip and ATmega328
2 1 LED Ring 5V RGB WS2812B 12-Bit 50mm
3 1 Push button module
4 1 LCD module with I2C interface
5 1 Breadboard and jumper
(Here in a set with power supply adapter)

Table 1: Hardware parts for Cyclone with WS2812B-LEDring

However, if you want to use a strip, you need the components Table 2. The other components to build the strip into a frame or similar are not taken into account.

Pos Number Component
1 1 Nano V3.0 with Atmega328 CH340
or
Nano V3.0 with FT232RL Chip and ATmega328
2 1 WS2812B LED-Strip
3 1 Push button module
4 1 LCD module with I2C interface
5 1

Breadboard and jumper  (Here in a set with power supply adapter)

6 1 Power supply for LEDs and nano

Table 2: Hardware parts for Cyclone with WS2812B strip

It should be mentioned right here that the more LEDs your WS2812B strip has, the more current the power supply unit has to supply. For 60 LEDs you need just under 4A if all LEDs light up.

Required software

The software required for this project is manageable:

The structure

For assembly with the WS2812B ring, the components must be as shown in illustration 1 shown to be connected to each other. If you have a WS2812B strip, you must connect the power supply and the data IN connection correctly.

Illustration 1: Wiring of the individual components

The following applies to both WS2812B variants: red is the phase (5 volts), black is the ground (GND) and gray is the data IN connection. In most cases you have four connections on WS2812B strips, so you should check in advance which is the correct Data IN pin (DI). The pin DO (= Data OUT) is for the optional connection of further WS2812B modules (series connection).

The source code

Either copy the code here from the blog, see Code 1, or you can use the Github repository from the author download.

//-----------------------------------------------------
// Game "CYCLONE" for Arduino
// Author: Joern Weise
// License: GNU GPl 3.0
// Created: Sep 20, 2020
// Update: Sep 25, 2020
//-----------------------------------------------------
// Include libraries
#include
#include
#include
#include

//Defines
#define NUMPIXELS 12 // Popular NeoPixel ring size or edit the number of LEDs
#define PIN 2 // Data-Pin to ring or strip
#define PINBTN 6 // Pin for Player-button
#define PINSCORERST 9 // Pin to reset score during first run

#define DISABLEWINDOW 3 //Rounds before the LED before and after target is not valid anymore

//Player-Dot speed defines
#define STARTINTERVAL 250 //"Normal" move
#define MAXINTERVAL 500 //Very slow move
#define MININTERVAL 50 //Very fast move

//Create objects
LiquidCrystal_I2C lcd(0x27,16,2); // set the LCD adress
Adafruit_NeoPixel pixels(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800); //Init NeoPixel object

bool bFirstRun, bSecureWindow;
int iState = 1;
int iTargetPos, iPlayerPos, iStoredHighscore, iRound, iScore, iInterval; //Vars for the game
int iLastButtonPressed, iButtonState, iDebounceButton; //Vars to debounce button
unsigned long iLastPlayerMove, ulLastDebounceTime; //Timer to debouce button
unsigned long ulDebounceButton = 10; //Debounce-time

void setup() {
Serial.begin(115200);
Serial.println("Init serial communication: DONE");

//Begin init for WS218B-ring or -strip
pixels.begin(); // INITIALIZE NeoPixel strip object (REQUIRED)
pixels.clear(); // Set all pixels to "off"
pixels.setBrightness(20); // Set brightness to 20%
pixels.show(); // Send the updated pixel colors to the hardware.
Serial.println("Init WS218B-ring: DONE");

//Begin init display
lcd.init();
lcd.backlight();
lcd.clear();
Serial.println("Init LCD display: DONE");

randomSeed(analogRead(0)); // Make randome more randome
Serial.println("Make randome more randome: DONE");

//Read latest highscore from EEPROM
iStoredHighscore = EEPROM.read(0);
Serial.println("Last stored highscore: " + String(iStoredHighscore));

//Init button with internal pullup-resistor
pinMode(PINBTN,INPUT_PULLUP); //GameBTN
pinMode(PINSCORERST,INPUT_PULLUP); //Reset-Pin for score

//Init some basic-vars
bFirstRun = true; //Enable firstrun
iLastButtonPressed = digitalRead(PINBTN);  //Init    iLastButtonPressed
iButtonState = digitalRead(PINBTN); //Init iButtonstate
}

void loop() {
int iDebounceButton = DebounceButton(); //Check and debounce button

if(!bFirstRun)
  {
if(iState == 1) //Startscreen
    {
bSecureWindow = true;
iRound = 1;
iScore = 0;
iInterval = STARTINTERVAL;
lcd.clear();
lcd.home();
lcd.print("Highscore: " + String(iStoredHighscore));
lcd.setCursor(0,1);
lcd.print("Press button ...");
iState = 2;
    }
if(iState == 2) //Get Button pressed
    {
if(iDebounceButton == LOW)
      {
if(iRound == 1) //Only show once during game
Serial.println("-------- New game --------");
lcd.clear();
lcd.home();
lcd.print("Release button");
lcd.setCursor(0,1);
lcd.print("to start");
iState = 3;
      }
    }
if(iState == 3) //Init next round
    {
if(iDebounceButton == HIGH)
      {
lcd.clear();
        lcd.home();
        lcd.print("Round: " + String(iRound));
        Serial.println("Round: " + String(iRound));
        lcd.setCursor(0,1);
        lcd.print("Score: " + String(iScore));
        Serial.println("Score: " + String(iScore));
        iTargetPos = random(0,NUMPIXELS-1);
        Serial.println("New target pos: " + String(iTargetPos));
        iPlayerPos = random(0,NUMPIXELS-1);
        while(iTargetPos == iPlayerPos)
          iPlayerPos = random(0,NUMPIXELS-1);
        Serial.println("Player start pos: " + String(iPlayerPos));
        iState = 4;
      }
    }
if(iState == 4) //Draw target and playes dot
    {
DrawNextTarget(iTargetPos, bSecureWindow); //Draw new target
DrawPlayer(iPlayerPos); //Draw player dot
iLastPlayerMove = millis(); //Update timer for moving
iState = 5;
    }
if(iState == 5) //Wait pressing button and move player dot
    {
if(iDebounceButton == LOW)
      {
iState = 6;
      }
else
      {
unsigned long currentMillis = millis();
if(currentMillis - iLastPlayerMove > iInterval)
        {
iPlayerPos++;
          if(iPlayerPos >= NUMPIXELS)
            iPlayerPos = 0;
          DrawNextTarget(iTargetPos, bSecureWindow);
          DrawPlayer(iPlayerPos);
          iLastPlayerMove = currentMillis;
        }
      }
    }
if(iState == 6) //Check if player win
    {
if(CheckPlayerPos()) //Winner or loser?
      {
iScore++; //Update score
        iRound++; //Update rounds
        iState = 2; //Go back to release button
        if(iRound > DISABLEWINDOW) //Only target
        {
          bSecureWindow = false;
          iInterval = random(MININTERVAL,MAXINTERVAL);
        }
        else
          iInterval = random(STARTINTERVAL-50,MAXINTERVAL);
        Serial.println("New interval: " + String(iInterval));
      }
      else
        iState = 90;
    }
if(iState == 90) //Game ends
    {
Serial.println("Game ends");
lcd.clear();
lcd.home();
iDebounceButton = HIGH;
iLastButtonPressed = HIGH;
iButtonState = HIGH;
if(iScore > iStoredHighscore) //New highscore?
      {
lcd.print("New highscore ");
lcd.setCursor(0,1);
lcd.print("New score: " + String(iScore));
Serial.println("New highscore is " + String(iScore));
EEPROM.write(0,iScore); //Store new highscore to EEPROM
iStoredHighscore = iScore;
      }
else //Loser
      {
lcd.print("Game Over");
lcd.setCursor(0,1);
lcd.print("You lose");
Serial.println("You lose!");
      }
Serial.println("-------- End game --------");
delay(2000);
iState = 1;
    }
  }
else
InitFirstRun(); //Init Firstrun to check LCD and WS218B-ring
}

//Function to make first run
void InitFirstRun()
{
if(digitalRead(PINSCORERST) == LOW) //Overwrite EEPROM with "0"
  {
Serial.println("Reset highscore");
for(int iCnt = 0; iCnt < EEPROM.length(); iCnt++)
EEPROM.write(iCnt,0);
  }
Serial.println("---- Start init ----");
lcd.home();
lcd.print("Game Cyclone");
Serial.println("Game Cyclone");
lcd.setCursor(0,1);
lcd.print("(c) M3taKn1ght");
Serial.print("(c) M3taKn1ght");
delay(1000);
lcd.clear();
lcd.home();
lcd.print("For AZ-Delivery");
Serial.println("For AZ-Delivery");
lcd.setCursor(0,1);
lcd.print("Testing ring ...");
Serial.println("Testing ring ...");
delay(1000);
pixels.clear();
//Check every single LED
for(int i = 0; i<=255; i+=51)
  {
InitRingTest(i,0,0);
delay(50);
  }
pixels.clear();
for(int i = 0; i<=255; i+=51)
  {
InitRingTest(0,i,0);
delay(50);
  }
pixels.clear();
for(int i = 0; i<=255; i+=51)
  {
InitRingTest(0,0,i);
delay(50);
  }
pixels.clear();
pixels.show();
Serial.println("---- End init ----");
bFirstRun = false;
Serial.println("bFirstRun: " + String(bFirstRun));
Serial.println("Activate state for game");
}

//Simple function to check LED-Ring one by one
void InitRingTest(int iRed, int iGreen, int iBlue)
{
Serial.println("R: " + String(iRed) + " G: " + String(iGreen) + " B: " +  String(iBlue));
for(int iPixel = 0; iPixel < NUMPIXELS; iPixel++)
  {
pixels.setPixelColor(iPixel, pixels.Color(iRed, iGreen, iBlue));
pixels.show();
delay(50);
  }
}

//Function to draw target an secure area for player
void DrawNextTarget(int iPos, bool bArea)
{
pixels.clear();
pixels.setPixelColor(iPos, pixels.Color(0, 255, 0));
if(bArea)
  {
if(iPos - 1 < 0)
pixels.setPixelColor(NUMPIXELS - 1, pixels.Color(255, 136, 0));
else
pixels.setPixelColor(iPos - 1, pixels.Color(255, 136, 0));

if(iPos + 1 >= NUMPIXELS)
pixels.setPixelColor(0, pixels.Color(255, 136, 0));
else
pixels.setPixelColor(iPos + 1, pixels.Color(255, 136, 0));
  }
}

//Function to draw players LED
void DrawPlayer(int iPos)
{
if(iPos == iTargetPos) //target and player-dot is equal
pixels.setPixelColor(iPos, pixels.Color(0, 0, 255)); //Dot color will blue
else
pixels.setPixelColor(iPos, pixels.Color(255, 0, 0)); //Otherwise red
pixels.show();
}

//Function to check after pressing button, if user hit the target
bool CheckPlayerPos()
{
if(iTargetPos == iPlayerPos) //Player hit target?
return true;
else
  {
if(bSecureWindow) //LED before and after target active?
    {
int iBeforeTarget = iTargetPos - 1;
int iAfterTarget = iTargetPos + 1;
if(iBeforeTarget < 0)
iBeforeTarget = NUMPIXELS - 1;
if(iAfterTarget >= NUMPIXELS)
iAfterTarget = 0;
if(iBeforeTarget == iPlayerPos || iAfterTarget == iPlayerPos)
return true;
else
return false;
    }
else
return false;
  }
}

//Function to debounce button
int DebounceButton()
{
int iCurrentButtonState = digitalRead(PINBTN);
if(iCurrentButtonState != iLastButtonPressed)
ulLastDebounceTime = millis();

if((millis() - ulLastDebounceTime) > ulDebounceButton)
  {
if(iCurrentButtonState != iButtonState)
iButtonState = iCurrentButtonState;
  }
iLastButtonPressed = iCurrentButtonState;
return iButtonState;
}

Code 1: Code for the game "Cyclone"

At this point you can simply upload the code via the Arduino IDE, but a few parts of the code should be explained in more detail.

In order to control the display and the WS2812B-LED, an appropriate object must first be created for both. You can see this right at the beginning of Code 1, after the comment "Create object". Immediately afterwards some global variables are generated and some of them are initialized, provided that this is not done directly afterwards in the setup () function. You may be interested in the line “iStoredHighscore = EEPROM.read (0);” in which the last saved value of the high score is read from the EEPROM, i.e. the memory that does not lose its values when switched off or when reset. If the high score was outbid during a game, the new high score is written to the EEPROM in the loop () function using the line "EEPROM.write (0, iScore);".

The InitFirstRun () function is only called in the code when the Arduino is restarted. If you want to delete a high score or old values ​​from the EEPROM, this is done in this function. To do this, connect digital pin 9 to GND before starting the Nano. With this procedure the EEPROM is completely set to zero before all colors of all LEDs are checked. For this test of the LEDs, it is important that you have a suitable power supply for your circuit.

The loop () function is at the heart of the game. On the one hand, the current state of the push button is determined directly at the beginning and debounced when it is actuated. Debounced means that there must be a clear signal change for a defined time, in this case for 10 ms. You can read more about debouncing here. The course of the game is controlled immediately afterwards: Which outputs must be visualized on the screen and, if applicable, which LEDs are displayed on the WS2812B ring.

The visualization of the target point and the player point has been implemented using the DrawNextTarget (int iPos, bool bArea) and DrawPlayer (int iPos) functions. These functions are called up with the LED position as soon as the time for the next step from the player point is reached. A bool flag is transferred to the DrawNextTarget function (int iPos, bool bArea) so that the LEDs before and after the target point are also displayed in the first laps. If the player now presses the push button and the debouncing is completed, the function bool CheckPlayerPos () is called. This function checks whether the player has pressed the push button at the right moment or not. In the first laps, if the LED in front of and behind the target point is still valid, the tolerance zone is still taken into account.

If the player has hit the target, the current score is increased, a new, random speed is determined and the position of the target point and the starting position of the player point are set. It cannot happen that the starting point and the target point are identical at the beginning of the game.

However, if the player pressed at the wrong time, a check is made to see whether the high score has been exceeded and a "Game over" screen is displayed. If the high score is outbid, the new score is written directly to the EEPROM.

Many comments have been added so that you can understand the code more quickly and, if necessary, implement modifications for a WS2818B strip. Of course, you can also program other effects for the WS2812B ring before the game or during "game over". First of all, the code should be the basis for your individual adjustments.

I hope you enjoy to make your own Cyclone.

This and other projects can be found on GitHub at https://github.com/M3taKn1ght/Blog-Repo.

For arduino

2 comments

Jörn Weise

Jörn Weise

Hallo WinTiger,
Danke für das Feedback und das sie so Spaß haben. Zu ihren Problem fallen mir vier Lösungsvorschläge ein:
1. Haben sie SDA und SCL richtig angeschlossen?
2. Hat das Display den richtigen Kontrast? Stellschraube auf der Rückseite.
3. Stimmt die Spannungsversorgung, da sonst das Display bzw. die Buchstaben zu hell sind.
4. Ggf. Stimmt die I2C Adresse nicht, dann müssen sie diese mit dem I2C-Skript aus dem Wire.h- Beispielen ermitteln

Ich hoffe ich konnte helfen. Am Rande, mein aktueller Rekord liegt bei 55,vllt. Haben sie ihm ja schon überboten.

Gruß
Weise

WinTIger

WinTIger

Hallo,
ich bedanke mich zunächst erstmal für dieses interessante Spiel.
Ich habe es nachgebaut und macht einen riesen Spaß. Nur das Display zeigt nichts an bei mir. Es leuchtet, aber es wird nichts drauf geschrieben. Ich freue mich sehr, wenn ihr mir Anregungen geben könnt. Ich habe alle Komponenten von AZ-Delivery und verwende den LED-Ring.

Mit freundlichen Grüßen
WinTiger

Leave a comment

All comments are moderated before being published

Recommended blog posts

  1. Install ESP32 now from the board manager
  2. Lüftersteuerung Raspberry Pi
  3. Arduino IDE - Programmieren für Einsteiger - Teil 1
  4. ESP32 - das Multitalent
  5. OTA - Over the Air - ESP programming via WLAN