SparkFun Inventor's Kit for Photon Experiment Guide

Pages
Contributors: Joel_E_B, b_e_n, HelloTechie, Knutson, jimblom, RBERLIA
Favorited Favorite 15

Experiment 10: Pong!

Introduction

In this experiment, we'll use the OLED Breakout, potentiometer, and a photo resistor to make a game of Pong, where the paddles are controlled by the analog values of the two sensors.

alt text

Parts Needed

You will need the following parts (besides your Photon RedBoard and Breadboard, of course):

  • 1x OLED Breakout Board
  • 1x Potentiometer
  • 1x Photoresistor
  • 1x 330 Ohm Resistor
  • 15x Jumper Wires
Using a Photon by Particle instead or you don't have the kit? No worries! You can still have fun and follow along with this experiment. We suggest using the parts below:
Trimpot 10K Ohm with Knob

Trimpot 10K Ohm with Knob

COM-09806
$1.05
6
Mini Photocell

Mini Photocell

SEN-09088
$1.60
7
SparkFun Micro OLED Breakout

SparkFun Micro OLED Breakout

LCD-13003
$17.50
27
Resistor 330 Ohm 1/4 Watt PTH - 20 pack (Thick Leads)

Resistor 330 Ohm 1/4 Watt PTH - 20 pack (Thick Leads)

PRT-14490
$1.05

Suggested Reading

Hardware Hookup

We'll be connecting three devices to our Photon RedBoard - the OLED breakout, a trim potentiometer, and a photocell. These last two are analog sensors that are going to act as the paddle controllers for each player, while the OLED screen displays the ball, the paddles, and the score.

alt text

Having a hard time seeing the circuit? Click on the Fritzing diagram to see a bigger image.

The photocell is hooked up to pin A0 and GND though a 330 (NOT 10K) Ohm resistor from one pin, while the other pin is connected to +3.3V. The potentiometer's middle pin goes to pin A1, while the side pins are connected to GND and 3.3V.

Here's a handy table for the OLED Breakout connections:

Photon RedBoard Pin OLED Breakout Pin
A2 CS
A3 SCK
A5 SDI
D5 D/C
D6 RST
3V3 3.3V
GND GND

The table and Fritzing image are much more helpful, but if you wanted to see inside the box, here's a shot of the breadboarded circuit:

alt text

Photon Code

Pong is a great programming exercise for getting used to a new language or environment. There are tons of great pong tutorials out there, so we're not going to focus too much on the general code beyond the parts that interact with the hardware.

The first thing we need to do after creating a new app is to add the SparkFun OLED library to our sketch. This let's us draw and write all manner of things to the OLED screen quite easily. First, click on the bookmark icon in the sidebar (it's the fourth one up from the bottom). Under 'Community Libraries', there should be a search box - type in OLED, and you should see the 'SparkFunMicroOLED' Library pop up, like so:

alt text

Click on the library name, and then on the big blue button that says "Include in App". That should cause a line like #include "SparkFunMicroOLED/SparkFunMicroOLED.h" to appear near the top of your sketch. Voila!

Now that you've imported the library, you can copy and paste the rest of the code beneath:

language:c
/*  SparkFun Inventor's Kit for Photon
    Experiment 10 - Part 1
    This sketch was written by SparkFun Electronics
    Ben Leduc-Mills
    August 31, 2015
    https://github.com/sparkfun

    This is an example sketch for an analog sensor Pong game.

    Development environment specifics:
    Particle Build environment (https://www.particle.io/build)
    Particle Photon RedBoard
    Released under the MIT License(http://opensource.org/licenses/MIT)
*/
//////////////////////////
// MicroOLED Definition //
//////////////////////////

#define PIN_RESET D6  // Connect RST to pin 6
#define PIN_DC    D5  // Connect DC to pin 5 (required for SPI)
#define PIN_CS    A2 // Connect CS to pin A2 (required for SPI)
//MicroOLED oled(MODE_SPI, PIN_RESET, PIN_DC, PIN_CS);
MicroOLED oled(MODE_SPI, PIN_RESET, PIN_DC, PIN_CS);

//#define SINGLE_PLAYER

const int player1Pin = A1;
#ifndef SINGLE_PLAYER
const int player2Pin = A0;
#endif

/*** Sensor Calibration ****/

