Spiel "Snake"

Hier habe ich einen Computer-Spielklassiker auf dem Arduino mit einer WS2812 RGB-LED-Matrix (sog. NeoPixel) nachprogrammiert.

Video: Live-Demonstration von Snake

Spielregeln und -verlauf

Ziel des Spiels ist es die Spielfigur (=Schlange) möglichst lange am Leben zu erhalten und gleichzeitig möglich viel Futter fressen zu lassen.
Man steuert eine Schlange über die LED-Matrix, die sich von selbst fortbewegt. Der Spieler kann lediglich die Richtung des Kopfes ändern. Trifft man auf ein Stück Nahrung (grün), wird die Schlange ein Stück länger. Berührt man mit dem Kopf der Schlange den Spielfeldrand oder den eigenen Körper, so ist das Spiel vorbei.

Snake mit Mikrotaster-Steuerung

Verwendete Bauteile

Aufbau

Die genaue Art des Aufbaus kann man aus dem Aufbau von WS2812-LEDs und Mikrotastern erfahren.

Aufbau der Schaltung
Abb.: Aufbau der Schaltung

Sketch

#define PIN_LED_MATRIX   2
#define PIN_BUTTON_UP    4
#define PIN_BUTTON_DOWN  5
#define PIN_BUTTON_LEFT  6
#define PIN_BUTTON_RIGHT 7

#define DEBOUNCE_TIME 300   // in ms

#define X_MAX 8
#define Y_MAX 8

#define GAME_DELAY 400      // in ms

#define LED_TYPE_SNAKE 1
#define LED_TYPE_OFF   2
#define LED_TYPE_FOOD  3
#define LED_TYPE_BLOOD 4

#define DIRECTION_NONE  0
#define DIRECTION_UP    1
#define DIRECTION_DOWN  2
#define DIRECTION_LEFT  3
#define DIRECTION_RIGHT 4

#define GAME_STATE_RUNNING 1
#define GAME_STATE_END     2
#define GAME_STATE_INIT    3

#define MAX_TAIL_LENGTH X_MAX * Y_MAX
#define MIN_TAIL_LENGTH 3

#include <Adafruit_NeoPixel.h>

struct Coords {
  int x;
  int y;
};

Adafruit_NeoPixel pixels = Adafruit_NeoPixel(X_MAX*Y_MAX, PIN_LED_MATRIX, NEO_GRB + NEO_KHZ800);
byte incomingByte = 0;
byte userDirection;
byte gameState;
Coords head;
Coords tail[MAX_TAIL_LENGTH];
Coords food;
unsigned long lastDrawUpdate = 0;
unsigned long lastButtonClick;
unsigned int wormLength = 0;

void setup()
{
  pinMode(PIN_BUTTON_UP, INPUT_PULLUP);
  pinMode(PIN_BUTTON_DOWN, INPUT_PULLUP);
  pinMode(PIN_BUTTON_LEFT, INPUT_PULLUP);
  pinMode(PIN_BUTTON_RIGHT, INPUT_PULLUP);
  Serial.begin(9600);
  pixels.begin();
  resetLEDs();
  gameState = GAME_STATE_END;
}

void loop()
{
  switch(gameState)
  {
    case GAME_STATE_INIT:
      initGame();
      break;
    case GAME_STATE_RUNNING:
      checkButtonPressed();
      updateGame();
      break;
    case GAME_STATE_END:
      checkButtonPressed();
      break;
  }
}

void resetLEDs()
{
  for(int i=0; i<X_MAX*Y_MAX; i++) {
    pixels.setPixelColor(i, pixels.Color(0, 0, 0));
  }
  pixels.show();
}

void initGame()
{
  resetLEDs();
  head.x = 0;
  head.y = 0;
  food.x = -1;
  food.y = -1;
  wormLength = MIN_TAIL_LENGTH;
  userDirection = DIRECTION_LEFT;
  lastButtonClick = millis();

  for(int i=0; i<MAX_TAIL_LENGTH; i++) {
    tail[i].x = -1;
    tail[i].y = -1;
  }
  updateFood();
  gameState = GAME_STATE_RUNNING;
}

