Fan Robot: EV3 Game

Python code and building instructions for the LEGO MINDSTORMS EV3 Official Fan Creations (31313).

Fan Robot: EV3 Game
Image credit: LEGO

This robot is all set to play tricks on you. Hide the red ball under the shell, use the IR Beacon to set your level, and watch the robot shuffle and hide the ball – but where? Challenge your friends to see who can find the red ball first!


This program requires LEGO® EV3 MicroPython v2.0 downloadable https://education.lego.com/en-us/support/mindstorms-ev3/python-for-ev3.

EV3Game works as follows:

  • Put a small ball or marble under the middle cup.

  • Choose the difficulty level by the IR beacon buttons: the Up buttons raise the level (up to 9) and the Down buttons lower the level (minimum 1). The level is printed on the screen.

  • Press the Touch Sensor to start the game and make the robot shuffle the cups.

  • Guess which cup has the ball by pressing:
    • Left Up IR button: choose left cup
    • Beacon IR button (press twice): choose middle cup
    • Right Up IR button: choose right cup
  • Repeat again and again to play many games.

The code for the EV3Game class is in ev3_game.py as follows:

from pybricks.hubs import EV3Brick
from pybricks.ev3devices import Motor, TouchSensor, InfraredSensor
from pybricks.media.ev3dev import SoundFile
from pybricks.parameters import Button, Color, Direction, Port, Stop
from pybricks.tools import wait

from random import randint
from time import time


