Yet Another Rubik's Cube Game

I had some spare time over the weekend, so I cobbled together a Rubik’s cube game in Blender. It doesn’t use the Game Engine, so I’m sorry if I’ve posted in the wrong place. I didn’t see a place for non-BGE games, so I figured it was a better fit here than anywhere else.

The User Interface is pretty simple. Each face can be rotated either clockwise (CW) or counter clockwise (CCW). Everything is done with regular Blender properties. The colors of the faces are beside each button and the colors can be tuned right there. It can start with a solved cube (as you’d get it out of the box) or it can generate random cubes (it starts with a solved cube and scrambles it a preselected number of random steps).

It has some handy settings in the “Additional Settings” sub-panel (some for Blender and some for the game). The two most useful game settings are “Steps per Rotation” and “Steps to Scramble”.

The “Steps per Rotation” controls how many clicks it takes to rotate the selected face 90 degrees. For normal play it should be 1 which will rotate 90 degrees for every click. But if you want to get a sense of the motion (or make screen shots), then higher numbers will take smaller steps. Note that the cube will literally go to pieces if you switch the rotation axis during a partial rotation. That’s because it uses a very simple algorithm to select which objects will be rotated. If you want something that resembles a “Picasso’s Cube” (rather than a Rubik’s Cube), try changing axes during partial rotations.

The “Steps to Scramble” setting determines how many random moves it makes from the solved cube when it scrambles it. This is useful for getting started. Set “Steps to Scramble” to a small number (less than 5) and see if you can solve it from there. It gets amazingly hard very fast!!

The “Unlock All” and “Lock All” buttons control selectability of the parts. Normally, nothing should be selectable since motion is controlled by the “CW” and “CCW” buttons. But if you find you need to select things, these buttons make it easy to disable and enable the “locking”. The “Show Only Render” button is there to hide the 3D Cursor and other normal 3D-View helpers (not generally needed here). The “View Lens” is handy to make it look a bit more orthoganal (I like it around 70). The Turntable/Trackball setting is there for those who like one or the other.

Here’s a screen shot of the interface:


Normally the “Additional Settings” is closed - leaving only the simpler interface. You start with either a Solved or Random Cube and then click the “CW” or “CCW” buttons to rotate each face. You use Blender’s normal 3D View manipulation to view the cube as you’re solving it.

I wrote this mostly as an exercise, so any comments are welcome. I’ll add the code separately as 3 separate posts since it’s well over the 10,000 character limit of this forum (a little silly since the image above is over twice the size of all the code). If there’s a better way to attach large text files, please let me know.

Have fun!!

First Section of the Addon File:

bl_info = {
  "version": "0.1",
  "name": "Rubiks Cube",
  'author': 'Bob',
  "category": "Games"
  }

import bpy
from bpy.props import *

import os
import pickle
import math
import random

