Tennis for Two Implemented on the RP2040
by Gabe Taylor and Jack Strope

“A recreation of one of the earliest video games depicting tennis displayed on an oscilloscope.”

Project Introduction

For our final project, we decided to recreate one of the earliest video games, “Tennis for Two”, which was displayed on an oscilloscope. “Tennis for Two'' was created at Brookhaven National Laboratory in 1958 by William Higinbotham and Robert Dvorak. The game follows pretty similar rules to actual tennis with regards to how points are played out (e.g. each player can only hit the ball on their side, the ball can not bounce twice on either side during play, etc.). Our main inspiration for wanting to recreate this game was the challenge of developing a game that could be displayed on an oscilloscope. We both had ample experience with oscilloscopes through previous classes, but using the oscilloscope in xy mode was a new learning experience for us. We also had an interest in tennis which added to our eagerness to recreate this game specifically on the oscilloscope.

High Level Design

Rationale and Sources of Project Idea

As previously mentioned, our project’s inspiration was drawn from the original “Tennis for Two” video game. We were able to find videos on YouTube of the original gameplay and it was helpful for us to see the pace the ball moved at and some of the underlying physics in the game. For example when we were setting parameters such as gravity and the velocity of the ball, we tried to match a similar ball flight as that to the original. We also drew inspiration from the original controllers they designed for the game which consisted of a push button and a knob. When designing and building the controllers, we aimed for creating a very similar model.

Oscilloscope and Background Math

Before delving further into how we designed our “Tennis for Two” game, how we drew to the oscilloscope needs to be discussed. We were able to draw to the oscilloscope by operating it in xy mode. More commonly the oscilloscope is operated in the yt mode. By eliminating the time element and adding the extra x axis, we had much more flexibility to draw than yt mode. Now, the voltage signal that Channel 1 read corresponded to the x position. The voltage signal that Channel 2 read corresponded to the y position. For example, if Channel 1 and Channel 2 were both hooked up to a 1V signal, then we would see a small dot at (x, y) = (1V, 1V) on the oscilloscope.
Because we were able to write points to the oscilloscope, the mathematics we utilized consisted primarily of those related computing trajectories as well as computing angles of the ball after the ball had interacted with a surface. We used the standard trajectory equations that you would find in a first year physics course. The equations that governed the position of the ball are as follows:
x = x0 + t*v0*cos(theta)
y = y0 + t*v0*sin(theta) + 0.5*g*t^2 

x0 and y0 represent the previous position of the ball, t represents the amount of time elapsed since the beginning of the trajectory, v0 is the initial velocity, g is gravity, and theta is the angle at the beginning of the trajectory. g and v0 are constants that we determined based on trial and error of gameplay and the speed of the game that we thought was best. The launch angle, theta , was determined by user input, specifically the knob on each player’s controller. Lastly we kept time t as a counter variable. In our case, we used an interrupt service routine (ISR) to configure a signal from the RP2040 that we sent to a DAC that was then sent to the oscilloscope. This will be discussed further when we talk more about the DAC, but time t was the number of interrupts since the beginning of the trajectory.
We implemented bouncing the ball off of surfaces by calculating the new launch angle theta when the ball hits the ground or net. When the ball hits a surface, the angle just before collision is found by measuring the difference between the x and y coordinates of the ball as it hits the surface and just before the collision, and using these to calculate the incident angle using arctangent. The reflected angle is the same as the incident angle, but reflected about the axis normal to the surface. This is used as the new launch angle for the trajectory equations above. We also implemented some kinetic energy loss on bounces by simply multiplying the initial velocity, v0, by a positive coefficient less than 1. For the net, we made this coefficient 0.5, since a real tennis ball loses a lot of speed when it hits the net. For bouncing off the ground, we found that the first bounce looked best with no loss of speed, which makes sense since real tennis balls lose very little speed when hitting the ground. However, if the ball bounces multiple times–after hitting the net, for example–some damping was necessary for the bouncing to look realistic. So, we set our damping coefficient to 0.9 when bouncing off the ground, but only if the ball has already bounced once, off the net or ground.

