# -*- 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