Mobile hackathon: Or turning a Pringles tube into an arcade game between Glasgow and London

1BmXOiZnIMAAKJ9PI’ve always fancied the idea of taking part in a Hackathon, but I never seem to be in the right place at the right time. This morning I’m heading from Glasgow to London on the Virgin Pendolino – a trip that takes 4hrs 32mins.

I was prepared in advance for this meeting (a rare event) so I decided to throw an Arduino and a few components in my laptop bag just in case inspiration for a project struck on the journey.

A tube of Pringles at the station gave me an idea: build a simple game, controlled by a single rotary encoder using the Adafruit Neopixel ring I had in my bag. Without any tools I knew it would be a real ghetto hack, but that made it all the more fun.

tubeBmX6Cn5IcAAJtKB

I started out with an idea based around the classic game Pong, but single-player pong turns out to be pretty dull so I adapted the code to make a Breakout clone.

And right now it’s running off solar power!

Playing Breakout in one, circular dimension with a ‘screen’ resolution of 0.000016 megapixels isn’t perhaps as intuitive as the traditional 2D version, so it might take some explaining.

How it works

Imagine you’re underneath a breakout game looking up. The ball, paddle and bricks are all translucent, so you can see them through each other.

The ball is represented by a blue dot. The intensity of the dot indicates the height of the ball with brightest being at the paddle end (i.e. closest). The paddle is the red cluster, and it is controlled by the rotary encoder, just like the original.

The white dot is the point where both of the side walls wrap around to meet – so both the paddle and ball bounce off this.

The bricks are represented by a green tint over the dots. There are two layers of bricks to clear in each position.

Like traditional breakout, it’s possible to influence the direction of the ball by catching the ball with the edge of the paddle.

When the ball hits the paddle the paddle turns green briefly. If the ball misses the paddle the whole circle flashes green and you lose a life (except, right now, lives are infinite).

What’s next?

The Pringles tube seems like a pretty sturdy enclosure and I have a return journey to make tomorrow night, so I might try making a different game. I like the idea of a circular version of 2048 Tetris, so I might have a go at that. Or if you have other ideas for a one-button and/or one-dial game, suggest something in the comments.

The code

// PONGels game by Grant Gibson
// Uses Neopixel rings from Adafruit and incorporates Adafruit Neopixel library
// http://www.adafruit.com/product/1463

#include <Adafruit_NeoPixel.h>
#include <math.h>

int neoPin = 7; // Pin for Neopixel data
Adafruit_NeoPixel pixels = Adafruit_NeoPixel(32, neoPin, NEO_GRB + NEO_KHZ800);

int encoderPin1 = 2;
int encoderPin2 = 3;
int encoderSwitchPin = 4; //push button switch

volatile int lastEncoded = 0;
volatile long encoderValue = 0;

long lastencoderValue = 0;

int lastMSB = 0;
int lastLSB = 0;

int isGood = 0;
int isBad = 0;

int paddleSize = 3;
float paddle1Pos = 7;
int numPixels = 16;
float ballPos = 8;    // Position around the end
int ballDist = 0;   // Distance along the tube
int ballSpeed = 1;
float ballAngle = 0; // Range: -5 to 5

String displayString[16];
int brickWall[16];

int i = 0;
int n = LOW;
int r = 0;
int b = 0;
int g = 0;

int encoder1PinA = 4;
int encoder1PinB = 5;
int encoder1PinALast = LOW;
int encoder2PinA = 6;
int encoder2PinB = 7;
int encoder2PinALast = LOW;

void setup() {
  Serial.begin (9600);
  pixels.begin();

  resetBricks();

  pinMode(encoderPin1, INPUT);
  pinMode(encoderPin2, INPUT);

  pinMode(encoderSwitchPin, INPUT);

  digitalWrite(encoderPin1, HIGH); //turn pullup resistor on
  digitalWrite(encoderPin2, HIGH); //turn pullup resistor on

  digitalWrite(encoderSwitchPin, HIGH); //turn pullup resistor on

  //call updateEncoder() when any high/low changed seen
  //on interrupt 0 (pin 2), or interrupt 1 (pin 3)
  attachInterrupt(0, updateEncoder, CHANGE);
  attachInterrupt(1, updateEncoder, CHANGE);
}