int sensor1Calibration = LCDHEIGHT; //photocell w/330
int sensor2Calibration = LCDHEIGHT; //potentiometer

//potentiometer
int sensor1Min = 0;
int sensor1Max = 4096;
//photocell
int sensor2Min = 100;
int sensor2Max = 1000;


/*** Game Settings ***/
const int renderDelay = 16;
const int startDelay = 2000;
const int gameOverDelay = 3000;
const int scoreToWin = 10;

int player1Score = 0;
int player2Score = 0;

const float paddleWidth = LCDWIDTH / 16.0;
const float paddleHeight = LCDHEIGHT / 3.0;
const float halfPaddleWidth = paddleWidth / 2.0;
const float halfPaddleHeight = paddleHeight / 2.0;

float player1PosX = 1.0 + halfPaddleWidth;
float player1PosY = 0.0;
float player2PosX = LCDWIDTH - 1.0 - halfPaddleWidth;
float player2PosY = 0.0;

// This is only used in SINGLE_PLAYER mode:
#ifdef SINGLE_PLAYER
float enemyVelY = 0.5;
#endif


/*** Ball Physics ***/
const float ballRadius = 2.0;
const float ballSpeedX = 1.0;
float ballPosX = LCDWIDTH / 2.0;
float ballPosY = LCDHEIGHT / 2.0;
float ballVelX = -1.0 * ballSpeedX;
float ballVelY = 0;

void setup()
{   
// in our setup, we call a few custom functions to get everything ready 
  initializeGraphics();
  initializeInput();
  displayGameStart();
  Serial.begin(9600);
}


void loop()
{
  updateGame(); //custom function to advance the game logic
  renderGame(); //custom function to dislay the current game state on the OLED screen
  // print out sensor values via serial so we can calibrate
  Serial.print(analogRead(A0));
  Serial.print(" <-A0 | A1-> ");
  Serial.println(analogRead(A1));

  //winning conditions
  if (player1Score >= scoreToWin)
  {
    gameOver(true);
  }
  else if (player2Score >= scoreToWin)
  {
    gameOver(false);
  }
}

//start the OLED and set the font type we want
void initializeGraphics()
{
  oled.begin();
  oled.setFontType(1);
}

//get either 1 or 2 pins ready for analog input
void initializeInput()
{
  pinMode(player1Pin, INPUT);
  #ifndef SINGLE_PLAYER
  pinMode(player2Pin, INPUT);
  #endif
}

//display the start screen
void displayGameStart()
{
  oled.clear(PAGE);
  renderString(20, 10, "Get");
  renderString(10, 30, "Ready!");
  oled.display();
  delay(startDelay);
}

//update the positions of the player paddles and the ball
void updateGame()
{
  updatePlayer1();
  updatePlayer2();
  updateBall();
}

//reset the score, ball position, and paddle positions for a new game
void resetGame()
{
  player2Score = 0;
  player1Score = 0;
  player2PosY = 0.0;
  ballPosX = LCDWIDTH / 2.0;
  ballPosY = LCDHEIGHT / 2.0;
  ballVelX = -1.0 * ballSpeedX;
  ballVelY = 0.0;
}


float clampPaddlePosY(float paddlePosY)
{
  float newPaddlePosY = paddlePosY;

  if (paddlePosY - halfPaddleHeight < 0)
  {
    newPaddlePosY = halfPaddleHeight;
  }
  else if (paddlePosY + halfPaddleHeight > LCDHEIGHT)
  {
    newPaddlePosY = LCDHEIGHT - halfPaddleHeight;
  }

  return newPaddlePosY;
}

void updatePlayer1()
{
  int potVal = analogRead(player1Pin);
  player1PosY = map(potVal, sensor1Min, sensor1Max, 0, sensor1Calibration);
}

void updatePlayer2()
{
  // If it's a single player game, update the AI's position
#ifdef SINGLE_PLAYER
  // Follow the ball at a set speed
  if (player2PosY < ballPosY)
  {
    player2PosY += enemyVelY;
  }
  else if (player2PosY > ballPosY)
  {
    player2PosY -= enemyVelY;
  }

  player2PosY = clampPaddlePosY(player2PosY);
#else  // Else if this is multiplayer, get player 2's position
  int lightVal = analogRead(player2Pin);
  player2PosY = map(lightVal, sensor2Min, sensor2Max, 0, sensor2Calibration);
  //Serial.println(player2PosY);
#endif
}