def update_blender ( context ):

    # Rubik's Cube Color layout:
    #     W
    #  G  R  B  O
    #     Y
    # Red   = +x  Orange  = -x
    # Blue  = +y  Green   = -y
    # White = +z  Yellow  = -z
    
    # Create materials as needed:

    black_mat = None
    if 'black' in bpy.data.materials:
      # Use existing black material
      black_mat = bpy.data.materials['black']
    else:
      # Add a new black material
      black_mat = bpy.data.materials.new('black')
    bpy.data.materials['black'].diffuse_color = [0,0,0]
    bpy.data.materials['black'].diffuse_intensity = 1.0
    bpy.data.materials['black'].emit = 0.0

    red_mat = None
    if 'red' in bpy.data.materials:
      # Use existing red material
      red_mat = bpy.data.materials['red']
    else:
      # Add a new red material
      red_mat = bpy.data.materials.new('red')
      bpy.data.materials['red'].diffuse_color = [1,0,0]
      bpy.data.materials['red'].diffuse_intensity = 1.0
      bpy.data.materials['red'].emit = 1.0

    blue_mat = None
    if 'blue' in bpy.data.materials:
      # Use existing blue material
      blue_mat = bpy.data.materials['blue']
    else:
      # Add a new blue material
      blue_mat = bpy.data.materials.new('blue')
      bpy.data.materials['blue'].diffuse_color = [0,0,1]
      bpy.data.materials['blue'].diffuse_intensity = 1.0
      bpy.data.materials['blue'].emit = 1.0

    white_mat = None
    if 'white' in bpy.data.materials:
      # Use existing white material
      white_mat = bpy.data.materials['white']
    else:
      # Add a new white material
      white_mat = bpy.data.materials.new('white')
      bpy.data.materials['white'].diffuse_color = [1,1,1]
      bpy.data.materials['white'].diffuse_intensity = 1.0
      bpy.data.materials['white'].emit = 1.0

    orange_mat = None
    if 'orange' in bpy.data.materials:
      # Use existing orange material
      orange_mat = bpy.data.materials['orange']
    else:
      # Add a new orange material
      orange_mat = bpy.data.materials.new('orange')
      bpy.data.materials['orange'].diffuse_color = [1,0.2,0]
      bpy.data.materials['orange'].diffuse_intensity = 1.0
      bpy.data.materials['orange'].emit = 1.0

    green_mat = None
    if 'green' in bpy.data.materials:
      # Use existing green material
      green_mat = bpy.data.materials['green']
    else:
      # Add a new green material
      green_mat = bpy.data.materials.new('green')
      bpy.data.materials['green'].diffuse_color = [0,0.5,0]
      bpy.data.materials['green'].diffuse_intensity = 1.0
      bpy.data.materials['green'].emit = 1.0

    yellow_mat = None
    if 'yellow' in bpy.data.materials:
      # Use existing yellow material
      yellow_mat = bpy.data.materials['yellow']
    else:
      # Add a new yellow material
      yellow_mat = bpy.data.materials.new('yellow')
      bpy.data.materials['yellow'].diffuse_color = [1,1,0]
      bpy.data.materials['yellow'].diffuse_intensity = 1.0
      bpy.data.materials['yellow'].emit = 1.0

    # Start by deleting all objects
    unlock_all()
    bpy.ops.object.select_all(action='SELECT')
    bpy.ops.object.delete(use_global=True)
    
    # Build the sub-cubes that make up the game cube
    
    for z in [-1,0,1]:
      for y in [-1,0,1]:
        for x in [-1,0,1]:
          name = 'x' + str(x) + '_y' + str(y) + '_z' + str(z)
          bpy.ops.mesh.primitive_cube_add()
          bpy.ops.object.material_slot_add()
          context.scene.objects.active.name = name
          bpy.data.objects[name].location = (x,y,z)
          s = 0.475   # 0.5 would leave no gaps between subsections of the cube
          bpy.data.objects[name].scale = (s,s,s)
          bpy.data.objects[name].material_slots[0].material = bpy.data.materials['black']

          bpy.ops.object.mode_set ( mode="OBJECT" )
          bpy.data.objects[name].select = False

          ss = 0.9   # How much of each face is covered by the label
          tl = 0.02  # Thickness of the label (times 2)

          for side in [-1,1]:
            if side == x:
              delta = side * s
              face_name = name+"_face_x_"+str(int(delta/abs(delta)))
              bpy.ops.mesh.primitive_cube_add()
              bpy.ops.object.material_slot_add()
              context.scene.objects.active.name = face_name
              bpy.data.objects[face_name].location = (x+delta,y,z)
              bpy.data.objects[face_name].scale = (tl*ss*s,ss*s,ss*s)
              if delta > 0:
                bpy.data.objects[face_name].material_slots[0].material = bpy.data.materials['red']
              else:
                bpy.data.objects[face_name].material_slots[0].material = bpy.data.materials['orange']

              bpy.ops.object.mode_set ( mode="OBJECT" )
              bpy.data.objects[face_name].select = False

          for side in [-1,1]:
            if side == y:
              delta = side * s
              face_name = name+"_face_y_"+str(int(delta/abs(delta)))
              bpy.ops.mesh.primitive_cube_add()
              bpy.ops.object.material_slot_add()
              context.scene.objects.active.name = face_name
              bpy.data.objects[face_name].location = (x,y+delta,z)
              bpy.data.objects[face_name].scale = (ss*s,tl*ss*s,ss*s)
              if delta > 0:
                bpy.data.objects[face_name].material_slots[0].material = bpy.data.materials['blue']
              else:
                bpy.data.objects[face_name].material_slots[0].material = bpy.data.materials['green']

              bpy.ops.object.mode_set ( mode="OBJECT" )
              bpy.data.objects[face_name].select = False

          for side in [-1,1]:
            if side == z:
              delta = side * s
              face_name = name+"_face_z_"+str(int(delta/abs(delta)))
              bpy.ops.mesh.primitive_cube_add()
              bpy.ops.object.material_slot_add()
              context.scene.objects.active.name = face_name
              bpy.data.objects[face_name].location = (x,y,z+delta)
              bpy.data.objects[face_name].scale = (ss*s,ss*s,tl*ss*s)
              if delta > 0:
                bpy.data.objects[face_name].material_slots[0].material = bpy.data.materials['white']
              else:
                bpy.data.objects[face_name].material_slots[0].material = bpy.data.materials['yellow']

              bpy.ops.object.mode_set ( mode="OBJECT" )
              bpy.data.objects[face_name].select = False

    # Set the center object as active so the "dot" will be centered
    select_none()
    context.scene.objects.active = bpy.data.objects['x0_y0_z0']

    # Finish by selecting none and locking all objects
    select_none()
    lock_all()


def state_changed_callback ( self, context ):
    self.state_changed_callback ( context )

class TTTCellProp(bpy.types.PropertyGroup):
    cell_state = BoolProperty ( name=" ", default=False, update=state_changed_callback )
    cell_coords = StringProperty ( name="cell_coords", default="0 0 0" )
    cell_owner = IntProperty ( name="cell_owner", default=0 )

    def state_changed_callback ( self, context ):

        ttt = context.scene.Rubik3D

        if ttt.game_over:
            try:
              # The state will have flipped already, so set it back.
              self.cell_state = not self.cell_state
            except:
              pass
        else:
            update = False
            if self.cell_owner == 0:
                self.cell_owner = ttt.current_player
                update = True
            if self.cell_state == False:
                self.cell_state = True

            if update:
                # Build the current cell lists to be used by the TicTacToeLogic
                # Start with either an empty list or the previously pickled versions
                player_1_list = []
                if len(ttt.player_1_list_pickle) > 0:
                    player_1_list = pickle.loads(ttt.player_1_list_pickle.encode('latin1'))
                player_2_list = []
                if len(ttt.player_2_list_pickle) > 0:
                    player_2_list = pickle.loads(ttt.player_2_list_pickle.encode('latin1'))
                # Now add any new locations (this keeps them in the order entered)
                for cell in ttt.cell_list:
                  if cell.cell_owner > 0:
                    z, y, x = cell.cell_coords.split()
                    z = (ttt.num_cells_per_dimension-1) - int(z)
                    loc = ( int(x), int(y), int(z) )
                    if cell.cell_owner == 1:
                      if not loc in player_1_list:
                        player_1_list.append ( loc )
                    elif cell.cell_owner == 2:
                      if not loc in player_2_list:
                        player_2_list.append ( loc )

                # Save the cell lists back as pickled versions
                ttt.player_1_list_pickle = pickle.dumps(player_1_list,protocol=0).decode('latin1')
                ttt.player_2_list_pickle = pickle.dumps(player_2_list,protocol=0).decode('latin1')


def new_game ( self, context ):
    ttt = self
    update_blender ( context )


Second section of the addon file:

class RubikProps(bpy.types.PropertyGroup):
    num_cells_per_dimension = IntProperty ( name="Num Cells", default=4, update=new_game )
    cell_list = CollectionProperty ( type=TTTCellProp )
    current_player = IntProperty ( name = "current_player", default = 1 )
    game_over = BoolProperty ( name = "Game_Over", default=False )
    winner = StringProperty ( name = "winner", default = "" )
    show_settings = BoolProperty ( name = "Settings", default=False )
    player_1_list_pickle = StringProperty ( default = "" )
    player_2_list_pickle = StringProperty ( default = "" )
    rotation_steps = IntProperty ( default = 1 )
    scramble_steps = IntProperty ( default = 100 )


