Foosball Scoring with Raspberry Pi


If you know me, you know besides corgis I have another passion and that’s for foosball. I might not be tournament level good, but I can keep up with my friends and in the bar scene. I had an old sporting goods store level table when I was in high school and got rid of it when I moved out of my folk’s house. I swore I’d never buy another one because no one would ever come over and play and every once in a while, during my adult life, I’d see a nice one pop up on Craigslist for a good price. I always passed up the chance of getting a new one. Until one day when I saw a Tornado going for around 400 bucks. I couldn’t pass up the deal. A few weeks after getting it setup in our sunroom, I was going through some boxes and found an old Raspberry Pi that I used to use for playing emulated games on. I thought to myself “what would be a good project to use this on?” and then it dawned on me. I should automate a scoreboard for the foosball table.

I took a look around the internet to see if anyone else had done an implementation of a Pi like this, but couldn’t find too much information. Which is a good thing because I feel like a lot of the time with Pi projects a lot of the coding is already done for you. This was going to have to be a completely from scratch project, and that excited me.

What I did gather from internets, was that instead of using trip wires, IR break beam sensors were a better solution for detecting something crossing a path. After a quick look around the foosball table, I decided underneath it, in the ball return would be a good place to mount them. I bought a few off eBay and then went down to the local Microcenter to get the rest of the materials needed.

I also needed a way to display the current score as well as how many matches had been won. There were a number of options, but I decided to go with a simple dot matrix display to show the current number of goals each player has made as well as a best out of 5 games display.

The original implementation had 3 buttons. One for a tournament reset and two to manually add goals if the IR sensor missed one. After testing out the IR sensors, I decided they were 99% reliable, so the manual score buttons weren’t needed. In addition to that, the buttons I bought off of Amazon were terrible and I soon replaced the “New Game” button with a traditional arcade style Start button.

If I was a good blogger, I would have created a nice wiring diagram to show how this was all wired up, but instead I’ll just give you a picture of the mess of wires below.

Mess of wires

As you can see the breadboard was minimally used, but I do remember having to mess with boot config in order to use both Serial Peripheral Interfaces (SPI) at the same time. You can see how to do that through this KB: https://raspberrypi.stackexchange.com/questions/61430/i-want-to-use-spi0-and-spi1-at-the-same-time

This was because the dot matrix displays had to use this interface and chaining 4 displays together with 6 feet of jumper wires between them didn’t seem like a good idea. Also through my testing, it seemed like going with anything longer than 6 inches between displays caused issues with the signals being sent.

Once I had everything wired up, it was time to star writing some code to interact with all the pieces. Again, there’s nothing really out there on the internet that I could find, so a lot of this was trial and error. It was a great learning experience because of that.

Instead of mounting everything to the table right from the get go, I attached it all to an old shoebox just to make sure the proof of concept would actually work before drilling holes and screwing things into my newly purchased table. After a week of trail and error I got something that worked:

I haven’t found a good way of attaching the Pi to the table so for now it sitting under it on a table of its own, but things are working! My buddy Kyle, another foosball enthusiast, helped install the sensors and did some initial testing with me. We discovered sometimes the ball would bounce off of the return shoot and score again if it had a lot of speed on it going into the goal. A small tweak of the code to keep track of time in between scores helped out with not double scoring.

The only other challenge I had in building all of this was how could I get the Pi to boot straight into the program and start scoring. I didn’t want to have a screen and mouse out to start the program myself every time. So after some searching around, I settled with doing so based off of the traditional method that can be found at the bottom of this forum topic: https://forums.raspberrypi.com/viewtopic.php?t=294014

The only problem was that the program would start and quit immediately, but that was remedied by adding a simple GUI.

All in all, I think this was a good use of the Pi that I never found a use for as well as it being a great learning experience with some Python I’d never write while working with data in my day to day.

I’ve added the code below if anyone in the future is interested in doing the same thing:

#!/usr/bin/python3

import tkinter as tk
import time
import RPi.GPIO as GPIO
from luma.led_matrix.device import max7219
from luma.core.interface.serial import spi, noop
from luma.core.render import canvas
from luma.core.legacy import text, show_message

YELLOW_SERIAL = spi(port=0, device=0, gpio=noop())
YELLOW_DEVICE = max7219(YELLOW_SERIAL, width=16, height=8, block_orientation=90)

BLACK_SERIAL = spi(port=1, device=0, gpio=noop())
BLACK_DEVICE = max7219(BLACK_SERIAL, width=16, height=8, block_orientation=90)

#dot matrix display for games won
GAME_ARR = (((0, 0, 0, 0, 0, 0, 0, 0),
            (0, 0, 0, 0, 0, 0, 0, 0),
            (0, 0, 0, 0, 0, 0, 0, 0),
            (0, 0, 0, 0, 0, 0, 0, 0),
            (0, 0, 0, 0, 0, 0, 0, 0),
            (0, 0, 0, 0, 0, 0, 0, 0),
            (0, 0, 0, 0, 0, 0, 0, 0),
            (0, 0, 0, 0, 0, 0, 0, 0))
          ,((1, 1, 1, 1, 1, 1, 1, 1),
            (1, 1, 1, 1, 1, 1, 1, 1),
            (0, 0, 0, 0, 0, 0, 0, 0),
            (0, 0, 0, 0, 0, 0, 0, 0),
            (0, 0, 0, 0, 0, 0, 0, 0),
            (0, 0, 0, 0, 0, 0, 0, 0),
            (0, 0, 0, 0, 0, 0, 0, 0),
            (0, 0, 0, 0, 0, 0, 0, 0))
          ,((1, 1, 1, 1, 1, 1, 1, 1),
            (1, 1, 1, 1, 1, 1, 1, 1),
            (0, 0, 0, 0, 0, 0, 0, 0),
            (1, 1, 1, 1, 1, 1, 1, 1),
            (1, 1, 1, 1, 1, 1, 1, 1),
            (0, 0, 0, 0, 0, 0, 0, 0),
            (0, 0, 0, 0, 0, 0, 0, 0),
            (0, 0, 0, 0, 0, 0, 0, 0),
            (0, 0, 0, 0, 0, 0, 0, 0))
          ,((1, 1, 1, 1, 1, 1, 1, 1),
            (1, 1, 1, 1, 1, 1, 1, 1),
            (0, 0, 0, 0, 0, 0, 0, 0),
            (1, 1, 1, 1, 1, 1, 1, 1),
            (1, 1, 1, 1, 1, 1, 1, 1),
            (0, 0, 0, 0, 0, 0, 0, 0),
            (1, 1, 1, 1, 1, 1, 1, 1),
            (1, 1, 1, 1, 1, 1, 1, 1)))

