Infineon Team
Published © MIT

The Alligator Bicep Trainer

Have you always wanted to be more consistent in the gym, but you just end up playing video games at home? Why not do both?

IntermediateFull instructions provided6 hours270

Things used in this project

Hardware components

PSOC™ 6 AI Evaluation Kit (CY8CKIT-062S2-AI)
Infineon PSOC™ 6 AI Evaluation Kit (CY8CKIT-062S2-AI)
×1

Software apps and online services

VS Code
Microsoft VS Code
Arduino IDE
Arduino IDE

Hand tools and fabrication machines

Watch strap
4 M3x10 screws
3D Printer (generic)
3D Printer (generic)

Story

Read more

Custom parts and enclosures

Swamp background

Save in assets->map

Alligator doing bicep curl

Save in assets->map

Gripper open

Save in sprites->player->gripper

Gripper closed

Save in sprites->player->gripper

Watch case (bottom)

Print upright with supports.

Watch case (top)

Print with supports and with the text face-down.

Code

get_angle.ino

C/C++
#include <Wire.h>
#include "SparkFun_BMI270_Arduino_Library.h"
#include <math.h>
#include <deque>

// Create a new sensor object
BMI270 imu;

int maxlen = 5;
std::deque<float> bufferx;
std::deque<float> bufferz;

// I2C address selection
uint8_t i2cAddress = BMI2_I2C_PRIM_ADDR; // 0x68
//uint8_t i2cAddress = BMI2_I2C_SEC_ADDR; // 0x69

void setup()
{
    // Start serial
    Serial.begin(9600);
    //Serial.println("BMI270 Example 1 - Basic Readings I2C");

    // Initialize the I2C library
    Wire.begin();

    // Check if sensor is connected and initialize
    // Address is optional (defaults to 0x68)
    while(imu.beginI2C(i2cAddress) != BMI2_OK)
    {
        // Not connected, inform user
        Serial.println("Error: BMI270 not connected, check wiring and I2C address!");

        // Wait a bit to see if connection is established
        delay(1000);
    }

    //Serial.println("BMI270 connected!");
}

void loop()
{
    imu.getSensorData();
    float x = imu.data.accelX;
    float z = imu.data.accelZ;

    //initialize buffers (except the last value)
    if((bufferx.size() < (maxlen - 1)) || (bufferz.size() < (maxlen - 1))){
        bufferx.push_back(x);
        bufferz.push_back(z);
    }
    //calculate x and z average and print angle
    else{
        float x_average = 0;
        float z_average = 0;
        //add in newest value
        bufferx.push_back(x);
        bufferz.push_back(z);
        //calculate average
        for (int i = 0; i < maxlen; i++) {
            x_average = x_average + bufferx[i];
            z_average = z_average + bufferz[i];
        }
        x_average = x_average/maxlen;
        z_average = z_average/maxlen;
        
        //pop out oldest value
        bufferx.pop_front();
        bufferz.pop_front();
      
        double hyp = sqrt(z_average*z_average + x_average*x_average);
        //calculate angle
        if(z_average < 0){
            Serial.println((acos(x_average/hyp)*180/PI));
        }
        //if z_average < 0 -> overrotation! 
        else{
            //arm all the way up -> 0 degrees
            if(x_average > 0){
                Serial.println("0.00");
            } 
            //arm all the way down -> 180 degrees
            else Serial.println("180.00"); 
        }

    }
    
    // ADD DELAY THING!
    delay(20);
}

game.py

Python
#import relevant libraries
import os, time, pygame, serial

# Load the next state from it's respective file
# Import state classes (from states directory)
from states.title import Title
from states.stretch_to_start import Stretch_To_Start

'''Manages the whole game'''
class Game():
        def __init__(self):
            #initializes all the imported Pygame module
            pygame.init()

            #game wide constants - screen size 
            self.GAME_SCALE = 3
            self.GAME_W,self.GAME_H = 768/2, 1152/2
            self.SCREEN_WIDTH,self.SCREEN_HEIGHT = self.GAME_W * self.GAME_SCALE, self.GAME_H * self.GAME_SCALE
            
            #create a screen and draw on it with the resolution we want
            self.game_canvas = pygame.Surface((self.GAME_W,self.GAME_H))
            self.screen = pygame.display.set_mode((self.SCREEN_WIDTH,self.SCREEN_HEIGHT))
            pygame.display.set_caption("The Alligator Bicep Trainer")

            #game wide constants - reps, sets and curl time 
            self.total_reps = 0
            self.total_sets = 0
            self.current_set = 1  # (1-indexed)
            self.current_rep = 0  # (0-indexed)
            self.time_for_curl = 5 #sec

            #game wide constants - angle boundaries
            self.min_angle = 40 #min angle to count as curled arm
            self.max_angle = 140 #max angle to count as stretched out

            #game wide constants - time tracking
            self.frame_duration = 0.15  # seconds per frame
            self.dt = 0 #delta time
            self.prev_time = 0 #time when last frame was drawn

            # game wide constants - text display
            self.distance_between_text_elements = self.GAME_H/15  # Vertical distance between elements
            
            # game wide constants - colors
            self.alligator_green = (20, 100, 20)  # Forest green color for backgrounds
        
            #flags that track game's progress
            self.running = True
            self.playing = True
            self.winning = True  #Set to False when chicken gets eaten
        
            #directory to Enter key presses for menu navigation and confirmations
            self.actions = {"enter": False}

            #stack that manages the current game state
            self.state_stack = []

            #call functions needed to begin the game 
            self.load_assets()
            self.load_states()

        '''Main function that loops while the game is being run'''
        def game_loop(self):
            while self.playing:
                self.get_dt()
                self.get_events()
                self.update()
                self.render()

        '''Get keyboards input events '''
        def get_events(self):
            for event in pygame.event.get():
                # Quit if player quits or presses ESCAPE
                if event.type == pygame.QUIT:
                    self.playing = False
                    self.running = False
                if event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_ESCAPE:
                        self.playing = False
                        self.running = False   
                    # Set enter to True if key has been pressed
                    if event.key == pygame.K_RETURN:
                        self.actions['enter'] = True  

                # Set enter to False after the key has been let go
                if event.type == pygame.KEYUP:
                    if event.key == pygame.K_RETURN:
                        self.actions['enter'] = False  

        '''Update whatever's at the top of the stack'''
        def update(self):
            self.state_stack[-1].update(self.dt,self.actions)

        '''Render the graphics (of our current state) onto the game canvas'''
        def render(self):
            self.state_stack[-1].render(self.game_canvas)
            #scale the canvas and copy it onto our screen
            self.screen.blit(pygame.transform.scale(self.game_canvas,(self.SCREEN_WIDTH, self.SCREEN_HEIGHT)), (0,0))
            pygame.display.flip()

        '''Compute delta time for frame rate independence(time passed since last update and render)'''
        def get_dt(self):
            now = time.time()
            self.dt = now - self.prev_time
            self.prev_time = now

        '''Draw text on screen''' 
        def draw_text(self, surface, text, color, x, y, font_size=None):
            # Use default font size based on screen dimensions if not specified
            if font_size is None:
                font_size = max(8, int(self.GAME_H / 50))  # Smaller font scaling
            
            # Create font with appropriate size
            font = pygame.font.Font(os.path.join(self.font_dir, "PressStart2P-vaV7.ttf"), font_size)
            text_surface = font.render(text, True, color)
            text_rect = text_surface.get_rect()
            text_rect.center = (x, y)
            surface.blit(text_surface, text_rect)

        '''create pointers to directories where our assets are stored (for pictures, etc.)'''
        def load_assets(self):
            self.assets_dir = os.path.join("assets")
            self.sprite_dir = os.path.join(self.assets_dir, "sprites")
            self.font_dir = os.path.join(self.assets_dir, "font")
            self.font= pygame.font.Font(os.path.join(self.font_dir, "PressStart2P-vaV7.ttf"), 20)

        '''make instance of the title and put it at the top of the stack'''
        def load_states(self):
            self.title_screen = Title(self)
            self.state_stack.append(self.title_screen)

        ''''reset all actions to default state (no key has been pressed)'''
        def reset_keys(self):
            for action in self.actions:
                self.actions[action] = False