class SolvedGame(bpy.types.Operator):
    bl_idname = "solved.game"
    bl_label = "Solved Cube"
    bl_description = ("New Game")

    def execute(self, context):
        ttt = context.scene.Rubik3D
        new_game ( ttt, context )
        return{'FINISHED'}


class RandomGame(bpy.types.Operator):
    bl_idname = "random.game"
    bl_label = "Random Cube"
    bl_description = ("New Game")

    def execute(self, context):
        ttt = context.scene.Rubik3D
        new_game ( ttt, context )

        saved_rotation_steps = ttt.rotation_steps
        ttt.rotation_steps = 1

        # This is a simple method to keep a random move from reversing itself
        inverses = { 0:1, 2:3, 4:5, 6:7, 8:9, 10:11, 1:0, 3:2, 5:4, 7:6, 9:8, 11:10 }
        last_choice = 0
        choice = 1
        
        for i in range(ttt.scramble_steps):

            choice = random.randint(0,11)
            # print ( "Chose first random choice " + str(choice) )
            while choice == inverses[last_choice]:
                choice = random.randint(0,11)
                # print ( "Chose an additional random choice " + str(choice) )
            last_choice = choice
            # print ( "Using choice " + str(choice) )

            if choice == 0:
                bpy.ops.rotate.pxcw()
            elif choice == 1:
                bpy.ops.rotate.pxccw()
            elif choice == 2:
                bpy.ops.rotate.nxcw()
            elif choice == 3:
                bpy.ops.rotate.nxccw()

            elif choice == 4:
                bpy.ops.rotate.pycw()
            elif choice == 5:
                bpy.ops.rotate.pyccw()
            elif choice == 6:
                bpy.ops.rotate.nycw()
            elif choice == 7:
                bpy.ops.rotate.nyccw()

            elif choice == 8:
                bpy.ops.rotate.pzcw()
            elif choice == 9:
                bpy.ops.rotate.pzccw()
            elif choice == 10:
                bpy.ops.rotate.nzcw()
            elif choice == 11:
                bpy.ops.rotate.nzccw()

            else:
                print ( "Chose " + str(choice) + " on step " + str(i) )
                pass

        ttt.rotation_steps = saved_rotation_steps
        return{'FINISHED'}


class SaveGame(bpy.types.Operator):
    bl_idname = "save.game"
    bl_label = "Save Game"
    bl_description = ("Save text form of current game")

    def execute(self, context):
        ttt = context.scene.Rubik3D
        save_file_name = os.path.join ( os.path.dirname(str(__file__)), "3D_TTT_Games.txt" )
        f = open ( save_file_name, mode="at" )
        
        f.write ( "Game Start" + os.linesep )
        player_1_list = []
        if len(ttt.player_1_list_pickle) > 0:
            player_1_list = pickle.loads(ttt.player_1_list_pickle.encode('latin1'))
        player_2_list = []
        if len(ttt.player_2_list_pickle) > 0:
            player_2_list = pickle.loads(ttt.player_2_list_pickle.encode('latin1'))
        for move in player_1_list:
            f.write ( "P1Move = " + str(move) + os.linesep )
        for move in player_2_list:
            f.write ( "P2Move = " + str(move) + os.linesep )
        f.write ( "Game End" + os.linesep )
        f.close()
        return{'FINISHED'}


def select_by_axis_sign ( axis_index, sign ):
    objs = bpy.data.objects
    unlock_all()
    for o in objs:
        o.hide_select = False
        if sign >= 0:
            if o.location[axis_index] > 0.5:
                o.select = True
            else:
                o.select = False
        else:
            if o.location[axis_index] < -0.5:
                o.select = True
            else:
                o.select = False
    lock_all()

def lock_all():
    objs = bpy.data.objects
    for o in objs:
        o.hide_select = True

def unlock_all():
    objs = bpy.data.objects
    for o in objs:
        o.hide_select = False

def select_none():
    objs = bpy.data.objects
    for o in objs:
        o.select = False

class UnlockAll(bpy.types.Operator):
    bl_idname = "unlock.all"
    bl_label = "Unlock All"
    bl_description = ("Unlock all objects so they can be selected")

    def execute(self, context):
        unlock_all()
        return{'FINISHED'}

class LockAll(bpy.types.Operator):
    bl_idname = "lock.all"
    bl_label = "Lock All"
    bl_description = ("Lock all objects so none can be selected")

    def execute(self, context):
        lock_all()
        return{'FINISHED'}


Third (and final) section of the addon file:


class Rotate_PosX_CW(bpy.types.Operator):
    bl_idname = "rotate.pxcw"
    bl_label = "CW"
    bl_description = ("Rotate selected face clockwise")

    def execute(self, context):
        ttt = context.scene.Rubik3D
        #print ( "Rotate " + self.bl_idname )
        select_by_axis_sign ( 0, 1 )
        bpy.ops.transform.rotate(value=-math.pi/(2*ttt.rotation_steps),axis=(1,0,0))
        select_none()
        return{'FINISHED'}

class Rotate_PosX_CCW(bpy.types.Operator):
    bl_idname = "rotate.pxccw"
    bl_label = "CCW"
    bl_description = ("Rotate selected face counter-clockwise")

    def execute(self, context):
        ttt = context.scene.Rubik3D
        #print ( "Rotate " + self.bl_idname )
        select_by_axis_sign ( 0, 1 )
        bpy.ops.transform.rotate(value=math.pi/(2*ttt.rotation_steps),axis=(1,0,0))
        select_none()
        return{'FINISHED'}

class Rotate_NegX_CW(bpy.types.Operator):
    bl_idname = "rotate.nxcw"
    bl_label = "CW"
    bl_description = ("Rotate selected face clockwise")

    def execute(self, context):
        ttt = context.scene.Rubik3D
        #print ( "Rotate " + self.bl_idname )
        select_by_axis_sign ( 0, -1 )
        bpy.ops.transform.rotate(value=math.pi/(2*ttt.rotation_steps),axis=(1,0,0))
        select_none()
        return{'FINISHED'}