Logical Structure

The structure of our code consists of two interrupt service routines and two threads. Both of our interrupt service routines are dedicated to configuring a SPI transaction to a DAC to output what we want to draw to the oscilloscope. One of the service interrupt service routines is responsible for drawing the ball and the other is responsible for drawing the ground and net to the screen. It was crucial for us to use two separate ISRs because the frequency at which each triggers is significantly different. The ISR responsible for drawing the ground and the net fires at a frequency of 66.67 Hz or every 15000 microseconds. The ISR responsible for drawing the ball fires at a frequency of 142.86 Hz or every 7000 microseconds. The frequency that the ball was drawn was balanced mainly on how fast we wanted the game to play out. The noticed that the higher we made the frequency for the net and ground, the noiser the net and ground became. At the same time we needed to draw these components fast enough where you could not see gaps in them. As a result, we found the lowest frequency where we thought the net and ground looked the best.
We had one main thread in which all of our mathematical computations, game scoring logic, and game physics were implemented. This thread was set up in a careful way. Because we calculate the future position of the ball in the thread, we were concerned about how many times the ball’s position could be updated between ISRs. Ideally, we only wanted the ball position to be updated once between ISRs so we would get a smooth and reliable ball motion on the oscilloscope. To achieve this, we set up a semaphore that triggered the game logic thread to run at the end of each ISR that was responsible for drawing the ball. This ensured that the thread would only run once between each ISR.
Our second thread we left in our attached code, but is not visible or utilized by the user. We set up a PuTTY connection to help us debug, specifically when we were determining if our math for determining angles was working. It was really convenient being able to print out the current angle to the PuTTY terminal.

Program/Hardware Design

Program Details

As previously mentioned, our code had two ISRs and two threads. Most of the “meat” of our code is in our game logic thread called game_logic. This is because the two ISRs are really just preparing the values to be sent to the DAC. In the game_logic thread we are essentially calculating the new position of the ball, then we are checking to see if any conditions are met by the new position of the ball to either end the current point, or change the direction of the ball. There are multiple ways that a point can end. They are below:
1. A player hits the ball out of bounds without hitting the other player’s side
2. A player lets the ball bounce twice on their side
3. A player lets the ball bounce once on their side then go out of bounds
4. A player hits the ball into the ground on their own side or it hits the net then bounces onto the ground on their own side

Similarly, there are multiple ways that the ball trajectory can change. They are below:

1. The ball hits the ground
2. A player hits the ball
3. The ball hits the net

Because all of these sudden changes to the game depend on a player's action or where the ball is, we use a lot of conditional statements in this thread to check button presses and ball position. At the end of the thread, we update our score if either player has won a point. This is simply done by setting GPIO pins high to light up an LED on our score tracking device. Our heavily commented code implementation is below in Appendix E.
One more feature we added to the game that hasn’t been mentioned so far is our “loading/start” screen. This screen is displayed after a game is played and when first booting up the RP2040. On this screen, there is a ball traveling in a perfect circle resembling the loading symbol. The game is waiting for either player to hit their respective button. Whichever player hits their button will be the server for the upcoming game. We added this screen in because we wanted the option for either player to be the server and we believed that having a different screen would best indicate this. Specifically we chose to make the loading circle our screen because at the beginning of this project we experimented with drawing trajectories with the direct digital synthesis algorithm as in lab 1. As a result, some of the code we used is repurposed from lab 1 such as building the sin table in the main function. Although the game still looked pretty cool and arcade-like when we first started off using direct digital synthesis to draw circles for trajectories, it looked much more realistic after we implemented the parabolic trajectories instead.

Hardware Details