#Handle case: game.py file is being run directly 
if __name__ == "__main__":
    # Create Game object 
    g = Game()
    # Loop through while running
    while g.running:
        g.game_loop()

state.py

Python
'''Parent class for all game states'''
class State():
    def __init__(self, game):
        # Make a reference to the game object
        self.game = game
        # Pointer to the previous state
        self.prev_state = None

    # Update function to be overridden by child classes
    def update(self, delta_time, actions):
        pass

    # Render function to be overridden by child classes
    def render(self, surface):
        pass

    # Enter this state (push onto the stack)
    def enter_state(self):
        # Handle case: previous state exists -> store it
        if len(self.game.state_stack) > 1:
            self.prev_state = self.game.state_stack[-1]
        # Push state onto the stack -> new current state
        self.game.state_stack.append(self)

    # Exit this state (pop from the stack)
    def exit_state(self):
        self.game.state_stack.pop()

title.py

Python
# Import required libraries
import pygame, os
# Import State (parent) class
from states.state import State
# Import next state
from states.get_reps_and_sets import Get_Reps_and_Sets

'''Render title screen and handle transition to get_reps_and_sets state'''
class Title(State):
    def __init__(self, game):
         # Inherit from State (parent) class
        State.__init__(self, game)
        
        # Load and scale the alligator bicep curl image
        image_path = os.path.join(self.game.assets_dir, "map", "Alligator_doing_bicep_curl.png")
        self.alligator_image = pygame.image.load(image_path)
        
        # Scale image to fit nicely on screen (about 60% of screen width)
        image_width = int(self.game.GAME_W * 0.6)
        image_height = int(image_width * self.alligator_image.get_height() / self.alligator_image.get_width())
        self.alligator_image = pygame.transform.scale(self.alligator_image, (image_width, image_height))
        
        # Store scaled dimensions for accurate centering
        self.image_width = image_width
        self.image_height = image_height
        
        # Position image so its center is exactly in the center of the screen
        self.image_x = self.game.GAME_W // 2 - self.image_width // 2
        self.image_y = self.game.GAME_H // 2 - self.image_height // 2
        
        # Calculate equal spacing from edges
        # Distance from top to title should equal distance from instruction to bottom
        spacing = self.game.GAME_H // 6  # Use 1/6 of screen height as margin (bigger spacing)
        
        # Position title and instruction equidistant from top/bottom
        self.title_x = self.game.GAME_W // 2
        self.title_y = spacing
        
        self.instruction_y = self.game.GAME_H - spacing
        
    '''Handle state transition'''
    def update(self, delta_time, actions):
        # Handle case: Enter key pressed to start game
        if actions["enter"]:
            # Get new state
            new_state = Get_Reps_and_Sets(self.game)
            # Transition to new state
            new_state.enter_state()
        self.game.reset_keys()

    '''Render title screen'''
    def render(self, display):
        display.fill(self.game.alligator_green)
        
        # Draw title above the image
        self.game.draw_text(display, "Alligator Bicep Trainer", (255, 255, 255), self.title_x, self.title_y)
        
        # Draw the alligator doing bicep curl image
        display.blit(self.alligator_image, (self.image_x, self.image_y))
        
        # Draw instruction below the image
        self.game.draw_text(display, "Press ENTER to continue", (200, 200, 200), self.title_x, self.instruction_y)

get_reps_and_sets.py

Python
# Import necessary modules
import pygame, os
# Import parent class
from states.state import State  
# Import next state
from states.stretch_to_start import Stretch_To_Start