void updateBall()
{
  ballPosY += ballVelY;
  ballPosX += ballVelX;

  // Top and bottom wall collisions
  if (ballPosY < ballRadius)
  {
    ballPosY = ballRadius;
    ballVelY *= -1.0;
  }
  else if (ballPosY > LCDHEIGHT - ballRadius)
  {
    ballPosY = LCDHEIGHT - ballRadius;
    ballVelY *= -1.0;
  }

  // Left and right wall collisions
  if (ballPosX < ballRadius)
  {
    ballPosX = ballRadius;
    ballVelX = ballSpeedX;
    player2Score++;
  }
  else if (ballPosX > LCDWIDTH - ballRadius)
  {
    ballPosX = LCDWIDTH - ballRadius;
    ballVelX *= -1.0 * ballSpeedX;
    player1Score++;
  }

  // Paddle collisions
  if (ballPosX < player1PosX + ballRadius + halfPaddleWidth)
  {
    if (ballPosY > player1PosY - halfPaddleHeight - ballRadius &&
        ballPosY < player1PosY + halfPaddleHeight + ballRadius)
    {
      ballVelX = ballSpeedX;
      ballVelY = 2.0 * (ballPosY - player1PosY) / halfPaddleHeight;
    }
  }
  else if (ballPosX > player2PosX - ballRadius - halfPaddleWidth)
  {
    if (ballPosY > player2PosY - halfPaddleHeight - ballRadius &&
        ballPosY < player2PosY + halfPaddleHeight + ballRadius)
    {
      ballVelX = -1.0 * ballSpeedX;
      ballVelY = 2.0 * (ballPosY - player2PosY) / halfPaddleHeight;
    }
  }
}


void renderGame()
{
  oled.clear(PAGE);

  renderScores(player1Score, player2Score);
  renderPaddle(player1PosX, player1PosY);
  renderPaddle(player2PosX, player2PosY);
  renderBall(ballPosX, ballPosY);

  oled.display();
  delay(renderDelay);
}

void renderString(int x, int y, String string)
{
  oled.setCursor(x, y);
  oled.print(string);
}

void renderPaddle(int x, int y)
{
  oled.rect(
    x - halfPaddleWidth,
    y - halfPaddleHeight,
    paddleWidth,
    paddleHeight);
}

void renderBall(int x, int y)
{
  oled.circle(x, y, 2);
}

void renderScores(int firstScore, int secondScore)
{
  renderString(10, 0, String(firstScore));
  renderString(LCDWIDTH - 14, 0, String(secondScore));
}

void gameOver(bool didWin)
{
  if (didWin)
  {
#ifdef SINGLE_PLAYER
    renderString(20, 10, "You");
    renderString(20, 30, "Win!");
#else
    renderString(0, 10, "Playr 1");
    renderString(15, 30, "Wins");
#endif
  }
  else
  {
#ifdef SINGLE_PLAYER
    renderString(20, 10, "You");
    renderString(15, 30, "Lose!");
#else
    renderString(0, 10, "Playr 2");
    renderString(15, 30, "Wins");
#endif
  }

  oled.display();
  delay(gameOverDelay);

  // Get ready to start the game again.
  resetGame();
  displayGameStart();
}

What You Should See

After successfully flashing the code onto your Photon RedBoard, you should see the words 'Get Ready!' on the OLED screen, followed soon after by the familiar Pong game screen. If you've calibrated your sensors well, you should be able to move each paddle up and down to defend your side. After a player wins, the game should start all over again.

If you turn the potentiometer or place your hand over the photocell, the paddles should move up and down, allowing you to play a game of Pong:

alt text

Code to Note

Whew! That's a lot of code to digest all at once. Let's look at a few of the key sections that make this work.

Defining our pin assignments and declaring our OLED object happens right at the top:

language:c
#define PIN_RESET D6  // Connect RST to pin 6
#define PIN_DC    D5  // Connect DC to pin 5 (required for SPI)
#define PIN_CS    A2 // Connect CS to pin A2 (required for SPI)
MicroOLED oled(MODE_SPI, PIN_RESET, PIN_DC, PIN_CS);

//#define SINGLE_PLAYER