There were three components to our hardware. The first component was the DAC that we communicated with via a SPI channel to output our desired voltages to the screen of the oscilloscope. The second component was the two controllers that we made. The third component was an LED display to display each player’s score on a separate breadboard.
We used a MCP4822 12-bit DAC. This was the same DAC that we used earlier in ECE 4760 for Lab 1. This DAC was particularly nice for us because it had two channels and we needed two channels (one for x position and one for y position). As a result, we only had to use one DAC. The connections between the RP2040 and the DAC were easy. Pins 2, 3, 4, 5, and 7 of the DAC were SPI connections to the RP2040. Pins 6 and 8 of the DAC were the voltage outputs to channel 1 and channel 2 of the oscilloscope. Lastly, we supplied VDD with an external power supply instead of the 3.3V signal that we could get from the RP2040. This is because the DAC could output voltages up to 4.096V and we wanted to utilize the full range of the DAC. So, the power supply was set at ~4.1V. The pinout for the MCP4822 is shown below and the link to the datasheet is included in the Appendix at the end of the report.

Figure 1: MCP4822 Pinout (from Manufacturer Datasheet)

Our controllers for each player consisted of a push button and knob. The push button was responsible for triggering the action of hitting the ball. The knob was used to allow the player to hit the ball at different angles. Images of both the controllers are shown below in Figure 2 and Figure 3. There are two connections on both of the push buttons. One connection is to the 3.3V signal produced by the RP2040 on the 3.3V pin. The other connection is to a GPIO pin on the RP2040 that we set up as an input pin. Essentially, when the push button is pressed, a short occurs which the input pin can now detect a signal. When the button is not pressed, the input pin does not detect a signal. These buttons were particularly nice because they did not require debouncing. They also had a tactile clicking sound which was aesthetically pleasing and an ode to the original game. Underneath the knob on the controllers is a 10k potentiometer. There are three connections to the potentiometer. The leftmost connection is the 3.3V signal from the 3.3V pin on the RP2040. The rightmost connection is to ground. The middle connection is to one of the ADC pins on the RP2040. This setup is a voltage divider where the output voltage changes as the knob on the potentiometer is turned. The built–in ADC on the RP2040 has a mux which we utilized to switch between the output voltage from the two controller’s potentiometers.

Figure 2: Player 1’s Controller (Player on Left Side of Court)

Figure 3: Player 2’s Controller (Player on Right Side of Court)

The last component of our hardware setup was our scoring tracker device which is displayed in Figure 4 below. This device consisted 4 green LEDs, 4 red LEDs, and 8 110Ω resistors. We set up 8 output GPIO pins on the RP2040 to connect to each of the resistors and LEDs in series. Then each of the negative LED terminals is connected to ground. The resistor value we used was decided based on testing multiple different valued resistors. We found with the 110Ω resistor that the current supplied to the LEDs was really bright for both colored LEDs so we went with that value.

Figure 4: Scoring Tracker Device

Things That We Tried That Did Not Work

The main thing that we tried that did not work as mentioned before was trajectories using direct digital synthesis. We were able to get some of the basic gameplay to work, but it ended up becoming really complicated to implement things such as hitting the ball at a different angle. Ultimately, it was a good switch to parabolic trajectories because it looked much more realistic and was much more simplistic to implement and debug.

Results of Our Design

Below is our final demo video which demonstrates some of the functionality of our game and some of the features we implemented.

We also took some additional footage to show off more of our game. The footage is below.

As a whole, we were really pleased with the usability of our game. Our gameplay was extremely responsive to the player's actions. Also, we believe we found the perfect balance of having enough time to hit the ball without making the game too easy. If the ball moves too slow, then the game can go on for a very long time. We wanted the player's reflexes and reaction time to be tested in our game and we believe that they are.

Conclusion

Overall, our results met our expectations. Having never seen a game on an oscilloscope besides in YouTube videos, we were not too sure what to expect along the way. For example, we found that running our game on different types of oscilloscopes affected the visuals. The oscilloscope in our final demonstration video is the oscilloscope that we had the best results with. If we had more time, it would be interesting to continue experimenting with analog oscilloscopes to bring more of the 1960s vibe to our game. We tried with several different analog oscilloscopes, but some of the analog oscilloscopes had issues due to old age. Then on an analog oscilloscope that we found to be working, our game experienced a lot of perceived lag. This could likely be due to the fact that as we designed our game it was tailored to the digital oscilloscope in our lab. Another interesting follow on would be running this game on a 4 channel oscilloscope. Only having access to 2 channel oscilloscopes, we had to switch between drawing the ball and drawing the net/ground. We believe this created some noise and we were able to see a faint trail between the ball and the net/ground at times. For this reason, we think it would be interesting to see the performance differences on a 4 channel oscilloscope instead. This would require another DAC, so 4 channels could be written to at a time. This should also allow the net and ground to be drawn at a much higher frequency which should decrease the noise around these objects.

