Mirko Pavleski
Published © GPL3+

Human vs Robot- Rock Paper Scissors with MyCobot 280 M5Stack

Complete interactive game between human and robot — the classic Rock-Paper-Scissors — using the MyCobot 280 M5 robotic arm

IntermediateFull instructions provided3 hours72
Human vs Robot- Rock Paper Scissors with MyCobot 280 M5Stack

Things used in this project

Hardware components

Elephant Robotics myCobot 280 M5Stack 2023 - 6 DOF Collaborative Robot
×1
Elephant Robotics Camera Flange 2.0 Version for myCobot
×1
Elephant Robotics myCobot Adaptive Gripper
×1
Elephant Robotics G-shape Base 2.0
×1

Software apps and online services

Arduino IDE
Arduino IDE
Pycom python

Hand tools and fabrication machines

Multitool, Screwdriver
Multitool, Screwdriver

Story

Read more

Code

Arduino Code

C/C++
...
/*
Human vs Robot – Rock Paper Scissors Interactive Game with MyCobot 280 & M5Stack by mirecemk
         Python + Arduino real-time interaction!
         by mircemk November 2025
*/

#include <M5Stack.h>
#include <MyCobotBasic.h>

// ===================== CONFIG =====================
#define SERVO_SPEED 70     // doubled speed (was ~35)
#define SPEAKER_PIN 25     // M5Stack Basic speaker (DAC1)
#define DAC_VOL_HI 160
#define DAC_VOL_LO 0

// ===================== SOUND (DAC-safe) =====================
static inline void initSpeaker() {
  pinMode(SPEAKER_PIN, OUTPUT);
  dacWrite(SPEAKER_PIN, DAC_VOL_LO);
}
static inline void stopTone() { dacWrite(SPEAKER_PIN, DAC_VOL_LO); }

static inline void playTone(int freq, int duration_ms) {
  if (freq <= 0 || duration_ms <= 0) { stopTone(); delay(duration_ms); return; }
  const uint32_t total_us = (uint32_t)duration_ms * 1000UL;
  const uint32_t half_period_us = 500000UL / (uint32_t)freq;
  if (half_period_us == 0) { stopTone(); delay(duration_ms); return; }
  const uint32_t t_start = micros();
  while ((micros() - t_start) < total_us) {
    dacWrite(SPEAKER_PIN, DAC_VOL_HI);
    delayMicroseconds(half_period_us);
    dacWrite(SPEAKER_PIN, DAC_VOL_LO);
    delayMicroseconds(half_period_us);
    yield();
  }
  stopTone();
}

static inline void countdownBeep(int n) {
  const int f = (n == 3) ? 1200 : (n == 2) ? 1000 : 800;
  playTone(f, 180);
}
static inline void shootSound()    { playTone(1500, 120); delay(60); playTone(1700, 120); }
static inline void victorySound()  { playTone(523,160); delay(60); playTone(659,160); delay(60); playTone(784,220); }
static inline void defeatSound()   { playTone(784,160); delay(60); playTone(659,160); delay(60); playTone(523,220); }
static inline void tieSound()      { playTone(1000,120); delay(80); playTone(1000,120); }
static inline void selectSound()   { playTone(900,80); }
static inline void clickSound()    { playTone(700,60); }

// ===================== ROBOT / GAME LOGIC =====================
MyCobotBasic myCobot;

enum GameState { READY, COUNTDOWN, REVEAL, RESULT, GAME_OVER };
GameState currentState = READY;

String humanGesture = "", robotGesture = "", gameResult = "";
unsigned long gameStartTime = 0;
int countdownNumber = 3;

int humanWins = 0, robotWins = 0;

void setGameLED(int r, int g, int b) { myCobot.setLEDRGB(r, g, b); }
void setReadyLED()        { setGameLED(0,0,0); }
void setGameRunningLED()  { setGameLED(0,0,255); }
void setRobotWinLED()     { setGameLED(0,255,0); }
void setHumanWinLED()     { setGameLED(255,0,0); }
void setTieLED()          { setGameLED(255,255,0); }

