The Controller is a really nifty input method from the user.
When my kids play with it, they drive it around like an RC car.
The control for this kind of input is rather straightforward. When pressed, do action. When released, stop.
I didn’t want to build just another “robot drives forward when button pressed” demo.
It’s fine for five minutes. It does not make you grin.
So I borrowed from fighting games.
In Street Fighter, you do not press a single button to throw a fireball. You perform a sequence of inputs and if it matches, the character will perform your desired special move. Quarter circle forward plus punch. (sometimes people know it as down forward + punch) The game listens to your history of inputs and decides whether you meant something special.
So I asked myself: How can I make the robot take in fighting game inputs?
This project tries to build that idea on VEX AIM:
-
A controller input stream (joystick + buttons)
-
A sliding window of recent directions
-
Pattern matching for “special moves”
Goal
Build a VEX AIM project where:
-
The joystick direction is discretized into fighting-game “numpad notation” (1–9)
-
Recent joystick directions are stored in a sliding window
-
When I press an attack button, it checks the window for a known pattern and triggers a move:
-
Fireball (Hadouken):
QCF+ Punch -
Spin kick:
QCB+ Kick
-
Step 1: Prove the wiring works.
Before getting clever, I needed proof that controller callbacks were behaving the way I thought.
So I did the smallest possible thing:
robot = Robot()
controller = Onestick()
def boop():
print("Boop")
controller.button_left.released(boop)
Press button.
Console prints “Boop”.
That tiny win is important. It tells me:
-
The controller binding works.
-
The event loop is alive.
-
I can attach behavior to button events.
Step 2: Fireball as a mechanical action
Next step: map a button to the kicker.
def fireball(force):
robot.kicker.kick(force)
controller.button_left.released(fireball, (MEDIUM,))
controller.button_up.released(fireball, (HARD,))
Two things I learned here:
-
Callback arguments must be passed as a tuple. (MEDIUM,) not MEDIUM.
I lost a few minutes on that. Worth remembering.
-
The kicker behaves differently when the robot is plugged in.
When powered via cable it doesn’t kick. Unplugged, it works cleanly.
Step 3: Learn the joystick - what values do we actually get?
Before any “quarter circle forward” logic, I wanted to understand the raw joystick readings.
I initially assumed the axis changed callback would give me values directly. It did not behave how I expected. So instead of guessing, I polled inside the handler.
def handle_vertical_axis_changed():
vertical_input = controller.axis1.position()
print("Vertical input:", vertical_input)
def handle_horizontal_axis_changed():
horizontal_input = controller.axis2.position()
print("Horizontal input:", horizontal_input)
controller.axis1.changed(handle_vertical_axis_changed)
controller.axis2.changed(handle_horizontal_axis_changed)
What I observed:
-
Horizontal range: -100 to 100
-
Vertical range: -100 to 100
-
Values are stable
This is Good. Knowing these ranges lets me easily convert these into joystick positions.
Step 4: Translating analog stick to fighting game notation
Fighting games use numpad notation:
7 8 9
4 5 6
1 2 3
-
5 is neutral.
-
2 is down.
-
6 is right.
-
3 is down-right.
I needed to convert the continuous raw inputs from the joystick into one of these 9 states.
My reasoning:
-
Use a deadzone so tiny jitters do not register.
-
Treat anything beyond ±30 as intentional movement.
def resolve_joystick_input(vertical_input, horizontal_input):
is_left = horizontal_input < -30
is_right = horizontal_input >30
is_up = vertical_input >30
is_down = vertical_input < -30
if is_left and is_down:
return 1
elif is_right and is_down:
return 3
elif is_down:
return 2
elif is_left and is_up:
return 7
elif is_right and is_up:
return 9
elif is_up:
return 8
elif is_left:
return 4
elif is_right:return6else:
return 5
Now the joystick speaks fighting game language.
Step 5: The sliding window. Think in history, not state.
A special move is not a single direction.
It is a sequence.
So I needed a memory of what has happened before.
The logic was simple: keep a record of the last 10 directions.
Ignore duplicates so holding a direction does not spam the list.
sliding_window = []
def handle_joystick_input(joystick_position):
if len(sliding_window) > 0 and sliding_window[-1] == joystick_position:
return
sliding_window.append(joystick_position)
if len(sliding_window) > 10:
sliding_window.pop(0)
Then whenever the joystick changes occur, update the window:
def handle_joystick_input_changed():
vertical_input = controller.axis1.position()
horizontal_input = controller.axis2.position()
joystick_position = resolve_joystick_input(vertical_input, horizontal_input)
print("Joystick position:", joystick_position)
handle_joystick_input(joystick_position)
controller.axis1.changed(handle_joystick_input_changed)
controller.axis2.changed(handle_joystick_input_changed)
Step 6: Pattern matching: Hadouken and Spin Kick
The Hadouken combo is QCF (quarter circle forward) + Punch.
The quarter circle maps to down, down right and right. I’m only doing it for one direction for now.
This maps to 2, 3, 6 positions for the joystick
Fireball detection: 2, 3, 6
This is how I detected the presence of the quarter circle forward.
def detect_fireball(sliding_window):
return any(
sliding_window[i:i+3] == [2, 3, 6]
for i in range(len(sliding_window) - 2)
)
Spin kick detection: (my current pattern) 2, 1, 4
Similarly, spin kick was a QCB (quarter circle back) + Kick.
def detect_spin_kick(sliding_window):
return any(
sliding_window[i:i+3] == [2, 1, 4]
for i in range(len(sliding_window) - 2)
)
Right now I’m ignoring the punches and kicks and see if I can detect the gesture.
Great! It works!
Step 7: Input plus button. Make it feel correct.
def hard_punch():
if detect_fireball(sliding_window):
fireball(HARD)
sliding_window.clear()
def medium_punch():
if detect_fireball(sliding_window):
fireball(MEDIUM)
sliding_window.clear()
controller.button_left.pressed(medium_punch)
controller.button_up.pressed(hard_punch)
The issue with using the released handler is that with fighting games, you want to have the satisfaction of triggering your action as soon as you hit the button. It helps feel more responsive.
Step 8: Spin kick choreography
The spin kick is just composition:
-
Spin 360 degrees
-
Kick multiple times
def spin():
robot.turn_for(RIGHT, 360, 100)
def kick():
robot.kicker.kick(HARD)
wait(500, MSEC)
robot.kicker.kick(HARD)
wait(500, MSEC)
robot.kicker.kick(HARD)
wait(500, MSEC)
robot.kicker.kick(HARD)
wait(500, MSEC)
def spin_kick():
Thread(spin)
Thread(kick)
One thing I learned experimentally: the kicker needs breathing room.
If you remove the waits, some kicks do not register. Around 500 milliseconds spacing works reliably.
Step 9: Polish
I spent some time creating sound effects for the robot.
I used Audacity and mixed together a voice clip of me calling out the move name:
“Robot Fireball” and “Spinning Robot Kick”
Then mixed it with some fighting sounds and magic.
Then I upgraded the fireball to feel more like a video game. There’s usually a charge up before launching the projectile. Dhalsim was one of my fav characters and he had a move called “Yoga Fire”. He would spit a fireball on the word Fire. The Yoga was a charge up for anticipation. I decided to emulate that with “Robot Fireball”. My sound clip takes 0.8 seconds before the “Fireball” so I introduced a small delay before the kick.
def fireball(force):
if not robot.has_sports_ball():
return
Thread(robot.sound.play_file, ("sound1.mp3",))
wait(800, MSEC)
Thread(robot.kicker.kick, (force, ))
I found that the CodeAIM IDE does not let me upload WAV files. That was a shame. I had tried using both the .wav and .WAV extensions.
I had to convert it to MP3 for it to work.
Now it:
-
Plays sound
-
Waits
-
Kicks
-
Only performs fireball if it is actually holding a ball
This makes the action feels less like an RC control and more like a video game move.
You can sell it even more with back and forth movement but I didn’t have much time left to experiment with that.
What I ended up with (the “demo loop”)
There’s so much extra we can do with this.
But for now, I can do a Hadouken on a robot… and it makes me laugh every time!
Here are is the sample code if you want to play around with it:
The forum doesn’t allow me to upload sound files, but if you want them, I can pass you a link privately.
This project was one that gave me the most joy. The feeling that you get from seeing robots perform your childhood moves is intensely satisfying. If you do get to try it out, I hope you can enjoy the same feeling too! Have a great weekend everyone!