We want to note too that we did start our project from the file we used in lab 1. Our code and file looks much different than lab 1, but some of the code regarding direct digital synthesis and setting up the DAC is from the lab 1 beep_beep.c file.

Appendix A (Permission)

The group approves this report for inclusion on the course website.
The group approves the video for inclusion on the course Youtube channel. (Our video has already been posted)

Appendix B (Contributions)

We worked as a pair on almost all aspects of our game. We believe our game required a lot of thought along the way, so we found it best to work as a pair. This also helped us minimize and solve our bugs quickly along the way.

Appendix C (Schematics)

Appendix D (References)

Datasheet link for the MCP4822 12-bit DAC: Datasheet Link
Link to ECE 4760 Lab 1 where we used code to set up DAC and Direct Digital Synthesis: Lab 1

Appendix E (Code)

In [2]:
/**
 *  Jack Strope and Gabe Taylor

    An implementation of Tennis for Two to run on an RP2040 microcontroller
    and displayed on a two-channel oscilloscope.

 */

// Include necessary libraries
#include <stdio.h>
#include <math.h>
#include <string.h>
#include "pico/stdlib.h"
#include "pico/multicore.h"
#include "hardware/spi.h"
#include "hardware/sync.h"
#include "hardware/adc.h"

// Include protothreads
#include "pt_cornell_rp2040_v1.h"

// Include libraries for keypad
#include <stdlib.h>
#include "hardware/pio.h"
#include "hardware/dma.h"

// Player buttons and knobs
#define B1      21
#define B2      22
#define KNOB_1  26
#define KNOB_2  27

// Scoring LEDs
#define GREEN1  16
#define GREEN2  17
#define GREEN3  18
#define GREEN4  19
#define RED1    13
#define RED2    12
#define RED3    11
#define RED4    10


// Macros for fixed-point arithmetic (faster than floating point)
typedef signed int fix15 ;
#define multfix15(a,b) ((fix15)((((signed long long)(a))*((signed long long)(b)))>>15))
#define float2fix15(a) ((fix15)((a)*32768.0))
#define fix2float15(a) ((float)(a)/32768.0)
#define absfix15(a) abs(a)
#define int2fix15(a) ((fix15)(a << 15))
#define fix2int15(a) ((int)(a >> 15))
#define char2fix15(a) (fix15)(((fix15)(a)) << 15)
#define divfix(a,b) (fix15)( (((signed long long)(a)) << 15) / (b))

//Direct Digital Synthesis (DDS) parameters
#define two32 4294967296.0  // 2^32 (a constant)
#define Fs 40000            // sample rate

// the DDS units - core 0
// Phase accumulator and phase increment. Increment sets output frequency.
volatile unsigned int phase_accum_main_0;
volatile unsigned int phase_incr_main_0 = (400.0*two32)/Fs ;

// DDS sine table (populated in main())
#define sine_table_size 256
fix15 sin_table[sine_table_size] ;

// Values output to DAC
int DAC_output_0 ;
int DAC_output_1 ;

// Amplitude for radius of circle in start screen
fix15 radius = float2fix15(0.5) ;

// SPI data
uint16_t DAC_data_x ; // output value
uint16_t DAC_data_y ; // output value

// DAC parameters (see the DAC datasheet)
// A-channel, 1x, active
#define DAC_config_chan_A 0b0001000000000000
// B-channel, 1x, active
#define DAC_config_chan_B 0b1001000000000000