// ===================== MOTION =====================
void moveToNeutral() {
  myCobot.writeAngle(1, 0, SERVO_SPEED);  delay(200);
  myCobot.writeAngle(2, 0, SERVO_SPEED);  delay(200);
  myCobot.writeAngle(3, 0, SERVO_SPEED);  delay(200);
  myCobot.writeAngle(4, -90, SERVO_SPEED);delay(200);
  myCobot.writeAngle(5, 0, SERVO_SPEED);  delay(200);
  myCobot.writeAngle(6, -45, SERVO_SPEED);delay(200);
  myCobot.setGripperState(1, 25);
}

void moveToGamePosition() { myCobot.writeAngle(4, 0, SERVO_SPEED*1.1); delay(800); }

void victoryRoutine() { myCobot.writeAngle(4, -50, SERVO_SPEED); delay(400); myCobot.writeAngle(4, 0, SERVO_SPEED); delay(300); }
void defeatRoutine()  { myCobot.writeAngle(4,  50, SERVO_SPEED); delay(400); myCobot.writeAngle(4, 0, SERVO_SPEED); delay(300); }
void tieRoutine()     { myCobot.writeAngle(5,  50, SERVO_SPEED); delay(250); myCobot.writeAngle(5,-50,SERVO_SPEED); delay(250); myCobot.writeAngle(5,0,SERVO_SPEED); delay(300); }

void showPaper()    { clickSound(); myCobot.setGripperState(0, 80); delay(1000); }
void showRock()     { clickSound(); myCobot.setGripperState(1, 40); delay(1000); }
void showScissors() {
  playTone(1000, 80); delay(120);
  playTone(1200, 80);
  myCobot.setGripperState(1, 80); delay(500);
  myCobot.setGripperState(0, 80); delay(500);
  myCobot.setGripperState(1, 80); delay(1000);
}

// ===================== DISPLAY =====================
void showReadyScreen() {
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setTextColor(WHITE);
  M5.Lcd.setTextSize(4);
  M5.Lcd.setCursor(50, 10);   M5.Lcd.print("ROCK");
  M5.Lcd.setCursor(170, 10);  M5.Lcd.print("PAPER");
  M5.Lcd.setCursor(80, 60);   M5.Lcd.println("SCISSORS");
  M5.Lcd.setCursor(110, 110); M5.Lcd.println("GAME");

  M5.Lcd.setTextSize(3);
  // Your custom positions preserved
  M5.Lcd.setCursor(30, 160);  M5.Lcd.println("A:  START");
  M5.Lcd.setCursor(30, 200);  M5.Lcd.println("B:  POSITION");
  M5.Lcd.setCursor(30, 240);  M5.Lcd.println("C:  RESULT");
}

void showGameOverScreen() {
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setTextColor(WHITE);
  M5.Lcd.setTextSize(4);
  M5.Lcd.setCursor(40, 40);   M5.Lcd.println("Game over");
  M5.Lcd.setTextSize(3);
  M5.Lcd.setCursor(30, 130);  M5.Lcd.println("A: New Game");
  M5.Lcd.setCursor(30, 170);  M5.Lcd.println("B: Reposition");
  M5.Lcd.setCursor(30, 210);  M5.Lcd.println("C: Total Score");
}

void showTotalResultScreen() {
  M5.Lcd.fillRect(0, 0, 320, 120, RED);
  M5.Lcd.setTextColor(BLACK);
  M5.Lcd.setTextSize(4);
  M5.Lcd.setCursor(40, 40);
  M5.Lcd.printf("HUMAN: %d", humanWins);

  M5.Lcd.fillRect(0, 120, 320, 120, GREEN);
  M5.Lcd.setTextColor(BLACK);
  M5.Lcd.setTextSize(4);
  M5.Lcd.setCursor(40, 160);
  M5.Lcd.printf("ROBOT: %d", robotWins);

  delay(2500);
  if (currentState == GAME_OVER) showGameOverScreen();
  else showReadyScreen();
}

// ===================== GAME LOGIC =====================
void initializeGame() {
  currentState = READY;
  humanGesture = ""; robotGesture = ""; gameResult = "";
  setReadyLED();
  showReadyScreen();
}