'''Get user input for number of reps, sets, and time for one curl'''
'''Backspace to erase or go back if needed; Enter to confirm each field; Enter to transition to stretch_to_start'''
class Get_Reps_and_Sets(State):
    def __init__(self, game):
        # Inherit from State (parent) class
        State.__init__(self, game)

        # Attributes for input handling
        self.stage = 0  # Current input stage; 0 = reps, 1 = sets, 2 = time
        self.inputs = ["", "", ""]  # reps, sets, time (displayed on screen)
        self.error_message = ''  # Error message to display if input invalid (currently nothing)
        self.cursor_timer = 0  # Start Timer for blinking cursor
        self.blink_speed = 2  # Cursor blink rate 
        self.keys_pressed_last_frame = set()  # Store all keys pressed in the last frame (set -> no duplicates)

    '''Handle user input, error checking, and state transition'''
    def update(self, delta_time, actions):
        # Update cursor timer for blinking effect
        self.cursor_timer += delta_time
        # Save the current state of every key in a boolean list (True = pressed, False = not pressed)
        keys = pygame.key.get_pressed()
        # Store all keys that are currently pressed in a set(no duplicates)
        current_keys_pressed = set()
        
        # Set maximum lengths for each input
        max_lengths = [2, 1, 2]  # reps, sets, time
        
        # Handle digit keys (0-9)
        for i in range(10):
            # Get key that represents the digit i 
            key = getattr(pygame, f'K_{i}')
            # Check if this key is currently being pressed
            if keys[key] == True:
                # Add key to currently pressed keys (to avoid multiple additions in one frame)
                current_keys_pressed.add(key)
                # Handle case: key wasn't pressed last frame (new press)
                if key not in self.keys_pressed_last_frame:
                    # Handle case: input area not already full 
                    if len(self.inputs[self.stage]) < max_lengths[self.stage]:
                        # Add current (to be written on screen)
                        self.inputs[self.stage] += str(i)
        
        # Handle case: Backspace key pressed
        if keys[pygame.K_BACKSPACE]:
            # Add to currently pressed keys
            current_keys_pressed.add(pygame.K_BACKSPACE)
            # Handle case: Backspace wasn't pressed last frame (new press)
            if pygame.K_BACKSPACE not in self.keys_pressed_last_frame:
                # Handle case: there's input -> erase it  
                if self.inputs[self.stage]:
                    self.inputs[self.stage] = self.inputs[self.stage][:-1]
                # Handle case: empty input -> go back a stage if possible
                elif self.stage > 0:
                    # Go up one input stage 
                    self.stage -= 1
                    # Clear error messages from previous input stage
                    self.error_message = ''

        # Update keys pressed last frame for next update call
        self.keys_pressed_last_frame = current_keys_pressed

        # Handle case: Enter key pressed -> go to next input stage or start stretch phase
        if actions["enter"]:
            # Set input ranges and corresponding error messages
            limits = [(1, 30), (1, 8), (1, 60)]
            errors = ["Enter reps (1-30)", "Enter sets (1-8)", "Enter time (1-60 seconds)"]

            # Handle case: valid input for current stage (with error handling)
            if self.inputs[self.stage] and limits[self.stage][0] <= int(self.inputs[self.stage]) <= limits[self.stage][1]:
                # Handle case: not the last stage
                if self.stage < 2:
                    # Go to next input stage
                    self.stage += 1
                    # Clear error message
                    self.error_message = ''
                # Handle case: last stage -> Save user input and go to stretch phase
                else:
                    # Save input from each input stage 
                    self.game.total_reps = int(self.inputs[0])
                    self.game.total_sets = int(self.inputs[1])
                    self.game.time_for_curl = int(self.inputs[2])
                    # Initialize variables to track progress
                    self.game.current_rep = 0
                    self.game.current_set = 1
                    self.game.winning = True
                    
                    # Transition to stretch phase
                    new_state = Stretch_To_Start(self.game)
                    new_state.enter_state()

            # Handle case: invalid input for current stage
            else:
                self.error_message = errors[self.stage]

        # Reset actions to avoid multiple triggers 
        self.game.reset_keys()

    '''Render the input fields instructions, and error messages onto screen'''
    def render(self, display):
        # Fill the screen with our background colour
        display.fill(self.game.alligator_green)

        # Calculate centered layout positions
        start_x = self.game.GAME_W / 2
        start_y = self.game.GAME_H / 4
        
        # Title
        self.game.draw_text(display, "SETUP WORKOUT", (255, 255, 255), start_x, start_y)
        
        # Initialize labels for the three input fields
        labels = ["Reps", "Sets", "Time"]
        
        # Display input fields with blinking cursor for current stage
        for i in range(3):
            display_text = self.inputs[i] if self.inputs[i] else "_"
            # Handle case: unit needed for time input
            if i == 2 and display_text != "_":  # Add 's' for time
                display_text += "s"
            elif i == 2:
                display_text = "_s"
            
            # Text color: white for current stage, grey for others
            color = (255, 255, 255) if i == self.stage else (200, 200, 200)
            
            # Calculate position for this input field
            input_y = start_y + self.game.distance_between_text_elements * (1.5 + i)
            
            # Draw the input field
            self.game.draw_text(display, f"{labels[i]}: {display_text}", color, start_x, input_y)

            # Make cursor blink for current stage blink_speed times per second
            if i == self.stage and int(self.cursor_timer * self.blink_speed) % 2:
                # Calculate cursor position (slightly to the right of the text)
                cursor_x = start_x + len(f"{labels[i]}: {display_text}") * 6  # Approximate character width
                self.game.draw_text(display, "|", (255, 255, 255), cursor_x, input_y)
        
        # Instructions 
        instructions = [
            ("Enter reps (1-30)", "Press ENTER to continue"),
            ("Enter sets (1-8)", "Press ENTER to continue"),
            ("Enter time (1-60 seconds)", "Press ENTER to start")
        ]
        # Get current instructions based on stage
        inst1, inst2 = instructions[self.stage]
        
        # Calculate positions for instructions (after the input fields)
        inst_y = start_y + self.game.distance_between_text_elements * 5.5
        
        # Draw instructions
        self.game.draw_text(display, inst1, (255, 255, 255), start_x, inst_y)
        self.game.draw_text(display, inst2, (200, 200, 200), start_x, inst_y + self.game.distance_between_text_elements * 0.7)
        self.game.draw_text(display, "Use BACKSPACE to erase", (200, 200, 200), start_x, inst_y + self.game.distance_between_text_elements * 1.4)
        
        # Handle case: Error -> Display error message
        if self.error_message:
            error_y = inst_y + self.game.distance_between_text_elements * 2.1
            self.game.draw_text(display, self.error_message, (255, 0, 0), start_x, error_y)

stretch_to_start.py

Python
# Import required libraries
import pygame, time, os
# Import State (parent) class
from states.state import State
# Import necessary functions
from states.assisting_functions.sensor_data import get_angle_from_port


'''Wait for the player to stretch out their arm before beginning gameplay'''
class Stretch_To_Start(State):
    def __init__(self, game):
        # Inherit from State class
        State.__init__(self, game)

        # Set positions for text elements
        self.start_y = self.game.GAME_H/2 - self.game.distance_between_text_elements  
        self.start_x = self.game.GAME_W/2  # Position text in center
        
        # Stretch detection parameters
        self.angle_leeway = 10 # Degrees of leeway for stretch detection 
        self.target_angle = self.game.max_angle - self.angle_leeway # Min angle where arm still counts as stretched out 
        
        # Time tracking parameters
        self.required_stretch_time = 3.0 # Time player needs to hold correct stretch
        self.stretch_timer = 0.0 # Accumulates time spent in correct position
       
        # Flag to indicate if arm is currently stretched correctly
        self.arm_stretched_correctly = False 

    '''Checks if Handle sensor data and state transition'''
    def update(self, delta_time, actions):
        # Get current angle from sensor
        current_angle = get_angle_from_port()
        
        # Handle case: there's angle data and the arm is in correct position
        if current_angle and current_angle >= self.target_angle:
            # Update stretched_correctly flag
            self.arm_stretched_correctly = True
            # Increment timer
            self.stretch_timer += delta_time

        # Handle case: arm not in correct position (anymore)
        else:
            # Update stretched_correctly flag -> no more counting up
            self.arm_stretched_correctly = False
            
        # Handle case: player has held stretch long enough -> transition to main game
        if self.stretch_timer >= self.required_stretch_time:
            from states.game_world import Game_World
            new_state = Game_World(self.game)
            new_state.enter_state()
        
        # Reset actions to avoid multiple triggers
        self.game.reset_keys()
    
    '''Render the text and countdown onto screen'''
    def render(self, display):
        # Fill the screen with our background colour
        display.fill(self.game.alligator_green)
        
        # Get the font from the font directory and create fonts
        font_path = os.path.join(self.game.font_dir, "PressStart2P-vaV7.ttf")
        small_font = pygame.font.Font(font_path, 12)
        tiny_font = pygame.font.Font(font_path, 8)
        
        # Temporarily replace the game's font
        original_font = self.game.font
        self.game.font = small_font
        
        # Show main instruction
        self.game.draw_text(display, "Stretch your arm out", (255, 255, 255), self.start_x, self.start_y)
        
        # Show progress timer
        progress_text = f"{self.stretch_timer:.1f}s / {self.required_stretch_time:.1f}s"
        self.game.font = tiny_font
        self.game.draw_text(display, progress_text, (200, 200, 200), self.start_x, self.start_y + 1*self.game.distance_between_text_elements)
        
        # Show arm status/flag
        status_symbol = "✓" if self.arm_stretched_correctly else "✗"
        status_color = (100, 255, 100) if self.arm_stretched_correctly else (255, 100, 100)
        status_text = f"Arm stretched out?: {status_symbol}"
        self.game.font = small_font
        self.game.draw_text(display, status_text, status_color, self.start_x, self.start_y + 2*self.game.distance_between_text_elements)
        
        # Restore original font
        self.game.font = original_font