//SPI configurations (note these represent GPIO number, NOT pin number)
#define PIN_MISO 4
#define PIN_CS   5
#define PIN_SCK  6
#define PIN_MOSI 7
#define LDAC     8
#define LED      25
#define SPI_PORT spi0
#define ISR_time 16

volatile int corenum_0 ;
volatile unsigned int frequency = 0;
volatile unsigned int count_0 = 0 ;
volatile int bounce = 0;
volatile bool button1_state = false;
volatile bool button2_state = false;

volatile int angle;
volatile int x;
volatile int x_0 = 0;
volatile int y = 1000;
volatile int y_0 = 1000;
volatile int v0 = 0;
volatile double g = 0;
volatile bool net = false;
volatile bool player = false;   // false == player 1; true == player 2

const float conversion_factor = 1.0f / (1 << 12);
volatile uint16_t result;

volatile bool change0 = true;
volatile bool change1 = true;
volatile int score_1;
volatile int score_2;
volatile bool p1_out;
volatile bool p1_once_out;
volatile bool p1_own_side;
volatile bool p1_bounce_twice;
volatile bool winner;
volatile bool p2_out;
volatile bool p2_once_out;
volatile bool p2_own_side;
volatile bool p2_bounce_twice;
volatile bool stop;
volatile bool gameOver = true;
volatile int last_y;
volatile int last_x;
volatile bool server;
volatile bool changeFromStart;
volatile int gameStartX;

volatile int before_ground_angle = 0;
volatile int bounceCount = 0;

static struct pt_sem game_semaphore ;

// This timer ISR is called on core 0
bool repeating_timer_callback_core_0(struct repeating_timer *t) {

    // Mask with DAC control bits
    DAC_data_y = (DAC_config_chan_B | (y & 0xffff))  ;
    DAC_data_x = (DAC_config_chan_A | (x & 0xffff))  ;

    // SPI write (no spinlock b/c of SPI buffer)
    spi_write16_blocking(SPI_PORT, &(DAC_data_x), 1) ;
    spi_write16_blocking(SPI_PORT, &(DAC_data_y), 1) ;

    // retrieve core number of execution
    corenum_0 = get_core_num() ;
    PT_SEM_SIGNAL(pt, &game_semaphore);
    return true;
}