void updateGame()
{
  if ((millis() - lastDrawUpdate) > GAME_DELAY) {
    toggleLed(tail[wormLength-1].x, tail[wormLength-1].y, LED_TYPE_OFF);
    switch(userDirection) {
      case DIRECTION_RIGHT:
        if (head.x > 0) {
          head.x--;
        }
        break;
      case DIRECTION_LEFT:
        if (head.x < X_MAX-1) {
          head.x++;
        }
        break;
      case DIRECTION_DOWN:
        if (head.y > 0) {
          head.y--;
        }
        break;
      case DIRECTION_UP:
        if (head.y < Y_MAX-1) {
          head.y++;
        }
        break;
    }

    if (isCollision() == true) {
      endGame();
      return;
    }

    updateTail();

    if (head.x == food.x && head.y == food.y) {
      if (wormLength < MAX_TAIL_LENGTH) {
        wormLength++;
      }
      updateFood();
    }

    lastDrawUpdate = millis();
    pixels.show();
  }
}

void endGame()
{
  gameState = GAME_STATE_END;
  toggleLed(head.x, head.y, LED_TYPE_BLOOD);
  pixels.show();
}

void updateTail()
{
  for(int i=wormLength-1; i>0; i--) {
    tail[i].x = tail[i-1].x;
    tail[i].y = tail[i-1].y;
  }
  tail[0].x = head.x;
  tail[0].y = head.y;

  for(int i=0; i<wormLength; i++) {
    if (tail[i].x > -1) {
      toggleLed(tail[i].x, tail[i].y, LED_TYPE_SNAKE);
    }
  }
}

void updateFood()
{
  bool found = true;
  do {
    found = true;
    food.x = random(0, X_MAX);
    food.y = random(0, Y_MAX);
    for(int i=0; i<wormLength; i++) {
      if (tail[i].x == food.x && tail[i].y == food.y) {
         found = false;
      }
    }
  } while(found == false);
  toggleLed(food.x, food.y, LED_TYPE_FOOD);
}

bool isCollision()
{
  if (head.x < 0 || head.x >= X_MAX) {
    return true;
  }
  if (head.y < 0 || head.y >= Y_MAX) {
    return true;
  }
  for(int i=1; i<wormLength; i++) {
    if (tail[i].x == head.x && tail[i].y == head.y) {
       return true;
    }
  }
  return false;
}


void checkButtonPressed()
{
  if (millis() - lastButtonClick < DEBOUNCE_TIME) {
    return;
  }
  if (digitalRead(PIN_BUTTON_UP) == LOW) {
    if(gameState == GAME_STATE_RUNNING) {
      userDirection = DIRECTION_UP;
    }
    lastButtonClick = millis();
  } else if (digitalRead(PIN_BUTTON_DOWN) == LOW) {
    if(gameState == GAME_STATE_RUNNING) {
      userDirection = DIRECTION_DOWN;
    }
    lastButtonClick = millis();
  } else if (digitalRead(PIN_BUTTON_RIGHT) == LOW) {
    if(gameState == GAME_STATE_RUNNING) {
      userDirection = DIRECTION_RIGHT;
    }
    lastButtonClick = millis();
  } else if (digitalRead(PIN_BUTTON_LEFT) == LOW) {
    if(gameState == GAME_STATE_RUNNING) {
      userDirection = DIRECTION_LEFT;
    } else if (gameState == GAME_STATE_END) {
      gameState = GAME_STATE_INIT;
    }
    lastButtonClick = millis();
  }
}

void toggleLed(int x, int y, byte type)
{
  int ledIndex = y * X_MAX + x;
  uint32_t color;

  switch(type) {
    case LED_TYPE_SNAKE:
      color = pixels.Color(0, 10, 10);
      break;
    case LED_TYPE_OFF:
      color = pixels.Color(0, 0, 0);
      break;
    case LED_TYPE_FOOD:
      color = pixels.Color(0, 15, 0);
      break;
    case LED_TYPE_BLOOD:
      color = pixels.Color(15, 0, 0);
      break;
  }

  pixels.setPixelColor(ledIndex, color);
}

