
Starter Code
Let's get started! Your robot is preloaded with the code below. Read through it while you're waiting to play so that you understand the robot's default behaviour, and it should give you some ideas for improvements!
The Robot's Logic
The robot runs in a basic control loop, and will behave differently based on its state:
- In the
findingBallstate, the robot will move around until its AI Vision camera spots the ball. The onboard camera is tuned to spot the shape of the ball, but it will also look for the color signature of the ball. Using both methods helps it to see the ball when it is moving (which adds motion blur). After a few attempts, if the ball hasn't been found, the robot will move around in a random direction. - If the ball is spotted, the
statuschanges togettingBall. Once again using the AI Vision camera the robot will attempt to align with the ball and move towards it, until it detects that the ball has been captured (the robot and the ball use small magnets to "pick up" the ball). - If the robot loses the ball during this state, it will go back to a status of
findingBall. If it manages to grab the ball, the state will change tofindingGoal. - In the
findingGoalstate, the robot will look for the barrels at the back of the opponent's goal. Importantly, the blue robot should look for the orange barrels and vice versa, because the robot should always aim at its opponent's goal! - Once found, the robot will use the height of the barrel object in its camera to judge the distance to the goal. If necessary, it will try to move closer, and finally kick the ball (hopefully into the goal!)
- A
crashingstate is triggered by a callback that is activated by the robot's intertial sensors. The robot is likely to crash into the sides of the pitch, or the other robot, so we need to handle crashing. When this happens, the robot will pause and then turn around, hopefully moving it away from whatever obstacle it crashed into.
The Code
The code can all be found on github here: https://github.com/timhberry/ai-roboball-robot/blob/main/main.py
You can read it there, or follow a more in-depth walkthrough below. Note that we're starting on line 80, because everything before that is just setup that gets automatically generated by Vex.
Setup and configuration
So first we define some states for our robot (note that micro-python doesn't support real enumerators!)
# Robot Status "Enum" (using a class for namespacing on MicroPython)
class RobotStatus:
FINDING_BALL = "findingBall"
GETTING_BALL = "gettingBall"
FINDING_GOAL = "findingGoal"
CRASHED = "crashed"
Next we define some constants. You'll see these values used throughout the code. Think of these as easily-tunable dials:
# Movement Constants
DEFAULT_SPEED = 100
PRECISION_MOVE_VELOCITY = 50
PRECISION_TURN_VELOCITY = 25 # Used for aligning with goal
SCAN_TURN_VELOCITY = 30 # New constant for ball scanning turns
SCAN_RANDOM_MOVE_MM = 20 # Distance to move after a full scan
# Vision and Behavior Constants
GOAL_HEIGHT_THRESHOLD = 30 # Minimum height of goal barrel to consider close enough
SCAN_MOVE_DURATION_SEC = 2
SCAN_CYCLES_BEFORE_ACTION = 3
APPROACH_MOVE_DURATION_MSEC = 20
TURN_MOVE_DURATION_MSEC = 20
Now we set the inital state of the robot to FINDING_BALL:
# Set the inital state for the robot
crashed = False
status = RobotStatus.FINDING_BALL
robot.set_move_velocity(DEFAULT_SPEED)
robot.set_turn_velocity(DEFAULT_SPEED)
The VEX framework allows to define a crashed_callback() which we'll use to simply set crashed to True. We can handle that change of state later on in our main loop logic:
# If we ever crash, use this callback
def crashed_callback():
global crashed
crashed = True
# Set up the callback and crash sensitivity
robot.inertial.crashed(crashed_callback)
robot.inertial.set_crash_sensitivity(NORMAL)
In our final bit of setup, we configure the colour signature for the ball, which gives us an extra way to help the robot see it (even though its camera model is already tuned for the object itself). We then make sure the robot's intetial sensors are callibrated and ready to go:
# AI Vision Color detection for the ball
BALLCOLOR = Colordesc(1, 206, 211, 138, 20, 0.3)
robot.vision.color_description(BALLCOLOR)
# Calibrate the robot and disable AprilTag sensing
robot.inertial.calibrate()
if robot.inertial.is_calibrating():
print("Waiting for callibration to complete")
wait(2, SECONDS)
Looking for the ball
Now we can start defining some functions that we'll use in our main loop. Our first function look_for_ball() just makes the robot move around in place. It's extremely basic, but it can be called in a loop later on in conjunction for checking the camera:
def look_for_ball():
"""Makes the robot rotate in place to scan for the ball."""
robot.turn(RIGHT, SCAN_TURN_VELOCITY) # Or LEFT, or alternate directions for a more thorough scan
wait(TURN_MOVE_DURATION_MSEC, MSEC) # Use a small duration for a quick turn
robot.stop_all_movement() # Stop after the turn
Aligning with the goal
In align_with_goal() we assume that the robot has the ball, and needs to find the goal. In this function we have logic to rotate the robot and check the camera for site of the opponent's goal. Note we can break out of this loop either if we drop the ball, or if we've turned 100 times and we haven't found the goal. This could happen if the goal is too far away, or if the view of the goal keeps getting blocked by the other robot.
Note: In this example, we're looking for the ORANGE_BARREL object, so we'd run this code on the blue robot. If you are coding the orange robot, change this to the BLUE_BARREL!
def align_with_goal():
"""Turns the robot to face the opponent's goal barrel."""
robot.set_turn_velocity(PRECISION_TURN_VELOCITY)
robot.set_move_velocity(PRECISION_MOVE_VELOCITY)
max_turns = 100 # Prevent infinite loop
turn_count = 0
while turn_count < max_turns:
# Check if we still have the ball at the start of each turn
if not robot.has_sports_ball():
print("Dropped the ball while aligning with goal!")
return False
vision_data = robot.vision.get_data(ORANGE_BARREL)
if vision_data and vision_data[0].exists:
robot.turn_to(robot.inertial.get_heading() + vision_data[0].bearing)
robot.stop_all_movement()
return True
else:
robot.turn(RIGHT)
wait(TURN_MOVE_DURATION_MSEC, MSEC)
robot.stop_all_movement()
turn_count += 1
print("Warning: Goal not found after many turns.")
return False
Approaching the goal
When we find the goal, we might still be quite far away from it, which reduces the change of us actually being able to score a goal. So we can get the height of the barrel object, and move closer until it meets a requirement (the GOAL_HEIGHT_THRESHOLD in pixels):
def approach_goal():
"""Moves the robot closer to the goal until a certain height threshold is met."""
while True:
vision_data = robot.vision.get_data(ORANGE_BARREL)
if vision_data and vision_data[0].exists:
goal_height = vision_data[0].height
if goal_height >= GOAL_HEIGHT_THRESHOLD:
robot.stop_all_movement()
return True
robot.move_at(vision_data[0].bearing)
wait(APPROACH_MOVE_DURATION_MSEC, MSEC)
else:
# Lost sight of the goal, stop approaching
robot.stop_all_movement()
return False
Kicking the ball
We've got the ball, we've found the goal and we've approached it! All that's left is to kick the ball. This function kicks the ball, performs a celebratory dance, and then resets the robot's heading and velocity:
def execute_kick():
"""Performs the kicking action and celebratory animations."""
robot.kicker.kick(MEDIUM)
robot.screen.show_emoji(AMAZED)
robot.sound.play(ACT_HAPPY)
for _ in range(3):
robot.led.on(ALL_LEDS, YELLOW)
wait(.25, SECONDS)
robot.led.off(ALL_LEDS)
wait(.25, SECONDS)
# Back to the game, reset velocity and turn around
wait(3, SECONDS)
robot.set_move_velocity(DEFAULT_SPEED)
robot.set_turn_velocity(DEFAULT_SPEED)
robot.turn(RIGHT)
wait(1, SECONDS)
robot.stop_all_movement()
Main loop: Finding the ball
Our main loop now runs and uses a combination of the above functions and other logic to handle the behaviour of the robot based on its state. First, in the FINDING_BALL state, we continuously call the look_for_ball() function while checking the camera for either the ball or its colour signature. If we detect the ball, we change states to GETTING_BALL:
while True:
if status == RobotStatus.FINDING_BALL:
# If we are finding the ball, we run the look_for_ball function in a thread
# and stop when we see the ball in our AI Vision camera.
# Since look_for_ball() is now non-blocking, we call it directly.
robot.screen.show_emoji(THINKING)
# Get the robot's rotation at the start of the scan
start_scan_rotation = robot.inertial.get_rotation()
ball_found = False
while True:
if crashed:
status = RobotStatus.CRASHED
break
look_for_ball() # Perform a small turn to scan
detected_ball_object = None
# Prioritize detection by custom color signature (BALLCOLOR)
vision_data_color = robot.vision.get_data(BALLCOLOR)
if vision_data_color: # Check if the tuple is not empty
detected_ball_object = vision_data_color[0]
# If not found by color, check by pre-trained SPORTS_BALL object type
elif not detected_ball_object: # Only check SPORTS_BALL if BALLCOLOR wasn't found
vision_data_sports_ball = robot.vision.get_data(SPORTS_BALL)
if vision_data_sports_ball: # Check if the tuple is not empty
detected_ball_object = vision_data_sports_ball[0]
if detected_ball_object:
robot.turn_to(robot.inertial.get_heading() + detected_ball_object.bearing)
status = RobotStatus.GETTING_BALL
ball_found = True
break
# After checking for the ball, see if we need to move
if abs(robot.inertial.get_rotation() - start_scan_rotation) >= 360:
print("Completed a full 360 scan, moving to a new spot.")
# Choose a random direction: 0=fwd, 90=right, 180=back, 270=left
move_directions = [0, 90, 180, 270]
random_direction = random.choice(move_directions)
# Move in that direction. This is a blocking call.
robot.move_at(random_direction, PRECISION_MOVE_VELOCITY)
wait(SCAN_MOVE_DURATION_SEC, SECONDS)
robot.stop_all_movement()
# Reset the scan rotation tracker for the next 360-degree scan
start_scan_rotation = robot.inertial.get_rotation()
if not ball_found and not crashed: # If loop broke for other reason
status = RobotStatus.FINDING_BALL # Re-enter finding ball state
Main loop: Getting the ball
In the GETTING_BALL state, we use another loop to keep moving the robot towards the ball until its magnet picks it up. If this works, we'll change the state to FINDING_GOAL, but if anything goes wrong we'll go back to FINDING_BALL:
elif status == RobotStatus.GETTING_BALL:
# If we've seen the ball, we now need to go get it!
robot.screen.show_emoji(THRILLED)
ball_acquired = False
while True:
if crashed:
status = RobotStatus.CRASHED
break
detected_ball_object = None
# Prioritize detection by custom color signature (BALLCOLOR)
vision_data_color = robot.vision.get_data(BALLCOLOR)
if vision_data_color: # Check if the tuple is not empty
detected_ball_object = vision_data_color[0]
# If not found by color, check by pre-trained SPORTS_BALL object type
elif not detected_ball_object: # Only check SPORTS_BALL if BALLCOLOR wasn't found
vision_data_sports_ball = robot.vision.get_data(SPORTS_BALL)
if vision_data_sports_ball: # Check if the tuple is not empty
detected_ball_object = vision_data_sports_ball[0]
if detected_ball_object: # We can still see the ball
if robot.has_sports_ball(): # Do we have it?
robot.stop_all_movement() # If so, break the While loop
ball_acquired = True
break
else:
robot.move_at(detected_ball_object.bearing) # If not, move towards it
wait(APPROACH_MOVE_DURATION_MSEC, MSEC)
else: # We can't see the ball
break # so break the While loop
if ball_acquired: # Did we get it?
status = RobotStatus.FINDING_GOAL # Great, go find the goal
else:
status = RobotStatus.FINDING_BALL # We lost it, go back to playing
Main loop: Finding the goal
In the FINDING_GOAL state, we'll leverage some of the functions we created earlier, first using align_with_goal(), then approach_goal(), and finally execute_kick(). Whatever happens we'll go back to our original state of FINDING_BALL after we've taken our shot, but note in the code that we can see the different potential outcomes of calling these functions:
elif status == RobotStatus.FINDING_GOAL:
# We've got the ball, now we need to find the goal and attempt to score
robot.screen.show_emoji(DETERMINED)
if not robot.has_sports_ball():
status = RobotStatus.FINDING_BALL # We dropped the ball
continue
if align_with_goal():
if approach_goal():
if robot.has_sports_ball():
execute_kick()
else:
status = RobotStatus.FINDING_BALL # Dropped ball while approaching
else:
status = RobotStatus.FINDING_BALL # Lost goal while approaching
else:
status = RobotStatus.FINDING_BALL # Failed to align with goal
status = RobotStatus.FINDING_BALL # After attempting to score, go back to finding ball
Main loop: Dealing with crashes
Finally, we need to handle a CRASHED state. Here we stop the robot moving, pause and turn around, before going back to the FINDING_BALL state, hopefully obstacle free!
elif status == RobotStatus.CRASHED:
# We crashed! We may have hit another robot, or the edge of the field
# We'll try turning around to see if that helps
robot.screen.show_emoji(STRESSED)
robot.sound.play(CRASH)
robot.stop_all_movement()
wait(3, SECONDS) # Allow some time for robot to settle
robot.turn(RIGHT)
wait(1, SECONDS)
crashed = False
status = RobotStatus.FINDING_BALL