Final project - Fabio Coelho Ribeiro

First idea and explanation

If you read the section of what I like, you can see I really like video games ever since I was young.

For a few years now, I've always wanted an arcade machine, so for the occasion and with the opportunity of the Fab Academy, I asked myself : "Why not make one ?".

The main idea is to have an arcade machine with lots of games that can be chosen from an interface and play with the different controls on the arcade. I also want to have a plug and play system where games are saved in a storage device and be able to take it off or interact with it to freely add or delete games.

Here's a basic sketch of what I'm aiming for :

I got the inspiration by looking at different arcade machines that exist and chose the one that I prefer (it comes from the following website : Dragon ball arcade machine).

Here's a list of it's components :

  1. Body : Made out of wood (MDF or plywood, not too thick or thin)
  2. Controls : 6 push-buttons to play, 2 push-buttons for menus and a joystick
  3. video output : Monitor (VGA or HDMI)
  4. Sound output : Speaker(s)
  5. Games : Start with simple games made with PyGame, then, if I have the time, download games trough the game engine Godot and load them in a storage device.
  6. Additional ideas : Lights, stickers, special ports for additional control panel, arm rest, foot rest, ...

Here's a block diagram :

To run games, I'm going to need a SBC (single board computer) because basic microcontrollers aren't powerful enough, for now, I'll use a Raspberry pi 3 (model B) (I eventually switched to a Pi 5).



Making the project

Enclosure

For the enclosure, I'll use the prototype I made on Week 07 (computer-controlled machining) and modify it to integrate everything together.

I also painted it black to have a better visual and because 2 types of wood were used which gave it a weird look.

Here's the final result after painting and assembling everything :



Monitor

To integrate my monitor to my project, I had to disassemble it to make measurements of the "skeleton" and make a new frame compatible with the enclosure.

After noting every important measurements, I made two new frames where the idea is to simply sandwich the monitor.

I then use a CNC machine to make the 2 pieces.

I had to make a second front frame because the first one was a bit too short and the monitor couldn't fit.

I eventually had to use metal files to embiggen the holes of the monitor buttons.

(I also had to add a tiny piece of wood to fix the Buttons PCB because it would move)

I used lock nuts to make sure to tighten the frames together.

Here's the final result :

Here's the final result after painting :



Back door

To get access to the inside of the project without having to dismantle everything, I modified the back to have a door and a locking system.

I then use these hinges to make the door.

Then, I add a 3D handle to easily open or close it as well as locking pieces to keep it locked.

To fix the 3D pieces to the enclosure, I use threaded inserts that i solder into the designated holes.

Here's the final result :

Here's the final result after painting and assembling :



PCB modules

Based on the blocks on the block diagram above, I can already make the PCBs of my arcade machine. I'll first do the input modules and then make the microcontroller PCB based on them to be sure the connections with the IDC connectors and ribbon cables are correct.

The PCBs were made with the Bantam PCB milling machine with different end mills for tracks and holes/edges.

I have to load the correct gerber files or the software will give me weird results : front/top layer, bottom layer and PTH (holes layer) :

  1. Joystick

    Input module to control the interface and play games.

    (End mills used : tracks : 0.3mm, holes/edges : 0.6mm)

  2. Buttons

    Input module to control the interface and play games.

    (End mills used : tracks : 0.3mm, holes/edges : 1mm)

  3. Buttons right and left

    Input modules to close the current game and close or launch the interface.

    (Button right module)

    (Button left module)

    (End mills used : tracks : 0.3mm, holes/edges : 0.6mm)

  4. Microcontroller

    Main module that receives the data from the input modules and sends it to te Raspberry pi 5.

    (End mills used : tracks : 0.5mm, holes/edges : 0.9mm)

I sometimes wanted to take pictures of the PCB after the tracks or holes/edges operations and this unfortunately messed up the origin of the machine which led me to drilling errors like these :

At some point, I also had to remake the PCB modules to make them bigger to have fixation holes that wont disturb the 3D buttons.

(Old PCB modules)



PCB modules integration

The PCB modules will be fixed on the controller board, which I had to remake to have the good dimensions for the buttons, the joystick hole and the fixation holes.

The buttons and the joystick are made in 3D, I first made arrays of holes, each with different dimensions to choose the best one.

After trying the holes, I choose the best ones, create a first design for both components and print them.

After trying the 3D pieces, I modify them slightly to match the holes (mainly the buttons) and 3D print the final versions.

When everything is ready, I can assemble the PCB modules on the controller board and add the ribbon cables.

I also added 2 stickers above the white buttons to be easily identified because they are menu buttons.



Raspberry Pi 5 integration

The Raspberry Pi 5 will be placed on a 3D holder which is fixed to the enclosure, there is also a hole in one of the wood pieces inside the enclosure that leads to the back door section where the cables for the power supply and monitor cable will pass through.

(At first, I used a Raspberry Pi 3, which is why the case changes color between pictures.)



Interface and games

The Raspberry pi 5's purpose is to handle the interface and games.

I plan on making an interface and make simple games using Pygame at first and if I have time, try to implement more complex games.

The first thing to do when using a Raspberry Pi is to install its OS, I can do that using the official Raspberry Pi Imager and a 32GB microSD card.

I choose the Raspberry Pi model :

I choose the desired OS :

I choose the microSD card :

The following parameters to enter some host preferences :

Host name :

Localization :

Username and password :

Then I can press the button Ecrire which will install the OS in the microSD card :

Finally, I can insert the microSD card into the Raspberry pi 5, power it using its power supply and with an additional mouse, keyboard and monitor, interact with it.

Result on the arcade machine monitor :

Now that everything is ready, I can start coding the USB data reading, handling, the interface and the games using Thonny which is already pre-installed on the Pi.