Snake mit Serial-Steuerung

Die zweite Version des Snake-Spiels kann man über eine serielle Konsolle (z.B. die der Arduino-IDE) steuern. Man benötigt also keine zusätzliche Eingabe-Hardware (Taster oder Knöpfe)

Verwendete Bauteile

Aufbau

siehe den Aufbau der ersten Version, jedoch können die Mikrotaster inkl. deren Anschlüsse zum Arduino weggelassen werden.

Sketch

/**
 * (c) 2018 Christian Grieger
 * GNU GENERAL PUBLIC LICENSE
 *
 * Snake game for Arduino and WS2812B-LED-Matrix
 * with the serial console as controlling input
 *
 * Example for serial console in Linuxw/Debian:
 *    Init:   screen /dev/ttyACM0 9600
 *    Exit:   CTRL-A \
 *
 * Controls:
 *    b   Begin new game
 *    p   Pause running game
 *    w   Snake up
 *    a   Snake left
 *    s   Snake down
 *    d   Snake right
 */

#define PIN_LED_MATRIX 2

#define X_MAX 8
#define Y_MAX 8

#define GAME_DELAY 400  // in ms

#define LED_TYPE_SNAKE 1
#define LED_TYPE_OFF   2
#define LED_TYPE_FOOD  3
#define LED_TYPE_BLOOD 4

#define DIRECTION_NONE  0
#define DIRECTION_UP    1
#define DIRECTION_DOWN  2
#define DIRECTION_LEFT  3
#define DIRECTION_RIGHT 4

#define GAME_STATE_RUNNING 1
#define GAME_STATE_END     2
#define GAME_STATE_INIT    3
#define GAME_STATE_PAUSED  4

#define MAX_TAIL_LENGTH X_MAX * Y_MAX
#define MIN_TAIL_LENGTH 3

#include <Adafruit_NeoPixel.h>

struct Coords {
  int x;
  int y;
};

Adafruit_NeoPixel pixels = Adafruit_NeoPixel(X_MAX*Y_MAX, PIN_LED_MATRIX, NEO_GRB + NEO_KHZ800);
byte incomingByte = 0;
byte userDirection;
byte gameState;
Coords head;
Coords tail[MAX_TAIL_LENGTH];
Coords food;
unsigned long lastDrawUpdate = 0;
unsigned int wormLength = 0;

void setup()
{
  Serial.begin(9600);
  pixels.begin();
  resetLEDs();
  gameState = GAME_STATE_END;
  Serial.println("Snake game");
}

void loop()
{
  switch(gameState)
  {
    case GAME_STATE_INIT:
      initGame();
      break;
    case GAME_STATE_RUNNING:
      updateGame();
      break;
    case GAME_STATE_PAUSED:
    case GAME_STATE_END:
      break;
  }
}

void resetLEDs()
{
  for(int i=0; i<X_MAX*Y_MAX; i++) {
    pixels.setPixelColor(i, pixels.Color(0, 0, 0));
  }
  pixels.show();
}

void initGame()
{
  resetLEDs();
  head.x = 0;
  head.y = 0;
  food.x = -1;
  food.y = -1;
  wormLength = MIN_TAIL_LENGTH;
  userDirection = DIRECTION_LEFT;

  for(int i=0; i<MAX_TAIL_LENGTH; i++) {
    tail[i].x = -1;
    tail[i].y = -1;
  }
  updateFood();
  gameState = GAME_STATE_RUNNING;
}