class Rotate_NegX_CCW(bpy.types.Operator):
    bl_idname = "rotate.nxccw"
    bl_label = "CCW"
    bl_description = ("Rotate selected face counter-clockwise")

    def execute(self, context):
        ttt = context.scene.Rubik3D
        #print ( "Rotate " + self.bl_idname )
        select_by_axis_sign ( 0, -1 )
        bpy.ops.transform.rotate(value=-math.pi/(2*ttt.rotation_steps),axis=(1,0,0))
        select_none()
        return{'FINISHED'}



class Rotate_PosY_CW(bpy.types.Operator):
    bl_idname = "rotate.pycw"
    bl_label = "CW"
    bl_description = ("Rotate selected face clockwise")

    def execute(self, context):
        ttt = context.scene.Rubik3D
        #print ( "Rotate " + self.bl_idname )
        select_by_axis_sign ( 1, 1 )
        bpy.ops.transform.rotate(value=-math.pi/(2*ttt.rotation_steps),axis=(0,1,0))
        select_none()
        return{'FINISHED'}

class Rotate_PosY_CCW(bpy.types.Operator):
    bl_idname = "rotate.pyccw"
    bl_label = "CCW"
    bl_description = ("Rotate selected face counter-clockwise")

    def execute(self, context):
        ttt = context.scene.Rubik3D
        #print ( "Rotate " + self.bl_idname )
        select_by_axis_sign ( 1, 1 )
        bpy.ops.transform.rotate(value=math.pi/(2*ttt.rotation_steps),axis=(0,1,0))
        select_none()
        return{'FINISHED'}

class Rotate_NegY_CW(bpy.types.Operator):
    bl_idname = "rotate.nycw"
    bl_label = "CW"
    bl_description = ("Rotate selected face clockwise")

    def execute(self, context):
        ttt = context.scene.Rubik3D
        #print ( "Rotate " + self.bl_idname )
        select_by_axis_sign ( 1, -1 )
        bpy.ops.transform.rotate(value=math.pi/(2*ttt.rotation_steps),axis=(0,1,0))
        select_none()
        return{'FINISHED'}

class Rotate_NegY_CCW(bpy.types.Operator):
    bl_idname = "rotate.nyccw"
    bl_label = "CCW"
    bl_description = ("Rotate selected face counter-clockwise")

    def execute(self, context):
        ttt = context.scene.Rubik3D
        #print ( "Rotate " + self.bl_idname )
        select_by_axis_sign ( 1, -1 )
        bpy.ops.transform.rotate(value=-math.pi/(2*ttt.rotation_steps),axis=(0,1,0))
        select_none()
        return{'FINISHED'}



class Rotate_PosZ_CW(bpy.types.Operator):
    bl_idname = "rotate.pzcw"
    bl_label = "CW"
    bl_description = ("Rotate selected face clockwise")

    def execute(self, context):
        ttt = context.scene.Rubik3D
        #print ( "Rotate " + self.bl_idname )
        select_by_axis_sign ( 2, 1 )
        bpy.ops.transform.rotate(value=-math.pi/(2*ttt.rotation_steps),axis=(0,0,1))
        select_none()
        return{'FINISHED'}

class Rotate_PosZ_CCW(bpy.types.Operator):
    bl_idname = "rotate.pzccw"
    bl_label = "CCW"
    bl_description = ("Rotate selected face counter-clockwise")

    def execute(self, context):
        ttt = context.scene.Rubik3D
        #print ( "Rotate " + self.bl_idname )
        select_by_axis_sign ( 2, 1 )
        bpy.ops.transform.rotate(value=math.pi/(2*ttt.rotation_steps),axis=(0,0,1))
        select_none()
        return{'FINISHED'}

class Rotate_NegZ_CW(bpy.types.Operator):
    bl_idname = "rotate.nzcw"
    bl_label = "CW"
    bl_description = ("Rotate selected face clockwise")

    def execute(self, context):
        ttt = context.scene.Rubik3D
        #print ( "Rotate " + self.bl_idname )
        select_by_axis_sign ( 2, -1 )
        bpy.ops.transform.rotate(value=math.pi/(2*ttt.rotation_steps),axis=(0,0,1))
        select_none()
        return{'FINISHED'}

class Rotate_NegZ_CCW(bpy.types.Operator):
    bl_idname = "rotate.nzccw"
    bl_label = "CCW"
    bl_description = ("Rotate selected face counter-clockwise")

    def execute(self, context):
        ttt = context.scene.Rubik3D
        #print ( "Rotate " + self.bl_idname )
        select_by_axis_sign ( 2, -1 )
        bpy.ops.transform.rotate(value=-math.pi/(2*ttt.rotation_steps),axis=(0,0,1))
        select_none()
        return{'FINISHED'}



class RubikPanel(bpy.types.Panel):
    bl_label = "Rubik's Cube"
    bl_space_type = "VIEW_3D"
    bl_region_type = "TOOLS"
    bl_category = "Rubik's Cube"

    def draw(self, context):
        ttt = context.scene.Rubik3D

        row = self.layout.row()
        row.operator ( "solved.game" )
        row = self.layout.row()
        row.operator ( "random.game" )
        #row = self.layout.row()
        #row.operator("save.game")

        row = self.layout.row()

        face_names = [('red','px'), ('orange','nx'), ('blue','py'), ('green','ny'), ('white','pz'), ('yellow','nz')]
        
        for f in face_names:
          c = f[0]
          if c in bpy.data.materials:
            row = self.layout.row()
            row.prop ( bpy.data.materials[c], 'diffuse_color', text="", )
            row.operator ( "rotate." + f[1] + "cw" )
            row.operator ( "rotate." + f[1] + "ccw" )

        row = self.layout.row(align=True)
        row.alignment = 'LEFT'
        if not ttt.show_settings:
            row.prop ( ttt, 'show_settings', icon = 'TRIA_RIGHT', text="Additional Settings", emboss=False )
        else:
            row.prop ( ttt, 'show_settings', icon = 'TRIA_DOWN', text="Additional Settings", emboss=False )

            row = self.layout.row()
            row.prop(context.user_preferences.inputs, "view_rotate_method", expand=True)

            areas = context.screen.areas
            areas_3d = [ areas[i] for i in range(len(areas)) if areas[i].type == 'VIEW_3D' ]
            for area in areas_3d:
              for space in area.spaces:
                if 'show_only_render' in dir(space):
                  row = self.layout.row()
                  row.prop ( space, 'show_only_render', text="Show Only Render" )
                if 'cursor_location' in dir(space):
                  row = self.layout.row()
                  row.prop ( space, 'cursor_location', text="3D Cursor" )
                if 'lens' in dir(space):
                  row = self.layout.row()
                  row.prop ( space, 'lens', text="View Lens" )

            row = self.layout.row()
            row.operator ( "view3d.snap_cursor_to_center" )

            row = self.layout.row()
            row.prop ( ttt, 'rotation_steps', text="Steps per Rotation" )
            row = self.layout.row()
            row.prop ( ttt, 'scramble_steps', text="Steps to Scramble" )
            row = self.layout.row()
            row.operator ( "unlock.all" )
            row = self.layout.row()
            row.operator ( "lock.all" )