game_world.py

Python
# Import required libraries
import pygame, os, time
# Import states from other files
from states.state import State
from states.next_set import Next_Set    
from states.game_over import Game_Over_Lose
from states.game_over import Game_Over_Win
# Import assisting functions from other files
from states.assisting_functions.sensor_data import angle_to_pixel
from states.assisting_functions.sprite_sheet_cutter import Chicken_SheetCutter
from states.assisting_functions.sprite_sheet_cutter import Alligator_SheetCutter


'''Handles main gameplay'''
class Game_World(State):
    def __init__(self, game):
        # Inherit from State class 
        State.__init__(self,game)

        # Main Game constants - screen boundaries for sprites
        self.lower_screen_boundary = game.GAME_H * 4 / 5 # Lower boundary for the gripper
        self.upper_screen_boundary = game.GAME_H / 6 # Upper boundary for the gripper

        self.pickup_boundary = self.upper_screen_boundary + (self.lower_screen_boundary - self.upper_screen_boundary)*9/10 # Boundary above which gripper is considered "above ground"
        
        # Main Game constants - Space between chickens in line up (relative to chicken width)
        chicken_width = game.GAME_W/4  # Same as chicken width calculation
        self.space_between_chickens = chicken_width * 0.5  # 50% less than chicken width

        # Get background image from directory and scale it to fit the screen
        background_img = pygame.image.load(os.path.join(self.game.assets_dir, "map", "swamp_background.png"))
        self.main_background_img = pygame.transform.scale(background_img, (game.GAME_W, game.GAME_H))

        # Create gripper and progress window
        self.gripper = Gripper(self.game, self.lower_screen_boundary, self.upper_screen_boundary)
        self.progress_window = Progress_Window(self.game)

        # Create first alligator and add to group 
        self.alligator_group = pygame.sprite.Group()
        self.alligator = Alligator(self.game, self.lower_screen_boundary)
        self.alligator_group.add(self.alligator)
        
        # Create <total_reps> amount of chickens, add them to sprite group and list them in pickup order 
        self.chicken_group = pygame.sprite.Group()
        for i in range(self.game.total_reps):
            self.chicken = Chicken(self.game, self.space_between_chickens, self.lower_screen_boundary,self.upper_screen_boundary, self.game.total_reps, i)
            self.chicken_group.add(self.chicken)
        self.chickens = list(self.chicken_group) #list of chickens to be picked up
        
        # Initialize last_chicken (will be used in update function)
        self.last_chicken = None
        
        # Initialize countdown to time_for_curl
        self.start_time = time.time()
        



    '''Handles sprite interaction and changes flags'''
    def update(self, delta_time, actions):
        # Only continue game logic if still winning (no chicken eaten yet)
        if self.game.winning:
            # Handle case: Still chickens left to pick up
            if (len(self.chickens) > 0):
                # Handle case: Still time left for current rep
                if (time.time() - self.start_time < self.game.time_for_curl):

                    # Handle case: gripper is at the bottom
                    if self.gripper.rect.centery >= self.lower_screen_boundary:
                        # If the gripper is open and collides with the next chicken
                        if self.gripper.gripper_open and self.gripper.rect.colliderect(self.chickens[0].rect):
                                self.chickens[0].picked_up = True
                                self.gripper.change_grip() 

                    # Handle case: gripper is above the pickup boundary and holding a chicken 
                    elif self.gripper.rect.centery > self.pickup_boundary and self.gripper.gripper_open == False:
                        # Change flag to indicate gripper is holding chicken (chicken is acutally above ground)
                        self.gripper.carrying_chicken = True

                    # Handle case: gripper holding chicken has made it to the top -> put down chicken
                    if self.gripper.rect.centery <= self.upper_screen_boundary and self.gripper.gripper_open == False and self.gripper.carrying_chicken:
                        
                        # Change sprite flags 
                        self.chickens[0].picked_up = False
                        self.chickens[0].put_down = True
                        self.gripper.carrying_chicken = False
                        self.gripper.change_grip()

                        # Increment rep count (successfully put down a chicken, rep completed)
                        self.game.current_rep += 1

                        # Make alligator submerge and tell it to die afterwards
                        self.alligator.set_animation("water_submerge")
                        self.alligator.should_die = True  


                        # Handle case: more chickens in queue
                        if len(self.chickens) > 1:
                            # Create new alligator immediately for next rep (to eat next chicken)
                            new_alligator = Alligator(self.game, self.lower_screen_boundary)
                            self.alligator_group.add(new_alligator)

                            # Make new alligator the current alligator
                            self.alligator = new_alligator

                            # Tell remaining chickens in line to walk right (by changing flag)
                            for i, chicken in enumerate(self.chickens):
                                if not chicken.put_down:  # Skip the chicken that was just put down
                                    chicken.walk_right = True
                            
                            # Reset timer for next rep 
                            self.start_time = time.time()

                        # Handle case: this was the last chicken
                        else:
                            # save the last chicken 
                            self.last_chicken = self.chickens[0]
                        
                        # Remove chicken we just put down from list 
                        self.chickens.pop(0)

                # Handle case: timer has run out -> run through bite animation -> game over :(
                else:
                    # Handle case: not already in bite sequence
                    if self.alligator.animation_state not in ["bite_open", "bite_close"]:
                        # Start Alligator open mouth animation and make chicken fall
                        self.alligator.set_animation("bite_open")
                        self.chickens[0].animation_state = "falling"
                        self.gripper.carrying_chicken = False
                        
                        # open gripper once
                        if (self.gripper.gripper_open == False):
                            self.gripper.change_grip()

                    # Handle case: Alligator is opening mouth to eat chicken + sprite collision
                    if self.alligator.animation_state == "bite_open" and pygame.sprite.collide_mask(self.alligator, self.chickens[0]):
                            # Start alligator close mouth animation, kill and remove chicken from list 
                            self.alligator.set_animation("bite_close")
                            self.chickens[0].kill()  
                            self.chickens.pop(0)  


            # Handle case: No chickens left and last chicken has flown off screen -> go to next state
            elif self.last_chicken and (self.last_chicken.rect.left > self.game.GAME_W):
                # Handle case: All sets completed -> transition directly to victory
                if self.game.current_set >= self.game.total_sets:
                    win_state = Game_Over_Win(self.game)
                    win_state.enter_state()

                # Handle case: More sets remaining -> go to next set screen
                else:
                    next_set_state = Next_Set(self.game)
                    next_set_state.enter_state()
                return  # Exit update to avoid further processing

            # Handle case: Alligator is has finished bite animation (even if no chickens left)
            if self.alligator.animation_state == "bite_close" and self.alligator.animation_finished:
                    # Set state to losing
                    self.game.winning = False
                    return  # Exit immediately to trigger lose state transition
  
            # Update sprites/sprite groups
            self.gripper.update(delta_time)
            self.progress_window.update(delta_time)
            self.alligator_group.update(delta_time)  
            self.chicken_group.update(delta_time)

        # Handle case: Game lost (chicken eaten) -> transition to lose screen
        else:
            lose_state = Game_Over_Lose(self.game)
            lose_state.enter_state()
        
    '''renders main game play area'''
    def render(self, display):
        # Draw the main background
        display.blit(self.main_background_img, (0,0))
        # Render sprites in layering order (back to front)
        self.progress_window.render(display)
        self.alligator_group.draw(display)  
        self.chicken_group.draw(display)
        self.gripper.render(display)