void startNewGame() {
  currentState = COUNTDOWN;
  countdownNumber = 3;
  gameStartTime = millis();

  moveToGamePosition();
  setGameRunningLED();

  M5.Lcd.fillScreen(BLUE);
  M5.Lcd.setTextColor(WHITE);
  M5.Lcd.setTextSize(4);
  M5.Lcd.setCursor(50, 100);
  M5.Lcd.println("Waiting...");
  delay(600);

  for (int i = 3; i > 0; i--) {
    M5.Lcd.fillScreen(BLACK);
    M5.Lcd.setTextColor(YELLOW);
    M5.Lcd.setTextSize(10);
    M5.Lcd.setCursor(140, 100);
    M5.Lcd.println(i);
    countdownBeep(i);
    delay(650);
  }

  M5.Lcd.fillScreen(GREEN);
  M5.Lcd.setTextColor(BLACK);
  M5.Lcd.setTextSize(5);
  M5.Lcd.setCursor(70, 100);
  M5.Lcd.println("SHOOT!");
  shootSound();
  delay(700);

  currentState = REVEAL;
  M5.Lcd.fillScreen(BLUE);
  M5.Lcd.setTextColor(WHITE);
  M5.Lcd.setTextSize(4);
  M5.Lcd.setCursor(35, 80);  M5.Lcd.println("Waiting for");
  M5.Lcd.setCursor(75, 120); M5.Lcd.println("gesture");
}

void generateRobotGesture() {
  int choice = random(0, 3);
  robotGesture = (choice == 0) ? "rock" : (choice == 1) ? "paper" : "scissors";
}

void determineWinner() {
  if (humanGesture == robotGesture) gameResult = "tie";
  else if ((humanGesture == "rock"     && robotGesture == "scissors") ||
           (humanGesture == "paper"    && robotGesture == "rock")     ||
           (humanGesture == "scissors" && robotGesture == "paper"))   gameResult = "human";
  else gameResult = "robot";
}

void showResults() {
  M5.Lcd.fillRect(0, 0, 320, 120, RED);
  M5.Lcd.setTextColor(BLACK);  M5.Lcd.setTextSize(4);
  M5.Lcd.setCursor(50, 40);    M5.Lcd.print("H: "); M5.Lcd.println(humanGesture);

  M5.Lcd.fillRect(0, 120, 320, 120, GREEN);
  M5.Lcd.setTextColor(BLACK);  M5.Lcd.setTextSize(4);
  M5.Lcd.setCursor(50, 160);   M5.Lcd.print("R: "); M5.Lcd.println(robotGesture);

  delay(600);

  if (robotGesture == "rock")      showRock();
  else if (robotGesture == "paper") showPaper();
  else                               showScissors();

  delay(400);

  if (gameResult == "robot") {
    robotWins++;
    setRobotWinLED();
    M5.Lcd.fillScreen(GREEN);
    M5.Lcd.setTextColor(BLACK);
    M5.Lcd.setTextSize(5);
    M5.Lcd.setCursor(80, 100); M5.Lcd.println("I WIN!");
    victorySound();
    victoryRoutine();
  } else if (gameResult == "human") {
    humanWins++;
    setHumanWinLED();
    M5.Lcd.fillScreen(RED);
    M5.Lcd.setTextColor(WHITE);
    M5.Lcd.setTextSize(5);
    M5.Lcd.setCursor(50, 100); M5.Lcd.println("YOU WIN!");
    defeatSound();
    defeatRoutine();
  } else {
    setTieLED();
    M5.Lcd.fillScreen(YELLOW);
    M5.Lcd.setTextColor(BLACK);
    M5.Lcd.setTextSize(5);
    M5.Lcd.setCursor(90, 100); M5.Lcd.println("TIE!");
    tieSound();
    tieRoutine();
  }

  delay(1600);
  moveToNeutral();
  showGameOverScreen();
  currentState = GAME_OVER;
  setReadyLED();
}

