'''
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()
##
##
##