const int player1Pin = A1; //connected to potentiometer
#ifndef SINGLE_PLAYER
const int player2Pin = A0; //connected to photocell
#endif

This is also where we decide a few important things: which communication mode we're using - SPI in our case means we put MODE_SPI as that first variable in the MicroOLED object. Below that, we're also deciding if we want a 1 player or 2 player game. To make the game one player, simply uncomment the //#define SINGLE_PLAYER line.

The next section is crucial - it's where we 'calibrate' our sensors, by telling the program what we expect the minimum and maximum values for each sensor to be. Changes are you'll have to run the program first and look at the serial monitor while testing your sensors to get a good 'sense' of the value ranges.

language:c
/*** Sensor Calibration ****/

int sensor1Calibration = LCDHEIGHT; //photocell w/330
int sensor2Calibration = LCDHEIGHT; //potentiometer

//potentiometer
int sensor1Min = 0;
int sensor1Max = 4096;
//photocell
int sensor2Min = 100;
int sensor2Max = 1000;

For example, if you're in a very brightly lit room, your photocell will get a wider range of values than if you're in a dimly lit place -- but we still want the paddle to move the entire range of the screen, which is why we save the LCDHEIGHT value to a separate variable to use later on.

After we set a number of global variables that control things like how fast the ball moves and what number each game goes until (the winning number), we're on to our setup() and loop() functions.

language:c
void setup()
{
  // in our setup, we call a few custom functions to get everything ready 
  initializeGraphics();
  initializeInput();
  displayGameStart();
  Serial.begin(9600);
}

void loop()
{
  updateGame(); //custom function to advance the game logic
  renderGame(); //custom function to dislay the current game state on the OLED screen
  // print out sensor values via serial so we can calibrate
  Serial.print(analogRead(A0));
  Serial.print(" <-A0 | A1-> ");
  Serial.println(analogRead(A1));

  //winning conditions
  if (player1Score >= scoreToWin)
  {
    gameOver(true);
  }
  else if (player2Score >= scoreToWin)
  {
    gameOver(false);
  }
}

You'll notice that we make use of a lot of custom functions in order to keep our main setup and loop short and readable. In the setup(), we call a few functions that help start up the OLED screen, initialize our input pins, display the start screen, and open up serial communication. The loop() is simply updating the game state, displaying that state, checking to see if anyone won, and ending the game if so. Dig into all of these functions to get a better sense of how the game works!

Troubleshooting

The main challenge (besides being good at Pong) is making sure your sensor inputs are calibrated to the point where each paddle covers the entire height of the OLED screen. Checking the serial monitor while testing your sensors is the best way to get an idea of what the range for each sensor is.

Part 2: Scoreboard!

What does every serious Pong competition need? A scoreboard, that's what! In this half of the exercise, we're going to use Particle.variable() with the Particle Cloud API to create a web page that stores our Pong scores locally (i.e., without a database) in HTML 5 localData, displays them in a table, and updates when a new score in ready.

alt text

Photon Code

Here's the new code that goes on the Photon RedBoard - in reality, there are just a few changes. We'll go over them in the next section, but this is the complete sketch on the Photon side.

language:c
/*  SparkFun Inventor's Kit for Photon
    Experiment 10 - Part 2
    This sketch was written by SparkFun Electronics
    Ben Leduc-Mills
    August 31, 2015
    https://github.com/sparkfun

    This is an example sketch for an analog sensor Pong game, with
    an html/javascript scoreboard.

    Development environment specifics:
    Particle Build environment (https://www.particle.io/build)
    Particle Photon RedBoard
    Released under the MIT License(http://opensource.org/licenses/MIT)
*/

//////////////////////////
// MicroOLED Definition //
//////////////////////////

#define PIN_RESET D6  // Connect RST to pin 6
#define PIN_DC    D5  // Connect DC to pin 5 (required for SPI)
#define PIN_CS    A2 // Connect CS to pin A2 (required for SPI)
//MicroOLED oled(MODE_SPI, PIN_RESET, PIN_DC, PIN_CS);
MicroOLED oled(MODE_SPI, PIN_RESET, PIN_DC, PIN_CS);



//#define SINGLE_PLAYER

const int player1Pin = A1;
#ifndef SINGLE_PLAYER
const int player2Pin = A0;
#endif

