Source code for rubix_cube.cube_game

# -*- coding: utf-8 -*-
"""Rubix :class:`Cube_Game` class Module

Module Description
==================

Collection of methods that define the main Rubix :class:`Cube Game <Cube_Game>`
class and how it manipulates a :class:`Cube <rubix_cube.cube.Cube>`
object, prints to console, logs a history of both moves and re-orientations,
scrambles the state of the cube, times game-play, and performs file-system
loading and saving of games.


Module Contents
===============

    *   :class:`Cube_Game` details code for manipulating :class:`Rubix
        Cube(s) <rubix_cube.cube.Cube>`.

        .. todo::

           *    Implement file I/O operations (loading and saving games).
           *    Implement scramble cube functions.
           *    Implement color changing functions.
           *    Implement game clock functions.


.. moduleauthor:: David Grethlein <djg329@drexel.edu>

"""

from pathlib import Path
from random import randint , randrange

import os
import sys
import json

import warnings

from typing import List, Tuple, Dict

import numpy as np

from .cube import Cube


[docs]class Cube_Game(object): """Class in charge of directly interacting with and logging changes to an instance of the :class:`Cube` class. Attributes: EVENT_TYPES (List[str]): Description __game_cube (Cube): :class:`Rubix Cube <Cube>` that is manipulated throughout game-play. __game_log (dict): Historical record of all moves, rotations, and other game events that manipulate the :attr:`game_cube`. __game_name (str): Name of the game. __player_name (str): Name of actor playing the game. __verbose (bool): [DEBUG]-style console output. Default value is ``False``. """ #========================================================================== # CONSTANTS FOR CUBE GAME(s) #========================================================================== EVENT_TYPES = ['<<__NEW_GAME__>>', 'F', 'Fi', 'B', 'Bi', 'L', 'Li', 'R', 'Ri', 'U', 'Ui', 'D', 'Di', 'M', 'Mi', 'E', 'Ei', 'S', 'Si', 'X', 'Xi', 'Y', 'Yi', 'Z', 'Zi', '<<__START_SCRAMBLE__>>', '<<__END_SCRAMBLE__>>', '<<__COLOR_CHANGE__>>', '<<__SAVE_GAME__>>', '<<__LOAD_GAME__>>', '<<__PAUSE_GAME__>>', '<<__RESUME_GAME__>>', '<<__SOLVE_CUBE__>>', '<<__CUBE_SOLVED__>>', '<<__QUIT_GAME__>>'] CUBE_FUNCS = {'U' : Cube.move_up, 'Ui' : Cube.move_up_inverse, 'D' : Cube.move_down, 'Di' : Cube.move_down_inverse, 'L' : Cube.move_left, 'Li' : Cube.move_left_inverse, 'R' : Cube.move_right, 'Ri' : Cube.move_right_inverse, 'F' : Cube.move_front, 'Fi' : Cube.move_front_inverse, 'B' : Cube.move_back, 'Bi' : Cube.move_back_inverse, 'M' : Cube.move_middle, 'Mi' : Cube.move_middle_inverse, 'E' : Cube.move_equator, 'Ei' : Cube.move_equator_inverse, 'S' : Cube.move_standing, 'Si' : Cube.move_standing_inverse, 'X' : Cube.rotate_pitch, 'Xi' : Cube.rotate_pitch_inverse, 'Y' : Cube.rotate_yaw, 'Yi' : Cube.rotate_yaw_inverse, 'Z' : Cube.rotate_roll, 'Zi' : Cube.rotate_roll_inverse} INVERSE_FUNCS = {'U' : 'Ui', 'Ui' : 'U', 'D' : 'Di', 'Di' : 'D', 'L' : 'Li', 'Li' : 'L', 'R' : 'Ri', 'Ri' : 'R', 'F' : 'Fi', 'Fi' : 'F', 'B' : 'Bi', 'Bi' : 'B', 'M' : 'Mi', 'Mi' : 'M', 'E' : 'Ei', 'Ei' : 'E', 'S' : 'Si', 'Si' : 'S', 'X' : 'Xi', 'Xi' : 'X', 'Y' : 'Yi', 'Yi' : 'Y', 'Z' : 'Zi', 'Zi' : 'Z'} #========================================================================== # CLASS CONSTRUCTOR #==========================================================================
[docs] def __init__(self, cube : Cube = None, player_name : str = None, game_name : str = None, game_log : Dict = None, scramble : bool = False, verbose : bool = False): """:class:`Cube_Game` class constructor Args: cube (Cube, optional): :class:`Cube` that will be directly manipulated throughout gameplay. player_name (str, optional): Name of current actor playing with :class:`Cube`. game_name (str, optional): Name of the current game being played. game_log (Dict, optional): Dictionary that contains a history of moves and other game events. scramble (bool, optional): Whether or not the game should scramble the :attr:`__game_cube` upon initialization. Default value is ``False``. verbose (bool, optional): [DEBUG]-style console output. Default value is ``False``. """ # Sets Up Default Game self.player_name = 'Anonymous' self.game_name = 'Untitled_Game' self.game_name = game_name self.game_cube = Cube() self.game_log = {'events' : [{'type' : '<<__NEW_GAME__>>', 'name' : self.game_name}]} # Attempts to reset property values with argument values. self.game_cube = cube self.game_log = game_log self.verbose = verbose # Initializes a default cube if self.game_cube == Cube(): if self.verbose: print(f"\n[DEBUG]\tNew DEFAULT Cube created for game : '{self.game_name}'\n") # Initializes a default game log if self.game_log != game_log: if self.verbose: print(f"\n[DEBUG]\tNew DEFAULT ``game_log`` created for game : '{self.game_name}'\n") else: if self.verbose: print(f"\n[DEBUG]\tNew game created with name : '{self.game_name}'\n")
#========================================================================== # PROPERTY INTERFACE(s) #========================================================================== @property def game_cube(self) -> Cube: """:class:`Rubix Cube <Cube>` object being manipulated in game. """ return self.__game_cube @game_cube.setter def game_cube(self , cube : Cube): if isinstance(cube, Cube)\ and cube.is_well_formed(): self.__game_cube = cube @property def game_name(self) -> str: """Name of the Game """ return self.__game_name @game_name.setter def game_name(self, name : str): if isinstance(name, str)\ and len(name) > 0: self.__game_name = name @property def player_name(self) -> str: """Name of actor playing game. """ return self.__player_name @player_name.setter def player_name(self, name : str): if isinstance(name, str)\ and len(name) > 0: self.__player_name = name @property def game_log(self) -> Dict: """JSON-style :class:`dict` recording all actions done to the :attr:`game_cube` stored under the ``events`` key which is a list of :class`dict` objects each of which has a ``type`` key with a value found in :attr:`EVENT_TYPES`. .. code-block:: :name: game_log_EVENT_TYPES :linenos: :caption: Potential values of ``type`` in :attr:`game_log`. EVENT_TYPES = ['<<__NEW_GAME__>>', 'F', 'Fi', 'B', 'Bi', 'L', 'Li', 'R', 'Ri', 'U', 'Ui', 'D', 'Di', 'M', 'Mi', 'E', 'Ei', 'S', 'Si', 'X', 'Xi', 'Y', 'Yi', 'Z', 'Zi', '<<__START_SCRAMBLE__>>', '<<__END_SCRAMBLE__>>', '<<__COLOR_CHANGE__>>', '<<__SAVE_GAME__>>', '<<__LOAD_GAME__>>', '<<__PAUSE_GAME__>>', '<<__RESUME_GAME__>>', '<<__SOLVE_CUBE__>>', '<<__QUIT_GAME__>>'] .. code-block:: :name: game_log :linenos: :caption: Required ``game_log`` dictionary keys. game_log = {'events' : [{'type' : '<<__NEW_GAME__>>', 'name' : '...'}, {'type' : ...}, ..., {'type' : ...}]} """ return self.__game_log @game_log.setter def game_log(self, game_log : Dict): if isinstance(game_log, dict)\ and 'events' in game_log\ and isinstance(game_log['events'], list)\ and len(game_log['events']) > 0\ and all([isinstance(event, dict) for event in game_log['events']]): # Has to ensure that all event types are valid valid_log = all(['type' in event\ and event['type'] in Cube_Game.EVENT_TYPES\ for event in game_log['events']]) if valid_log: self.__game_log = game_log @property def verbose(self) -> bool: """[DEBUG]-style console output. Default value is ``False``. """ return self.__verbose @verbose.setter def verbose(self, verbose : bool): if isinstance(verbose, bool): self.__verbose = verbose else: self.__verbose = False #========================================================================== # GAME-PLAY METHOD(s) #==========================================================================
[docs] def manipulate_cube(self, cube_func : str): """Function that interfaces the :class:`Cube_Game` class with the :class:`Cube` class to turn the layers or rotate the orientation. Args: cube_func (str): Look-up key to recover the proper :class:`Cube` method to call from :attr:`CUBE_FUNCS` class attribute to call a move using the :attr:`game_cube`. .. code-block:: :name: move_cube_CUBE_FUNCS :linenos: :caption: Parameter ``cube_func`` will determine which :attr:`game_cube` move function is called. If not found, nothing happens. CUBE_FUNCS = {'U' : Cube.move_up, 'Ui' : Cube.move_up_inverse, 'D' : Cube.move_down, 'Di' : Cube.move_down_inverse, 'L' : Cube.move_left, 'Li' : Cube.move_left_inverse, 'R' : Cube.move_right, 'Ri' : Cube.move_right_inverse, 'F' : Cube.move_front, 'Fi' : Cube.move_front_inverse, 'B' : Cube.move_back, 'Bi' : Cube.move_back_inverse, 'M' : Cube.move_middle, 'Mi' : Cube.move_middle_inverse, 'E' : Cube.move_equator, 'Ei' : Cube.move_equator_inverse, 'S' : Cube.move_standing, 'Si' : Cube.move_standing_inverse, 'X' : Cube.rotate_pitch, 'Xi' : Cube.rotate_pitch_inverse, 'Y' : Cube.rotate_yaw, 'Yi' : Cube.rotate_yaw_inverse, 'Z' : Cube.rotate_roll, 'Zi' : Cube.rotate_roll_inverse} """ if cube_func in Cube_Game.CUBE_FUNCS\ and self.game_cube.is_well_formed(): # Performs the desired cube move on the game cube Cube_Game.CUBE_FUNCS[cube_func](self.game_cube) if self.verbose: print(f"[DEBUG]\tCalling game cube function: '{cube_func}'") print(f"\t\tNum Matching Adjacent Tiles : {self.game_cube.get_num_matching_adjacent_tiles()}") self.game_log['events'].append({'type' : cube_func})
[docs] def get_scramble_sequence(n_steps : int = 50, cube_funcs : List[str] = None) -> List[str]: """Compiles a sequence of moves for scrambling the :attr:`game_cube` attribute for applying a sequence of ``n_steps`` semi-randomly selected cube manipulations using a defined a provided sub-set of cube functions ``cube_funcs``. Args: n_steps (int, optional): The number of :class:`Rubix Cube <Cube>` manipulations to be applied to the :attr:`game_cube`. Note: Valid range of values, 0 < ``n_steps`` <= 500. Won't call any :class:`Cube` function(s) if not in range. cube_funcs (List[str], optional): Sub-list of :attr:`CUBE_FUNCS` that defines the options for manipulating the cube. Default value is ``None`` which allows all functions in :attr:`CUBE_FUNCS` to be selected. Returns: List[str]: **sequence** - A list of cube manipulation function str(s) for use with :func:`manipulate_cube` function. Note: Won't perform the following sequences: * ``<<ACTION>>`` , ``<<ACTION_INVERSE>>`` * ``<<ACTION>>``, ``<<ACTION>>``, ``<<ACTION>>``, ``<<ACTION>>`` .. code-block:: :name: scramble_cube_INVERSE_FUNCS :linenos: :caption: :class:`Cube_Game` static :attr:`INVERSE_FUNCS` attribute for looking up the inverse function for each potential cube manipulation. INVERSE_FUNCS = {'U' : 'Ui', 'Ui' : 'U', 'D' : 'Di', 'Di' : 'D', 'L' : 'Li', 'Li' : 'L', 'R' : 'Ri', 'Ri' : 'R', 'F' : 'Fi', 'Fi' : 'F', 'B' : 'Bi', 'Bi' : 'B', 'M' : 'Mi', 'Mi' : 'M', 'E' : 'Ei', 'Ei' : 'E', 'S' : 'Si', 'Si' : 'S', 'X' : 'Xi', 'Xi' : 'X', 'Y' : 'Yi', 'Yi' : 'Y', 'Z' : 'Zi', 'Zi' : 'Z'} """ # Default length for sequence of random moves to be generated if not isinstance(n_steps, int)\ or not (n_steps > 0 and n_steps <= 500): n_steps = 0 # Default list of cube functions is all of them if not isinstance(cube_funcs , list)\ or not all([func in Cube_Game.CUBE_FUNCS for func in cube_funcs]): cube_funcs = list(Cube_Game.CUBE_FUNCS.keys()) sequence = [] while len(sequence) < n_steps: # Generates a random choice from the current options # of potential cube-functions to call choice_func = cube_funcs[randrange(len(cube_funcs))] if len(sequence) == 0: sequence.append(choice_func) else: last_func = sequence[-1] # No <<FUNCTION>> , <<INVERSE_FUNCTION>> sub-sequences if last_func == Cube_Game.INVERSE_FUNCS[choice_func]: continue if len(sequence) > 2: last_three = sequence[-3:] # No 4 of the same function in a row if all([func == choice_func for func in last_three]): continue sequence.append(choice_func) return sequence
[docs] def compute_inverse_log_sequence(self) -> List[str]: """Computes the inverse sequence of moves from the :attr:`game_log` that ``SHOULD`` lead to the :class:`Rubix Cube <rubix_cube.cube.Cube>` being solved! Returns: List[str]: **sequence** - A list of cube manipulation function str(s) for use with :func:`manipulate_cube` function. """ # Ensures the game is valid if self.game_cube.is_well_formed()\ and isinstance(self.game_log, dict)\ and 'events' in self.game_log: # Gets a reversed list of all log event types event_types = np.array([event['type'] for event in self.game_log['events']]) r_event_types = np.flip(event_types) if '<<__CUBE_SOLVED__>>' in event_types: last_solved_idx = np.where(r_event_types == '<<__CUBE_SOLVED__>>')[0][0] print(f"Last Solved Idx : {last_solved_idx}") if last_solved_idx == 0: sub_events = np.array([]) else: sub_events = r_event_types[:last_solved_idx + 1] print(f"\n[DEBUG]\tOnly considering events : \n{json.dumps(sub_events.tolist(),indent=2)}") #event_types = event_types[last_solved_idx:] # Filters down to cube-moves only log_moves = list(filter(lambda e_type: e_type in\ Cube_Game.INVERSE_FUNCS, r_event_types)) # Computes the inverse sequence sequence = [Cube_Game.INVERSE_FUNCS[mv] for mv in log_moves] return sequence