class EV3Game:
    N_LEVELS = 9
    N_OFFSET_DEGREES_FOR_HOLD_CUP = 60
    N_SHUFFLE_SECONDS = 15

    def __init__(
            self,
            b_motor_port: Port = Port.B, c_motor_port: Port = Port.C,
            grip_motor_port: Port = Port.A,
            touch_sensor_port: Port = Port.S1,
            ir_sensor_port: Port = Port.S4, ir_beacon_channel: int = 1):
        self.ev3_brick = EV3Brick()

        self.b_motor = Motor(port=b_motor_port,
                             positive_direction=Direction.CLOCKWISE)
        self.c_motor = Motor(port=c_motor_port,
                             positive_direction=Direction.CLOCKWISE)

        self.grip_motor = Motor(port=grip_motor_port,
                                positive_direction=Direction.CLOCKWISE)

        self.touch_sensor = TouchSensor(port=touch_sensor_port)

        self.ir_sensor = InfraredSensor(port=ir_sensor_port)
        self.ir_beacon_channel = ir_beacon_channel

    def calibrate_grip(self):
        self.grip_motor.run_until_stalled(
            speed=-100,
            then=Stop.HOLD,
            duty_limit=None)

        self.grip_motor.run_angle(
            speed=100,
            rotation_angle=30,
            then=Stop.HOLD,
            wait=True)

    def display_level(self):
        self.ev3_brick.screen.clear()

        self.ev3_brick.screen.print('Level {}'.format(self.level))

        wait(300)

    def start_up(self):
        self.ev3_brick.light.on(color=Color.RED)

        self.calibrate_grip()

        self.level = 1

        self.display_level()

        self.choice = 2

        self.current_b = self.current_c = 1

    def select_level(self):
        while not self.touch_sensor.pressed():
            ir_buttons_pressed = \
                set(self.ir_sensor.buttons(channel=self.ir_beacon_channel))

            if ir_buttons_pressed.intersection(
                    {Button.LEFT_UP, Button.RIGHT_UP}) and \
                    (self.level < self.N_LEVELS):
                self.level += 1

                self.display_level()

            elif ir_buttons_pressed.intersection(
                    {Button.LEFT_DOWN, Button.RIGHT_DOWN}) and \
                    (self.level > 1):
                self.level -= 1

                self.display_level()

        self.ev3_brick.speaker.play_file(file=SoundFile.GO)

    def move_1_rotate_b(self):
        if self.current_b == 1:
            self.rotate_b = self.N_OFFSET_DEGREES_FOR_HOLD_CUP + 180

        elif self.current_b == 2:
            self.rotate_b = 2 * self.N_OFFSET_DEGREES_FOR_HOLD_CUP + 180

        elif self.current_b == 3:
            self.rotate_b = 180

    def move_1_rotate_c(self):
        if self.current_c == 1:
            self.rotate_c = 0

        elif self.current_c == 2:
            self.rotate_c = -self.N_OFFSET_DEGREES_FOR_HOLD_CUP

        elif self.current_c == 3:
            self.rotate_c = self.N_OFFSET_DEGREES_FOR_HOLD_CUP

    def move_1(self):
        self.move_1_rotate_b()
        self.move_1_rotate_c()

        self.current_b = 3
        self.current_c = 1

    def move_2_rotate_b(self):
        if self.current_b == 1:
            self.rotate_b = -self.N_OFFSET_DEGREES_FOR_HOLD_CUP - 180

        elif self.current_b == 2:
            self.rotate_b = -180

        elif self.current_b == 3:
            self.rotate_b = -2 * self.N_OFFSET_DEGREES_FOR_HOLD_CUP - 180

    move_2_rotate_c = move_1_rotate_c

    def move_2(self):
        self.move_2_rotate_b()
        self.move_2_rotate_c()

        self.current_b = 2
        self.current_c = 1

    def move_3_rotate_b(self):
        if self.current_b == 1:
            self.rotate_b = 0

        elif self.current_b == 2:
            self.rotate_b = self.N_OFFSET_DEGREES_FOR_HOLD_CUP

        elif self.current_b == 3:
            self.rotate_b = -self.N_OFFSET_DEGREES_FOR_HOLD_CUP

    def move_3_rotate_c(self):
        if self.current_c == 1:
            self.rotate_c = self.N_OFFSET_DEGREES_FOR_HOLD_CUP + 180

        elif self.current_c == 2:
            self.rotate_c = 180

        elif self.current_c == 3:
            self.rotate_c = 2 * self.N_OFFSET_DEGREES_FOR_HOLD_CUP + 180

    def move_3(self):
        self.move_3_rotate_b()
        self.move_3_rotate_c()

        self.current_b = 1
        self.current_c = 2

    move_4_rotate_b = move_3_rotate_b

    def move_4_rotate_c(self):
        if self.current_c == 1:
            self.rotate_c = -self.N_OFFSET_DEGREES_FOR_HOLD_CUP - 180

        elif self.current_c == 2:
            self.rotate_c = -2 * self.N_OFFSET_DEGREES_FOR_HOLD_CUP - 180

        elif self.current_c == 3:
            self.rotate_c = -180

    def move_4(self):
        self.move_4_rotate_b()
        self.move_4_rotate_c()

        self.current_b = 1
        self.current_c = 3

    def execute_move(self):
        speed = 100 * self.level

        if self.current_b == 1:
            self.b_motor.run_angle(
                speed=speed,
                rotation_angle=self.rotate_b,
                then=Stop.HOLD,
                wait=True)

            self.c_motor.run_angle(
                speed=speed,
                rotation_angle=self.rotate_c,
                then=Stop.HOLD,
                wait=True)

        else:
            assert self.current_c == 1

            self.c_motor.run_angle(
                speed=speed,
                rotation_angle=self.rotate_c,
                then=Stop.HOLD,
                wait=True)

            self.b_motor.run_angle(
                speed=speed,
                rotation_angle=self.rotate_b,
                then=Stop.HOLD,
                wait=True)

    def update_ball_cup(self):
        if self.move in {1, 2}:
            if self.cup_with_ball == 1:
                self.cup_with_ball = 2

            elif self.cup_with_ball == 2:
                self.cup_with_ball = 1

        else:
            if self.cup_with_ball == 2:
                self.cup_with_ball = 3

            elif self.cup_with_ball == 3:
                self.cup_with_ball = 2

    def shuffle(self):
        shuffle_start_time = time()

        while time() - shuffle_start_time < self.N_SHUFFLE_SECONDS:
            self.move = randint(1, 4)

            if self.move == 1:
                self.move_1()

            elif self.move == 2:
                self.move_2()

            elif self.move == 3:
                self.move_3()

            elif self.move == 4:
                self.move_4()

            self.execute_move()
            self.update_ball_cup()

    def reset_motor_positions(self):
        """
        Resetting motors' positions like it is done when the moves finish
        """
        # Resetting Motor B to Position 1,
        # which, for Motor B corresponds to Move 3
        self.move_3_rotate_b()

        # Reseting Motor C to Position 1,
        # which, for Motor C corresponds to Move 1
        self.move_1_rotate_c()

        self.current_b = self.current_c = 1

        # Executing the reset for both motors
        self.execute_move()

    def select_choice(self):
        self.choice = None

        while not self.choice:
            ir_buttons_pressed = \
                set(self.ir_sensor.buttons(channel=self.ir_beacon_channel))

            if ir_buttons_pressed == {Button.LEFT_UP}:
                self.choice = 1

            elif ir_buttons_pressed == {Button.BEACON}:
                self.choice = 2

                # wait for BEACON button to turn off
                while set(self.ir_sensor.buttons(
                            channel=self.ir_beacon_channel)) \
                        == {Button.BEACON}:
                    wait(10)

            elif ir_buttons_pressed == {Button.RIGHT_UP}:
                self.choice = 3

    def cup_to_center(self):
        # Saving a copy of the current Level
        self.level_copy = self.level

        # Using Level 1 to rotate the chosen cup to the center
        self.level = 1

        if self.choice == 1:
            self.move = 1
            self.move_1()

            self.execute_move()
            self.update_ball_cup()

        elif self.choice == 3:
            self.move = 3
            self.move_3()

            self.execute_move()
            self.update_ball_cup()

        self.reset_motor_positions()

        # Restoring previous value of Level
        self.level = self.level_copy

    def lift_cup(self):
        self.grip_motor.run_angle(
            speed=100,
            rotation_angle=220,
            then=Stop.HOLD,
            wait=True)

The code for the main program is in main.py as follows:

#!/usr/bin/env pybricks-micropython


from pybricks.media.ev3dev import SoundFile
from pybricks.parameters import Color
from pybricks.tools import wait

from ev3_game import EV3Game


ev3_game = EV3Game()

ev3_game.start_up()

while True:
    ev3_game.cup_with_ball = 2

    ev3_game.select_level()

    ev3_game.ev3_brick.light.on(color=Color.GREEN)

    ev3_game.shuffle()

    ev3_game.reset_motor_positions()

    ev3_game.ev3_brick.light.off()

    correct_choice = False

    while not correct_choice:
        ev3_game.select_choice()

        ev3_game.cup_to_center()

        # The choice will be now in the middle, Position 2

        ev3_game.lift_cup()

        correct_choice = (ev3_game.cup_with_ball == 2)

        if correct_choice:
            ev3_game.ev3_brick.light.on(color=Color.GREEN)

            ev3_game.ev3_brick.speaker.play_file(file=SoundFile.CHEERING)

        else:
            ev3_game.ev3_brick.light.on(color=Color.RED)

            ev3_game.ev3_brick.speaker.play_file(file=SoundFile.BOO)

        wait(2000)

        ev3_game.calibrate_grip()


This project was submitted by The Lương-Phạm Family.