/*** Sensor Calibration ****/

int sensor1Calibration = LCDHEIGHT; //photocell w/330
int sensor2Calibration = LCDHEIGHT; //potentiometer

//potentiometer
int sensor1Min = 0;
int sensor1Max = 4096;
//photocell
int sensor2Min = 100;
int sensor2Max = 1000;


/*** Game Settings ***/
const int renderDelay = 16;
const int startDelay = 2000;
const int gameOverDelay = 3000;
const int scoreToWin = 10;

int player1Score = 0;
int player2Score = 0;

const float paddleWidth = LCDWIDTH / 16.0;
const float paddleHeight = LCDHEIGHT / 3.0;
const float halfPaddleWidth = paddleWidth / 2.0;
const float halfPaddleHeight = paddleHeight / 2.0;

float player1PosX = 1.0 + halfPaddleWidth;
float player1PosY = 0.0;
float player2PosX = LCDWIDTH - 1.0 - halfPaddleWidth;
float player2PosY = 0.0;

// This is only used in SINGLE_PLAYER mode:
#ifdef SINGLE_PLAYER
float enemyVelY = 0.5;
#endif


/*** Ball Physics ***/
const float ballRadius = 2.0;
const float ballSpeedX = 1.0;
float ballPosX = LCDWIDTH / 2.0;
float ballPosY = LCDHEIGHT / 2.0;
float ballVelX = -1.0 * ballSpeedX;
float ballVelY = 0;


//score keeper
char postScore[64];

//game ID
int gameID = 0;


void setup()
{
  // in our setup, we call a few custom functions to get everything ready 
  //here's our call to Particle.variable - we're just sending a string called 'data' out to the cloud    
  Particle.variable("data", &postScore, STRING);
  initializeGraphics();
  initializeInput();
  displayGameStart();
  Serial.begin(9600);
}


void loop()
{
  updateGame(); //custom function to advance the game logic
  renderGame(); //custom function to dislay the current game state on the OLED screen

  //winning conditions
  if (player1Score >= scoreToWin)
  {
    gameOver(true);
  }
  else if (player2Score >= scoreToWin)
  {
    gameOver(false);
  }
}

//start the OLED and set the font type we want
void initializeGraphics()
{
  oled.begin();
  oled.setFontType(1);
}

//get either 1 or 2 pins ready for analog input
void initializeInput()
{
  pinMode(player1Pin, INPUT);
  #ifndef SINGLE_PLAYER
  pinMode(player2Pin, INPUT);
  #endif
}

//display the start screen
void displayGameStart()
{
  oled.clear(PAGE);
  renderString(20, 10, "Get");
  renderString(10, 30, "Ready!");
  oled.display();
  delay(startDelay);
}

//update the positions of the player paddles and the ball
void updateGame()
{
  updatePlayer1();
  updatePlayer2();
  updateBall();
}

//reset the score, ball position, and paddle positions for a new game
void resetGame()
{
  player2Score = 0;
  player1Score = 0;
  player2PosY = 0.0;
  ballPosX = LCDWIDTH / 2.0;
  ballPosY = LCDHEIGHT / 2.0;
  ballVelX = -1.0 * ballSpeedX;
  ballVelY = 0.0;
}


float clampPaddlePosY(float paddlePosY)
{
  float newPaddlePosY = paddlePosY;

  if (paddlePosY - halfPaddleHeight < 0)
  {
    newPaddlePosY = halfPaddleHeight;
  }
  else if (paddlePosY + halfPaddleHeight > LCDHEIGHT)
  {
    newPaddlePosY = LCDHEIGHT - halfPaddleHeight;
  }

  return newPaddlePosY;
}

void updatePlayer1()
{
  int potVal = analogRead(player1Pin);
  player1PosY = map(potVal, sensor1Min, sensor1Max, 0, sensor1Calibration);
}

void updatePlayer2()
{
  // If it's a single player game, update the AI's position
#ifdef SINGLE_PLAYER
  // Follow the ball at a set speed
  if (player2PosY < ballPosY)
  {
    player2PosY += enemyVelY;
  }
  else if (player2PosY > ballPosY)
  {
    player2PosY -= enemyVelY;
  }

  player2PosY = clampPaddlePosY(player2PosY);
#else  // Else if this is multiplayer, get player 2's position
  int lightVal = analogRead(player2Pin);
  player2PosY = map(lightVal, sensor2Min, sensor2Max, 0, sensor2Calibration);
  //Serial.println(player2PosY);
#endif
}