// ===================== COMMAND & MAIN LOOP =====================
void processHumanGesture(String gesture) {
  if (currentState != REVEAL) return;
  humanGesture = gesture;
  generateRobotGesture();
  determineWinner();
  showResults();
}

void processCommand(String command) {
  command.trim(); command.toLowerCase();
  if (command.startsWith("human:")) {
    String gesture = command.substring(6);
    if (gesture == "rock" || gesture == "paper" || gesture == "scissors") {
      selectSound();
      processHumanGesture(gesture);
    }
  } else if (command == "reset") {
    clickSound();
    initializeGame();
  }
}

void setup() {
  M5.begin();
  Serial.begin(115200);
  myCobot.setup();
  myCobot.powerOn();
  initSpeaker();
  for (int i = 0; i < 3; ++i) { playTone(900 + i*150, 90); delay(90); }
  randomSeed(analogRead(0));
  initializeGame();
  }


void loop() {
  M5.update();

  if (M5.BtnB.wasPressed()) {
    clickSound();
    moveToNeutral();
    setReadyLED();
    showReadyScreen();
    currentState = READY;
  }

  if (M5.BtnA.wasPressed()) {
    selectSound();
    if (currentState == READY || currentState == GAME_OVER)
      startNewGame();
  }

  if (M5.BtnC.wasPressed()) {
    if (currentState == READY || currentState == GAME_OVER) {
      clickSound();
      showTotalResultScreen();
    }
  }

  if (Serial.available() > 0) {
    String command = Serial.readStringUntil('\n');
    processCommand(command);
  }

  delay(100);
}

First Test Code

C/C++
..
#include <M5Stack.h>

void setup() {
  M5.begin();
  Serial.begin(115200);
  
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setTextColor(WHITE);
  M5.Lcd.setTextSize(3);
  M5.Lcd.setCursor(50, 100);
  M5.Lcd.println("M5Core Works!");
  
  Serial.println("Basic M5Core test - OK");
}

void loop() {
  M5.update();
  
  if (M5.BtnA.wasPressed()) {
    M5.Lcd.fillScreen(BLUE);
    M5.Lcd.setCursor(50, 100);
    M5.Lcd.println("Button A!");
  }
  
  if (M5.BtnB.wasPressed()) {
    M5.Lcd.fillScreen(GREEN);
    M5.Lcd.setCursor(50, 100);
    M5.Lcd.println("Button B!");
  }
  
  if (M5.BtnC.wasPressed()) {
    M5.Lcd.fillScreen(RED);
    M5.Lcd.setCursor(50, 100);
    M5.Lcd.println("Button C!");
  }
  
  delay(100);
}

Systematic Test Code

C/C++
...
#include <M5Stack.h>
#include <MyCobotBasic.h>

MyCobotBasic myCobot;

void setup() {
  M5.begin();
  Serial.begin(115200);
  
  Serial.println("=== MYCOBOT YOUTUBE DEMO ===");
  
  myCobot.setup();
  myCobot.powerOn();
  
  // Initial display
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setTextColor(WHITE);
  M5.Lcd.setTextSize(3);
  M5.Lcd.setCursor(40, 30);
  M5.Lcd.println("MYCOBOT 280");
  M5.Lcd.setTextSize(2);
  M5.Lcd.setCursor(50, 80);
  M5.Lcd.println("YouTube Demo");
  M5.Lcd.setCursor(30, 120);
  M5.Lcd.println("Press any button");
  M5.Lcd.setCursor(40, 150);
  M5.Lcd.println("to start test");
  
  delay(3000);
  
  // Show test menu
  showTestMenu();
}

void showTestMenu() {
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setTextColor(WHITE);
  M5.Lcd.setTextSize(2);
  M5.Lcd.setCursor(60, 20);
  M5.Lcd.println("TEST MENU");
  M5.Lcd.setTextSize(2);
  M5.Lcd.setCursor(30, 60);
  M5.Lcd.println("A: Joints 1-5 Demo");
  M5.Lcd.setCursor(30, 90);
  M5.Lcd.println("B: Joint 6 Demo");
  M5.Lcd.setCursor(30, 120);
  M5.Lcd.println("C: Gripper Demo");
}