'''Sprite that player controls, used to pick up chickens'''
class Gripper():
    def __init__(self,game, lower_screen_boundary, upper_screen_boundary):
        self.game = game
        # Pass attributes to class
        self.lower_screen_boundary = lower_screen_boundary
        self.upper_screen_boundary = upper_screen_boundary

        # Load in gripper sprites and create rect 
        self.load_sprites()
        self.rect = self.curr_image.get_rect()

        # Initialize position
        self.rect.center = (self.game.GAME_W/2, self.game.GAME_H/2)

        # Initialize flags
        self.gripper_open = True
        self.carrying_chicken = False

    '''Loads images and saves them in animation list'''
    def load_sprites(self):
        # Sprite parameters - size
        self.gripper_width = self.game.GAME_W/13
        self.gripper_height = self.gripper_width*1024/512
        # Get the directory with sprite images
        self.sprite_dir = os.path.join(self.game.sprite_dir, "gripper")
        # Create list with animation frames
        self.gripping_images = [] 
        # Load in the frames 
        self.gripping_images.append(pygame.image.load(os.path.join(self.sprite_dir, "gripper_open" + ".png")))
        self.gripping_images.append(pygame.image.load(os.path.join(self.sprite_dir, "gripper_closed" + ".png")))
        # Scale frames correctly 
        for i in range(len(self.gripping_images)):
            self.gripping_images[i] = pygame.transform.scale(self.gripping_images[i], (self.gripper_width, self.gripper_height))
        # Initialize image to open gripper
        self.current_frame = 0
        self.curr_image = self.gripping_images[self.current_frame]
        

    '''Switch grip image'''
    def change_grip(self):
        # Change from open to closed or vice versa
        self.current_frame = (self.current_frame + 1) % 2
        self.curr_image = self.gripping_images[self.current_frame]
        # Update flag
        self.gripper_open = not self.gripper_open

    '''Make gripper move up and down with sensor'''
    def update(self, delta_time):
        self.rect.centery = angle_to_pixel(self.lower_screen_boundary, self.upper_screen_boundary, self.game.min_angle, self.game.max_angle)
        
    '''Render image'''
    def render(self, display):
        display.blit(self.curr_image, self.rect)
    
'''Sprites that all need to be picked up by the gripper in a certain amount of time to win'''
class Chicken(pygame.sprite.Sprite):
    def __init__(self, game, space_between_chickens, lower_screen_boundary, upper_screen_boundary, reps, i):
        # Inherit from State class
        super().__init__()

        # Pass attributes
        self.game = game
        self.lower_screen_boundary = lower_screen_boundary
        self.upper_screen_boundary = upper_screen_boundary
        self.space_between_chickens = space_between_chickens
        
        # Chicken parameters - size
        self.chicken_width = game.GAME_W/4
        self.chicken_height = self.chicken_width

        # Chicken parameters - speeds
        self.chicken_speed_walking = 100/1 #pixels/sec
        self.chicken_speed_falling = 100/1 #pixels/sec

        # Set position, line up in order of pickup list
        self.y = lower_screen_boundary
        self.x = game.GAME_W/2 - i * space_between_chickens
        
        # State flags
        self.picked_up = False
        self.put_down = False
        self.walk_right = False

        # Get address of sprite image folder and create sprite sheetcutter
        self.sprite_dir = os.path.join(self.game.sprite_dir, "chicken")
        self.sprite_sheet_cutter = Chicken_SheetCutter()

        # Map animation states to corresponding animation sheet names 
        animation_files = {
            "idle_blink": "chicken_idle_strip4.png",
            "flying_away": "chicken_fly_strip4.png",
            "walk_right": "chicken_walk_strip8.png",
            "falling": "chicken_fall_strip2.png"
        }

        # Create dictionary that maps animation name to corresponding animation list
        self.animations = {}
        for anim_name, filename in animation_files.items():
            #load appropriate image from sprite directory
            img = pygame.image.load(os.path.join(self.sprite_dir, filename)).convert_alpha()
            #animations directory stores lists of frames for respective animation names (keys)
            self.animations[anim_name] = self.sprite_sheet_cutter.extract_all_chicken_frames(img, self.chicken_width, self.chicken_height)
        
        # Initialize animation state
        self.animation_state = "idle_blink"
        self.current_frame = 0
        self.last_frame_update = 0 #time since last frame update
        self.curr_anim_list = self.animations[self.animation_state]
        self.image = self.curr_anim_list[0]
        
        # Create mask and rect 
        self.rect = self.image.get_rect()
        self.mask = pygame.mask.from_surface(self.image)

        # Initialize position
        self.rect.center = (self.x, self.y)



    '''Handles movement and animation state changes'''
    def update(self, delta_time):
        # Handle case: chicken's been picked up from the platform
        if self.picked_up:
            # Handle case: because time has run out -> falling -> make fall 
            if self.animation_state == "falling":
                #if the timer has run out -> falling into alligators mouth
                self.rect.centery = self.rect.centery + self.chicken_speed_falling * delta_time
            #Handle case: still time left -> same y coordinates as the gripper
            else:
                self.rect.centery = angle_to_pixel(self.lower_screen_boundary, self.upper_screen_boundary, self.game.min_angle, self.game.max_angle)
                
                
        # Handle case: if has reached the upper boundary -> put down and fly away
        elif self.put_down:
            # Fly away
            self.rect.centery = self.upper_screen_boundary
            self.rect.centerx += self.chicken_speed_walking * delta_time
            self.animation_state = "flying_away"
            # Kill chicken when center reaches screen_width + chicken_width/2
            if self.rect.centerx > (self.game.GAME_W + self.chicken_width/2):
                self.kill()

        #Handle case:needs to walk right (still in line and current chicken has been put down)
        elif self.walk_right:
            self.rect.centerx += self.chicken_speed_walking * delta_time
            self.animation_state = "walk_right"
            # Handle case: covered required distance between chicken -> stop walking
            if self.rect.centerx - self.x >= self.space_between_chickens:
                self.walk_right = False
                self.x = self.rect.centerx
                self.animation_state = "idle_blink"
                self.current_frame = 0

        self.animate(delta_time)
    '''Renders current image onto screen'''
    def render(self, display):
        display.blit(self.image, self.rect)

    '''Loops through the current animation list at frame_rate'''
    def animate(self, delta_time):
        # Increment last_frame_update by time since last game update 
        self.last_frame_update += delta_time
        # Get current animation list (using our current state)
        self.curr_anim_list = self.animations.get(self.animation_state, self.animations["idle_blink"])
        # Handle case: frame duration amount of time has passed -> switch to next animation frame
        if self.last_frame_update > self.game.frame_duration: 
            # Restart timer for frame update 
            self.last_frame_update = 0
            # Go to next frame (loop through), update image and get mask
            self.current_frame = (self.current_frame + 1) % len(self.curr_anim_list)
            self.image = self.curr_anim_list[self.current_frame]
            self.mask = pygame.mask.from_surface(self.image)
            