void updateBall()
{
  ballPosY += ballVelY;
  ballPosX += ballVelX;

  // Top and bottom wall collisions
  if (ballPosY < ballRadius)
  {
    ballPosY = ballRadius;
    ballVelY *= -1.0;
  }
  else if (ballPosY > LCDHEIGHT - ballRadius)
  {
    ballPosY = LCDHEIGHT - ballRadius;
    ballVelY *= -1.0;
  }

  // Left and right wall collisions
  if (ballPosX < ballRadius)
  {
    ballPosX = ballRadius;
    ballVelX = ballSpeedX;
    player2Score++;
  }
  else if (ballPosX > LCDWIDTH - ballRadius)
  {
    ballPosX = LCDWIDTH - ballRadius;
    ballVelX *= -1.0 * ballSpeedX;
    player1Score++;
  }

  // Paddle collisions
  if (ballPosX < player1PosX + ballRadius + halfPaddleWidth)
  {
    if (ballPosY > player1PosY - halfPaddleHeight - ballRadius &&
        ballPosY < player1PosY + halfPaddleHeight + ballRadius)
    {
      ballVelX = ballSpeedX;
      ballVelY = 2.0 * (ballPosY - player1PosY) / halfPaddleHeight;
    }
  }
  else if (ballPosX > player2PosX - ballRadius - halfPaddleWidth)
  {
    if (ballPosY > player2PosY - halfPaddleHeight - ballRadius &&
        ballPosY < player2PosY + halfPaddleHeight + ballRadius)
    {
      ballVelX = -1.0 * ballSpeedX;
      ballVelY = 2.0 * (ballPosY - player2PosY) / halfPaddleHeight;
    }
  }
}


void renderGame()
{
  oled.clear(PAGE);

  renderScores(player1Score, player2Score);
  renderPaddle(player1PosX, player1PosY);
  renderPaddle(player2PosX, player2PosY);
  renderBall(ballPosX, ballPosY);

  oled.display();
  delay(renderDelay);
}

void renderString(int x, int y, String string)
{
  oled.setCursor(x, y);
  oled.print(string);
}

void renderPaddle(int x, int y)
{
  oled.rect(
    x - halfPaddleWidth,
    y - halfPaddleHeight,
    paddleWidth,
    paddleHeight);
}

void renderBall(int x, int y)
{
  oled.circle(x, y, 2);
}

void renderScores(int firstScore, int secondScore)
{
  renderString(10, 0, String(firstScore));
  renderString(LCDWIDTH - 14, 0, String(secondScore));
}

// OK - here's the new bit
void gameOver(bool didWin)
{
  //at the end of a game, increment the gameID by 1        
  gameID += 1;
  //now, send out the player 1 score, player 2 score, and the game ID
  //we do this by using 'sprintf', which takes a bunch of random data types (in our case, integers),
  //and puts them all into a nicely formatted string - which is exactly what our Particle.variable is supposed to be
  sprintf(postScore, "{\"player1Score\":%d, \"player2Score\":%d, \"gameID\":%d}", player1Score, player2Score, gameID);
  //give us a sec, and start a new game
  delay(1000);    

  if (didWin)
  {
#ifdef SINGLE_PLAYER
    renderString(20, 10, "You");
    renderString(20, 30, "Win!");
#else
    renderString(0, 10, "Playr 1");
    renderString(15, 30, "Wins");
#endif
  }
  else
  {
#ifdef SINGLE_PLAYER
    renderString(20, 10, "You");
    renderString(15, 30, "Lose!");
#else
    renderString(0, 10, "Playr 2");
    renderString(15, 30, "Wins");
#endif
  }

  oled.display();
  delay(gameOverDelay);

  // Get ready to start the game again.
  resetGame();
  displayGameStart();
}

HTML Code

This is where the real action is happening for this exercise - on the web. We'll be using a combination of good ol' HTML, the HTML 5 localStorage ability, and some basic javaScript to create a page that reads the values of our Particle.variable, stores them (without using a database), displays them in a table, and checks every 15 seconds for a new score to come in.