def register():
    print ("Registering ", __name__)
    bpy.utils.register_module(__name__)
    bpy.types.Scene.Rubik3D = bpy.props.PointerProperty(type=RubikProps)

def unregister():
    print ("Unregistering ", __name__)
    del bpy.types.Scene.Rubik3D
    bpy.utils.unregister_module(__name__)

if __name__ == "__main__":
    register()


Python has some handy image functionality.

Here’s an updated version of the previous addon posted as an encoded image:


Unfortunately, the forum software won’t accept the code to decode that image in this post.

The forum software is rejecting certain posts.

Additionally unfortunately, you didn’t describe how to decode it either.

I pasted the code in, and it works as advertised! Very neat!
I was a little disappointed that there is so much copy-paste code, but that’s understandable for an experiment.
Also disappointed that the rotation wasn’t animated. If you replace:

        bpy.ops.transform.rotate(value=-math.pi/(2*ttt.rotation_steps),axis=(1,0,0))

with:

        rotation_amount = math.pi/(2*ttt.rotation_steps*7)
        for i in range(7):
            bpy.ops.transform.rotate(value=rotation_amount,axis=(0,0,1))
            bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)

you’ll get animations as well! Just be sure to change the sign on rotation_amount and the axis values for each button… which is why it’s nice to functionalize instead of copy-pasting everything! :smiley:

Hello dudecon,

Thanks for the feedback. I’m not sure why I was having trouble posting the code. As I recall, I was bumping into a text limit, so I figured I could convert the code to an image and get around it. Then I couldn’t even post the small amount of code to decode it!! I gave up in frustration, but the new forum seems to be better behaved (one nice feature).

Here’s the little decoder that I wrote to extract “code.dat” from “code.png” (not really needed with the new forum and the updated version below):

import sys
import Image as img


def decode ( infile="code.png", outfile='code.dat' ):

    ifile = img.open ( infile )

    idata = ifile.getdata()
    num_rows = ifile.size[0]
    num_cols = ifile.size[1]

    row = 0
    col = 0
    index = 0

    pix = idata.getpixel ( (row,col) )

    num_bytes = (pix[0] << 24) + (pix[1] << 16) + (pix[2] << 8) + pix[3]

    print ( "File contains " + str(num_bytes) + " coded bytes" )

    f = open ( outfile, "w" )

    i = 0
    while i < num_bytes:
      pix_index = 1 + (i / 4)
      row = pix_index / num_cols
      col = pix_index % num_cols
      pix_value = idata.getpixel ( (row,col) )
      print ( "At row=" + str(row) + ", c=" + str(col) + " got " + str(pix_value) )
      f.write ( chr(pix_value[0]) )
      i += 1
      if i < num_bytes:
        f.write ( chr(pix_value[1]) )
        i += 1
        if i < num_bytes:
          f.write ( chr(pix_value[2]) )
          i += 1
          if i < num_bytes:
            f.write ( chr(pix_value[3]) )
            i += 1

if __name__ == '__main__':
    if len(sys.argv) == 3:
        decode ( infile=sys.argv[1], outfile=sys.argv[2] )
    else:
        print ( "Provide 2 file names: input output" )

Thanks also for the suggestion for the animated rotation. That will be a neat trick to learn and it might justify cleaning up my code a bit (another good suggestion). :slight_smile:

======================================================

UPDATE:

I added the lines you suggested, and it’s very slick. Thanks!!

I do, however, get a warning message from Blender (2.78c):

Warning: 1 x Draw Window and Swap: 22.0730 ms, average: 22.07303047 ms

Any thoughts on that?

Here’s the updated code (still not well functionalized, but works):

bl_info = {
  "version": "0.1",
  "name": "Rubiks Cube",
  'author': 'Bob',
  "category": "Games"
  }

import bpy
from bpy.props import *

import os
import pickle
import math
import random