void updateGame()
{
  if ((millis() - lastDrawUpdate) > GAME_DELAY) {
    toggleLed(tail[wormLength-1].x, tail[wormLength-1].y, LED_TYPE_OFF);
    switch(userDirection) {
      case DIRECTION_RIGHT:
        if (head.x > 0) {
          head.x--;
        }
        break;
      case DIRECTION_LEFT:
        if (head.x < X_MAX-1) {
          head.x++;
        }
        break;
      case DIRECTION_DOWN:
        if (head.y > 0) {
          head.y--;
        }
        break;
      case DIRECTION_UP:
        if (head.y < Y_MAX-1) {
          head.y++;
        }
        break;
    }

    if (isCollision() == true) {
      endGame();
      return;
    }

    updateTail();

    if (head.x == food.x && head.y == food.y) {
      if (wormLength < MAX_TAIL_LENGTH) {
        wormLength++;
      }
      updateFood();
    }

    lastDrawUpdate = millis();
    pixels.show();
  }
}

void endGame()
{
  gameState = GAME_STATE_END;
  toggleLed(head.x, head.y, LED_TYPE_BLOOD);
  Serial.println("GAME OVER");
  pixels.show();
}

void updateTail()
{
  for(int i=wormLength-1; i>0; i--) {
    tail[i].x = tail[i-1].x;
    tail[i].y = tail[i-1].y;
    Serial.println("tail " + String(tail[i].x) + " "  + String(tail[i].y));
  }
  tail[0].x = head.x;
  tail[0].y = head.y;

  for(int i=0; i<wormLength; i++) {
    if (tail[i].x > -1) {
      toggleLed(tail[i].x, tail[i].y, LED_TYPE_SNAKE);
    }
  }
}

void updateFood()
{
  bool found = true;
  do {
    found = true;
    food.x = random(0, X_MAX);
    food.y = random(0, Y_MAX);
    for(int i=0; i<wormLength; i++) {
      if (tail[i].x == food.x && tail[i].y == food.y) {
         found = false;
      }
    }
  } while(found == false);
  toggleLed(food.x, food.y, LED_TYPE_FOOD);
}

bool isCollision()
{
  if (head.x < 0 || head.x >= X_MAX) {
    return true;
  }
  if (head.y < 0 || head.y >= Y_MAX) {
    return true;
  }
  for(int i=1; i<wormLength; i++) {
    if (tail[i].x == head.x && tail[i].y == head.y) {
       return true;
    }
  }
  return false;
}

void serialEvent()
{
    while (Serial.available()) {
        incomingByte = Serial.read();
    }
    switch(incomingByte) {
        case 'b':
        if (gameState == GAME_STATE_END) {
            gameState = GAME_STATE_INIT;
            Serial.println("BEGIN");
        }
        break;
    case 'p':
        if (gameState == GAME_STATE_PAUSED) {
            gameState = GAME_STATE_RUNNING;
        } else if(gameState == GAME_STATE_RUNNING) {
            gameState = GAME_STATE_PAUSED;
            Serial.println("PAUSE");
        }
        break;
    case 'a':
        if(gameState == GAME_STATE_RUNNING) {
            userDirection = DIRECTION_LEFT;
            Serial.println("left");
        }
        break;
    case 'd':
        if(gameState == GAME_STATE_RUNNING) {
            userDirection = DIRECTION_RIGHT;
            Serial.println("right");
        }
        break;
    case 's':
        if(gameState == GAME_STATE_RUNNING) {
            userDirection = DIRECTION_DOWN;
            Serial.println("down");
        }
        break;
    case 'w':
        if(gameState == GAME_STATE_RUNNING) {
            userDirection = DIRECTION_UP;
            Serial.println("up");
        }
        break;
    }
}

void toggleLed(int x, int y, byte type)
{
    int ledIndex = y * X_MAX + x;
    uint32_t color;

    switch(type) {
        case LED_TYPE_SNAKE:
            color = pixels.Color(0, 10, 10);
            break;
        case LED_TYPE_OFF:
            color = pixels.Color(0, 0, 0);
            break;
        case LED_TYPE_FOOD:
            color = pixels.Color(0, 15, 0);
            break;
        case LED_TYPE_BLOOD:
            color = pixels.Color(15, 0, 0);
            break;
    }

    pixels.setPixelColor(ledIndex, color);
}

Snake mit Gamecontroller-Steuerung

Die dritte Version nutzt einen Game-Controller (SNES), um die Schlange zu steuern.