Create a new html file in your favorite text editor, save it as 'experiment10.html' and paste in the code below (doesn't matter where you save it, just remember where it is):

language:javascript
<!DOCTYPE html>
<html>
<head>
    <meta charset=utf-8 />
    <META HTTP-EQUIV="refresh" CONTENT="15"><!-- refresh our page every 15 seconds -->
<!-- 
SparkFun Inventor's Kit for Photon
  Experiment 10 - Part 2
  This sketch was written by SparkFun Electronics
  Ben Leduc-Mills
  August 31, 2015
  https://github.com/sparkfun

  This is an example sketch for an analog sensor Pong game, with
  an html/javascript scoreboard.

  Development environment specifics:
  Particle Build environment (https://www.particle.io/build)
  Particle Photon RedBoard
  Released under the MIT License(http://opensource.org/licenses/MIT) 
-->
        <title>SparkFun Photon SIK Experiment 10 - Part 2</title>
    </head>
    <body>
        <div style="height:300px;overflow:auto;"><!-- make a div set to overflow so our table scrolls -->
            <table id="scorekeeper"> <!-- table id - important soon -->

            </table>
        </div>
        <FORM> <!-- form button to clear data if we want -->
            <INPUT TYPE="button" onClick="handlers.clearAppData()" VALUE="Clear Scores">
            </FORM>
            <!-- javascript starts here -->
            <script type="text/javascript">

            //object to contain our data
            var app = {
                scores: [{
                }]
            };

            //to keep track of game number - don't want to record the same game more than once
            var gameNum =0; 

            //set of functions to handle getting and setting the local storage data from our api call
            //thanks to stackoverflow user bundleofjoy(http://stackoverflow.com/users/1217785/bundleofjoy) for the inspiration
            var handlers = {

                //make an api call and save new data from our Particle sketch into our local storage object
                saveData: function () {
                    var xmlhttp = new XMLHttpRequest();

                    // IMPORTANT: replace this with your device name, Particle.variable name, and your access token!
                    var url = "https://api.particle.io/v1/devices/{your device name}/{your Particle.variable name}?access_token={your access_token}";
                    xmlhttp.open("GET", url, true);
                    xmlhttp.send();


                    xmlhttp.onreadystatechange = function(){
                        if(xmlhttp.readyState == 4 && xmlhttp.status == 200) {
                            var data = JSON.parse(xmlhttp.responseText);
                            var result = decodeURI(data.result);

                            //parse our json object
                            var p = JSON.parse(result);

                            //check if the latest game number from Particle is greater than what we last stored locally
                            //if so, add a new score to our scores array
                            if(p.gameID > gameNum){
                                console.log("saved new");
                                app.scores.push({player1Score: p.player1Score, player2Score: p.player2Score, gameID: p.gameID});
                                localStorage.setItem("app", JSON.stringify(app));
                                gameNum = p.gameID;

                            }

                        }
                    }
                },

                //get the data currently stored in our HTML 5 localStorage object
                getData: function () {
                    // Retrieves the object app from localStorage
                    var retrievedObject = localStorage.getItem("app");
                    var savedData = JSON.parse(retrievedObject);

                    //if there's something in saved data, let's grab it!
                    if(savedData !== null){

                        //and put the data into something we can see on the page (our table)
                        handlers.displayTable();

                        //let's also update our local gameID variable with the latest gameID 
                        for(scores in savedData) {
                        var current = savedData.scores.length-1;
                        gameNum = savedData.scores[current].gameID;
                    }
                  }  

                  return savedData;
                },

                //deletes all our local data
                clearAppData: function (event) {
                    localStorage.clear();
                    return false;
                },

                //displays our data in an html table
                displayTable: function() {
                    var retrievedObject = localStorage.getItem("app");
                    var savedData = JSON.parse(retrievedObject);
                    var out = "<th>Game Number</th><th>Player 1 Score</th><th>Player 2 Score</th>";

                    //iterate through all the scores in our saved data and put them into one long string
                    for(var i = 1; i < savedData.scores.length-1; i++) {
                        out += "<tr>" + "<td>"; 
                        out += savedData.scores[i].gameID+ "</td>";
                        out += "<td>" + savedData.scores[i].player1Score+ "</td>";
                        out += "<td>" + savedData.scores[i].player2Score + "</td>";
                        out +=  "</tr>"
                    }

                    console.log(out);
                    //put that long string into our table using out table's id property
                    document.getElementById('scorekeeper').innerHTML = out;
                }

            };

        //when the window loads (like on refresh), this gets called 
        window.onload = function () {
            //get data from localStorage
            var saved = handlers.getData();
            //check if its null / undefined
            if ([null, undefined].indexOf(saved) === -1) {

                //it's not null/undefined - data exists!
                console.log("Data exists. Here's the data : ", saved);
                //set your "app" to data from localStorage
                app = saved;

                handlers.saveData();

            } else {
                //localStorage is empty
                console.log("Data does not exist, save to localStorage");
                //so, save incoming data in localStorage
                handlers.saveData();
            }
        };
</script> 
</body>
</html>

What You Should See

Plug in your Photon RedBoard, and make sure it's running Pong and posting a JSON object to the URL you specified. Now, if you open the HTML file in your browser of choice (the URL might be something like 'file://localhost/Users/User1/Desktop/Experiment10.html' on a mac), you should see something like this:

alt text

If the page is blank, give it a minute or two - the updates are sometimes a little out of sync with what the Pong game is currently doing (and remember, it only posts new data after a game has been won or lost).

The page will refresh every 15 seconds, check for a new score, add it if there is one, and show it to you. You can push the 'clear scores' button to empty the scores table, though to reset the gameID you'll have to restart your Photon.

Sweet! Now, as long as your Photon is online you can point your friends to a live scoreboard of your match scores.

Code to Note: Photon Part 2

Some important bits have changed, so pay attention.

//score keeper
char postScore[64];

//game ID
int gameID = 0;

We add to variable at the top of our code to contain postScore[] a character array that we will register as our Particle.variable(), and gameID, which we will use as a unique identifier for each game played.

In setup(), we register our postScore[] variable so we can access from the cloud:

Particle.variable("data", &postScore, STRING);

That first argument ("data" in our case) is what we will use in the Particle api to retrieve our info - but it could be called anything.

Now for the fun part - formatting our data to a string to be accessed by the Particle cloud API.

// OK - here's the new bit
void gameOver(bool didWin)
{
  //at the end of a game, increment the gameID by 1        
  gameID += 1;
  //now, send out the player 1 score, player 2 score, and the game ID
  //we do this by using 'sprintf', which takes a bunch of random data types (in our case, integers),
  //and puts them all into a nicely formatted string - which is exactly what our Particle.variable is supposed to be
  sprintf(postScore, "{\"player1Score\":%d, \"player2Score\":%d, \"gameID\":%d}", player1Score, player2Score, gameID);

Read the comments for a better idea of what's going on, or if you're feeling very adventurous, check out the c++ reference page for sprintf.

Code to Note: HTML

Ok, so it looks like a mess of gibberish right now - that's fine. The most important thing is that you find the URL variable and change it to match your credentials. It looks like this (should be around line 39):

var url = "https://api.particle.io/v1/devices/{your device name}/{your Particle.variable name}?access_token={your access_token}";

The parts in curly brackets {} are the parts you need to replace. If even one thing is off, none of this will work. Computers are pretty finicky that way, trust me. A great way to check is to paste your URL in a browser (make sure your Photon code is uploaded and your Photon RedBoard is plugged in first) - you should see that URL return a JSON object (it's a nice way of organizing data for the web). It might look something like this (I took bits out for privacy):

{
"cmd": "VarReturn",
"name": "data",
"result": "{\"player1Score\":10, \"player2Score\":2, \"gameID\":1}",
"coreInfo": {
"last_app": "",
"last_heard": "2015-08-26T21:35:28.021Z",
"connected": true
}

If you see something like, you're all set - if not, check your credentials and try again.

Troubleshooting

Granted, this is a rather complicated experiment (I'm not even sure that anyone had done this with a Photon yet when we wrote this). Being comfortable with HTML and javaScript is obviously a big help here. Here are a few links that might help you out (they helped me out, anyway) while looking through the code and comments:

Going Further

There is SO much potential to go further with this project. Display the data with the last game played at the top of the table. Add some CSS and make it look pretty. Keep track of player 1 wins vs. player 2 wins over time. Calculate the average margin of victory. Visualize the scores in a graph over time. So. Many. Possibilities. Please share with us if you make some improvements!