'''Sprite that tries to eat chickens, gets to the middle in time_for_curl amount of time'''
class Alligator(pygame.sprite.Sprite):
    def __init__(self, game, lower_screen_boundary):
        # Inherit from State class
        super().__init__()
        self.game = game
        # pass on game attributes and initialize size and starting point
        self.lower_screen_boundary = lower_screen_boundary

        
        # Size parameters
        self.alligator_width = game.GAME_W/2.5
        self.alligator_height = self.alligator_width*64/96

        # Calculate where mouth is relative to left edge (to ensure smoother animation)
        self.mouth_offset = self.alligator_width * 0.02  # 5% of width
        # Where the mouth should be at the end of the swim and where the left edge should be respectively
        self.target_mouth_x = self.game.GAME_W / 2
        # Where the left side of the rect to be at the end (so that the mouth is in the middle)
        self.target_left_x = self.target_mouth_x - self.mouth_offset

        # Speed parameters
        self.alligator_speed = (self.game.GAME_W/2-self.mouth_offset)/self.game.time_for_curl #pixels/sec
        
        # Initialize position
        self.initial_y = self.lower_screen_boundary 
        self.initial_x = game.GAME_W + self.alligator_width/2   

        # Sprite flags
        self.should_die = False  # Flag to make alligator disappear after submerging

        # Locate the sprite directory and load the sprite sheet
        self.sprite_sheet_cutter = Alligator_SheetCutter()
        self.sprite_dir = os.path.join(self.game.sprite_dir, "alligator")
        self.sprite_sheets_img = pygame.image.load(os.path.join(self.sprite_dir, "alligator_spritesheets.png")).convert_alpha()
        
        # Load all animations into a dictionary, resize frames
        self.animations = {}
        
        # Load all standard animations (using the map made in the cutter class)
        for anim_name in self.sprite_sheet_cutter.animation_map.keys():
            self.animations[anim_name] = self.sprite_sheet_cutter.extract_all_alligator_frames(
                anim_name, self.sprite_sheets_img, self.alligator_width, self.alligator_height)
        
        # Split bite animation into bite_wait (frames 0-4) and bite_close (frames 4-8) from water_bite
        bite_frames = self.sprite_sheet_cutter.extract_all_alligator_frames(
            "water_bite", self.sprite_sheets_img, self.alligator_width, self.alligator_height)
        self.animations["bite_open"] = bite_frames[:5]  # frames 0-4
        self.animations["bite_close"] = bite_frames[4:] # frames 4-7 (or 4-8 if 8 frames)

        # Define which animations should loop (only the ones we'll use in our game)
        self.looping_animations = {
            "water_swim": True,
            "water_bite": False,
            "water_submerge": False,
            "water_emerge": False,
            "bite_open": False,
            "bite_close": False
        }

        #set beginnning animation state
        self.set_animation("water_emerge")  

        #get rect (for positioning)
        self.rect = self.image.get_rect()
        self.rect.center = (self.initial_x, self.initial_y)
    
        # Create mask - transparent background means only actual sprite pixels count
        self.mask = pygame.mask.from_surface(self.image)

    ''''''        
    def update(self, delta_time):
        #make it move left if in swimming/emerging/submerging state
        if self.animation_state in ["water_swim", "water_emerge", "water_submerge"]:
            self.rect.left = self.rect.left - self.alligator_speed * delta_time

        #keep in bounds
        if (self.rect.left <= self.target_left_x):
            self.rect.left = self.target_left_x

        #transition emerge to swim
        if self.animation_state == "water_emerge" and self.animation_finished:
            self.set_animation("water_swim")

        #transition submerge to emerge
        # Note: This transition is triggered externally when the player successfully puts down a chicken
        if self.animation_state == "water_submerge" and self.animation_finished:
            if self.should_die:
                self.kill()
        
        self.animate(delta_time)

    def render(self, display):
        display.blit(self.image, self.rect)

    #Set a new animation state, reset frame counter if different or if first time
    def set_animation(self, state):
        if not hasattr(self, 'animation_state') or self.animation_state != state:
            self.animation_state = state
            self.curr_anim_list = self.animations[state]
            self.current_frame = 0
            self.last_frame_update = 0
            self.animation_finished = False
            # Initialize the image for the first frame
            self.image = self.curr_anim_list[0]

    """General animation function that handles looping and non-looping animations"""
    def animate(self, delta_time):
        # Increment timer 
        self.last_frame_update += delta_time
        
        # Go to next frame if enough time has passed 
        if self.last_frame_update >= self.game.frame_duration:
            self.last_frame_update = 0
            self.current_frame += 1

            # Check if reached the end
            if self.current_frame >= len(self.curr_anim_list):
                # Handle case: looping animation -> back to first frame
                if self.looping_animations.get(self.animation_state, False):
                    self.current_frame = 0
                    self.animation_finished = False  # Reset for looping animations
                # Handle case: one-time animation -> stay at last frame
                else:
                    self.current_frame = len(self.curr_anim_list) - 1
                    self.animation_finished = True

            # Update the image and mask only when frame changes
            self.image = self.curr_anim_list[self.current_frame]
            self.mask = pygame.mask.from_surface(self.image)


