'''
  May 2018 Math Puzzler
  Dave Vogel - dvogel003@monroecc.edu
  
  Below are three identical dice. However, these are not standard
  dice as opposite faces do not total 7. Each of the numbers from
  1 to 6 appears on exactly one face of each die and the touching
  faces of the dice have the same number. Indicate the arrangement
  of the die faces by labeling the sides in the "flattened view"
  shown below. To get things started, the side corresponding to
  1 has already been labeled.


     ?           ?           B                 Back
  ?  6  A     A  1  ?     5  3  ?       Left   Top    Right
     3           B           ?                 Front
     ?           ?           ?                 Bottom

     I          II          III

  Note: 'A' is where I and II touch, 'B' is where II and III touch
'''
from __future__ import print_function

import itertools as it
import copy

class Die(object):
  '''
    A Die is a 6 sided block with the number 1-6 on a side.  The
    placement of the numbers are NOT necessarily as per a real Die
    and can be in any fashion.  Each side is labeled with a character
    from the list of ['1', '2', '3', '4', '5', '6']
  '''
  def __init__(self, args=[]):

    self.back = args[0]
    self.left = args[1]
    self.top = args[2]
    self.right = args[3]
    self.front = args[4]
    self.bottom = args[5]
    self.hash = self.makeHash()
    


  def __repr__(self):
    '''
      Text representation of a Dice object.  The values of each side is
      printed as if the block is opened as per:

             Back
      Left   Top    Right
             Front
             Bottom
    '''
    return "\n  " + str(self.back) + "\n" + str(self.left) + " " +\
           str(self.top) + " " + str(self.right) + "\n" + "  " +\
           str(self.front) + "\n  " + str(self.bottom) + "\n" +\
           str(self.hash) + "\n"


  def displayThree(self):
    '''
      Display the Die in the three orientations shown in the problem
    '''
    dieLeft = copy.copy(self)
    dieCenter = copy.copy(self)
    dieRight = copy.copy(self)
    spaces = "        "
    
    dieLeft.onTop('6', '3')             #Display with 6 on top, 3 in front
    dieCenter.onTop('1', dieLeft.right) #One on top and what is right on Die 1
    dieCenter.spinLeft()                # in front then rotate to left
    dieRight.onTop('3', '5')            #Put 3 on top and 5 in front
    dieRight.spinLeft()                 # and rotate 5 to the left

    print("\nOrientation when die is:")
    print("  Left      Center      Front")
    print("    " + str(dieLeft.back) + spaces + "  " + str(dieCenter.back) \
          + spaces + "  " + str(dieRight.back))
    print("   " + str(dieLeft.left) + str(dieLeft.top) + str(dieLeft.right) + \
      spaces + str(dieCenter.left) + str(dieCenter.top) + str(dieCenter.right) + \
      spaces + str(dieRight.left) + str(dieRight.top) + str(dieRight.right))
    print("    " + str(dieLeft.front) + spaces + "  " + str(dieCenter.front) \
          + spaces + "  " + str(dieRight.front))
    print("    " + str(dieLeft.bottom) + spaces + "  " + str(dieCenter.bottom) \
          + spaces + "  " + str(dieRight.bottom))
    print("hash = " + str(dieLeft.hash) + "\n")


    
  def makeHash(self):
    '''
      Create a unique integer that represents a Die.  The integer is
      created by placing the '1' side on top and then taking the 4 digits
      representing the 4 sides (front, right, back, left) of the
      periphery of the Die starting with the lowest side numerically.
    '''
    self.onTop('1')
    periphery = self.front + self.right + self.back + self.left
    
    #Duplicate the periphery for wrapping purposes
    peripheryDup = periphery + periphery

    #Start with the lowest number (either 2 or 3)
    if '2' in peripheryDup:
      findchar = '2'
    else:
      findchar = '3'
      
    #Find the position of the low number
    found = peripheryDup.find(findchar)

    #  and take that and the next 3 digits and convert to an int
    return int((peripheryDup)[found:found+4])


      
  def __eq__(self, other):
    '''
      Determine if 2 Dice are equal by comparing their unique hashes
    '''
    if isinstance(self, other.__class__):
      return self.hash == other.hash
    return False


  def __ne__(self, other):
    return (not self.__eq__(other))

  
  def __hash__(self):
    return self.hash



  def rotateForward(self):
    '''
      Rotate a Die forward (top to front, back to top, ...)
    '''
    temp = self.top
    self.top = self.back
    self.back = self.bottom
    self.bottom = self.front
    self.front = temp


  def rotateLeft(self):
    '''
      Rotate a Die left (top to left, left to bottom, ...)
    '''
    temp = self.top
    self.top = self.right
    self.right = self.bottom
    self.bottom = self.left
    self.left = temp


  def spinLeft(self):
    '''
      Spin a Die left (front to left, left to back, ...)
    '''
    temp = self.front
    self.front = self.right
    self.right = self.back
    self.back = self.left
    self.left = temp


  def onTop(self, topside, frontside=''):
    '''
      Change the Die position such that the 'topside' of this Die ison top
      and, if used, the 'frontside' is forward.  Note that if a Die is
      sent here where 'frontside' is opposite 'topside' the result will
      (of course) be invalid.
    '''
    for n in range(3):
      if self.top == topside:
        break
      self.rotateForward()

    while self.top != topside:
      self.rotateLeft()

    if frontside != '':
      for m in range(4):
        if self.front == frontside:
          break
        self.spinLeft()