Verwendete Bauteile

Aufbau

Schaltplan mit Game-Controller
Abb.: Schaltplan mit Game-Controller

Sketch

Für diesen Sketch wird die Library ArduinoGameController von bitluni verwendet. (Einfach die Datei GameControllers.h der Library in denselben Ordner wie die .ino-Datei legen.)

#define PIN_LED_MATRIX 7
#define PIN_GC_LATCH   8
#define PIN_GC_CLOCK   9
#define PIN_GC_DATA    10

#define DEBOUNCE_TIME 300   // in ms

#define X_MAX 8
#define Y_MAX 8

#define GAME_DELAY 400      // in ms

#define LED_TYPE_SNAKE 1
#define LED_TYPE_OFF   2
#define LED_TYPE_FOOD  3
#define LED_TYPE_BLOOD 4

#define DIRECTION_NONE  0
#define DIRECTION_UP    1
#define DIRECTION_DOWN  2
#define DIRECTION_LEFT  3
#define DIRECTION_RIGHT 4

#define GAME_STATE_RUNNING 1
#define GAME_STATE_END     2
#define GAME_STATE_INIT    3

#define MAX_TAIL_LENGTH X_MAX * Y_MAX
#define MIN_TAIL_LENGTH 3

#include "GameControllers.h"
#include <Adafruit_NeoPixel.h>

struct Coords {
  int x;
  int y;
};

Adafruit_NeoPixel pixels = Adafruit_NeoPixel(X_MAX*Y_MAX, PIN_LED_MATRIX, NEO_GRB + NEO_KHZ800);
GameControllers controllers;
byte incomingByte = 0;
byte userDirection;
byte gameState;
Coords head;
Coords tail[MAX_TAIL_LENGTH];
Coords food;
unsigned long lastDrawUpdate = 0;
unsigned long lastButtonClick;
unsigned int wormLength = 0;

void setup()
{
  Serial.begin(9600);
  controllers.init(PIN_GC_LATCH, PIN_GC_CLOCK);
  controllers.setController(0, GameControllers::SNES, PIN_GC_DATA);
  pixels.begin();
  resetLEDs();
  gameState = GAME_STATE_END;
}

void loop()
{
  switch (gameState)
  {
    case GAME_STATE_INIT:
      initGame();
      break;
    case GAME_STATE_RUNNING:
      checkButtonPressed();
      updateGame();
      break;
    case GAME_STATE_END:
      checkButtonPressed();
      break;
  }
}

void resetLEDs()
{
  for (int i = 0; i < X_MAX * Y_MAX; i++) {
    pixels.setPixelColor(i, pixels.Color(0, 0, 0));
  }
  pixels.show();
}

void initGame()
{
  resetLEDs();
  head.x = 0;
  head.y = 0;
  food.x = -1;
  food.y = -1;
  wormLength = MIN_TAIL_LENGTH;
  userDirection = DIRECTION_LEFT;
  lastButtonClick = millis();

  for (int i = 0; i < MAX_TAIL_LENGTH; i++) {
    tail[i].x = -1;
    tail[i].y = -1;
  }
  updateFood();
  gameState = GAME_STATE_RUNNING;
}

void updateGame()
{
  if ((millis() - lastDrawUpdate) > GAME_DELAY) {
    toggleLed(tail[wormLength - 1].x, tail[wormLength - 1].y, LED_TYPE_OFF);
    switch (userDirection) {
      case DIRECTION_RIGHT:
        if (head.x > 0) {
          head.x--;
        }
        break;
      case DIRECTION_LEFT:
        if (head.x < X_MAX - 1) {
          head.x++;
        }
        break;
      case DIRECTION_DOWN:
        if (head.y > 0) {
          head.y--;
        }
        break;
      case DIRECTION_UP:
        if (head.y < Y_MAX - 1) {
          head.y++;
        }
        break;
    }

    if (isCollision() == true) {
      endGame();
      return;
    }

    updateTail();

    if (head.x == food.x && head.y == food.y) {
      if (wormLength < MAX_TAIL_LENGTH) {
        wormLength++;
      }
      updateFood();
    }

    lastDrawUpdate = millis();
    pixels.show();
  }
}

