John Bradnam
Published © GPL3+

Tennis for Two

Resurrecting Tennis for Two, a video game from 1958. This is arguably the first documented video game.

IntermediateFull instructions provided8 hours931

Things used in this project

Hardware components

Microchip AVR64DD28 Microprocessor
×1
1117-33 3.3V SOT23-5 Regulator
×1
PTV09A-4025F 10K Linear Potentiometer
×2
Tactile Switch, Top Actuated
Tactile Switch, Top Actuated
13mm Shaft with button tops
×2
Stereo Socket PCB mount
×1
DC Power Socket PCB mount
×1
Passive Components
14 x 10K 0805 1% resistors, 18 x 20K 0805 1% resistors, 0.1uF 0805 ceramic capacitor, 100uF/16V radial capacitor.
×1

Software apps and online services

Arduino IDE
Arduino IDE

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)
Soldering iron (generic)
Soldering iron (generic)

Story

Read more

Custom parts and enclosures

STL Files

Files for 3D printing

Article from Creative Computing - 1982 - 10

Schematics

Schematic

PCB

Eagle files

Schematic & PCB in Eagle format

Code

Tennis_V2.ino

Arduino
/**************************************************************************
 Oscilloscope Tennis
 Copyright 2007 Windell H. Oskay

 Author:         Windell Oskay
 Date Created:   7/7/08
 Last Modified:  7/15/08
 Purpose:  Tennis for two
 More complete description here: http://www.evilmadscientist.com/article.php/tennis

 2023-05-22 John Bradnam (jbrad2089@gmail.com)
   Create program for AVR64DD28

 --------------------------------------------------------------------------
 Arduino IDE:
 --------------------------------------------------------------------------
  BOARD: AVR DD-Series (no bootloader)
  Chip: AVR64DD28
  Clock Speed: 24MHz Internal
  millis()/micros() timer: TB2 (default for 28/32 pins)
  MultiVoltage I/O (MVIO): Disabled
  Programmer: SerialUPDI - 230400 baud

  Pins mapped to Ardunio Pins
              _____
   PA7  7   1|*    |28  6   PA6
   PC0  8   2|     |27  5   PA5
   PC1  9   3|     |26  4   PA4
   PC2  10  4|     |25  3   PA3
   PC3  11  5|     |24  2   PA2
   VDD      6|     |23  1   PA1
   PD1  13  7|     |22  0   PA0
   PD2  14  8|     |21      GND
   PD3  15  9|     |20      VDD
   PD4  16 10|     |19  27  PF7 (UPDI)
   PD5  17 11|     |18  26  PF6 (~RESET)
   PD6  18 12|     |17  21  PF1
   PD7  19 13|     |16  20  PF0
   VDD     14|_____|15      GND

 **************************************************************************/

 //X-AXIS DAC PF0, PD1, PD2, PD3, PD4, PD5, PD6, PD7
 //Y-AXIS DAC PA0, PA1, PA2, PA3, PA4, PA5, PA6, PA7

#include <avr/io.h> 
#include <math.h> 
#include <stdlib.h>    //gives rand() function

 //Controls
#define LEFT_POT 10  //PC2
#define LEFT_SW  11  //PC3
#define RIGHT_POT 9  //PC1
#define RIGHT_SW 8   //PC0
#define ADC_LEFT 30  //AIN30
#define ADC_RIGHT 29 //AIN29


#define XOUT(b) (PORTA.OUT = b)
#define YOUT(b) {PORTD.OUT = (b & 0xFE) | (PORTD.IN & 0x01); PORTF.OUT = (b & 0x01) | (PORTF.IN & 0xFE);}

#define g 0.8           //gravitational acceleration (should be positive.)
#define ts 0.025        // TimeStep

#define historyLength 7 

float sintable[64];
float costable[64];

uint8_t xOldList[historyLength];
uint8_t yOldList[historyLength];

float xOld; // a few x & y position values
float yOld; // a few x & y position values


float VxOld; //  x & y velocity values 
float VyOld; //  x & y velocity values 

float Xnew, Ynew, VxNew, VyNew;

uint8_t  deadball = 0;

uint8_t Langle, Rangle;

uint8_t xp = 0;
uint8_t yp = 0;

unsigned int ADoutTemp;

uint8_t NewBall = 101;