####End of Class Die



####Main Functions
def createSet():
  '''
    Create a set of all the possibilities for Die objects.  Each Die starts
    with the bottom being a '1'.  All the possibility (5!) of locations of
    the other faces are created.  Non-unique Die are removed by adding to a
    set.  The set contains only the 30 unique dice.
  '''

  theDice = set()
  
  #all possibilities with '1' in single position (this results in many
  #  non-unique dice)
  possibles = it.permutations('23456', 5)
  
  for n in possibles:
    m = list(n)
    m.append('1')         #Add side '1' to the bottom position
    theDice.add(Die(m))   #Add to a set so we keep only unique dice

  return theDice

  
def createSetDirectly():
  '''
    Create a set of all the possibilities for Die objects.  Each Die starts
    with the top being a '1'.  The remaining numbers '2' - '6' are set to the
    bottom. To each of those a left panel is arbitrarily set to one of the
    remaining sides and the front, back and right sides are set to all
    possible remaining permutations.  There are 30 possibilities (5 * 3!).

    This approach doesn't require the Die equals, and hash functionality.
  '''
  
  theList = []                            #List of possible Die assignments
  sides = []                              #List of 5 sides of a Die
  theSides = ['2', '3', '4', '5', '6']    #Remaining sides after top is a '1'
  possible = []                           #Possible Die labeling
  thePossible = ['', '', '1', '', '', ''] #List used for reset with top a '1'
  
  for bottom in theSides:

    possible = copy.copy(thePossible)   #Reset with complete lists
    sides = copy.copy(theSides)         #  ditto
    
    possible[5] = bottom                #Bottom is each of the remaining sides
    sides.remove(bottom)                #Bottom is definded, remove
    
    possible[1] = sides[0]              #Arbitrarily set left side to
    sides = sides[1:]                   #  something and remove it

    #Now create all possibilities for remaining sides
    rest = it.permutations(sides, 3)
    for n in rest:
      possible[0] = n[0]                #Set the back side
      possible[3] = n[1]                # and the right side
      possible[4] = n[2]                # and the front
      theList.append(Die(possible))     #Create a Die with this pattern
      
  return set(theList)