void endGame()
{
  gameState = GAME_STATE_END;
  toggleLed(head.x, head.y, LED_TYPE_BLOOD);
  pixels.show();
}

void updateTail()
{
  for (int i = wormLength - 1; i > 0; i--) {
    tail[i].x = tail[i - 1].x;
    tail[i].y = tail[i - 1].y;
  }
  tail[0].x = head.x;
  tail[0].y = head.y;

  for (int i = 0; i < wormLength; i++) {
    if (tail[i].x > -1) {
      toggleLed(tail[i].x, tail[i].y, LED_TYPE_SNAKE);
    }
  }
}

void updateFood()
{
  bool found = true;
  do {
    found = true;
    food.x = random(0, X_MAX);
    food.y = random(0, Y_MAX);
    for (int i = 0; i < wormLength; i++) {
      if (tail[i].x == food.x && tail[i].y == food.y) {
        found = false;
      }
    }
  } while (found == false);
  toggleLed(food.x, food.y, LED_TYPE_FOOD);
}

bool isCollision()
{
  if (head.x < 0 || head.x >= X_MAX) {
    return true;
  }
  if (head.y < 0 || head.y >= Y_MAX) {
    return true;
  }
  for (int i = 1; i < wormLength; i++) {
    if (tail[i].x == head.x && tail[i].y == head.y) {
      return true;
    }
  }
  return false;
}


void checkButtonPressed()
{
  if (millis() - lastButtonClick < DEBOUNCE_TIME) {
    return;
  }

  controllers.poll();
  if (controllers.pressed(0, GameControllers::START)) {
    lastButtonClick = millis();
    if (gameState == GAME_STATE_END) {
      gameState = GAME_STATE_INIT;
    }
  } else if (controllers.pressed(0, GameControllers::UP)) {
    if (gameState == GAME_STATE_RUNNING) {
      userDirection = DIRECTION_UP;
    }
    lastButtonClick = millis();
  } else if (controllers.pressed(0, GameControllers::DOWN)) {
    if (gameState == GAME_STATE_RUNNING) {
      userDirection = DIRECTION_DOWN;
    }
    lastButtonClick = millis();
  } else if (controllers.pressed(0, GameControllers::LEFT)) {
    if (gameState == GAME_STATE_RUNNING) {
      userDirection = DIRECTION_LEFT;
    }
    lastButtonClick = millis();
  } else if (controllers.pressed(0, GameControllers::RIGHT)) {
    if (gameState == GAME_STATE_RUNNING) {
      userDirection = DIRECTION_RIGHT;
    }
    lastButtonClick = millis();
  }
}

void toggleLed(int x, int y, byte type)
{
  int ledIndex = y * X_MAX + x;
  uint32_t color;

  switch (type) {
    case LED_TYPE_SNAKE:
      color = pixels.Color(0, 10, 10);
      break;
    case LED_TYPE_OFF:
      color = pixels.Color(0, 0, 0);
      break;
    case LED_TYPE_FOOD:
      color = pixels.Color(0, 15, 0);
      break;
    case LED_TYPE_BLOOD:
      color = pixels.Color(15, 0, 0);
      break;
  }

  pixels.setPixelColor(ledIndex, color);
}

Variante mit 16×16-Matrix

Nachdem ich eine 16×16-Matrix mit WS2812b-LEDs erhalten hatte, lag es nahe, Snake auch für dieses Matrix auszuprobieren. Allerdings kam eine erste Schwierigkeit auf, denn die Matrix hatte eine andere Reihenfolge der LEDs, so dass ein zusätzliches Mapping notwendig war. Daher wurde die Funktion getLedMapping() eingeführt. Je nach verwendeter Matrix muss diese Funktion entsprechend angepasst werden. Zusätzlich ergab sich noch das Problem des geringen Arbeitsspeichers, der bei einer 16×16 Matrix knapp wurde. Durch ein paar Optimierungen lief das Spiel aber letztendlich gut in dieser Größe.
Hinweis: Es sollte hier (wie auch bei der 8x8 Matrix) eine externe Spannungsquelle verwendet werden, um den Arduino nicht zu überlasten bzw. zu beschädigen!