To start, I need to install pygame and python 3.11 (best for pygame compatibility) on my Raspberry using the terminal and bash commands :

 
                cd /usr/src
                sudo wget https://www.python.org/ftp/python/3.11.8/Python-3.11.8.tgz
                sudo tar xzf Python-3.11.8.tgz
                cd Python-3.11.8
                sudo ./configure --enable-optimizations
                sudo make -j4
                sudo make altinstall
                python3.11 -m pip install pygame
              

Then, I heavily rely on ChatGPT to make the interface and games exactly how I want them to look and work like.

  1. Controller

    Code of the Raspberry Pi Pico, sends data based on the buttons pressed or the joystick's position.

     
                        from machine import Pin, ADC
                        import time
    
                        # Joystick
                        Joystick_Axis_X = ADC(Pin(26))
                        Joystick_Axis_Y = ADC(Pin(27))
                        Joystick_Push_button = Pin(28, Pin.IN)
    
                        # Button right
                        Button_Right = Pin(15, Pin.IN)
    
                        # Button left
                        Button_Left = Pin(16, Pin.IN)
    
                        # Buttons
                        Buttons_A = Pin(0, Pin.IN)
                        Buttons_B = Pin(1, Pin.IN)
                        Buttons_C = Pin(2, Pin.IN)
                        Buttons_D = Pin(5, Pin.IN)
                        Buttons_E = Pin(4, Pin.IN)
                        Buttons_F = Pin(3, Pin.IN)
    
                        while True:
                            # Joystick reading
                            Joystick_Axis_X_Value = Joystick_Axis_X.read_u16()
                            Joystick_Axis_Y_Value = Joystick_Axis_Y.read_u16()
                            Joystick_Push_button_Value = Joystick_Push_button.value()
    
                            # Button right reading
                            Button_Right_Value = Button_Right.value()
                            
                            # Button left reading
                            Button_Left_Value = Button_Left.value()
                            
                            # Buttons reading
                            Buttons_A_Value = Buttons_A.value()
                            Buttons_B_Value = Buttons_B.value()
                            Buttons_C_Value = Buttons_C.value()
                            Buttons_D_Value = Buttons_D.value()
                            Buttons_E_Value = Buttons_E.value()
                            Buttons_F_Value = Buttons_F.value()
    
                            # Joystick data transmission
                            if Joystick_Axis_X_Value < 20000:
                                print("LEFT")
                            if Joystick_Axis_X_Value > 40000:
                                print("RIGHT")
                            if Joystick_Axis_Y_Value < 20000:
                                print("DOWN")
                            if Joystick_Axis_Y_Value > 40000:
                                print("UP")
                            if (Joystick_Axis_X_Value > 20000) and (Joystick_Axis_X_Value < 40000) and 
                               (Joystick_Axis_Y_Value > 20000) and (Joystick_Axis_Y_Value < 40000):
                                print("STOP")
                            if Joystick_Push_button_Value == 1:
                                print("PB")
                                
                            # Button right data transmission
                            if Button_Right_Value == 1:
                                print("CLOSE")
                            
                            # Button left data transmission
                            if Button_Left_Value == 1:
                                print("RASP")
                            
                            # Buttons data transmission
                            if Buttons_A_Value == 1:
                                print("A")
                            if Buttons_B_Value == 1:
                                print("B")
                            if Buttons_C_Value == 1:
                                print("C")
                            if Buttons_D_Value == 1:
                                print("D")
                            if Buttons_E_Value == 1:
                                print("E")
                            if Buttons_F_Value == 1:
                                print("F")
    
                            # Delay
                            time.sleep(0.2)
                      

  2. Interface

    Initial screen of the arcade machine, let's me choose the games to play.

     
                        import pygame
                        import sys
                        from usb_reader import USBReader
                        import subprocess
    
                        # ----------------------------
                        # USB START
                        # ----------------------------
                        usb = USBReader()
    
                        # ----------------------------
                        # PYGAME SETUP
                        # ----------------------------
                        pygame.init()
                        pygame.mouse.set_visible(False)
    
                        WIDTH, HEIGHT = 1440, 900
                        screen = pygame.display.set_mode((WIDTH, HEIGHT))
                        pygame.display.set_caption("ARCAGEDDON UI")
    
                        bg_image = pygame.image.load("background.jpg").convert()
                        bg_image = pygame.transform.scale(bg_image, (WIDTH, HEIGHT))
    
                        clock = pygame.time.Clock()
    
                        # ----------------------------
                        # COLORS
                        # ----------------------------
                        BG = (18, 18, 18)
                        BTN = (70, 130, 200)
                        BTN_SELECTED = (255, 180, 50)
                        WHITE = (255, 255, 255)
                        YELLOW = (255, 215, 0)
    
                        # ----------------------------
                        # FONTS
                        # ----------------------------
                        font = pygame.font.SysFont(None, 45)
                        title_font = pygame.font.SysFont("Press Start 2P", 80)
    
                        # ----------------------------
                        # BUTTON CLASS
                        # ----------------------------
                        class Button:
                            def __init__(self, x, y, w, h, text):
                                self.rect = pygame.Rect(x, y, w, h)
                                self.text = text
    
                            def draw(self, surface, selected=False):
                                color = BTN_SELECTED if selected else BTN
                                pygame.draw.rect(surface, color, self.rect, border_radius=12)
    
                                label = font.render(self.text, True, WHITE)
                                surface.blit(label, label.get_rect(center=self.rect.center))
    
                        # ----------------------------
                        # LAYOUT
                        # ----------------------------
                        menu_btn_w, menu_btn_h = 250, 80
                        btn_w, btn_h = 260, 80
                        spacing = 30
    
                        center_x = WIDTH // 2 - btn_w // 2
                        start_y = HEIGHT // 2 - 140
    
                        center_buttons = [
                            Button(center_x, start_y, btn_w, btn_h, "PING"),
                            Button(center_x, start_y + btn_h + spacing, btn_w, btn_h, "COBRA"),
                            Button(center_x, start_y + 2 * (btn_h + spacing), btn_w, btn_h, "N.A."),
                        ]
    
                        menu_button = Button(
                            WIDTH - menu_btn_w - 30,
                            HEIGHT - menu_btn_h - 30,
                            menu_btn_w,
                            menu_btn_h,
                            "INPUTS CHECK"
                        )
    
                        # ----------------------------
                        # STATE
                        # ----------------------------
                        selected_index = 0
                        saved_index = 0
                        focus_mode = "CENTER"
                        game_process = None
                        input_process = None
    
                        # ----------------------------
                        # MAIN LOOP
                        # ----------------------------
                        running = True
    
                        while running:
                            screen.blit(bg_image, (0, 0))
    
                            pygame.draw.rect(
                                screen,
                                YELLOW,
                                screen.get_rect(),
                                width=10
                            )
    
                            # ----------------------------
                            # CHECK GAME STATUS
                            # ----------------------------
                            if game_process is not None and game_process.poll() is not None:
                                game_process = None
    
                            # ----------------------------
                            # USB INPUT (SAFE POLLING)
                            # ----------------------------
                            cmd = usb.read()
    
                            # EMERGENCY EXIT COMMAND
                            if cmd == "RASP":
                                running = False
    
                            if cmd:
                                if focus_mode == "CENTER":
    
                                    if cmd == "UP":
                                        selected_index = (selected_index - 1) % len(center_buttons)
    
                                    elif cmd == "DOWN":
                                        selected_index = (selected_index + 1) % len(center_buttons)
    
                                    elif cmd == "RIGHT":
                                        saved_index = selected_index
                                        focus_mode = "MENU"
    
                                    elif cmd == "A":
                                        if game_process is None:
                                            selected_game = center_buttons[selected_index].text
    
                                            if selected_game == "PING":
                                                game_process = subprocess.Popen(
                                                    [sys.executable, "game_1.py"],
                                                    stdin=subprocess.PIPE,
                                                    text=True
                                                )
    
                                            elif selected_game == "COBRA":
                                                game_process = subprocess.Popen(
                                                    [sys.executable, "game_2.py"],
                                                    stdin=subprocess.PIPE,
                                                    text=True
                                                )
    
                                            elif selected_game == "N.A.":
                                                game_process = subprocess.Popen(
                                                    [sys.executable, "game_3.py"],
                                                    stdin=subprocess.PIPE,
                                                    text=True
                                                )
    
                                elif focus_mode == "MENU":
    
                                    if cmd == "LEFT":
                                        focus_mode = "CENTER"
                                        selected_index = saved_index
    
                                    elif cmd == "A":
                                        if input_process is None or input_process.poll() is not None:
    
                                            input_process = subprocess.Popen(
                                                [sys.executable, "input.py"],
                                                stdin=subprocess.PIPE,
                                                text=True
                                            )
    
                            # ----------------------------
                            # SEND INPUT TO INPUTS CHECK
                            # ----------------------------
                            if input_process is not None and input_process.poll() is None:
                                try:
                                    input_process.stdin.write(cmd + "\n")
                                    input_process.stdin.flush()
                                except:
                                    pass
    
                            # ----------------------------
                            # SEND INPUT TO GAME
                            # ----------------------------
                            if game_process is not None and game_process.poll() is None:
                                try:
                                    if cmd:
                                        game_process.stdin.write(cmd + "\n")
                                        game_process.stdin.flush()
                                except:
                                    pass
    
                            # ----------------------------
                            # EVENTS
                            # ----------------------------
                            for event in pygame.event.get():
                                if event.type == pygame.QUIT:
                                    running = False
    
                                if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
                                    running = False
    
                            # ----------------------------
                            # TITLE (RESTORED SHADOW EFFECT)
                            # ----------------------------
                            # bottom layer (furthest shadow) - RED
                            shadow_red = title_font.render("ARCAGEDDON", True, (255, 0, 0))
                            shadow_red_rect = shadow_red.get_rect(center=(WIDTH // 2 + 8, 108))
                            screen.blit(shadow_red, shadow_red_rect)
    
                            # middle layer - ORANGE
                            shadow_orange = title_font.render("ARCAGEDDON", True, (255, 120, 0))
                            shadow_orange_rect = shadow_orange.get_rect(center=(WIDTH // 2 + 4, 104))
                            screen.blit(shadow_orange, shadow_orange_rect)
    
                            # top layer (main text) - YELLOW
                            main_text = title_font.render("ARCAGEDDON", True, YELLOW)
                            main_rect = main_text.get_rect(center=(WIDTH // 2, 100))
                            screen.blit(main_text, main_rect)
    
                            # ----------------------------
                            # DRAW UI
                            # ----------------------------
                            menu_button.draw(screen, selected=(focus_mode == "MENU"))
    
                            for i, b in enumerate(center_buttons):
                                b.draw(screen, selected=(focus_mode == "CENTER" and i == selected_index))
    
                            pygame.display.flip()
                            clock.tick(60)
    
                        # ----------------------------
                        # EXIT
                        # ----------------------------
                        usb.stop()
                        pygame.quit()
                        sys.exit()
                      

  3. USB data

    Program that reads the USB communication.

     
                        import serial
                        import time
    
                        class USBReader:
                            def __init__(self):
                                self.PORT = "/dev/serial/by-id/usb-MicroPython_Board_in_FS_mode_de6258c0c3143639-if00"
                                self.BAUD = 115200
    
                                print("Connecting to device...")
                                self.ser = serial.Serial(self.PORT, self.BAUD, timeout=1)
                                time.sleep(2)
                                print("Connected!")
    
                                self.latest = None
    
                            def start(self):
                                # intentionally empty (kept for compatibility)
                                pass
    
                            def read(self):
                                try:
                                    line = self.ser.readline()
    
                                    if not line:
                                        return self.latest
    
                                    line = line.decode("utf-8", errors="ignore").strip()
    
                                    if line:
                                        self.latest = line.upper()
    
                                except Exception as e:
                                    print("USB ERROR:", e)
    
                                return self.latest
    
                            def stop(self):
                                try:
                                    self.ser.close()
                                except:
                                    pass
                      

  4. Game 1 (Pong)

    Game 1 of the arcade machine, copy of the popular game "Pong" with a customized screen.

     
                        import pygame
                        import sys
                        import os
                        import select
    
                        def get_cmd():
                            if select.select([sys.stdin], [], [], 0)[0]:
                                return sys.stdin.readline().strip().upper()
                            return None
    
                        def close_game():
                            pygame.quit()
                            os._exit(0)
    
                        pygame.init()
                        pygame.mouse.set_visible(False)
    
                        WIDTH, HEIGHT = 1440, 900
                        screen = pygame.display.set_mode((WIDTH, HEIGHT))
                        pygame.display.set_caption("Pong Arcade")
    
                        clock = pygame.time.Clock()
                        FPS = 60
    
                        BLACK = (10, 10, 10)
                        WHITE = (255, 255, 255)
                        YELLOW = (255, 215, 0)
                        DARK = (25, 25, 25)
    
                        HUD_HEIGHT = int(HEIGHT * 0.10)
                        GAME_HEIGHT = HEIGHT - HUD_HEIGHT
    
                        player_score = 0
                        ai_score = 0
    
                        font = pygame.font.SysFont(None, 60)
                        small_font = pygame.font.SysFont(None, 40)
                        big_font = pygame.font.SysFont(None, 90)
    
                        PADDLE_W, PADDLE_H = 15, 140
                        BALL_SIZE = 18
                        PADDLE_SPEED = 8
    
                        left_paddle = pygame.Rect(
                            50,
                            HUD_HEIGHT + GAME_HEIGHT // 2 - PADDLE_H // 2,
                            PADDLE_W,
                            PADDLE_H
                        )
    
                        right_paddle = pygame.Rect(
                            WIDTH - 50 - PADDLE_W,
                            HUD_HEIGHT + GAME_HEIGHT // 2 - PADDLE_H // 2,
                            PADDLE_W,
                            PADDLE_H
                        )
    
                        ball = pygame.Rect(
                            WIDTH // 2,
                            HUD_HEIGHT + GAME_HEIGHT // 2,
                            BALL_SIZE,
                            BALL_SIZE
                        )
    
                        ball_vel_x = 6
                        ball_vel_y = 6
    
                        move_up = False
                        move_down = False
    
                        # ----------------------------
                        # WIN SYSTEM (FIXED)
                        # ----------------------------
                        game_over = False
                        winner_text = None
                        win_timer = None
                        WIN_WAIT = 3000  # 3 seconds
    
                        def reset_ball(direction=1):
                            global ball_vel_x, ball_vel_y
                            ball.center = (WIDTH // 2, HUD_HEIGHT + GAME_HEIGHT // 2)
                            ball_vel_x = 6 * direction
                            ball_vel_y = 6
    
                        AI_SPEED = 5
                        AI_ERROR = 20
    
                        def move_ai():
                            target_y = ball.centery + AI_ERROR
    
                            if right_paddle.centery < target_y:
                                right_paddle.y += AI_SPEED
                            if right_paddle.centery > target_y:
                                right_paddle.y -= AI_SPEED
    
                            right_paddle.y = max(
                                HUD_HEIGHT,
                                min(HUD_HEIGHT + GAME_HEIGHT - PADDLE_H, right_paddle.y)
                            )
    
                        running = True
    
                        while running:
                            clock.tick(FPS)
    
                            for event in pygame.event.get():
                                if event.type == pygame.QUIT:
                                    close_game()
    
                                if event.type == pygame.KEYDOWN:
                                    if event.key == pygame.K_ESCAPE:
                                        close_game()
    
                                if event.type == pygame.MOUSEBUTTONDOWN:
                                    mx, my = pygame.mouse.get_pos()
                                    if my < HUD_HEIGHT and mx > WIDTH - 140:
                                        close_game()
    
                            cmd = get_cmd()
    
                            # ----------------------------
                            # GAME INPUT ONLY IF NOT OVER
                            # ----------------------------
                            if not game_over:
                                # EMERGENCY EXIT COMMAND
                                if cmd == "RASP":
                                    running = False
                                if cmd == "UP":
                                    move_up = True
                                    move_down = False
                                elif cmd == "DOWN":
                                    move_down = True
                                    move_up = False
                                elif cmd == "STOP":
                                    move_up = False
                                    move_down = False
                                elif cmd == "CLOSE":
                                    close_game()
    
                                if move_up:
                                    left_paddle.y -= PADDLE_SPEED
                                if move_down:
                                    left_paddle.y += PADDLE_SPEED
    
                                left_paddle.y = max(
                                    HUD_HEIGHT,
                                    min(HUD_HEIGHT + GAME_HEIGHT - PADDLE_H, left_paddle.y)
                                )
    
                                move_ai()
    
                                ball.x += ball_vel_x
                                ball.y += ball_vel_y
    
                                if ball.top <= HUD_HEIGHT or ball.bottom >= HEIGHT:
                                    ball_vel_y *= -1
    
                                if ball.colliderect(left_paddle) and ball_vel_x < 0:
                                    ball_vel_x *= -1
    
                                if ball.colliderect(right_paddle) and ball_vel_x > 0:
                                    ball_vel_x *= -1
    
                                if ball.left <= 0:
                                    ai_score += 1
                                    reset_ball(1)
    
                                if ball.right >= WIDTH:
                                    player_score += 1
                                    reset_ball(-1)
    
                                WIN_SCORE = 10
    
                                if player_score >= WIN_SCORE:
                                    winner_text = "PLAYER WINS"
                                    game_over = True
                                    win_timer = pygame.time.get_ticks()
    
                                elif ai_score >= WIN_SCORE:
                                    winner_text = "AI WINS"
                                    game_over = True
                                    win_timer = pygame.time.get_ticks()
    
                            # ----------------------------
                            # WIN TIMER (NON-BLOCKING)
                            # ----------------------------
                            if game_over and win_timer is not None:
                                if pygame.time.get_ticks() - win_timer > WIN_WAIT:
                                    close_game()
    
                            # ----------------------------
                            # DRAW
                            # ----------------------------
                            screen.fill(BLACK)
    
                            pygame.draw.rect(screen, DARK, (0, 0, WIDTH, HUD_HEIGHT))
    
                            score_text = font.render(f"{player_score} : {ai_score}", True, WHITE)
                            screen.blit(score_text, score_text.get_rect(center=(WIDTH // 2, HUD_HEIGHT // 2)))
    
                            close_text = small_font.render("CLOSE", True, WHITE)
                            close_rect = close_text.get_rect(center=(WIDTH - 80, HUD_HEIGHT // 2))
    
                            box_rect = close_rect.inflate(40, 30)
                            pygame.draw.rect(screen, WHITE, box_rect, 5, border_radius=8)
                            screen.blit(close_text, close_rect)
    
                            pygame.draw.rect(screen, YELLOW, screen.get_rect(), 10)
                            pygame.draw.line(screen, YELLOW, (0, HUD_HEIGHT), (WIDTH, HUD_HEIGHT), 10)
    
                            pygame.draw.rect(screen, YELLOW, left_paddle)
                            pygame.draw.rect(screen, WHITE, right_paddle)
    
                            if not game_over:
                                pygame.draw.ellipse(screen, WHITE, ball)
    
                            # WIN OVERLAY (NO BLACK SCREEN)
                            if game_over:
                                text = big_font.render(winner_text, True, YELLOW)
                                text_rect = text.get_rect(center=(WIDTH // 2, HUD_HEIGHT + GAME_HEIGHT // 2))
    
                                box_rect = text_rect.inflate(80, 50)
    
                                pygame.draw.rect(screen, WHITE, box_rect, 6, border_radius=12)
                                pygame.draw.rect(screen, BLACK, box_rect.inflate(-10, -10), 0, border_radius=12)
    
                                screen.blit(text, text_rect)
    
                            pygame.display.flip()
    
                        pygame.quit()
                        sys.exit()
                      

  5. Game 2 (Cobra)

    Game 2 of the arcade machine, copy of the popular game "Snake" with a customized screen.

     
                        import pygame
                        import sys
                        import os
                        import select
                        import random
    
                        def get_cmd():
                            if select.select([sys.stdin], [], [], 0)[0]:
                                return sys.stdin.readline().strip().upper()
                            return None
    
                        def close_game():
                            pygame.quit()
                            os._exit(0)
    
                        pygame.init()
                        pygame.mouse.set_visible(False)
    
                        # ----------------------------
                        # SCREEN
                        # ----------------------------
                        WIDTH, HEIGHT = 1440, 900
                        screen = pygame.display.set_mode((WIDTH, HEIGHT))
                        pygame.display.set_caption("Snake Arcade")
    
                        clock = pygame.time.Clock()
                        FPS = 10
    
                        # ----------------------------
                        # COLORS
                        # ----------------------------
                        BLACK = (10, 10, 10)
                        WHITE = (255, 255, 255)
                        YELLOW = (255, 215, 0)
                        DARK = (25, 25, 25)
                        GREEN = (0, 200, 0)
                        RED = (220, 50, 50)
    
                        # ----------------------------
                        # LAYOUT
                        # ----------------------------
                        HUD_HEIGHT = int(HEIGHT * 0.10)
                        GAME_HEIGHT = HEIGHT - HUD_HEIGHT
    
                        # ----------------------------
                        # FONTS
                        # ----------------------------
                        font = pygame.font.SysFont(None, 60)
                        small_font = pygame.font.SysFont(None, 40)
                        big_font = pygame.font.SysFont(None, 90)
    
                        # ----------------------------
                        # CLOSE BUTTON (PONG STYLE)
                        # ----------------------------
                        close_text = small_font.render("CLOSE", True, WHITE)
                        close_rect = close_text.get_rect(center=(WIDTH - 80, HUD_HEIGHT // 2))
                        close_box = close_rect.inflate(40, 30)
    
                        # ----------------------------
                        # GRID
                        # ----------------------------
                        CELL_SIZE = 30
                        GRID_W = WIDTH // CELL_SIZE
                        GRID_H = GAME_HEIGHT // CELL_SIZE
    
                        # ----------------------------
                        # SNAKE STATE
                        # ----------------------------
                        snake = [(GRID_W // 2, GRID_H // 2)]
                        direction = (1, 0)
                        pending_direction = direction
    
                        points = 0
                        game_over = False
    
                        # ----------------------------
                        # GAME OVER TIMER (NEW)
                        # ----------------------------
                        game_over_time = None
                        GAME_OVER_DELAY = 3000  # 3 seconds
    
                        def spawn_apple():
                            while True:
                                pos = (random.randint(0, GRID_W - 1),
                                      random.randint(0, GRID_H - 1))
                                if pos not in snake:
                                    return pos
    
                        apple = spawn_apple()
    
                        def draw_cell(pos, color):
                            x, y = pos
                            pygame.draw.rect(
                                screen,
                                color,
                                (x * CELL_SIZE,
                                HUD_HEIGHT + y * CELL_SIZE,
                                CELL_SIZE - 2,
                                CELL_SIZE - 2)
                            )
    
                        # ----------------------------
                        # MAIN LOOP
                        # ----------------------------
                        running = True
    
                        while running:
                            clock.tick(FPS)
    
                            # ----------------------------
                            # EVENTS
                            # ----------------------------
                            for event in pygame.event.get():
                                if event.type == pygame.QUIT:
                                    close_game()
    
                                if event.type == pygame.MOUSEBUTTONDOWN:
                                    mx, my = pygame.mouse.get_pos()
                                    if close_box.collidepoint(mx, my):
                                        close_game()
    
                                if event.type == pygame.KEYDOWN:
                                    if event.key == pygame.K_ESCAPE:
                                        close_game()
    
                            cmd = get_cmd()
    
                            # ----------------------------
                            # INPUT + GAME LOGIC
                            # ----------------------------
                            if not game_over:
                                # EMERGENCY EXIT COMMAND
                                if cmd == "RASP":
                                    running = False
                                if cmd == "UP" and direction != (0, 1):
                                    pending_direction = (0, -1)
                                elif cmd == "DOWN" and direction != (0, -1):
                                    pending_direction = (0, 1)
                                elif cmd == "LEFT" and direction != (1, 0):
                                    pending_direction = (-1, 0)
                                elif cmd == "RIGHT" and direction != (-1, 0):
                                    pending_direction = (1, 0)
                                elif cmd == "STOP":
                                    pending_direction = direction
                                elif cmd == "CLOSE":
                                    close_game()
    
                                direction = pending_direction
    
                                # ----------------------------
                                # MOVE SNAKE
                                # ----------------------------
                                head_x, head_y = snake[0]
                                dx, dy = direction
                                new_head = (head_x + dx, head_y + dy)
    
                                # WALL COLLISION
                                if new_head[0] < 0 or new_head[0] >= GRID_W or new_head[1] < 0 or new_head[1] >= GRID_H:
                                    game_over = True
                                    game_over_time = pygame.time.get_ticks()
    
                                # SELF COLLISION
                                elif new_head in snake:
                                    game_over = True
                                    game_over_time = pygame.time.get_ticks()
    
                                else:
                                    snake.insert(0, new_head)
    
                                    if new_head == apple:
                                        points += 1
                                        apple = spawn_apple()
                                    else:
                                        snake.pop()
    
                            # ----------------------------
                            # AUTO CLOSE AFTER 3 SECONDS
                            # ----------------------------
                            if game_over and game_over_time is not None:
                                if pygame.time.get_ticks() - game_over_time > GAME_OVER_DELAY:
                                    close_game()
    
                            # ----------------------------
                            # DRAW
                            # ----------------------------
                            screen.fill(BLACK)
    
                            # HUD
                            pygame.draw.rect(screen, DARK, (0, 0, WIDTH, HUD_HEIGHT))
    
                            score_text = font.render(f"Points : {points}", True, WHITE)
                            screen.blit(score_text, score_text.get_rect(center=(WIDTH // 2, HUD_HEIGHT // 2)))
    
                            # Close button
                            pygame.draw.rect(screen, WHITE, close_box, 5, border_radius=8)
                            screen.blit(close_text, close_rect)
    
                            # Border
                            pygame.draw.rect(screen, YELLOW, screen.get_rect(), 10)
                            pygame.draw.line(screen, YELLOW, (0, HUD_HEIGHT), (WIDTH, HUD_HEIGHT), 10)
    
                            # Apple
                            draw_cell(apple, RED)
    
                            # Snake
                            for segment in snake:
                                draw_cell(segment, GREEN)
    
                            # ----------------------------
                            # GAME OVER UI (PONG STYLE)
                            # ----------------------------
                            if game_over:
                                text = big_font.render("GAME OVER", True, YELLOW)
                                rect = text.get_rect(center=(WIDTH // 2, HUD_HEIGHT + GAME_HEIGHT // 2))
    
                                box = rect.inflate(80, 50)
    
                                pygame.draw.rect(screen, WHITE, box, 6, border_radius=12)
                                pygame.draw.rect(screen, BLACK, box.inflate(-10, -10), 0, border_radius=12)
    
                                screen.blit(text, rect)
    
                            pygame.display.flip()
    
                        pygame.quit()
                        sys.exit()
                      

  6. Inputs check

    Program that lets me test the inputs and make sure everything is read correctly.

     
                        import pygame
                        import sys
                        import os
                        import select
                        import time
    
                        def get_cmd():
                            if select.select([sys.stdin], [], [], 0)[0]:
                                return sys.stdin.readline().strip().upper()
                            return None
    
                        def close_game():
                            pygame.quit()
                            os._exit(0)
    
                        pygame.init()
                        pygame.mouse.set_visible(False)
    
                        # ----------------------------
                        # SCREEN
                        # ----------------------------
                        WIDTH, HEIGHT = 1440, 900
                        screen = pygame.display.set_mode((WIDTH, HEIGHT))
                        pygame.display.set_caption("Control Interface")
                        clock = pygame.time.Clock()
                        FPS = 60
    
                        # ----------------------------
                        # COLORS
                        # ----------------------------
                        BLACK = (10, 10, 10)
                        WHITE = (255, 255, 255)
                        YELLOW = (255, 215, 0)
                        DARK = (25, 25, 25)
                        GREEN = (0, 220, 0)
                        RED = (220, 50, 50)
                        BLUE = (80, 160, 255)
                        CYAN = (0, 255, 255)
    
                        # ----------------------------
                        # LAYOUT
                        # ----------------------------
                        HUD_HEIGHT = int(HEIGHT * 0.10)
                        MAIN_HEIGHT = HEIGHT - HUD_HEIGHT
    
                        LEFT_WIDTH = int(WIDTH * 0.5)
                        RIGHT_WIDTH = WIDTH - LEFT_WIDTH
    
                        # ----------------------------
                        # FONT
                        # ----------------------------
                        font = pygame.font.SysFont(None, 60)
                        small_font = pygame.font.SysFont(None, 40)
    
                        # ----------------------------
                        # CLOSE BUTTON
                        # ----------------------------
                        close_text = small_font.render("CLOSE", True, WHITE)
                        close_rect = close_text.get_rect(center=(WIDTH - 80, HUD_HEIGHT // 2))
                        close_box = close_rect.inflate(40, 30)
    
                        # ----------------------------
                        # HELPERS
                        # ----------------------------
                        def draw_circle(x, y, r, color, width=3):
                            pygame.draw.circle(screen, color, (x, y), r, width)
    
                        def draw_filled_circle(x, y, r, color):
                            pygame.draw.circle(screen, color, (x, y), r)
    
                        def draw_arrow(x, y, direction, size=35, color=WHITE, filled=False):
                            if direction == "UP":
                                pts = [(x, y - size), (x - size, y + size), (x + size, y + size)]
                            elif direction == "DOWN":
                                pts = [(x, y + size), (x - size, y - size), (x + size, y - size)]
                            elif direction == "LEFT":
                                pts = [(x - size, y), (x + size, y - size), (x + size, y + size)]
                            elif direction == "RIGHT":
                                pts = [(x + size, y), (x - size, y - size), (x - size, y + size)]
                            else:
                                return
    
                            pygame.draw.polygon(screen, color, pts, 0 if filled else 3)
    
                        # ----------------------------
                        # POSITIONS
                        # ----------------------------
    
                        # Left arrows
                        center_x = LEFT_WIDTH // 2
                        center_y = HUD_HEIGHT + MAIN_HEIGHT // 2
                        offset = 120
    
                        arrows = {
                            "UP": (center_x, center_y - offset),
                            "DOWN": (center_x, center_y + offset),
                            "LEFT": (center_x - offset, center_y),
                            "RIGHT": (center_x + offset, center_y),
                        }
    
                        pb_circle = (center_x, center_y)
    
                        # Right circles (A-F)
                        circle_radius = 45
    
                        right_start_x = LEFT_WIDTH + RIGHT_WIDTH // 2 - 180
                        right_start_y = HUD_HEIGHT + MAIN_HEIGHT // 2 - 120
                        spacing = 160
                        row_shift = spacing // 2
    
                        circles = {
                            "A": (right_start_x + 0 * spacing, right_start_y),
                            "B": (right_start_x + 1 * spacing, right_start_y),
                            "C": (right_start_x + 2 * spacing, right_start_y),
    
                            "D": (right_start_x + 0 * spacing + row_shift, right_start_y + spacing),
                            "E": (right_start_x + 1 * spacing + row_shift, right_start_y + spacing),
                            "F": (right_start_x + 2 * spacing + row_shift, right_start_y + spacing),
                        }
    
                        # ----------------------------
                        # ACTIVE STATES (timed pulses)
                        # ----------------------------
                        ACTIVE_TIME = 0.3  # seconds
                        active_until = {}
    
                        def activate(key):
                            active_until[key] = time.time() + ACTIVE_TIME
    
                        def is_active(key):
                            return active_until.get(key, 0) > time.time()
    
                        # ----------------------------
                        # MAIN LOOP
                        # ----------------------------
                        running = True
                        while running:
                            clock.tick(FPS)
    
                            # cleanup expired
                            now = time.time()
                            active_until = {k: v for k, v in active_until.items() if v > now}
    
                            for event in pygame.event.get():
                                if event.type == pygame.QUIT:
                                    close_game()
    
                                if event.type == pygame.MOUSEBUTTONDOWN:
                                    if close_box.collidepoint(pygame.mouse.get_pos()):
                                        close_game()
    
                                if event.type == pygame.KEYDOWN:
                                    if event.key == pygame.K_ESCAPE:
                                        close_game()
    
                            cmd = get_cmd()
    
                            if cmd:
                                # EMERGENCY EXIT COMMAND
                                if cmd == "RASP":
                                    running = False
    
                                # arrows
                                if cmd in ["UP", "DOWN", "LEFT", "RIGHT"]:
                                    activate(cmd)
    
                                # center PB
                                elif cmd == "PB":
                                    activate("PB")
    
                                # right grid
                                elif cmd in ["A", "B", "C", "D", "E", "F"]:
                                    activate(cmd)
    
                                elif cmd == "CLOSE":
                                    close_game()
    
                            # ----------------------------
                            # DRAW
                            # ----------------------------
                            screen.fill(BLACK)
    
                            # HUD
                            pygame.draw.rect(screen, DARK, (0, 0, WIDTH, HUD_HEIGHT))
                            text = font.render(f"INPUTS CHECK", True, WHITE)
                            screen.blit(text, text.get_rect(center=(WIDTH // 2, HUD_HEIGHT // 2)))
    
                            # close
                            pygame.draw.rect(screen, WHITE, close_box, 5, border_radius=8)
                            screen.blit(close_text, close_rect)
    
                            pygame.draw.line(screen, YELLOW, (0, HUD_HEIGHT), (WIDTH, HUD_HEIGHT), 6)
    
                            # ----------------------------
                            # LEFT SIDE (ARROWS + PB)
                            # ----------------------------
                            for name, pos in arrows.items():
                                draw_arrow(
                                    pos[0],
                                    pos[1],
                                    name,
                                    color=CYAN if is_active(name) else WHITE,
                                    filled=is_active(name)
                                )
    
                            # center PB circle
                            draw_filled_circle(
                                pb_circle[0],
                                pb_circle[1],
                                60,
                                GREEN if is_active("PB") else DARK
                            )
                            draw_circle(pb_circle[0], pb_circle[1], 60, WHITE)
    
                            # ----------------------------
                            # RIGHT SIDE (A-F circles)
                            # ----------------------------
                            for key, pos in circles.items():
                                color = RED if is_active(key) else BLUE
                                if is_active(key):
                                    draw_filled_circle(pos[0], pos[1], circle_radius, color)
                                draw_circle(pos[0], pos[1], circle_radius, WHITE)
    
                            pygame.draw.rect(screen, YELLOW, screen.get_rect(), 8)
    
                            pygame.display.flip()
    
                        pygame.quit()
                        sys.exit()
                      

  7. USB test

    Program that lets me see what data the Raspberry Pi Pico sends.

     
                        import serial
                        import time
    
                        # ----------------------------
                        # USB SERIAL PORT (your device)
                        # ----------------------------
                        PORT = "/dev/serial/by-id/usb-MicroPython_Board_in_FS_mode_de6258c0c3143639-if00"
                        BAUD = 115200
    
                        # ----------------------------
                        # Actions (replace with your real logic)
                        # ----------------------------
    
                        def button_a(): print("A")
                        def button_b(): print("B")
                        def button_c(): print("C")
                        def button_d(): print("D")
                        def button_e(): print("E")
                        def button_f(): print("F")
    
                        def button_close(): print("CLOSE")
                        def button_menu(): print("MENU")
                        def button_left(): print("LEFT")
                        def button_right(): print("RIGHT")
                        def button_up(): print("UP")
                        def button_down(): print("DOWN")
                        def button_pb(): print("PB")
    
                        # ----------------------------
                        # Command map
                        # ----------------------------
    
                        commands = {
                            "A": button_a,
                            "B": button_b,
                            "C": button_c,
                            "D": button_d,
                            "E": button_e,
                            "F": button_f,
                            "CLOSE": button_close,
                            "MENU": button_menu,
                            "LEFT": button_left,
                            "RIGHT": button_right,
                            "UP": button_up,
                            "DOWN": button_down,
                            "PB": button_pb,
                        }
    
                        # ----------------------------
                        # Connect to Pico
                        # ----------------------------
    
                        print("Connecting to Pico...")
    
                        ser = serial.Serial(PORT, BAUD, timeout=1)
                        time.sleep(2)  # allow Pico reset
    
                        print("Connected to Pico!")
    
                        # ----------------------------
                        # Main loop
                        # ----------------------------
    
                        while True:
                            try:
                                line = ser.readline().decode("utf-8").strip().upper()
    
                                if line:
                                    if line in commands:
                                        commands[line]()
                                    else:
                                        print("Unknown command:", line)
    
                            except Exception as e:
                                print("Error:", e)
                                time.sleep(0.1)
                      

  8. System startup

    I also modified some files in the Pi to make it so on startup, it displays a custom made splashscreen instead of the default Raspberry splashscreen and when everything is ready it launches the interface program immediately.

    The splashscreen was made on Inkscape :

    To replace the default splashscreen by the custom one, I place the picture in the desktop and type on the terminal :

     
                        sudo apt install plymouth plymouth-themes
                        sudo cp my_splash.png /usr/share/plymouth/themes/pix/splashscreen.png
                      

    To start on the interface on startup :

     
                        sudo nano /etc/systemd/system/arcageddon.service
                      

    This opens a new windows, I have to type and save the following :

     
                        [Unit]
                        Description=Arcageddon UI
                        After=graphical.target
                        Wants=graphical.target
    
                        [Service]
                        User=fabio
                        WorkingDirectory=/home/fabio/Desktop/MicroPython_codes
                        Environment=DISPLAY=:0
                        Environment=XAUTHORITY=/home/fabio/.Xauthority
                        ExecStartPre=/bin/sleep 5
                        ExecStart=/usr/bin/python3 /home/fabio/Desktop/MicroPython_codes/interface.py
                        Restart=no
    
                        [Install]
                        WantedBy=graphical.target
                      

    And finally, I have to enable it :

     
                        sudo systemctl enable arcageddon.service
                      

    Currently, on startup, the monitor first shows my splashscreen, then opens on the Pi desktop and after 5-10 seconds, my interface opens up.



Bill Of Materials

Item Quantity Unity price [€]
Wood plank (12mm) NA ~25,00
Paint canister (black) 2 2,33
Monitor (19 inches) 1 NA
Raspberry Pi 5 4GB RAM starter kit 1 215,00
Raspberry Pi Pico 1 ~4,00
Joystick 1 2,66
Keyboard switch 8 0,86
SMD resistor (1206) 9 NA
SMD capacitor (1206) 12 NA
HDMI coupler 1 6,45
USB-A to USB-C cable 1 13,95
VGA to HDMI cable 1 ~10,00
Power cord 1 ~10,00


Final result



Slide



Video

(Music source)



Useful file(s) (Click to download)

    PCB modules

  1. Joystick module (KiCad)
  2. Buttons module (KiCad)
  3. Button right module (KiCad)
  4. Button left module (KiCad)
  5. Microcontroller module (KiCad)
  6. 2D

  7. Close button - Vinyl design (Inkscape)
  8. Raspberry button - Vinyl design (Inkscape)
  9. Splashscreen (Inkscape)
  10. 3D

  11. Back door pieces (Autodesk Fusion)
  12. Buttons and joystick pieces (Autodesk Fusion)
  13. Raspberry holder (Autodesk Fusion)
  14. Arcade machine pieces (Autodesk Fusion)
  15. Codes

  16. Controller - MicroPython code (Thonny)
  17. Interface - Python code (Thonny)
  18. USB data - Python code (Thonny)
  19. Game 1 (Ping) - Python code (Thonny)
  20. Game 2 (Cobra) - Python code (Thonny)
  21. Inputs check - Python code (Thonny)
  22. USB communication test - Python code (Thonny)


ChatGPT prompts



Assignments checklist

  1. ✅Made your slide 1920 x 1080 pixels with your name, project name, Fab Lab name, a photo/render/sketch of your project, a brief description of what your project is/does.
  2. ✅Made a ~1 minute (25MB/1080p) video of your final project showing its fabrication and functionality.
  3. ✅Made a separate Final Project page that summarizes/documents your project.
  4. ✅Included the BOM (Bill of Materials) for your project.
  5. ✅Linked from this page to any weeks that you worked on your final project.
  6. ✅Documented how you implemented system integration in your final project.
  7. ✅Linked to your presentation.png and presentation.mp4; make sure they are located to the root of your website.
  8. ✅Included all of your original design files in the archive (2D & 3D, board files & code). No external hosting of final project files - discuss file sizes with your instructor.
  9. ✅Included the license you chose.
  10. ✅Acknowledged work done by others.