void loop() {
  // Read player one rotary encoder
  paddle1Pos += encoderValue/2;
  encoderValue = 0;

  // Don't let paddle go past end...
  if(paddle1Pos < 1) {
    paddle1Pos = 1;
  } else if (paddle1Pos > (numPixels-paddleSize)) {
    paddle1Pos = numPixels - paddleSize;
  }

  // Set the ball rotation
  ballPos = ballPos + ((ballAngle/50)*abs(ballSpeed));
  Serial.println(ballPos);

  // Bounce off walls
  if(ballPos < 1) {
    ballPos = 1;
    ballAngle *= -1;
  } else if(ballPos > 15) {
    Serial.println("ballPos > 15");
    ballPos = 15;
    ballAngle *= -1;
  }

  // Set the ball distance
  ballDist = ballDist + ballSpeed;

  // If ball is at the end make it go the other way
  if(ballDist < 0) {
    ballDist = 0;
    ballSpeed *= -1;
  } else if (ballDist > 64) {
    ballDist = 64;
    ballSpeed *= -1;
  }

  // Blank the display array
  for(i=0; i<numPixels; i++) {
    displayString[i] = "-";
  }

  // Inset the wall
  displayString[0] = "w";

  // Insert the paddles and check for collision
  if(ballDist == 0) {
    // At head end
    if((round(ballPos) >= paddle1Pos) && (round(ballPos) < (paddle1Pos + paddleSize))) {
      // Overlap between ball and paddle - good!
      isGood = 1;
      for(i=0; i<paddleSize; i++) {
        displayString[i+ round(paddle1Pos)] = "q";
      }
      // Alter ball angle based on paddle hit position
      if(round(ballPos) == paddle1Pos) {
        // At the first end, alter the angle positively
        if(ballAngle > -5) {
          ballAngle -= 1;
        }
      } else if (round(ballPos) == (paddle1Pos + (paddleSize-1))) {
        // At the other end, alter the angle negatively
        if(ballAngle < 5) {
          ballAngle += 1;
        }
      }
    } else {
      // Paddle missed ball - bad!
      isBad = 1;
      ballAngle = 0;
      for(i=0; i<numPixels; i++) {
        displayString[i] = "q";
      }
    }
  } else {
    for(i=0; i<paddleSize; i++) {
      displayString[i+ round(paddle1Pos)] = "p";
    }
  }
  if(ballDist == 64) {
    // Ball is at the far end, clear a brick
    if(brickWall[round(ballPos)] > 0) {
      brickWall[round(ballPos)] --;
    }
  }

  // Insert the ball
  if(displayString[round(ballPos)] == "p") {
    displayString[round(ballPos)] = "x";
  } else {
    displayString[round(ballPos)] = "b";
  }

  // For debugging, output to serial monitor
//  for(i=0; i<numPixels; i++) {
//    Serial.print(displayString[i]);
//  }
//  Serial.println(ballDist);
//  Serial.println("");

  // Output to the pixels
  for(i=0; i<numPixels; i++) {
    if(displayString[i] == "-") {
      // Pixel off
      r = 0; g = 0; b = 0;
    } else if (displayString[i] == "p") {
      // Pixel is paddle
      r = 16; g = 0; b = 0;
    } else if (displayString[i] == "q") {
      // Pixel is 'hit'
      r = 0; g = 16; b = 0;
    } else if (displayString[i] == "b") {
      // Pixel is ball
      r = 0; g = 0; b = 64 - ballDist;
    } else if (displayString[i] == "x") {
      // Pixel is ball over paddle
      r = 16; g = 0; b = 64 - ballDist;
    } else if (displayString[i] == "w") {
      // Pixel is ball at player 1 end
      r = 16; g = 16; b = 16;
    }
    // Now add a tint if the 'brick' is still there
    g += brickWall[i] * 2;
    pixels.setPixelColor(i, r, g, b);
  }
  pixels.show();
  if(isGood) {
    delay(50);
    isGood = 0;
  } else if(isBad) {
    delay(1000);
    isBad = 0;
  } else {
    delay(20);
  }
}

void updateEncoder(){
  int MSB = digitalRead(encoderPin1); //MSB = most significant bit
  int LSB = digitalRead(encoderPin2); //LSB = least significant bit

  int encoded = (MSB << 1) |LSB; //converting the 2 pin value to single number
  int sum  = (lastEncoded << 2) | encoded; //adding it to the previous encoded value

  if(sum == 0b1101 || sum == 0b0100 || sum == 0b0010 || sum == 0b1011) encoderValue ++;
  if(sum == 0b1110 || sum == 0b0111 || sum == 0b0001 || sum == 0b1000) encoderValue --;

  lastEncoded = encoded; //store this value for next time
}

void resetBricks() {
  for(i=0;i<numPixels;i++) {
    brickWall[i] = 2;
  }
}

Wiring

BmX6CjEIcAA5GfiIn hardware terms there is very little to it – a rotary encoder hooked to the interrupt pins (digital 2 & 3) plus ground, the Adafruit Neopixel ring hooked to D7, 5V and ground and power coming in via the mini USB port on the Nano I’m using.

Questions, comments?

Feel free to ask in the comments thread below or, if the comments are closed, via the contact page in the nav bar.

Links