#define PIN_LED_MATRIX 7
#define PIN_GC_LATCH   8
#define PIN_GC_CLOCK   9
#define PIN_GC_DATA    10

#define DEBOUNCE_TIME 300   // in ms
#define lmillis() ((long)millis())

#define X_MAX 16
#define Y_MAX 16

#define GAME_DELAY 400      // in ms

#define LED_TYPE_SNAKE 1
#define LED_TYPE_OFF   2
#define LED_TYPE_FOOD  3
#define LED_TYPE_BLOOD 4

#define DIRECTION_NONE  0
#define DIRECTION_UP    1
#define DIRECTION_DOWN  2
#define DIRECTION_LEFT  3
#define DIRECTION_RIGHT 4

#define GAME_STATE_RUNNING 1
#define GAME_STATE_END     2
#define GAME_STATE_INIT    3

#define MAX_TAIL_LENGTH X_MAX*Y_MAX
#define MIN_TAIL_LENGTH 3

#include "GameControllers.h"
#include <Adafruit_NeoPixel.h>

struct Coords {
  byte x;
  byte y;
};

Adafruit_NeoPixel pixels = Adafruit_NeoPixel(MAX_TAIL_LENGTH, PIN_LED_MATRIX, NEO_GRB + NEO_KHZ800);
GameControllers controllers;
byte userDirection, gameState;
Coords head, food, tail[MAX_TAIL_LENGTH];
long lastDrawUpdate = 0, lastButtonClick = 0;
short wormLength = 0;

void setup()
{
  controllers.init(PIN_GC_LATCH, PIN_GC_CLOCK);
  controllers.setController(0, GameControllers::SNES, PIN_GC_DATA);
  pixels.begin();
  resetLEDs();
  gameState = GAME_STATE_END;
}

void loop()
{
  switch (gameState) {
    case GAME_STATE_INIT:
      initGame();
      break;
    case GAME_STATE_RUNNING:
      checkButtonPressed();
      updateGame();
      break;
    case GAME_STATE_END:
      checkButtonPressed();
      break;
  }
}

void resetLEDs()
{
  for (int i = 0; i < MAX_TAIL_LENGTH; i++) {
    pixels.setPixelColor(i, pixels.Color(0, 0, 0));
  }
  pixels.show();
}

void initGame()
{
  resetLEDs();
  head.x = 0;
  head.y = 0;
  food.x = 0;
  food.y = 0;
  wormLength = MIN_TAIL_LENGTH;
  userDirection = DIRECTION_LEFT;

  for (int i = 0; i < MAX_TAIL_LENGTH; i++) {
    tail[i].x = -1;
    tail[i].y = -1;
  }
  updateFood();
  gameState = GAME_STATE_RUNNING;
}

void updateGame()
{
  if (lmillis() - lastDrawUpdate < 0) {
    return;
  }

  toggleLed(tail[wormLength - 1].x, tail[wormLength - 1].y, LED_TYPE_OFF);
  switch (userDirection) {
    case DIRECTION_RIGHT:
      if (head.x > 0) {
        head.x--;
      }
      break;
    case DIRECTION_LEFT:
      if (head.x < X_MAX - 1) {
        head.x++;
      }
      break;
    case DIRECTION_DOWN:
      if (head.y > 0) {
        head.y--;
      }
      break;
    case DIRECTION_UP:
      if (head.y < Y_MAX - 1) {
        head.y++;
      }
      break;
  }

  if (isCollision() == true) {
    endGame();
    return;
  }

  updateTail();

  if (head.x == food.x && head.y == food.y) {
    if (wormLength < MAX_TAIL_LENGTH) {
      wormLength++;
    }
    updateFood();
  }

  lastDrawUpdate = lmillis() + GAME_DELAY;
  pixels.show();

}