void testJoints1to5() {
  M5.Lcd.fillScreen(BLUE);
  M5.Lcd.setTextColor(WHITE);
  M5.Lcd.setTextSize(2);
  M5.Lcd.setCursor(40, 20);
  M5.Lcd.println("JOINTS 1-5 TEST");
  M5.Lcd.setCursor(20, 50);
  M5.Lcd.println("Moving each joint");
  M5.Lcd.setCursor(20, 80);
  M5.Lcd.println("0 -> 50 -> 0 deg");
  
  Serial.println("=== JOINTS 1-5 SEQUENTIAL TEST ===");
  
  // Test each joint sequentially
  for (int joint = 1; joint <= 5; joint++) {
    M5.Lcd.fillScreen(BLUE);
    M5.Lcd.setCursor(50, 20);
    M5.Lcd.print("JOINT ");
    M5.Lcd.println(joint);
    M5.Lcd.setCursor(30, 60);
    M5.Lcd.println("Moving to 0 deg");
    
    Serial.print("Joint ");
    Serial.print(joint);
    Serial.println(": Moving to 0 degrees");
    myCobot.writeAngle(joint, 0, 40);
    delay(1500);
    
    M5.Lcd.fillScreen(GREEN);
    M5.Lcd.setCursor(50, 20);
    M5.Lcd.print("JOINT ");
    M5.Lcd.println(joint);
    M5.Lcd.setCursor(30, 60);
    M5.Lcd.println("Moving to 50 deg");
    
    Serial.print("Joint ");
    Serial.print(joint);
    Serial.println(": Moving to 50 degrees");
    myCobot.writeAngle(joint, 50, 40);
    delay(2000);
    
    M5.Lcd.fillScreen(BLUE);
    M5.Lcd.setCursor(50, 20);
    M5.Lcd.print("JOINT ");
    M5.Lcd.println(joint);
    M5.Lcd.setCursor(30, 60);
    M5.Lcd.println("Returning to 0 deg");
    
    Serial.print("Joint ");
    Serial.print(joint);
    Serial.println(": Returning to 0 degrees");
    myCobot.writeAngle(joint, 0, 40);
    delay(1500);
  }
  
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setTextColor(WHITE);
  M5.Lcd.setTextSize(2);
  M5.Lcd.setCursor(30, 50);
  M5.Lcd.println("Joints 1-5 Test");
  M5.Lcd.setCursor(50, 80);
  M5.Lcd.println("COMPLETE!");
  Serial.println("=== JOINTS 1-5 TEST COMPLETE ===");
  delay(2000);
}

void testJoint6() {
  M5.Lcd.fillScreen(RED);
  M5.Lcd.setTextColor(WHITE);
  M5.Lcd.setTextSize(2);
  M5.Lcd.setCursor(60, 20);
  M5.Lcd.println("JOINT 6 TEST");
  M5.Lcd.setCursor(20, 60);
  M5.Lcd.println("0 -> 50 -> 0 deg");
  
  Serial.println("=== JOINT 6 TEST ===");
  
  // Position 1: 0 degrees
  M5.Lcd.fillScreen(RED);
  M5.Lcd.setCursor(50, 50);
  M5.Lcd.println("Position: 0 deg");
  Serial.println("Joint 6: Moving to 0 degrees");
  myCobot.writeAngle(6, 0, 30);
  delay(2000);
  
  // Position 2: 50 degrees
  M5.Lcd.fillScreen(GREEN);
  M5.Lcd.setCursor(50, 50);
  M5.Lcd.println("Position: 50 deg");
  Serial.println("Joint 6: Moving to 50 degrees");
  myCobot.writeAngle(6, 50, 30);
  delay(2000);
  
  // Position 3: Back to 0 degrees
  M5.Lcd.fillScreen(RED);
  M5.Lcd.setCursor(50, 50);
  M5.Lcd.println("Position: 0 deg");
  Serial.println("Joint 6: Returning to 0 degrees");
  myCobot.writeAngle(6, 0, 30);
  delay(2000);
  
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setTextColor(WHITE);
  M5.Lcd.setTextSize(2);
  M5.Lcd.setCursor(50, 50);
  M5.Lcd.println("Joint 6 Test");
  M5.Lcd.setCursor(50, 80);
  M5.Lcd.println("COMPLETE!");
  Serial.println("=== JOINT 6 TEST COMPLETE ===");
  delay(2000);
}