IR_YELLOW = 4
IR_BLACK = 17
BTN_NEW = 27

SCORE_TO_WIN = 5
MATCHES_TO_WIN = 3

YELLOW_SCORE = 0
YELLOW_WINS = 0
BLACK_SCORE = 0
BLACK_WINS = 0
SECS_BETWEEN_SCORES = 3
TIME_OF_LAST_SCORE = time.time()

def break_beam_callback(channel):
    global YELLOW_SCORE, BLACK_SCORE, YELLOW_WINS, BLACK_WINS, YELLOW_DEVICE
    global ZRO_GAME, ONE_GAME, TWO_GAME, TRE_GAME
    global TIME_OF_LAST_SCORE
    retry_count = 0
    now = time.time()
    print("Time diff:" + str(int(now - TIME_OF_LAST_SCORE)))
    if int(now - TIME_OF_LAST_SCORE) > SECS_BETWEEN_SCORES:
        if YELLOW_WINS < MATCHES_TO_WIN and BLACK_WINS < MATCHES_TO_WIN:
            if not GPIO.input(IR_YELLOW):
                YELLOW_SCORE += 1
                TIME_OF_LAST_SCORE = time.time()
                if YELLOW_SCORE < SCORE_TO_WIN:    
                    print("Yellow Score: " + str(YELLOW_SCORE))
                else:
                    YELLOW_WINS += 1
                    print("Yellow wins match!")
                    BLACK_SCORE = 0
                    YELLOW_SCORE = 0                
              
                if YELLOW_WINS == MATCHES_TO_WIN:
                    print("Yellow wins tournament!")

            if not GPIO.input(IR_BLACK): 
                BLACK_SCORE += 1   
                TIME_OF_LAST_SCORE = time.time()
                if BLACK_SCORE < SCORE_TO_WIN:
                    print("Black Score: " + str(BLACK_SCORE))
                else:
                    BLACK_WINS += 1
                    print("Red wins match!")
                    BLACK_SCORE = 0
                    YELLOW_SCORE = 0                
                    
                if BLACK_WINS == MATCHES_TO_WIN:
                    print("Black wins tournament!")
                    
            #update device with everything at once instead of trying to isolate updates to a single display at a time
            draw_on_device(YELLOW_DEVICE, YELLOW_WINS, YELLOW_SCORE, BLACK_DEVICE, BLACK_WINS, BLACK_SCORE)
        else:
            print("Press green button for new game")
    else:
        print("Not enough time has passed for another score")
         

def btn_callback(channel):
    global YELLOW_SCORE, BLACK_SCORE, YELLOW_WINS, BLACK_WINS
    YELLOW_SCORE = 0
    YELLOW_WINS = 0
    BLACK_SCORE = 0
    BLACK_WINS = 0
    YELLOW_DEVICE.clear()
    BLACK_DEVICE.clear()
    draw_on_device(YELLOW_DEVICE, 0, 0, BLACK_DEVICE, 0, 0)
    print("New game")
        
def draw_on_device(y_device, y_win, y_num, b_device, b_win, b_num):
    with canvas(y_device) as draw:
        text(draw, (0, 0), str(y_num), fill="white")
        for x in range(0, 8):   
            for y in range(0, 8):
                if GAME_ARR[y_win][x][y] == 1:
                    draw.point((x + 8, y), fill="white")
    with canvas(b_device) as draw:
        text(draw, (0, 0), str(b_num), fill="white")
        for x in range(0, 8):   
            for y in range(0, 8):
                if GAME_ARR[b_win][x][y] == 1:
                    draw.point((x + 8, y), fill="white")
        
if __name__ == "__main__":
    YELLOW_DEVICE.clear()
    BLACK_DEVICE.clear()
    draw_on_device(YELLOW_DEVICE, 0, 0, BLACK_DEVICE, 0, 0)
        
    GPIO.setwarnings(False)
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(IR_YELLOW, GPIO.IN, pull_up_down=GPIO.PUD_UP)
    GPIO.add_event_detect(IR_YELLOW, GPIO.BOTH, callback=break_beam_callback)
    GPIO.setup(IR_BLACK, GPIO.IN, pull_up_down=GPIO.PUD_UP)
    GPIO.add_event_detect(IR_BLACK, GPIO.BOTH, callback=break_beam_callback)
    
    GPIO.setup(BTN_NEW, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
    GPIO.add_event_detect(BTN_NEW, GPIO.RISING, callback=btn_callback, bouncetime=300)

    top = tk.Tk()
    title = tk.Label(
        text="Foosball Score 9000",
        width=20,
        height=10
    )
    title.pack()
    
    top.mainloop()

Leave a Reply

Your email address will not be published. Required fields are marked *