unsigned int NewBallDelay = 0;

//Dummy variables: 
uint8_t k = 0;
uint8_t m = 0;

uint8_t server = ADC_LEFT;
//uint8_t CheckNet = 0;

uint8_t ballside;
uint8_t Lused = 0;
uint8_t Rused = 0;

void setup()
{
  // Create trig look-up table to keep things snappy.
  // 64 steps, total range: near pi.  Maybe a little more. 

  uint8_t m = 0;
  while (m < 64)
  {
    sintable[m] = sin((float) 0.0647 * (float)m - (float) 2.07);
    costable[m] = cos((float) 0.0647 * (float)m - (float) 2.07);
    m++;
  }

  yOld = 0;
  VyOld = 0;

  //Inputs:
  pinMode(LEFT_POT, INPUT);
  pinMode(LEFT_SW, INPUT_PULLUP);
  pinMode(RIGHT_POT, INPUT);
  pinMode(RIGHT_SW, INPUT_PULLUP);

  //Outputs:
  //DAC pins are outputs
  PORTA.DIRSET = 0xFF;
  PORTD.DIRSET = 0xFE;
  PORTF.DIRSET = 0x01;
  //Set to zero
  XOUT(0);
  YOUT(0);
/*
  // ADC Setup
  PRR &= ~(_BV(ICF1));  //Allow ADC to be powered up

  //ADC 3-5
  ADMUX = server * 4;
  ADCSRA = 197;  // Enable ADC & start, prescale at 32
*/
  //Setup ADC
  VREF.ADC0REF = VREF_REFSEL_1V024_gc;
  ADC0.MUXPOS = server;
  ADC0.CTRLC = ADC_PRESC_DIV256_gc;                   // 94kHz clock
  ADC0.CTRLA = ADC_RESSEL_10BIT_gc | ADC_ENABLE_bm;   // Single, 10-bit

  ballside = 0;
}