def trimSet(largeSet):
  '''
    Remove patterns from the set that have opposite sides sum to 7 (see
    rules:  Sums of opposite sides do not equal seven.  Remove the dice
    that have '3' opposite to '5' or '6').  Remove the ones where '1'
    would be touching another die.
  '''
  trimmedSet = set()
  for element in largeSet:
    #Rule: Based on text, opposite sides can not sum to 7
    flag = True
    if int(element.front) + int(element.back) == 7:
      flag = False
    if int(element.left) + int(element.right) == 7:
      flag = False
    if int(element.top) + int(element.bottom) == 7:
      flag = False

    #Rule: Based on picture, 3 can not be opposite 5 or 6
    element.onTop('3')
    if element.bottom == '5' or element.bottom == '6':
      flag = False

    #Rule: Based on picture, 1 is shown on center die, hence 1 can not
    # be touching either other die
    element.onTop('6', '3')     #Orientation of left die
    if element.right == '1':
      flag = False

    element.onTop('3', '5')
    element.spinLeft()          #Orientation of front die
    if element.back == '1':
      flag = False
      
    element.onTop('1')          #Reorient with 1 on top
    if flag:
      trimmedSet.add(element)

  return trimmedSet


def checkForSolution(die1, die2, die3):
  '''
    Test this Die pattern match the 3 positions shown in the
    problem.  Also test that opposite sides don't equal 7.
  '''
  flag = True

  #Test that the orientation of the sides match the problem
  if die1.top != '6':
    flag = False
  if die1.front != '3':
    flag = False
  if die2.top != '1':
    flag = False
  if die3.top  != '3':
    flag = False
  if die3.left != '5':
    flag = False
  if die1.right != die2.left:
    flag = False
  if die2.front != die3.back:
    flag = False

  return flag

def checkForSolution2(dieCenter):


  dieLeft = copy.copy(dieCenter)
  dieForward = copy.copy(dieCenter)
  
  dieLeft.onTop('6', '3')
  dieCenter.onTop('1')
  dieForward.onTop('3', '5')
  dieForward.spinLeft()

  flag = False
  for n in range(3):
    if dieLeft.right == dieCenter.left and \
       dieForward.back == dieCenter.front:
      flag = True
      break
    else:
      dieCenter.spinLeft()
  return flag
      
  

####Main####
if __name__ == '__main__':

  #Two approaches to make the set of possible die configurations
  # createSet() - Creates all possible configurations with one side
  #   defined.  Each configuration is added to a set so that dice with
  #   equivalent configurations will not be duplicated
  # createSetDirectly() - Creates only unique configurations directly

  #Both create the same set of all 30 possible dice (without restricting
  # based on conditions of the puzzle

  #largeSet = createSetDirectly()
  largeSet = createSet()

  #The following removes all the possibilities that would violate the
  # conditions of the puzzle
  mySet = trimSet(largeSet)
  
  print("Size of set of all possible configurations: " + str(len(largeSet)))
  print("Size of set after removing rules violators: " + str(len(mySet)))

  dice = mySet

  print("\n\nSolution:")
  for die in dice:
    if checkForSolution2(die):
      die.displayThree()

####The following was the first approach to solve this
#### It brute forced through all 30 possibilities
#### the above is more efficient
##    #create 3 Dice with the same pattern
##    die1 = copy.copy(die)
##    die2 = copy.copy(die)
##    die3 = copy.copy(die)
##
##    #Place die1 with each of the 6 numbers on top
##    # and spin it for each of the 4 possible positions (total of 24
##    # possibilities)
##    for top1 in ['1', '2', '3', '4', '5', '6']:
##      die1.onTop(top1)
##      for spin in range(4):
##        die1.spinLeft()
##
##        #Ditto for die2
##        for top2 in ['1', '2', '3', '4', '5', '6']:
##          die2.onTop(top2)
##          for spin in range(4):
##            die2.spinLeft()
##
##            #Ditto again for die3 and test for a solution
##            for top3 in ['1', '2', '3', '4', '5', '6']:
##              die3.onTop(top3)
##              for spin in range(4):
##                die3.spinLeft()
##                
##                if checkForSolution(die1, die2, die3):
##                  die1.displayThree()
##                    
##      
##