def update_blender ( context ):

    # Rubik's Cube Color layout:
    #     W
    #  G  R  B  O
    #     Y
    # Red   = +x  Orange  = -x
    # Blue  = +y  Green   = -y
    # White = +z  Yellow  = -z
    
    # Create materials as needed:

    black_mat = None
    if 'black' in bpy.data.materials:
      # Use existing black material
      black_mat = bpy.data.materials['black']
    else:
      # Add a new black material
      black_mat = bpy.data.materials.new('black')
    bpy.data.materials['black'].diffuse_color = [0,0,0]
    bpy.data.materials['black'].diffuse_intensity = 1.0
    bpy.data.materials['black'].emit = 0.0

    red_mat = None
    if 'red' in bpy.data.materials:
      # Use existing red material
      red_mat = bpy.data.materials['red']
    else:
      # Add a new red material
      red_mat = bpy.data.materials.new('red')
      bpy.data.materials['red'].diffuse_color = [1,0,0]
      bpy.data.materials['red'].diffuse_intensity = 1.0
      bpy.data.materials['red'].emit = 1.0

    blue_mat = None
    if 'blue' in bpy.data.materials:
      # Use existing blue material
      blue_mat = bpy.data.materials['blue']
    else:
      # Add a new blue material
      blue_mat = bpy.data.materials.new('blue')
      bpy.data.materials['blue'].diffuse_color = [0,0,1]
      bpy.data.materials['blue'].diffuse_intensity = 1.0
      bpy.data.materials['blue'].emit = 1.0

    white_mat = None
    if 'white' in bpy.data.materials:
      # Use existing white material
      white_mat = bpy.data.materials['white']
    else:
      # Add a new white material
      white_mat = bpy.data.materials.new('white')
      bpy.data.materials['white'].diffuse_color = [1,1,1]
      bpy.data.materials['white'].diffuse_intensity = 1.0
      bpy.data.materials['white'].emit = 1.0

    orange_mat = None
    if 'orange' in bpy.data.materials:
      # Use existing orange material
      orange_mat = bpy.data.materials['orange']
    else:
      # Add a new orange material
      orange_mat = bpy.data.materials.new('orange')
      bpy.data.materials['orange'].diffuse_color = [1,0.2,0]
      bpy.data.materials['orange'].diffuse_intensity = 1.0
      bpy.data.materials['orange'].emit = 1.0

    green_mat = None
    if 'green' in bpy.data.materials:
      # Use existing green material
      green_mat = bpy.data.materials['green']
    else:
      # Add a new green material
      green_mat = bpy.data.materials.new('green')
      bpy.data.materials['green'].diffuse_color = [0,0.5,0]
      bpy.data.materials['green'].diffuse_intensity = 1.0
      bpy.data.materials['green'].emit = 1.0

    yellow_mat = None
    if 'yellow' in bpy.data.materials:
      # Use existing yellow material
      yellow_mat = bpy.data.materials['yellow']
    else:
      # Add a new yellow material
      yellow_mat = bpy.data.materials.new('yellow')
      bpy.data.materials['yellow'].diffuse_color = [1,1,0]
      bpy.data.materials['yellow'].diffuse_intensity = 1.0
      bpy.data.materials['yellow'].emit = 1.0

    # Start by deleting all objects
    unlock_all()
    bpy.ops.object.select_all(action='SELECT')
    bpy.ops.object.delete(use_global=True)
    
    # Build the sub-cubes that make up the game cube
    
    for z in [-1,0,1]:
      for y in [-1,0,1]:
        for x in [-1,0,1]:
          name = 'x' + str(x) + '_y' + str(y) + '_z' + str(z)
          bpy.ops.mesh.primitive_cube_add()
          bpy.ops.object.material_slot_add()
          context.scene.objects.active.name = name
          bpy.data.objects[name].location = (x,y,z)
          s = 0.475   # 0.5 would leave no gaps between subsections of the cube
          bpy.data.objects[name].scale = (s,s,s)
          bpy.data.objects[name].material_slots[0].material = bpy.data.materials['black']

          bpy.ops.object.mode_set ( mode="OBJECT" )
          bpy.data.objects[name].select = False

          ss = 0.9   # How much of each face is covered by the label
          tl = 0.02  # Thickness of the label (times 2)

          for side in [-1,1]:
            if side == x:
              delta = side * s
              face_name = name+"_face_x_"+str(int(delta/abs(delta)))
              bpy.ops.mesh.primitive_cube_add()
              bpy.ops.object.material_slot_add()
              context.scene.objects.active.name = face_name
              bpy.data.objects[face_name].location = (x+delta,y,z)
              bpy.data.objects[face_name].scale = (tl*ss*s,ss*s,ss*s)
              if delta > 0:
                bpy.data.objects[face_name].material_slots[0].material = bpy.data.materials['red']
              else:
                bpy.data.objects[face_name].material_slots[0].material = bpy.data.materials['orange']

              bpy.ops.object.mode_set ( mode="OBJECT" )
              bpy.data.objects[face_name].select = False

          for side in [-1,1]:
            if side == y:
              delta = side * s
              face_name = name+"_face_y_"+str(int(delta/abs(delta)))
              bpy.ops.mesh.primitive_cube_add()
              bpy.ops.object.material_slot_add()
              context.scene.objects.active.name = face_name
              bpy.data.objects[face_name].location = (x,y+delta,z)
              bpy.data.objects[face_name].scale = (ss*s,tl*ss*s,ss*s)
              if delta > 0:
                bpy.data.objects[face_name].material_slots[0].material = bpy.data.materials['blue']
              else:
                bpy.data.objects[face_name].material_slots[0].material = bpy.data.materials['green']

              bpy.ops.object.mode_set ( mode="OBJECT" )
              bpy.data.objects[face_name].select = False

          for side in [-1,1]:
            if side == z:
              delta = side * s
              face_name = name+"_face_z_"+str(int(delta/abs(delta)))
              bpy.ops.mesh.primitive_cube_add()
              bpy.ops.object.material_slot_add()
              context.scene.objects.active.name = face_name
              bpy.data.objects[face_name].location = (x,y,z+delta)
              bpy.data.objects[face_name].scale = (ss*s,ss*s,tl*ss*s)
              if delta > 0:
                bpy.data.objects[face_name].material_slots[0].material = bpy.data.materials['white']
              else:
                bpy.data.objects[face_name].material_slots[0].material = bpy.data.materials['yellow']

              bpy.ops.object.mode_set ( mode="OBJECT" )
              bpy.data.objects[face_name].select = False

    # Set the center object as active so the "dot" will be centered
    select_none()
    context.scene.objects.active = bpy.data.objects['x0_y0_z0']

    # Finish by selecting none and locking all objects
    select_none()
    lock_all()


def new_game ( self, context ):
    game_cube = self
    update_blender ( context )


class RubikProps(bpy.types.PropertyGroup):
    show_settings = BoolProperty ( name = "Settings", default=False )
    rotation_steps = IntProperty ( default = 1 )
    scramble_steps = IntProperty ( default = 100 )


class SolvedGame(bpy.types.Operator):
    bl_idname = "solved.game"
    bl_label = "Solved Cube"
    bl_description = ("New Game")

    def execute(self, context):
        game_cube = context.scene.Rubik3D
        new_game ( game_cube, context )
        return{'FINISHED'}