static PT_THREAD (game_logic(struct pt *pt)){
    PT_BEGIN(pt) ;
    while(1){
        PT_SEM_WAIT(pt, &game_semaphore);
        if (gameOver){  // Start screen; draw circle animation
            frequency = 150;

            phase_incr_main_0 = frequency * two32 / Fs ;
            phase_accum_main_0 += phase_incr_main_0  ;
            if (winner){
                x = fix2int15(multfix15(radius, sin_table[((int)two32/2 + phase_accum_main_0)>>24])) + 2047 ;
                y = fix2int15(multfix15(radius, sin_table[phase_accum_main_0>>24])) + 2047 ;
            }
            else{
                x = fix2int15(multfix15(radius, sin_table[(-(int)two32/2 + phase_accum_main_0)>>24])) + 2047 ;
                y = fix2int15(multfix15(radius, sin_table[phase_accum_main_0>>24])) + 2047 ;
            }

            // Check which player hits the button first and set the ball up for their serve
            if (gpio_get(B1)){
                gameOver = false;
                score_1 = 0;
                score_2 = 0;
                gpio_put(RED1, 0);
                gpio_put(RED2, 0);
                gpio_put(RED3, 0);
                gpio_put(RED4, 0);
                gpio_put(GREEN1, 0);
                gpio_put(GREEN2, 0);
                gpio_put(GREEN3, 0);
                gpio_put(GREEN4, 0);
                server = false;
                player = false;
                x_0 = 50;
                x = 50;
                gameStartX = 50;
            }
            else if (gpio_get(B2)){
                gameOver = false;
                score_1 = 0;
                score_2 = 0;
                gpio_put(RED1, 0);
                gpio_put(RED2, 0);
                gpio_put(RED3, 0);
                gpio_put(RED4, 0);
                gpio_put(GREEN1, 0);
                gpio_put(GREEN2, 0);
                gpio_put(GREEN3, 0);
                gpio_put(GREEN4, 0);
                player = true;
                server = true;
                x_0 = 4040;
                x = 4040;
                gameStartX = 4040;
            }
        }

        else if (!gameOver){
            if (!changeFromStart){
                if (!gpio_get(B2) && !gpio_get(B1)){
                    changeFromStart = true;
                }
            }
            else{
                if (!button1_state && gpio_get(B1) && !player && y < 1570 &&  x < 2046 && change0) {
                button1_state = true;
                change0 = false;
                bounceCount = 0;
                } else {
                    button1_state = false;
                }
                if (!gpio_get(B1)){
                    change0 = true;
                }

                if (!button2_state && gpio_get(B2) && player && y < 1570 && x > 2046 && change1) {
                    button2_state = true;
                    change1 = false;
                    bounceCount = 0;
                } else {
                    button2_state = false;
                }
                if (!gpio_get(B2)){
                    change1 = true;
                }

                if ((button1_state || button2_state)) { // button press
                    if (button2_state && player) { // player 2 presses and it is player 2's turn
                        player = false;
                        adc_select_input(1);
                        result = adc_read();
                        angle = 135 + result*conversion_factor*90;
                    }
                    else if (button1_state && !player) { // player 1 presses and it is player 1's turn
                        player = true;
                        adc_select_input(0);
                        result = adc_read();
                        angle = result*conversion_factor*90 + 315;
                    }
                    v0 = 37;
                    x_0 = x;
                    y_0 = y;
                    g = -0.6;
                    count_0 = 0;
                }
            }


            x = x_0 + v0*count_0*cos(angle*M_PI/180);
            y = y_0 + v0*count_0*sin(angle*M_PI/180) + 0.5*g*count_0*count_0;

            // If the ball hits the ground
            if (y < 50) {
                if (x - last_x > 0){
                    before_ground_angle = 180 + atan2(x - last_x, y - last_y)*180/M_PI;
                    angle = before_ground_angle - 270;
                }
                else{
                    before_ground_angle = atan2(x - last_x, y - last_y)*180/M_PI;
                    angle = before_ground_angle - 90;
                }

                x_0 = x;
                y_0 = 50;
                count_0 = 0;
                bounceCount++;
                if (bounceCount >= 1){
                    if (bounceCount >= 2){
                        v0 *= 0.9;
                    }

                    if (bounceCount == 2 && !stop){
                        if (x > 2046){
                            p2_bounce_twice = true;
                            stop = true;
                        }
                        else {
                            p1_bounce_twice = true;
                            stop = true;
                        }
                    }
                    if (bounceCount == 1 && !stop){
                        if (x > 2046 && !player){

                            p2_own_side = true;
                            stop = true;
                        }
                        else if (x <= 2046 && player){
                            p1_own_side = true;
                            stop = true;
                        }
                    }
                }
            }

            if (bounceCount > 3){
                x = gameStartX;
                x_0 = x;
                y = 1000;
                y_0 = y;
                v0 = 0;
                g = 0;
                net = false;
                player = server;
                bounce = 0;
                bounceCount = 0;
                stop = false;
            }

            // Bounce if the ball hits the net
            if (y < 513 && (x > 2006 && x < 2086)) {
                net = true;
                if (x < 2046) x_0 = 2006;
                else if (x > 2046) x_0 = 2086;
                y_0 = y;
                v0 = 0.5*v0;
                angle += 180;
                count_0 = 0;
                bounce = 1;
            }

            // Out of bounds
            if (x < 0 || x > 4080) {
                if (!p1_bounce_twice && !p2_bounce_twice){
                    if (player && bounceCount < 1 && !stop){
                        p1_out = true;
                        stop = true;
                    }
                    else if (!player && bounceCount < 1 && !stop){

                        p2_out = true;
                        stop = true;
                    }
                    else if (player && bounceCount == 1 && !stop){
                        p2_once_out = true;
                        stop = true;
                    }
                    else if (!player && bounceCount == 1 && !stop){
                        p1_once_out = true;
                        stop = true;
                    }
                }
                x = gameStartX;
                x_0 = x;
                y = 1000;
                y_0 = y;
                v0 = 0;
                g = 0;
                net = false;
                player = server;
                bounce = 0;
                bounceCount = 0;
                stop = false;
            }

            if (y < 0 || y > 4080) {
                x = gameStartX;
                x_0 = x;
                y = 1000;
                y_0 = y;
                v0 = 0;
                g = 0;
                net = false;
                player = server;
                bounce = 0;
                bounceCount = 0;
                stop = false;
            }

            last_y= y;
            last_x= x;

            count_0++;

            if (p1_out || p1_bounce_twice || p1_own_side || p1_once_out){
                score_2++;
                p1_out = false;
                p1_own_side = false;
                p1_bounce_twice = false;
                p1_once_out = false;
            }
            if (p2_out || p2_bounce_twice || p2_own_side || p2_once_out){
                score_1++;
                p2_out = false;
                p2_own_side = false;
                p2_bounce_twice = false;
                p2_once_out = false;
            }

            int oldScore1 = score_1;
            switch(score_1){
                case 0:
                    break;
                case 1:
                    gpio_put(GREEN1, 1);
                    break;
                case 2:
                    gpio_put(GREEN2, 1);
                    break;
                case 3:
                    gpio_put(GREEN3, 1);
                    if (score_2 == 3){
                        gpio_put(GREEN3, 0);
                        score_1 = 2;
                    }
                    break;
                case 4:
                    gpio_put(GREEN4, 1);
                    gameOver = true;
                    changeFromStart = false;
                    winner = false;
                    break;
            }

            switch(score_2){
                case 0:
                    break;
                case 1:
                    gpio_put(RED1, 1);
                    break;
                case 2:
                    gpio_put(RED2, 1);
                    break;
                case 3:
                    gpio_put(RED3, 1);
                    if (oldScore1 == 3){
                        gpio_put(RED3, 0);
                        score_2 = 2;
                    }
                    break;
                case 4:
                    gpio_put(RED4, 1);
                    gameOver = true;
                    changeFromStart = false;
                    winner = true;
                    break;
            }
            gpio_put(LED, changeFromStart);
        }



    }
    PT_END(pt);
}

