ASSY-03Team ProjectMSE 3302BJan 2026 – Apr 2026
Rock-Paper-Scissors Robotic Hands
Two tendon-driven, 3D-printed hands that play Rock-Paper-Scissors-Minus-One on their own: four DC motors form the gestures, servo-driven covers hide the dropped hand, and two ESP32 boards run a game-theory strategy and talk to each other over ESP-NOW.
Visualization based on final design
The challenge
Rock-Paper-Scissors-Minus-One (RPS-1) is a variation where each player shows two gestures, one on each hand. After both players reveal their gestures, each withdraws one hand at the same time, and the gestures left decide the round. That adds a second decision: you have to guess which hand your opponent will keep.
The brief was to build a mechatronic system that plays RPS-1 against a person. It had to:
- physically form rock, paper and scissors on two hands,
- take in the opponent’s two gestures,
- decide which of its own hands to withdraw to give itself the best chance of winning,
- physically remove that hand, and
- reset and do it again, reliably, round after round.
Starting point: the course provided the robotic hand and its CAD model (palm, finger segments and ball-joint holder). Our team designed everything around it: the tendon actuation, the spools, the motor and servo mounts, the servo-driven covers and the wooden base, plus all of the electronics and firmware.
My role
The five-person team split into a mechanical side and a software side, with regular meetings and a shared progress tracker to keep them in step. I worked on both sides, which was useful because every mechanical change (joint friction, tendon tension, spool size) changed how long the motors had to run.
Mechanical
- Built and tested the tendon actuation: fishing-line tendons tied through drilled fingertips, elastic bands for the return stroke, and two finger groups per hand.
- Worked on the custom PLA parts that turn the provided hand into a working system: the grooved spool, motor holder, servo mount, servo holder (cover arm) and the plywood base.
- Ran the hands-on test-and-fix cycles: sanding stiff joints, finding a fingertip anchor that held, and fixing line tangling on the spool.
Software
- Wrote and tested the Arduino firmware in the team repo, starting with motor and servo test sketches and a two-board command link, and ending with the master and slave programs.
- Programmed timed motor control for each gesture and an active reset back to paper between rounds.
- Implemented the game strategy: picking a pair of opening gestures that are never the same, then the three-case logic that decides which hand to cover.
- Debugged the integrated system: startup glitches, servo initialization and communication between the two boards.
How it works
Two ESP32 boards split the work. The master board runs the game, reads the user’s input from the Serial Monitor and drives the left hand. The slave board drives the right hand and only carries out commands the master sends it. It makes no decisions of its own. Each hand has two DC motors that reel fishing-line “tendons” onto spools to curl the fingers, and a servo-driven cover that swings shut to take that hand out of play.
- Reset. The user types
n. Both covers open and both hands drive back to paper, the default open hand. - Select. The master picks one of three opening pairs, always two different gestures (paper–rock, paper–scissors or rock–scissors), and never the same pair twice in a row.
- Gesture. The left hand forms its gesture directly. The right-hand gesture goes to the slave as a wireless packet.
- Input. The user enters the opponent’s pair as two letters, e.g.
PR. Anything that isn’t exactly two of R / P / S is rejected. - Decide. The strategy picks which gesture to keep.
- Cover. The servo on the other hand closes its cover. The visible hand is the final throw, and the system goes back to idle.
Mechanical design
Provided vs. designed: the hand itself (a palm on a ball-joint holder, with three-segment fingers, a two-segment thumb and click-together ball joints) came from the course. The actuation, spools, mounts, covers and base described below are our team’s design.
We based the actuation on the human hand: motors act as the muscles and fishing line as the tendons. Watching our own hands throw each gesture showed that the index and middle fingers always move together, and so do the thumb, ring and pinky. So instead of one actuator per finger, each hand uses two motors:
| Gesture | Motor A: thumb, ring, pinky | Motor B: index, middle |
|---|---|---|
| Paper (default) | Off, fingers open | Off, fingers open |
| Rock | Pull | Pull |
| Scissors | Pull | Off |
| Reset to paper | Reverse, then stop | Reverse, then stop |
- Tendons and returns. Fishing line is tied through holes drilled in each fingertip and wound onto a spool on the motor. Elastic bands run from the fingertips to the back of the palm, so the fingers spring back open when the line goes slack. Early tests showed that line has to do the pulling and elastics only the returning.
- Grooved spool. One motor has to reel in several lines without tangling them. Two spools wouldn’t fit on the motor shaft, so we made one Ø30 mm spool with a raised centre divider that keeps each line in its own channel.
- Covers for the “minus one”. Physically pulling a whole hand away would have needed far more mechanism and space. Instead, each hand has a flap on a servo-driven arm, hinged at the top of the board, that only has to swing about 90°, well inside a servo’s 180° range.
- Mounts and base. A ¾″ plywood base holds a 300 mm backboard. Each hand’s holder cup screws to the backboard so the hand sticks straight out, with its two motor holders mounted below it. The cover servos sit on the top edge of the board, and each flap swings down from upright to lie flat over its hand.
- Manufacturing. All custom parts are PLA, which is cheap, quick to reprint and strong enough for these low loads. Stiff joints and mating faces were sanded after printing to cut friction.
The SolidWorks drawings for the spool, motor holder, servo mount, servo holder and base are in the gallery.
Electronics
Each hand has its own ESP32-WROOM-32 (on an MSE Duino board), its own MX1508 motor driver and its own cover servo. Both boards use the same pin layout, which made wiring, debugging and copying the hardware much simpler. The work is split across two boards partly because one board couldn’t power four DC motors and two servos at once.
- DC motors run through the MX1508’s dual H-bridge. Each motor uses two GPIO inputs: driving one high spins it one way and driving the other high reverses it, which we need to both curl and reset the fingers. A motor that spun the wrong way could be fixed by swapping its leads at the driver output.
- Servos connect straight to a GPIO pin and are positioned with a PWM signal. They need 5 V while the ESP32 runs at 3.3 V, so they are fed from the board’s 5 V rail (USB or an external pack) rather than the logic supply.
| Function | GPIO |
|---|---|
| Motor A, input 1 / 2 | 26 / 16 |
| Motor B, input 1 / 2 | 27 / 17 |
| Cover servo signal | 4 |
| Slave address | MAC in master code |
| Item | Qty | Purpose |
|---|---|---|
| 3D-printed hands (PLA) | 2 | Articulated hand mechanism |
| MSE Duino (ESP32) boards | 2 | Master and slave controllers |
| DC motors | 4 | Pull the finger tendons |
| Servos | 2 | Swing the hand covers |
| Motor mounts, spools (PLA) | 2 + 2 | Hold motors and wind the line |
| Fishing line / elastic bands | 10 / 10 | Tendons and return springs |
| Plywood backboard | 1 | Rigid base for the whole system |
| Screws, hot glue, wire | As req. | Fastening, line-to-spool, connections |
Software & game strategy
The firmware is a simple state machine, idle → resetting → selecting → gesturing → waiting for input → computing → covering → idle, so actions can’t run out of order. That was a real problem in early testing.
The two boards communicate over ESP-NOW, a peer-to-peer protocol that needs no router. The master knows the slave’s MAC address and sends it small packets, each with a sequence number, a command (SET_GESTURE, SET_COVER or RESET_HAND), a gesture and a cover state. The slave only acts on packets newer than the last one it handled, so stale or repeated commands are dropped.
Gestures are open-loop: each motor runs for a tuned length of time. There’s no position feedback, so getting those timings right took a lot of testing (see below).
Why the opening pair is never doubled
If we showed the same gesture on both hands, we would have no choice left to make in the minus-one step. Working through every case against an opponent who plays randomly but always withdraws their weaker hand, a doubled pair wins 16.7%, draws 33.3% and loses 50% of the time. Against an opponent who also never doubles, it never wins (0% / 33.3% / 66.7%). So the firmware only ever opens with two different gestures.
Choosing which hand to keep
| Opponent shows | What the code does | Typical outcome |
|---|---|---|
| Two of the same gesture | Score each of our gestures against theirs with rpsResult() and keep the higher one. | Always a win or a draw, because our two gestures differ. |
| The same pair we did | Keep the gesture that beats the other one in our pair (dominantHandOfPair()). | Usually a draw. Wins if the opponent keeps the wrong one. |
| A different pair | Find the gesture both players share (getSharedHand()). Keep it 2⁄3 of the time and the other gesture 1⁄3, a mixed strategy from the Nash-equilibrium payoffs. | Unpredictable to the opponent, and optimal in expectation. |
// Case 3: opponent showed a different, non-identical pair
char shared = getSharedHand(myA, myB, oppA, oppB);
char other = getOtherHand(myA, myB, shared);
int roll = random(0, 3); // 0, 1, 2
return (roll < 2) ? shared : other; // keep shared 2/3, other 1/3
Testing & iteration
We built and tested one hand first, so problems got fixed before we printed the second. Below are the mechanical tests, the elastic strength data and the firmware and integration problems we found and fixed.
| Test | Attempt | Result | What we learned / changed |
|---|---|---|---|
| Joint mobility | Assemble straight off the printer | FAIL | Joints were too stiff, so the fingers couldn’t be extended. |
| Sand inside each joint | PASS | Smooth motion, and the motors and elastics move the fingers without straining. | |
| Tendon material | Elastic bands as the tendons | FAIL | The elastic pulled back against the fingers as they extended. |
| Fishing line pulls, elastics only return | PASS | Line transmits force and elastics reopen the hand. | |
| Line-to-finger fixture | Heat a wire and push it into the PLA to form a loop | FAIL | Couldn’t get deep enough into the PLA to hold. |
| Heat the hand, then push the wire in | FAIL | PLA softens at around 60 °C and lost its strength at this high-stress point. | |
| Drill through the fingertip and tie the line | PASS | Held under spooling tension, and the part stayed intact. | |
| Line spooling | Two spools on one motor | FAIL | The motor shaft was too short for two spools. |
| One spool with a centre divider | PASS | One spool now works as two separate channels. | |
| Spool groove depth | 0.3 cm groove | FAIL | At full retraction, line jumped the divider and tangled. |
| 1.0 cm groove | PASS | Full tendon length winds in with no crossover. |
Elastic band tensile test
To check whether rubber bands could provide the return force, we pulled bands to failure with a digital luggage scale, with the other end anchored under a couch so a snapping band would fly away from whoever was pulling. The results were inconsistent: bands that had already been stretched past their elastic limit broke far earlier. Doubling the bands raised the breaking load, but by less than double, and both bands failed in quick succession.
| Trial | lbs |
|---|---|
| 1 | 4.4 |
| 2 | 7.9 |
| 3 | 9.5 |
| 4 | 4.1 |
| 5 | 15.3 |
| 6 | 17.0 |
| 7 | 8.6 |
| Trial | Band | lbs |
|---|---|---|
| 1 | 1st to break | 22.3 |
| 1 | 2nd to break | 22.1 |
| 2 | 1st to break | 25.4 |
| 2 | 2nd to break | 24.6 |
Firmware & integration
| Area | Issue observed | Root cause | Fix | Result |
|---|---|---|---|---|
| Startup | Motors spun randomly at power-on | GPIO pins left uninitialized, and logic ran too early | Set every driver pin as an output and drive it low before anything else runs | Stable startup |
| Servos | Covers misaligned, or only one side initialized | Servo position not set before the boards synced | Force both covers open and reset both hands at boot | Both hands start visible |
| Board link | Early sketches sent single-character commands over a wired UART link between the two halves | Extra wiring between the hands, and plain characters were hard to verify | Moved to ESP-NOW with structured command packets | Self-contained, easier to debug |
| Pairing | Slave not responding | Wrong MAC address / ESP-NOW peer setup | A utility sketch prints the slave’s MAC, which is copied into the master code | Reliable response |
| Sync | Slave ran old or repeated commands | No check on incoming packets | Sequence numbers. The slave keeps only the newest packet. | No duplicate or late actions, even in rapid rounds |
| Reset | Fingers didn’t return to the same start position | Elastic return alone was unreliable | Active reset: drive both motors in reverse for a set time | Each round starts from a known state |
| Motor control | Uneven motors, inconsistent gestures | Friction and tendon tension vary, and control is open-loop | Tuned the motor run times through repeated trials | Better repeatability |
Results & takeaways
The final system played full rounds of RPS-1 consistently and repeatably. It formed all three gestures with just two motors per hand, chose which hand to keep for every possible opponent pair, and covered the correct hand at the end of every round. The master and slave stayed in sync over repeated test cycles with no significant packet loss.
Hardware and software can’t be separated
Joint friction, tendon tension and spool size all changed how long the motors needed to run. The two sides had to be developed together.
Open-loop control has limits
Timed actuation worked, but only after a lot of tuning, and it drifts as the elastics wear. This is why real systems measure their own state.
Start from a known state
Initializing pins, forcing covers open and resetting the hands actively turned random startup behaviour into something reliable.
Distributed systems need validation
Correct pairing and sequence-numbered packets fixed missed, late and duplicated commands between the boards.
Limitations and what I’d do next
- Close the loop. Add motor encoders for position feedback instead of timing. The schematic already shows motor/encoder units and the firmware reserves encoder pins.
- See the opponent. Replace typed Serial Monitor input with computer-vision gesture detection, so the robot doesn’t need a laptop or someone typing.
- Clean up the wiring. Two boards, four motors, drivers and servos got cluttered. A custom PCB or proper cable management would make it more reliable and easier to debug.
- Longer-life tendons. Line and elastic tension change over time, and the grooved spool reduces tangling but doesn’t eliminate it over long use.