Source code for song_match.song_robot

"""Module containing :class:`~song_match.song_robot.SongRobot`."""

from asyncio import TimeoutError
from asyncio import sleep
from random import random
from typing import List, Tuple, Union

from cozmo.anim import Animation
from cozmo.anim import AnimationTrigger
from cozmo.objects import LightCube1Id, LightCube2Id, LightCube3Id
from cozmo.objects import LightCubeIDs
from cozmo.robot import Robot, world
from cozmo.robot import SayText
from cozmo.util import degrees

from song_match.cube_mat import CubeMat
from .cube import NoteCube
from .game_constants import MAX_STRIKES
from .song import Song, Note


[docs]class SongRobot: """Wrapper class for Cozmo :class:`~cozmo.robot.Robot` instance.""" _NOTE_DELAY = 0.25 # Time to delay blinking the cube and playing the note _SLEEP_TIME = 0.1 # Time to sleep for while animation finishes def __init__(self, robot: Robot, song: Song): self._robot = robot self._song = song self._prev_cube_id = None # Keep track of previously tapped cube self._initial_angle = robot.pose_angle self.num_wrong = 0 # Keep track of the number of wrong notes Cozmo taps
[docs] async def play_notes(self, notes: List[Note], with_error=False) -> Tuple[bool, Union[None, Note]]: """Make Cozmo play a series of notes. :param notes: The series of notes to play. :param with_error: Whether to play the series of notes with a chance for error. :return: Whether cozmo played the correct notes and the incorrect note he played if any. :rtype: Tuple[bool, Union[None, Note]] """ for note in notes: sequence_length = len(notes) error = self.__get_chance_for_error(sequence_length) if with_error and error: is_correct, note = await self.play_note_with_error(note, sequence_length) if not is_correct: return False, note else: await self.play_note(note) return True, None
[docs] async def play_note(self, note: Note) -> None: """Make Cozmo play a note. :param note: The :class:`~song_match.song.note.Note` to play. :return: None """ cube_id = self._song.get_cube_id(note) note_cube = NoteCube.of(self, cube_id) action = await self.tap_cube(cube_id) await sleep(self._NOTE_DELAY) await note_cube.blink_and_play_note() await action.wait_for_completed()
[docs] async def play_note_with_error(self, note: Note, sequence_length: int = 1) -> Tuple[bool, Note]: """Make Cozmo play a :class:`~song_match.song.note.Note` with a chance for error. :param note: The :class:`~song_match.song.note.Note` to play. :param sequence_length: The length of the sequence to play. :return: Whether Cozmo played the correct note, and the :class:`~song_match.song.note.Note` he played. :rtype: Tuple[bool, Note] """ played_correct_note = True cube_id = self._song.get_cube_id(note) error = self.__get_chance_to_play_wrong_note(sequence_length) if error: played_correct_note = False cube_id = cube_id % len(LightCubeIDs) + 1 wrong_note = self._song.get_note(cube_id) await self.play_note(wrong_note) else: await self.play_note(note) return played_correct_note, self._song.get_note(cube_id)
def __get_chance_to_play_wrong_note(self, sequence_length: int) -> bool: difficulty = 0.1 if self._song.is_sequence_long(sequence_length): difficulty *= 1.5 error = self.__get_chance_for_error(sequence_length, difficulty=difficulty) return error @staticmethod def __get_chance_for_error(sequence_length: int, difficulty: float = .01) -> bool: return random() < (difficulty * sequence_length)
[docs] async def turn_back_to_center(self, in_parallel=False) -> None: """Turn Cozmo back to the center. :param in_parallel: Whether to do the action in parallel or wait until it's completed. :return: None """ if in_parallel: self._robot.turn_in_place(self._initial_angle, is_absolute=True) else: await self._robot.turn_in_place(self._initial_angle, is_absolute=True).wait_for_completed() self._prev_cube_id = self.__get_middle_cube_id()
[docs] async def turn_to_cube(self, cube_id: int) -> None: """Make Cozmo turn in place until the specified cube is visible. :param cube_id: :attr:`~cozmo.objects.LightCube.cube_id` to turn to. :return: None """ timeout = 0.1 try: cube = await self.world.wait_for_observed_light_cube(timeout=timeout) except TimeoutError: cube = None # Didn't find cube while cube is None or cube.cube_id != cube_id: await self._robot.turn_in_place(degrees(30)).wait_for_completed() try: cube = await self.world.wait_for_observed_light_cube(timeout=timeout) except TimeoutError: cube = None # Didn't find cube if cube is not None and cube.cube_id == cube_id: break
@property def did_win(self) -> bool: """Property for accessing whether Cozmo won the game. :return: Whether Cozmo won the game. """ return self.num_wrong < MAX_STRIKES @property def world(self) -> world: """Property for accessing :attr:`~cozmo.robot.Robot.world`.""" return self._robot.world @property def robot(self) -> Robot: """Property for accessing :class:`~cozmo.robot.Robot`.""" return self._robot @property def song(self) -> Song: """Property for accessing :class:`~song_match.song.song.Song`.""" return self._song
[docs] async def tap_cube(self, cube_id) -> Animation: """Make Cozmo tap a cube. :param cube_id: :attr:`~cozmo.objects.LightCube.cube_id` :return: :class:`~cozmo.anim.Animation` """ if self._prev_cube_id is None: self._prev_cube_id = self.__get_middle_cube_id() animation = self.__get_tap_animation(cube_id) self._prev_cube_id = cube_id return await self.play_anim(animation, in_parallel=True)
@staticmethod def __get_middle_cube_id() -> int: mat_positions = CubeMat.get_positions() return mat_positions[1] def __get_tap_animation(self, cube_id) -> str: """Returns a tap animation based upon the current and previously tapped cubes.""" animation_lookup = self.__get_tap_animation_lookup() key = (cube_id, self._prev_cube_id) return animation_lookup[key] @staticmethod def __get_tap_animation_lookup() -> dict: """Build a tap animation lookup dictionary. The key is (cube_id, prev_cube_id), where cube_id is the ID of the cube Cozmo is tapping, and prev_cube_id is the ID of the previously tapped cube. There are 5 animations: 1. center 2. small right 3. big right 4. small left 5. big left :return: The animation to tap the cube. """ mat_positions = CubeMat.get_positions() # Build center animations keys = [(LightCube1Id, LightCube1Id), (LightCube2Id, LightCube2Id), (LightCube3Id, LightCube3Id)] center = 'anim_memorymatch_pointcenter_01' animations = [center, center, center] # Build small right animations first_two_elements = tuple(mat_positions[:-1]) last_two_elements = tuple(mat_positions[-2:]) keys.append(first_two_elements) keys.append(last_two_elements) small_right = 'anim_memorymatch_pointsmallright_fast_01' animations.append(small_right) animations.append(small_right) # Build big right animations first_and_last_elements = tuple([mat_positions[0], mat_positions[-1]]) keys.append(first_and_last_elements) big_right = 'anim_memorymatch_pointbigright_01' animations.append(big_right) # Build small left animations first_two_elements_reversed = tuple(mat_positions[:-1][::-1]) # ::-1 reverses the order last_two_elements_reversed = tuple(mat_positions[-2:][::-1]) keys.append(first_two_elements_reversed) keys.append(last_two_elements_reversed) small_left = 'anim_memorymatch_pointsmallleft_fast_01' animations.append(small_left) animations.append(small_left) # Build big left animations first_and_last_elements_reversed = tuple([mat_positions[0], mat_positions[-1]][::-1]) keys.append(first_and_last_elements_reversed) big_left = 'anim_memorymatch_pointbigleft_01' animations.append(big_left) animation_lookup = dict(zip(keys, animations)) return animation_lookup
[docs] async def play_anim(self, animation_name: str, **kwargs) -> Animation: """Wrapper method for :meth:`~cozmo.robot.Robot.play_anim`. :param animation_name: The name of the animation. :param kwargs: See :meth:`~cozmo.robot.Robot.play_anim`. :return: :class:`~cozmo.anim.Animation` """ return self._robot.play_anim(animation_name, **kwargs)
[docs] def play_anim_trigger(self, animation_trigger, **kwargs) -> AnimationTrigger: """Wrapper method for :meth:`~cozmo.robot.Robot.play_anim_trigger`. :param animation_trigger: The animation trigger. :param kwargs: See :meth:`~cozmo.robot.Robot.play_anim_trigger`. :return: :class:`~cozmo.anim.AnimationTrigger` """ return self._robot.play_anim_trigger(animation_trigger, **kwargs)
[docs] def say_text(self, text: str) -> SayText: """Wrapper method for :meth:`~cozmo.robot.Robot.say_text`. :return: :class:`~cozmo.robot.SayText` """ return self._robot.say_text(text)