char keytext[100];
static PT_THREAD (protothread_serial(struct pt *pt))
{
    PT_BEGIN(pt) ;
    while(1) {
        sprintf(pt_serial_out_buffer, "ground: %d; before ground: %d\n", angle, score_2);
        serial_write;
    }
    PT_END(pt) ;
}

// This timer ISR is called on core 0
bool repeating_timer_net(struct repeating_timer *t) {
    if (!gameOver){
        DAC_output_1 = 50;

        // Draw the ground
        DAC_output_0 = 0;
        while (DAC_output_0 < 4096)
        {
            DAC_data_x = (DAC_config_chan_A | (DAC_output_0 & 0xffff));
            DAC_data_y = (DAC_config_chan_B | (DAC_output_1 & 0xffff));

            // SPI write (no spinlock b/c of SPI buffer)
            spi_write16_blocking(SPI_PORT, &DAC_data_x, 1) ;
            spi_write16_blocking(SPI_PORT, &DAC_data_y, 1) ;
            DAC_output_0 += 3;
        }

        // Draw the net
        DAC_output_0 = 2046;
        while (DAC_output_1 < 513){
            DAC_data_x = (DAC_config_chan_A | (DAC_output_0 & 0xffff));
            DAC_data_y = (DAC_config_chan_B | (DAC_output_1 & 0xffff));

            // SPI write (no spinlock b/c of SPI buffer)
            spi_write16_blocking(SPI_PORT, &DAC_data_x, 1) ;
            spi_write16_blocking(SPI_PORT, &DAC_data_y, 1) ;
            DAC_output_1 += 2;
        }
    }


    return true;
}