class Progress_Window():
    def __init__(self, game):
        self.game = game
        self.width = game.GAME_W / 5  # Medium width (between /4 and /6)
        self.height = game.GAME_H / 10  # Medium height (between /8 and /12)
        self.x = game.GAME_W / 40  # Margin from left edge (2.5% of screen width)
        self.y = game.GAME_H / 40  # Margin from top edge (2.5% of screen height)
        self.rect = pygame.Rect(self.x, self.y, self.width, self.height)
        
        # Colors
        self.bg_color = (50, 50, 50, 180)  # Semi-transparent dark gray
        self.border_color = (255, 255, 255)  # White border
        self.text_color = (255, 255, 255)  # White text

    def update(self, delta_time):
        # Progress window doesn't need updating, but method exists for consistency
        pass

    def render(self, display):
        # Create a surface with per-pixel alpha for transparency
        progress_surface = pygame.Surface((self.width, self.height), pygame.SRCALPHA)
        
        # Fill background with semi-transparent color
        progress_surface.fill((50, 50, 50, 180))
        
        # Draw border
        pygame.draw.rect(progress_surface, self.border_color, 
                        (0, 0, self.width, self.height), 2)
        
        # Calculate current rep (show actual completed reps, not "next rep")
        current_rep_display = self.game.current_rep
        if current_rep_display > self.game.total_reps:
            current_rep_display = self.game.total_reps

        # Create text
        set_text = f"Set {self.game.current_set}/{self.game.total_sets}"
        rep_text = f"Rep {current_rep_display}/{self.game.total_reps}"

        # Render text with font size scaled to window
        font_size = max(8, int(self.game.GAME_H / 60))  # Scale font size based on game height
        small_font = pygame.font.Font(os.path.join(self.game.font_dir, "PressStart2P-vaV7.ttf"), font_size)
        
        set_surface = small_font.render(set_text, True, self.text_color)
        rep_surface = small_font.render(rep_text, True, self.text_color)
        
        # Calculate equal margins on all sides (based on smallest dimension for consistency)
        margin = min(self.width, self.height) / 48  # Use 1/48 of smaller dimension as margin (extremely tight spacing)
        
        # Calculate available space for text
        available_height = self.height - (2 * margin)
        text_spacing = (available_height - set_surface.get_height() - rep_surface.get_height()) / 3
        
        # Position text with equal margins
        set_rect = set_surface.get_rect()
        set_rect.centerx = self.width // 2
        set_rect.y = margin + text_spacing
        
        rep_rect = rep_surface.get_rect()
        rep_rect.centerx = self.width // 2
        rep_rect.y = set_rect.bottom + text_spacing
        
        # Blit text to progress surface
        progress_surface.blit(set_surface, set_rect)
        progress_surface.blit(rep_surface, rep_rect)
        
        # Blit progress surface to main display
        display.blit(progress_surface, (self.x, self.y))
        
    


    

next_set.py

Python
# Import required libraries
import pygame, os
# Import State (parent) class
from states.state import State
# Import next state
from states.stretch_to_start import Stretch_To_Start

'''Renders the "next set" screen and goes back to stretch_to_start when ENTER is pressed'''
class Next_Set(State):
    def __init__(self, game):
        # Inherit from State class
        State.__init__(self, game)

        # Set positions for text elements
        self.start_y = self.game.GAME_H/2 - self.game.distance_between_text_elements  # Start above center
        self.start_x = self.game.GAME_W/2  # Position text in center
        
    '''Handle state transition when ENTER is pressed'''
    def update(self, delta_time, actions):
        # Handle case: Enter key has been pressed
        if actions["enter"]:
            self.game.current_set += 1 # Increment current set
            self.game.current_rep = 0  # Reset reps for new set
            self.game.winning = True  # Reset winning flag
            new_state = Stretch_To_Start(self.game) # Create new Stretch_To_Start state
            new_state.enter_state() # Transition to Stretch_To_Start state
        
        # Reset actions to avoid multiple triggers
        self.game.reset_keys()

    '''Render the "next set" screen'''
    def render(self, display):
        # Fill the screen with background color
        display.fill(self.game.alligator_green)  
        
        # Create smaller fonts for the screen
        font_path = os.path.join(self.game.font_dir, "PressStart2P-vaV7.ttf")
        small_font = pygame.font.Font(font_path, 12)
        
        # Temporarily replace the game's font
        original_font = self.game.font
        self.game.font = small_font
        
        # Draw title, progress and instructions
        self.game.draw_text(display, "REST TIME", (255, 255, 255), self.start_x, self.start_y)
        set_text = f"Set {self.game.current_set} of {self.game.total_sets} Complete!"
        self.game.draw_text(display, set_text, (200, 200, 200), self.start_x, self.start_y + 1*self.game.distance_between_text_elements)
        instruction_start = self.start_y + 2 * self.game.distance_between_text_elements
        self.game.draw_text(display, "Press ENTER for next set", (255, 255, 255), self.start_x, instruction_start)

        # Restore original font
        self.game.font = original_font

game_over.py

Python
import pygame, os
from states.state import State

# Game Over screen when player loses (chicken gets eaten)
class Game_Over_Lose(State):
    def __init__(self, game):
        State.__init__(self, game)
        
        # Set positions for text elements
        self.start_y = self.game.GAME_H/2 - self.game.distance_between_text_elements  # Start above center
        self.start_x = self.game.GAME_W/2  # Position text in center

    def render(self, display):
        display.fill((50, 0, 0))  # Dark red background
        
        # Create smaller fonts for the screen
        font_path = os.path.join(self.game.font_dir, "PressStart2P-vaV7.ttf")
        large_font = pygame.font.Font(font_path, 16)
        small_font = pygame.font.Font(font_path, 10)
        
        # Temporarily replace the game's font
        original_font = self.game.font
        
        # Draw main message
        self.game.font = large_font
        self.game.draw_text(display, "GAME OVER", (255, 100, 100), self.start_x, self.start_y)
        
        # Draw subtitle
        self.game.font = small_font
        self.game.draw_text(display, "The alligator got you!", (255, 255, 255), self.start_x, self.start_y + 1*self.game.distance_between_text_elements)
        
        # Restore original font
        self.game.font = original_font
        

    def update(self, delta_time, actions):
        pass

# Victory screen when player completes all sets
class Game_Over_Win(State):
    def __init__(self, game):
        State.__init__(self, game)
        
        # Set positions for text elements
        self.start_y = self.game.GAME_H/2 - self.game.distance_between_text_elements  # Start above center
        self.start_x = self.game.GAME_W/2  # Position text in center

    def render(self, display):
        display.fill(self.game.alligator_green)  # Dark green background
        
        # Create smaller fonts for the screen
        font_path = os.path.join(self.game.font_dir, "PressStart2P-vaV7.ttf")
        large_font = pygame.font.Font(font_path, 16)
        small_font = pygame.font.Font(font_path, 10)
        
        # Temporarily replace the game's font
        original_font = self.game.font
        
        # Draw main message
        self.game.font = large_font
        self.game.draw_text(display, "VICTORY!", (100, 255, 100), self.start_x, self.start_y)
        
        # Draw workout completed message
        self.game.font = small_font
        self.game.draw_text(display, "Workout Complete!", (255, 255, 255), self.start_x, self.start_y + 1*self.game.distance_between_text_elements)
        
        # Draw stats
        stats_text = f"Sets: {self.game.total_sets}  Reps: {self.game.total_reps}"
        self.game.draw_text(display, stats_text, (200, 255, 200), self.start_x, self.start_y + 2*self.game.distance_between_text_elements)
        
        # Restore original font
        self.game.font = original_font
        
    def update(self, delta_time, actions):
        pass

