Back to projects

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.

Top-down 3D visualization of the final system: two pink 3D-printed hands stick straight out from a plywood backboard, each mounted above its two DC motors with grooved spools and fishing-line tendons. Servo-driven flaps hinge from the top edge of the board: the left flap stands straight up so the left hand shows scissors, and the right flap lies straight out over the right hand, covering it. An ESP32 board and motor driver sit beside each hand. Visualization based on final design
Top-down view of a final round: the left hand keeps scissors and the right hand’s flap has swung down flat to cover it. Hands and cover arms from the real STL files; base, mounts and spools from the CAD drawings.

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.

  1. Reset. The user types n. Both covers open and both hands drive back to paper, the default open hand.
  2. 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.
  3. Gesture. The left hand forms its gesture directly. The right-hand gesture goes to the slave as a wireless packet.
  4. 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.
  5. Decide. The strategy picks which gesture to keep.
  6. Cover. The servo on the other hand closes its cover. The visible hand is the final throw, and the system goes back to idle.
FIG 1System architecture: the master runs the strategy and drives the left hand; the slave drives the right.

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:

Finger groups and gestures
GestureMotor A: thumb, ring, pinkyMotor B: index, middle
Paper (default)Off, fingers openOff, fingers open
RockPullPull
ScissorsPullOff
Reset to paperReverse, then stopReverse, 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.
FIG 2Wiring for one hand: ESP32 → MX1508 → two motors, plus the cover servo on the 5 V rail.
Pin assignments (same on both boards)
FunctionGPIO
Motor A, input 1 / 226 / 16
Motor B, input 1 / 227 / 17
Cover servo signal4
Slave addressMAC in master code
Bill of materials
ItemQtyPurpose
3D-printed hands (PLA)2Articulated hand mechanism
MSE Duino (ESP32) boards2Master and slave controllers
DC motors4Pull the finger tendons
Servos2Swing the hand covers
Motor mounts, spools (PLA)2 + 2Hold motors and wind the line
Fishing line / elastic bands10 / 10Tendons and return springs
Plywood backboard1Rigid base for the whole system
Screws, hot glue, wireAs 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.

FIG 3Firmware state flow for one round.

Choosing which hand to keep

Decision logic: chooseFinalHand()
Opponent showsWhat the code doesTypical outcome
Two of the same gestureScore 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 didKeep the gesture that beats the other one in our pair (dominantHandOfPair()).Usually a draw. Wins if the opponent keeps the wrong one.
A different pairFind 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
FIG 4Working out the odds of opening with a doubled pair, which is why the firmware never does it.

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.

Mechanical tests
TestAttemptResultWhat we learned / changed
Joint mobilityAssemble straight off the printerFAILJoints were too stiff, so the fingers couldn’t be extended.
Sand inside each jointPASSSmooth motion, and the motors and elastics move the fingers without straining.
Tendon materialElastic bands as the tendonsFAILThe elastic pulled back against the fingers as they extended.
Fishing line pulls, elastics only returnPASSLine transmits force and elastics reopen the hand.
Line-to-finger fixtureHeat a wire and push it into the PLA to form a loopFAILCouldn’t get deep enough into the PLA to hold.
Heat the hand, then push the wire inFAILPLA softens at around 60 °C and lost its strength at this high-stress point.
Drill through the fingertip and tie the linePASSHeld under spooling tension, and the part stayed intact.
Line spoolingTwo spools on one motorFAILThe motor shaft was too short for two spools.
One spool with a centre dividerPASSOne spool now works as two separate channels.
Spool groove depth0.3 cm grooveFAILAt full retraction, line jumped the divider and tangled.
1.0 cm groovePASSFull 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.

Single band, force at break
Triallbs
14.4
27.9
39.5
44.1
515.3
617.0
78.6
Two bands together, force at break
TrialBandlbs
11st to break22.3
12nd to break22.1
21st to break25.4
22nd to break24.6

Firmware & integration

Problems found in system testing
AreaIssue observedRoot causeFixResult
StartupMotors spun randomly at power-onGPIO pins left uninitialized, and logic ran too earlySet every driver pin as an output and drive it low before anything else runsStable startup
ServosCovers misaligned, or only one side initializedServo position not set before the boards syncedForce both covers open and reset both hands at bootBoth hands start visible
Board linkEarly sketches sent single-character commands over a wired UART link between the two halvesExtra wiring between the hands, and plain characters were hard to verifyMoved to ESP-NOW with structured command packetsSelf-contained, easier to debug
PairingSlave not respondingWrong MAC address / ESP-NOW peer setupA utility sketch prints the slave’s MAC, which is copied into the master codeReliable response
SyncSlave ran old or repeated commandsNo check on incoming packetsSequence numbers. The slave keeps only the newest packet.No duplicate or late actions, even in rapid rounds
ResetFingers didn’t return to the same start positionElastic return alone was unreliableActive reset: drive both motors in reverse for a set timeEach round starts from a known state
Motor controlUneven motors, inconsistent gesturesFriction and tendon tension vary, and control is open-loopTuned the motor run times through repeated trialsBetter 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.