void testGripper() {
  M5.Lcd.fillScreen(YELLOW);
  M5.Lcd.setTextColor(BLACK);
  M5.Lcd.setTextSize(2);
  M5.Lcd.setCursor(50, 20);
  M5.Lcd.println("GRIPPER TEST");
  M5.Lcd.setCursor(30, 60);
  M5.Lcd.println("Open - Close");
  M5.Lcd.setCursor(30, 90);
  M5.Lcd.println("Cycle x2");
  
  Serial.println("=== GRIPPER TEST ===");
  
  // Cycle 1
  M5.Lcd.fillScreen(GREEN);
  M5.Lcd.setTextColor(BLACK);
  M5.Lcd.setCursor(60, 50);
  M5.Lcd.println("OPENING");
  Serial.println("Gripper: Opening");
  myCobot.setGripperState(0, 30);  // OPEN (based on your discovery)
  delay(2000);
  
  M5.Lcd.fillScreen(RED);
  M5.Lcd.setTextColor(WHITE);
  M5.Lcd.setCursor(60, 50);
  M5.Lcd.println("CLOSING");
  Serial.println("Gripper: Closing");
  myCobot.setGripperState(1, 30);  // CLOSE (based on your discovery)
  delay(2000);
  
  // Cycle 2
  M5.Lcd.fillScreen(GREEN);
  M5.Lcd.setTextColor(BLACK);
  M5.Lcd.setCursor(60, 50);
  M5.Lcd.println("OPENING");
  Serial.println("Gripper: Opening");
  myCobot.setGripperState(0, 30);  // OPEN
  delay(2000);
  
  M5.Lcd.fillScreen(RED);
  M5.Lcd.setTextColor(WHITE);
  M5.Lcd.setCursor(60, 50);
  M5.Lcd.println("CLOSING");
  Serial.println("Gripper: Closing");
  myCobot.setGripperState(1, 30);  // CLOSE
  delay(2000);
  
  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setTextColor(WHITE);
  M5.Lcd.setTextSize(2);
  M5.Lcd.setCursor(50, 50);
  M5.Lcd.println("Gripper Test");
  M5.Lcd.setCursor(50, 80);
  M5.Lcd.println("COMPLETE!");
  Serial.println("=== GRIPPER TEST COMPLETE ===");
  delay(2000);
}

void loop() {
  M5.update();
  
  if (M5.BtnA.wasPressed()) {
    testJoints1to5();
    showTestMenu();
  }
  
  if (M5.BtnB.wasPressed()) {
    testJoint6();
    showTestMenu();
  }
  
  if (M5.BtnC.wasPressed()) {
    testGripper();
    showTestMenu();
  }
  
  delay(100);
}

Python code

Python
source
import time
import cv2
import serial
import serial.tools.list_ports
import mediapipe as mp

# ---------- Serial helpers ----------
def pick_serial_port(default="COM1"):
    ports = list(serial.tools.list_ports.comports())
    if not ports:
        print("No ports found. Using default:", default)
        return default
    print("Available ports:")
    for i, p in enumerate(ports):
        print(f"[{i}] {p.device} - {p.description}")
    try:
        choice = int(input(f"Select port index (Enter for {default}): ") or "-1")
        if 0 <= choice < len(ports):
            return ports[choice].device
    except Exception:
        pass
    return default

SERIAL_PORT = pick_serial_port("COM1")
ser = None
try:
    ser = serial.Serial(SERIAL_PORT, 115200, timeout=1)
    time.sleep(1.5)
    print(f"Serial connected on {SERIAL_PORT}")
except Exception as e:
    print("Serial not connected:", e)

def send_serial(msg: str):
    print(msg.strip())
    if ser:
        try:
            ser.write((msg if msg.endswith("\n") else msg + "\n").encode("utf-8"))
        except Exception as e:
            print("Serial write failed:", e)