class RandomGame(bpy.types.Operator):
    bl_idname = "random.game"
    bl_label = "Random Cube"
    bl_description = ("New Game")

    def execute(self, context):
        game_cube = context.scene.Rubik3D
        new_game ( game_cube, context )

        saved_rotation_steps = game_cube.rotation_steps
        game_cube.rotation_steps = 1

        # This is a simple method to keep a random move from reversing itself
        inverses = { 0:1, 2:3, 4:5, 6:7, 8:9, 10:11, 1:0, 3:2, 5:4, 7:6, 9:8, 11:10 }
        last_choice = 0
        choice = 1
        
        for i in range(game_cube.scramble_steps):

            choice = random.randint(0,11)
            # print ( "Chose first random choice " + str(choice) )
            while choice == inverses[last_choice]:
                choice = random.randint(0,11)
                # print ( "Chose an additional random choice " + str(choice) )
            last_choice = choice
            # print ( "Using choice " + str(choice) )

            if choice == 0:
                bpy.ops.rotate.pxcw()
            elif choice == 1:
                bpy.ops.rotate.pxccw()
            elif choice == 2:
                bpy.ops.rotate.nxcw()
            elif choice == 3:
                bpy.ops.rotate.nxccw()

            elif choice == 4:
                bpy.ops.rotate.pycw()
            elif choice == 5:
                bpy.ops.rotate.pyccw()
            elif choice == 6:
                bpy.ops.rotate.nycw()
            elif choice == 7:
                bpy.ops.rotate.nyccw()

            elif choice == 8:
                bpy.ops.rotate.pzcw()
            elif choice == 9:
                bpy.ops.rotate.pzccw()
            elif choice == 10:
                bpy.ops.rotate.nzcw()
            elif choice == 11:
                bpy.ops.rotate.nzccw()

            else:
                print ( "Chose " + str(choice) + " on step " + str(i) )
                pass

        game_cube.rotation_steps = saved_rotation_steps
        return{'FINISHED'}


def select_by_axis_sign ( axis_index, sign ):
    objs = bpy.data.objects
    unlock_all()
    for o in objs:
        o.hide_select = False
        if sign >= 0:
            if o.location[axis_index] > 0.5:
                o.select = True
            else:
                o.select = False
        else:
            if o.location[axis_index] < -0.5:
                o.select = True
            else:
                o.select = False
    lock_all()

def lock_all():
    objs = bpy.data.objects
    for o in objs:
        o.hide_select = True

def unlock_all():
    objs = bpy.data.objects
    for o in objs:
        o.hide_select = False

def select_none():
    objs = bpy.data.objects
    for o in objs:
        o.select = False

class UnlockAll(bpy.types.Operator):
    bl_idname = "unlock.all"
    bl_label = "Unlock All"
    bl_description = ("Unlock all objects so they can be selected")

    def execute(self, context):
        unlock_all()
        return{'FINISHED'}

class LockAll(bpy.types.Operator):
    bl_idname = "lock.all"
    bl_label = "Lock All"
    bl_description = ("Lock all objects so none can be selected")

    def execute(self, context):
        lock_all()
        return{'FINISHED'}

def rotate_cube_face ( game_cube, axis, direction ):
    # bpy.ops.transform.rotate(value=direction*math.pi/(2*game_cube.rotation_steps),axis=axis)
    rotation_amount = direction*math.pi/(2*game_cube.rotation_steps*7)
    for i in range(7):
        bpy.ops.transform.rotate(value=rotation_amount,axis=axis)
        bpy.ops.wm.redraw_timer (type='DRAW_WIN_SWAP', iterations=1)


class Rotate_PosX_CW(bpy.types.Operator):
    bl_idname = "rotate.pxcw"
    bl_label = "CW"
    bl_description = ("Rotate selected face clockwise")

    def execute(self, context):
        game_cube = context.scene.Rubik3D
        #print ( "Rotate " + self.bl_idname )
        select_by_axis_sign ( 0, 1 )
        rotate_cube_face ( game_cube, (1,0,0), -1 )
        select_none()
        return{'FINISHED'}

class Rotate_PosX_CCW(bpy.types.Operator):
    bl_idname = "rotate.pxccw"
    bl_label = "CCW"
    bl_description = ("Rotate selected face counter-clockwise")

    def execute(self, context):
        game_cube = context.scene.Rubik3D
        #print ( "Rotate " + self.bl_idname )
        select_by_axis_sign ( 0, 1 )
        rotate_cube_face ( game_cube, (1,0,0), 1 )
        select_none()
        return{'FINISHED'}

class Rotate_NegX_CW(bpy.types.Operator):
    bl_idname = "rotate.nxcw"
    bl_label = "CW"
    bl_description = ("Rotate selected face clockwise")

    def execute(self, context):
        game_cube = context.scene.Rubik3D
        #print ( "Rotate " + self.bl_idname )
        select_by_axis_sign ( 0, -1 )
        rotate_cube_face ( game_cube, (1,0,0), 1 )
        select_none()
        return{'FINISHED'}

class Rotate_NegX_CCW(bpy.types.Operator):
    bl_idname = "rotate.nxccw"
    bl_label = "CCW"
    bl_description = ("Rotate selected face counter-clockwise")

    def execute(self, context):
        game_cube = context.scene.Rubik3D
        #print ( "Rotate " + self.bl_idname )
        select_by_axis_sign ( 0, -1 )
        rotate_cube_face ( game_cube, (1,0,0), -1 )
        select_none()
        return{'FINISHED'}



class Rotate_PosY_CW(bpy.types.Operator):
    bl_idname = "rotate.pycw"
    bl_label = "CW"
    bl_description = ("Rotate selected face clockwise")

    def execute(self, context):
        game_cube = context.scene.Rubik3D
        #print ( "Rotate " + self.bl_idname )
        select_by_axis_sign ( 1, 1 )
        rotate_cube_face ( game_cube, (0,1,0), -1 )
        select_none()
        return{'FINISHED'}