# Keep the old Game_Over for backwards compatibility (alias to lose screen)
class Game_Over(Game_Over_Lose):
    pass

sprite_sheet_cutter,py

Python
#should I save it somewhere else? its not really a state.....
import pygame

class Chicken_SheetCutter:
    
    def __init__(self):
        self.chicken_frame_width = 40
        self.chicken_frame_height = 40

    #extracts an individual frame from a sprite sheet and returns it as a surface
    def extract_chicken_frame(self, sheet_img, index, chicken_width=None, chicken_height=None):
        #calculate the beginning x,y coordinates of the frame 
        #(assumes all frames are in one row)
        self.sheet = sheet_img
        self.sheet_width, self.sheet_height = self.sheet.get_size()
        x = index * self.chicken_frame_width
        y = 0  
        #creates a new surface for the frame 
        frame_surface = pygame.Surface((self.chicken_frame_width, self.chicken_frame_height), pygame.SRCALPHA)
        frame_surface.blit(self.sheet, (0, 0), (x, y, self.chicken_frame_width, self.chicken_frame_height))
        # Always remove black background
        frame_surface.set_colorkey((0,0,0))
        # Resize if new dimensions are provided
        if chicken_width is not None and chicken_height is not None:
            frame_surface = pygame.transform.scale(frame_surface, (int(chicken_width), int(chicken_height)))
        return frame_surface
    
    #extracts individual frames from a sprite sheet and returns them as a list
    def extract_all_chicken_frames(self, sheet_img, chicken_width=None, chicken_height=None, scale=1):
        self.sheet_width, self.sheet_height = sheet_img.get_size()
        num_frames = self.sheet_width // self.chicken_frame_width
        frames = []
        for i in range(num_frames):
            frames.append(self.extract_chicken_frame(sheet_img, i, chicken_width, chicken_height))
        return frames
    


class Alligator_SheetCutter:
    def __init__(self):
        self.alligator_frame_width = 96
        self.alligator_frame_height = 64
        self.alligator_frames_per_sheet = [6,6,8,8,8,8,8,8,8,8,8,8]
        # Map animation names to sheet indices

        # mapping the animations according to the sprite sheet layout
        self.animation_map = {
            "ground_crawling": 0,
            "ground_idle": 1,
            "ground_bite": 2,
            "ground_hurt": 3,
            "ground_spin": 4,
            "water_swim": 5,
            "water_idle": 6,
            "water_bite": 7,
            "water_hurt": 8,
            "water_spin": 9,
            "water_submerge": 10,
            "water_emerge": 11
        }

    #extracts an entire sheet (row) from the sprite sheet and returns it as a surface
    def extract_alligator_sheet(self, index_sheet, sheets_img, transparent=(0,0,0)):
        self.sheets = sheets_img
        #width of sheet is the amount of frames times frame width
        sheet_width = self.alligator_frames_per_sheet[index_sheet] * self.alligator_frame_width
        sheet_height = self.alligator_frame_height
        x = 0
        y = index_sheet * self.alligator_frame_height
        #creates a new surface for the frame
        frame_surface = pygame.Surface((sheet_width, sheet_height), pygame.SRCALPHA)
        frame_surface.blit(self.sheets, (0, 0), (x, y, sheet_width, sheet_height))
        if transparent is not None:
            frame_surface.set_colorkey(transparent)
        return frame_surface

    #extracts an individual frame from a sprite sheet and returns it as a surface
    def extract_alligator_frame(self, index_sheet, sheets_img, index_frame, alligator_width=None, alligator_height=None):
        #calculate the beginning x,y coordinates of the frame 
        self.sheet = self.extract_alligator_sheet(index_sheet, sheets_img, transparent=(0,0,0))
        self.sheet_width, self.sheet_height = self.sheet.get_size()
        x = index_frame * self.alligator_frame_width
        y = 0  
        #creates a new surface for the frame 
        frame_surface = pygame.Surface((self.alligator_frame_width, self.alligator_frame_height), pygame.SRCALPHA) 
        frame_surface.blit(self.sheet, (0, 0), (x, y, self.alligator_frame_width, self.alligator_frame_height))
        frame_surface.set_colorkey((0,0,0))
        #flip the images (cause we're going in the opposite direction)
        flip_horizontal=True
        if flip_horizontal:
            frame_surface = pygame.transform.flip(frame_surface, True, False)
        # Resize if new dimensions are provided
        if alligator_width is not None and alligator_height is not None:
            frame_surface = pygame.transform.scale(frame_surface, (int(alligator_width), int(alligator_height)))
        return frame_surface

    #returns a list of all frames for a given animation 
    def extract_all_alligator_frames(self, animation, sheet_img, alligator_width=None, alligator_height=None):
        """
        animation: str or int
            If str, looks up the sheet index from animation_map.
            If int, uses as sheet index directly.
        """
        if isinstance(animation, str):
            index_sheet = self.animation_map.get(animation)
            if index_sheet is None:
                raise ValueError(f"Unknown animation name: {animation}")
        else:
            index_sheet = animation
        frames = []
        num_frames = self.alligator_frames_per_sheet[index_sheet]
        for i in range(num_frames):
            frames.append(self.extract_alligator_frame(index_sheet, sheet_img, i, alligator_width, alligator_height))
        return frames

sensor_data.py

Python
import serial

# Initialize serial port connection
ser = serial.Serial('COM5', 9600, timeout=1)

''''''
def get_angle_from_port(max_retries=5):
    #Try reading a line from the main port and convert it to a float 
    for attempt in range(max_retries):
        try:
            data = ser.readline().decode().strip()
            angle = float(data)
            return angle
        except ValueError:
            print(f"Couldn't convert '{data}' to float.")
            if attempt == max_retries - 1:  # Last attempt
                return None
            continue  # Now this is valid inside the loop
        except serial.SerialException:
            print("No data available.")
            return None
    
'''Map angle to pixel position on screen'''
def angle_to_pixel(lower_screen_boundary, upper_screen_boundary, min_angle, max_angle):
    angle = get_angle_from_port()
    # Calculate the normalized angle (between 0 and 1)
    normalized_angle = (angle - min_angle) / (max_angle - min_angle)
    # Map the normalized angle to the screen coordinates
    y_pixel = upper_screen_boundary + (lower_screen_boundary - upper_screen_boundary) * normalized_angle
    # Keep in bounds
    if(y_pixel > lower_screen_boundary): y_pixel = lower_screen_boundary
    elif(y_pixel < upper_screen_boundary): y_pixel = upper_screen_boundary

    return y_pixel

Credits

Infineon Team
114 projects • 186 followers

Comments