# ---------- MediaPipe Hands ----------
mp_hands = mp.solutions.hands
mp_drawing = mp.solutions.drawing_utils

hands = mp_hands.Hands(
    static_image_mode=False,
    max_num_hands=1,
    model_complexity=0,
    min_detection_confidence=0.6,
    min_tracking_confidence=0.6,
)

# Utility to decide if fingers are extended using landmarks
# Uses the common rule: a finger is "up" if its tip is above its PIP joint (y smaller, because origin is top-left).
# For the thumb we compare x positions depending on handedness.
def classify_rps(landmarks, handedness_label):
    # Indexes in MediaPipe Hands
    TIP = [4, 8, 12, 16, 20]
    PIP = [2, 6, 10, 14, 18]  # for thumb we’ll compare differently

    # Collect convenience points
    lm = landmarks
    def y(i): return lm[i].y
    def x(i): return lm[i].x

    # Finger states (True = extended)
    fingers = [False]*5

    # Thumb: for right hand, extended if tip.x < ip.x (thumb to the left of its joint);
    # for left hand, extended if tip.x > ip.x.
    right_hand = handedness_label.lower().startswith("right")
    thumb_tip = 4
    thumb_ip = 3
    if right_hand:
        fingers[0] = x(thumb_tip) < x(thumb_ip)
    else:
        fingers[0] = x(thumb_tip) > x(thumb_ip)

    # Other fingers: tip above PIP => extended
    for fi, tip_idx in enumerate(TIP[1:], start=1):
        pip_idx = PIP[fi]
        fingers[fi] = y(tip_idx) < y(pip_idx)

    # Decide gesture
    up_count = sum(fingers)

    # ROCK: all down (allow slight thumb ambiguity)
    if up_count == 0 or (up_count == 1 and fingers[0]):
        return "rock"

    # PAPER: all up
    if all(fingers):
        return "paper"

    # SCISSORS: index and middle up, others down (thumb may vary)
    if fingers[1] and fingers[2] and not fingers[3] and not fingers[4]:
        return "scissors"

    return "unknown"

# ---------- Camera loop ----------
cap = cv2.VideoCapture(0)
if not cap.isOpened():
    print("Cannot open camera")
    exit()

print("Press 1/2/3 for manual rock/paper/scissors. ESC to quit.")

last_sent = None
last_time = 0
cooldown = 0.8  # seconds

while True:
    ok, frame = cap.read()
    if not ok:
        print("Failed to grab frame")
        break

    frame = cv2.flip(frame, 1)
    rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
    result = hands.process(rgb)

    gesture = "unknown"
    conf_text = ""

    if result.multi_hand_landmarks and result.multi_handedness:
        hand_landmarks = result.multi_hand_landmarks[0]
        handedness_label = result.multi_handedness[0].classification[0].label  # "Right"/"Left"

        gesture = classify_rps(hand_landmarks.landmark, handedness_label)
        # draw landmarks for feedback
        mp_drawing.draw_landmarks(
            frame, hand_landmarks, mp_hands.HAND_CONNECTIONS
        )

    cv2.putText(frame, f"{gesture}", (20, 40),
                cv2.FONT_HERSHEY_SIMPLEX, 1.1, (0, 255, 0), 2)

    cv2.imshow("RPS (MediaPipe)", frame)

    now = time.time()
    if gesture in ("rock", "paper", "scissors"):
        if gesture != last_sent or (now - last_time) > cooldown:
            send_serial(f"human:{gesture}")
            last_sent, last_time = gesture, now

    k = cv2.waitKey(10) & 0xFF
    if k == 27:  # ESC
        break
    elif k == ord('1'):
        send_serial("human:rock")
    elif k == ord('2'):
        send_serial("human:paper")
    elif k == ord('3'):
        send_serial("human:scissors")

cap.release()
cv2.destroyAllWindows()
if ser:
    ser.close()

All Codes

C/C++
..
No preview (download only).

Libraries

C/C++
...
No preview (download only).

Credits

Mirko Pavleski
202 projects • 1513 followers

Comments