class Rotate_PosY_CCW(bpy.types.Operator):
    bl_idname = "rotate.pyccw"
    bl_label = "CCW"
    bl_description = ("Rotate selected face counter-clockwise")

    def execute(self, context):
        game_cube = context.scene.Rubik3D
        #print ( "Rotate " + self.bl_idname )
        select_by_axis_sign ( 1, 1 )
        rotate_cube_face ( game_cube, (0,1,0), 1 )
        select_none()
        return{'FINISHED'}

class Rotate_NegY_CW(bpy.types.Operator):
    bl_idname = "rotate.nycw"
    bl_label = "CW"
    bl_description = ("Rotate selected face clockwise")

    def execute(self, context):
        game_cube = context.scene.Rubik3D
        #print ( "Rotate " + self.bl_idname )
        select_by_axis_sign ( 1, -1 )
        rotate_cube_face ( game_cube, (0,1,0), 1 )
        select_none()
        return{'FINISHED'}

class Rotate_NegY_CCW(bpy.types.Operator):
    bl_idname = "rotate.nyccw"
    bl_label = "CCW"
    bl_description = ("Rotate selected face counter-clockwise")

    def execute(self, context):
        game_cube = context.scene.Rubik3D
        #print ( "Rotate " + self.bl_idname )
        select_by_axis_sign ( 1, -1 )
        rotate_cube_face ( game_cube, (0,1,0), -1 )
        select_none()
        return{'FINISHED'}



class Rotate_PosZ_CW(bpy.types.Operator):
    bl_idname = "rotate.pzcw"
    bl_label = "CW"
    bl_description = ("Rotate selected face clockwise")

    def execute(self, context):
        game_cube = context.scene.Rubik3D
        #print ( "Rotate " + self.bl_idname )
        select_by_axis_sign ( 2, 1 )
        rotate_cube_face ( game_cube, (0,0,1), -1 )
        select_none()
        return{'FINISHED'}

class Rotate_PosZ_CCW(bpy.types.Operator):
    bl_idname = "rotate.pzccw"
    bl_label = "CCW"
    bl_description = ("Rotate selected face counter-clockwise")

    def execute(self, context):
        game_cube = context.scene.Rubik3D
        #print ( "Rotate " + self.bl_idname )
        select_by_axis_sign ( 2, 1 )
        rotate_cube_face ( game_cube, (0,0,1), 1 )
        select_none()
        return{'FINISHED'}

class Rotate_NegZ_CW(bpy.types.Operator):
    bl_idname = "rotate.nzcw"
    bl_label = "CW"
    bl_description = ("Rotate selected face clockwise")

    def execute(self, context):
        game_cube = context.scene.Rubik3D
        #print ( "Rotate " + self.bl_idname )
        select_by_axis_sign ( 2, -1 )
        rotate_cube_face ( game_cube, (0,0,1), 1 )
        select_none()
        return{'FINISHED'}

class Rotate_NegZ_CCW(bpy.types.Operator):
    bl_idname = "rotate.nzccw"
    bl_label = "CCW"
    bl_description = ("Rotate selected face counter-clockwise")

    def execute(self, context):
        game_cube = context.scene.Rubik3D
        #print ( "Rotate " + self.bl_idname )
        select_by_axis_sign ( 2, -1 )
        rotate_cube_face ( game_cube, (0,0,1), -1 )
        select_none()
        return{'FINISHED'}



class RubikPanel(bpy.types.Panel):
    bl_label = "Rubik's Cube"
    bl_space_type = "VIEW_3D"
    bl_region_type = "TOOLS"
    bl_category = "Rubik's Cube"

    def draw(self, context):
        game_cube = context.scene.Rubik3D

        row = self.layout.row()
        row.operator ( "solved.game" )
        row = self.layout.row()
        row.operator ( "random.game" )

        row = self.layout.row()

        face_names = [('red','px'), ('orange','nx'), ('blue','py'), ('green','ny'), ('white','pz'), ('yellow','nz')]
        
        for f in face_names:
          c = f[0]
          if c in bpy.data.materials:
            row = self.layout.row()
            row.prop ( bpy.data.materials[c], 'diffuse_color', text="", )
            row.operator ( "rotate." + f[1] + "cw" )
            row.operator ( "rotate." + f[1] + "ccw" )

        row = self.layout.row(align=True)
        row.alignment = 'LEFT'
        if not game_cube.show_settings:
            row.prop ( game_cube, 'show_settings', icon = 'TRIA_RIGHT', text="Additional Settings", emboss=False )
        else:
            row.prop ( game_cube, 'show_settings', icon = 'TRIA_DOWN', text="Additional Settings", emboss=False )

            row = self.layout.row()
            row.prop(context.user_preferences.inputs, "view_rotate_method", expand=True)

            areas = context.screen.areas
            areas_3d = [ areas[i] for i in range(len(areas)) if areas[i].type == 'VIEW_3D' ]
            for area in areas_3d:
              for space in area.spaces:
                if 'show_only_render' in dir(space):
                  row = self.layout.row()
                  row.prop ( space, 'show_only_render', text="Show Only Render" )
                if 'cursor_location' in dir(space):
                  row = self.layout.row()
                  row.prop ( space, 'cursor_location', text="3D Cursor" )
                if 'lens' in dir(space):
                  row = self.layout.row()
                  row.prop ( space, 'lens', text="View Lens" )

            row = self.layout.row()
            row.operator ( "view3d.snap_cursor_to_center" )

            row = self.layout.row()
            row.prop ( game_cube, 'rotation_steps', text="Steps per Rotation" )
            row = self.layout.row()
            row.prop ( game_cube, 'scramble_steps', text="Steps to Scramble" )
            row = self.layout.row()
            row.operator ( "unlock.all" )
            row = self.layout.row()
            row.operator ( "lock.all" )

def register():
    print ("Registering ", __name__)
    bpy.utils.register_module(__name__)
    bpy.types.Scene.Rubik3D = bpy.props.PointerProperty(type=RubikProps)

def unregister():
    print ("Unregistering ", __name__)
    del bpy.types.Scene.Rubik3D
    bpy.utils.unregister_module(__name__)

if __name__ == "__main__":
    register()

With the new animation, it’s fun just watching it scramble itself!! :sunglasses:

A Rubik’s cube game, huh? Well, something to pass time, I guess.