void endGame()
{
  gameState = GAME_STATE_END;
  toggleLed(head.x, head.y, LED_TYPE_BLOOD);
  pixels.show();
}

void updateTail()
{
  for (int i = wormLength - 1; i > 0; i--) {
    tail[i].x = tail[i - 1].x;
    tail[i].y = tail[i - 1].y;
  }
  tail[0].x = head.x;
  tail[0].y = head.y;

  for (int i = 0; i < wormLength; i++) {
    if (tail[i].x > -1) {
      toggleLed(tail[i].x, tail[i].y, LED_TYPE_SNAKE);
    }
  }
}

void updateFood()
{
  bool found = true;
  do {
    found = true;
    food.x = random(0, X_MAX);
    food.y = random(0, Y_MAX);
    for (int i = 0; i < wormLength; i++) {
      if (tail[i].x == food.x && tail[i].y == food.y) {
        found = false;
      }
    }
  } while (found == false);
  toggleLed(food.x, food.y, LED_TYPE_FOOD);
}

bool isCollision()
{
  if (head.x < 0 || head.x >= X_MAX) {
    return true;
  }
  if (head.y < 0 || head.y >= Y_MAX) {
    return true;
  }
  for (int i = 1; i < wormLength; i++) {
    if (tail[i].x == head.x && tail[i].y == head.y) {
      return true;
    }
  }
  return false;
}


void checkButtonPressed()
{
  if (lmillis() - lastButtonClick < 0) {
    return;
  }

  controllers.poll();
  if (gameState == GAME_STATE_END && controllers.pressed(0, GameControllers::START)) {
    gameState = GAME_STATE_INIT;
    lastButtonClick = lmillis() +  DEBOUNCE_TIME;
  }
  if (gameState == GAME_STATE_RUNNING) {
    if (controllers.pressed(0, GameControllers::UP)) {
      userDirection = DIRECTION_UP;
      lastButtonClick = lmillis() +  DEBOUNCE_TIME;
    } else if (controllers.pressed(0, GameControllers::DOWN)) {
      userDirection = DIRECTION_DOWN;
      lastButtonClick = lmillis() +  DEBOUNCE_TIME;
    } else if (controllers.pressed(0, GameControllers::LEFT)) {
      userDirection = DIRECTION_RIGHT;
      lastButtonClick = lmillis() +  DEBOUNCE_TIME;
    } else if (controllers.pressed(0, GameControllers::RIGHT)) {
      userDirection = DIRECTION_LEFT;
      lastButtonClick = lmillis() +  DEBOUNCE_TIME;
    }
  }
}

void toggleLed(int x, int y, byte type)
{
  uint32_t color;

  switch (type) {
    case LED_TYPE_SNAKE:
      color = pixels.Color(0, 10, 10);
      break;
    case LED_TYPE_OFF:
      color = pixels.Color(0, 0, 0);
      break;
    case LED_TYPE_FOOD:
      color = pixels.Color(0, 15, 0);
      break;
    case LED_TYPE_BLOOD:
      color = pixels.Color(15, 0, 0);
      break;
  }

  pixels.setPixelColor(getLedMapping(y * X_MAX + x), color);
}

short getLedMapping(int searchIndex)
{
  int *tmp[X_MAX];
  int mapIdx = 0;
  int ledIndex = 0;

  for (int x = 0; x < X_MAX; x++) {
    for (int y = 0; y < Y_MAX; y++) {
      tmp[y] = x * X_MAX + y;
    }
    if (x % 2 == 1) {
      for (int i = (X_MAX - 1); i >= 0; i--) {
        ledIndex = tmp[i];
        mapIdx++;
        if (mapIdx == searchIndex) {
          return ledIndex;
        }
      }
    } else {
      for (int i = 0; i < X_MAX; i++) {
        ledIndex = tmp[i];
        mapIdx++;
        if (mapIdx == searchIndex) {
          return ledIndex;
        }
      }
    }
  }
}

Weiterentwicklung & Ideen

zurück