// Core 0 entry point
int main() {
    // Initialize stdio/uart (printf won't work unless you do this!)
    stdio_init_all();
    printf("Hello, friends!\n");

    // Initialize SPI channel (channel, baud rate set to 20MHz)
    spi_init(SPI_PORT, 20000000) ;
    // Format (channel, data bits per transfer, polarity, phase, order)
    spi_set_format(SPI_PORT, 16, 0, 0, 0);

    // Map SPI signals to GPIO ports
    gpio_set_function(PIN_MISO, GPIO_FUNC_SPI);
    gpio_set_function(PIN_SCK, GPIO_FUNC_SPI);
    gpio_set_function(PIN_MOSI, GPIO_FUNC_SPI);
    gpio_set_function(PIN_CS, GPIO_FUNC_SPI) ;

    // Set up pin for timing ISR
    gpio_init(ISR_time);
    gpio_set_dir(ISR_time, GPIO_OUT);
    gpio_put(ISR_time, 0);

    // Map LDAC pin to GPIO port, hold it low (could alternatively tie to GND)
    gpio_init(LDAC) ;
    gpio_set_dir(LDAC, GPIO_OUT) ;
    gpio_put(LDAC, 0) ;

    // Map LED to GPIO port, make it low
    gpio_init(LED) ;
    gpio_set_dir(LED, GPIO_OUT) ;
    gpio_put(LED, 0) ;


    // Set up button pins
    gpio_init(B1);
    gpio_set_dir(B1, GPIO_IN);
    gpio_pull_down(B1);

    gpio_init(B2);
    gpio_set_dir(B2, GPIO_IN);
    gpio_pull_down(B2);

    //LEDs for Scoring
    gpio_init(RED1) ;
    gpio_set_dir(RED1, GPIO_OUT) ;

    gpio_init(RED2) ;
    gpio_set_dir(RED2, GPIO_OUT) ;

    gpio_init(RED3) ;
    gpio_set_dir(RED3, GPIO_OUT) ;

    gpio_init(RED4) ;
    gpio_set_dir(RED4, GPIO_OUT) ;

    gpio_init(GREEN1) ;
    gpio_set_dir(GREEN1, GPIO_OUT) ;

    gpio_init(GREEN2) ;
    gpio_set_dir(GREEN2, GPIO_OUT) ;

    gpio_init(GREEN3) ;
    gpio_set_dir(GREEN3, GPIO_OUT) ;

    gpio_init(GREEN4) ;
    gpio_set_dir(GREEN4, GPIO_OUT) ;

    gpio_put(RED1, 0);
    gpio_put(RED2, 0);
    gpio_put(RED3, 0);
    gpio_put(RED4, 0);
    gpio_put(GREEN1, 0);
    gpio_put(GREEN2, 0);
    gpio_put(GREEN3, 0);
    gpio_put(GREEN4, 0);

    // Set up the ADC
    adc_init();
    adc_gpio_init(KNOB_1);
    adc_gpio_init(KNOB_2);


    // Build the sine lookup table
    // scaled to produce values between 0 and 4096 (for 12-bit DAC)
    int ii;
    for (ii = 0; ii < sine_table_size; ii++){
         sin_table[ii] = float2fix15(2047*sin((float)ii*6.283/(float)sine_table_size));
    }

    // Create a repeating timer that calls
    // repeating_timer_callback (defaults core 0)
    struct repeating_timer timer_core_0;

    // Negative delay so means we will call repeating_timer_callback, and call it
    // again 25us (40kHz) later regardless of how long the callback took to execute
    add_repeating_timer_us(-7000,
        repeating_timer_callback_core_0, NULL, &timer_core_0);

    struct repeating_timer timer_net;

    add_repeating_timer_us(-15000,
        repeating_timer_net, NULL, &timer_net);

    // Add core 0 threads
    pt_add_thread(protothread_serial) ;
    pt_add_thread(game_logic);

    // Start scheduling core 0 threads
    pt_schedule_start ;

}
  File "<ipython-input-2-6ef297f585ce>", line 1
    **()
     ^
SyntaxError: invalid syntax