void loop()
{
  if (ballside != (xOld >= 127))
  {
    ballside = (xOld >= 127);

    if (ballside)
      Rused = 0;
    else
      Lused = 0;

    //CheckNet = 1;
  }

  if (NewBall > 10) // IF ball has run out of energy, make a new ball!
  {
    NewBall = 0;
    deadball = 0;
    NewBallDelay = 1;
    server = (ballside == 0) ? ADC_LEFT : ADC_RIGHT;

    if (server == ADC_LEFT)
    {
      xOld = (float)230;
      VxOld = 0;// (float) -2*g; 
      ballside = 1;
      Rused = 0;
      Lused = 1;
    }
    else
    {
      xOld = (float)25;
      VxOld = 0;// (float) 2*g; 
      ballside = 0;
      Rused = 1;
      Lused = 0;
    }

    yOld = (float)110;

    m = 0;
    while (m < historyLength)
    {
      xOldList[m] = xOld;
      yOldList[m] = yOld;
      m++;
    }
  }

  // Physics time!
  // x' = x + v*t + at*t/2
  // v' = v + a*t
  //
  // Horizontal (X) axis: No acceleration; a = 0.
  // Vertical (Y) axis: a = -g

  if ((ADC0.COMMAND & ADC_STCONV_bm) == 0)		// If ADC conversion has finished
  {
    ADoutTemp = ADC0.RES;			// Read out ADC value	

    /* We are using *ONE* ADC, but sequentially multiplexing it to sample
    the two different input lines.	*/

    if (ADC0.MUXPOS == ADC_LEFT)
      Langle = ADoutTemp >> 4; //ADoutTemp >> 2;
    else
      Rangle = ADoutTemp >> 4; // ADoutTemp >> 2;

    // 64 angles allowed
    ADC0.MUXPOS = (ballside) ? ADC_LEFT : ADC_RIGHT;
    ADC0.COMMAND = ADC_STCONV_bm;	// Start new ADC conversion 
  }

  if (NewBallDelay)
  {
    if (digitalRead(LEFT_SW) == 0 || digitalRead(RIGHT_SW) == 0)
      NewBallDelay = 10000;

    NewBallDelay++;

    if (NewBallDelay > 5000)	// was 5000
      NewBallDelay = 0;

    m = 0;
    while (m < 255)
    {
      YOUT(yp);
      XOUT(xp);
      m++;
    }

    VxNew = VxOld;
    VyNew = VyOld;
    Xnew = xOld;
    Ynew = yOld;
  }
  else
  {
    Xnew = xOld + VxOld;
    Ynew = yOld + VyOld - 0.5*g*ts*ts;

    VyNew = VyOld - g*ts;
    VxNew = VxOld;

    // Bounce at walls
    if (Xnew < 0)
    {
      VxNew *= -0.05;
      VyNew *= 0.1;
      Xnew = 0.1;
      deadball = 1;
      NewBall = 100;
    }

    if (Xnew > 255)
    {
      VxNew *= -0.05;
      Xnew = 255;
      deadball = 1;
      NewBall = 100;
    }

    if (Ynew <= 0)
    {
      Ynew = 0;

      if (VyNew*VyNew < 10)
        NewBall++;

      if (VyNew < 0)
        VyNew *= -0.75;
    }

    if (Ynew >= 255)
    {
      Ynew = 255;
      VyNew = 0;
    }

    if (ballside)
    {
      if (Xnew < 127)
      {
        if (Ynew <= 63)
        {
          // Bounce off of net
          VxNew *= -0.5;
          VyNew *= 0.5;
          Xnew = 128.00;
          deadball = 1;

        }
      }
    }

    if (ballside == 0)
    {
      if (Xnew > 127)
      {
        if (Ynew <= 63)
        {
          // Bounce off of net
          VxNew *= -0.5;
          VyNew *= 0.5;
          Xnew = 126.00;
          deadball = 1;
        }
      }
    }

    // Simple routine to detect button presses: works, if the presses are slow enough.
    if (xOld < 120)
    {
      if (digitalRead(LEFT_SW) == 0)
      {
        if ((Lused == 0) && (deadball == 0))
        {
          VxNew = 1.5*g*costable[Langle];
          VyNew = g + 1.5*g*sintable[Langle];

          Lused = 1;
          NewBall = 0;
        }
      }
    }
    else if (xOld > 134)	// Ball on right side of screen
    {
      if (digitalRead(RIGHT_SW) == 0)
      {
        if ((Rused == 0) && (deadball == 0))
        {
          VxNew = -1.5*g*costable[Rangle];
          VyNew = g + -1.5*g*sintable[Rangle];

          Rused = 1;
          NewBall = 0;
        }
      }
    }
  }

  //Figure out which point we're going to draw. 
  xp = (int)floor(Xnew);
  yp = (int)floor(Ynew);
  //yp = 511 - (int) floor(Ynew);

  //Draw Ground and Net

  k = 0;
  while (k < 20)
  {
    k++;

    m = 0;
    while (m < 127)
    {
      YOUT(0);		// Y-position
      XOUT(m);		// X-position

      m++;
    }

    XOUT(127);   // X-position of NET

    m = 0;
    while (m < 61)
    {
      YOUT(m);		// Y-position
      m += 2;
    }

    while (m > 1)
    {
      YOUT(m);		// Y-position
      m -= 2;
    }

    YOUT(0);		 // Y-position
    XOUT(127);   //Redundant, but allows time for scope trace to catch up.

    m = 127;
    while (m < 255)
    {
      YOUT(0);		// Y-position
      XOUT(m);		// X-position
      m++;
    }
  }

  m = 0;
  while (m < historyLength)
  {
    k = 0;
    while (k < (4 * m*m))
    {
      XOUT(xOldList[m]);
      YOUT(yOldList[m]);
      k++;
    }
    m++;
  }


  // Write the point to the buffer
  YOUT(yp);
  XOUT(xp);

  m = 0;
  while (m < (historyLength - 1))
  {
    xOldList[m] = xOldList[m + 1];
    yOldList[m] = yOldList[m + 1];
    m++;
  }

  xOldList[(historyLength - 1)] = xp;
  yOldList[(historyLength - 1)] = yp;

  m = 0;
  while (m < 100)
  {
    YOUT(yp);
    XOUT(xp);
    m++;
  }

  //Age variables for the next iteration
  VxOld = VxNew;
  VyOld = VyNew;

  xOld = Xnew;
  yOld = Ynew;
}

Credits

John Bradnam

John Bradnam

141 projects • 167 followers
Thanks to Windell Oskay.

Comments