'''
November 2016 Puzzle

Below are two arrangements of six numbered disks.  Your goal
is to convert the triangular arrangement to the linear arrangement
in exactly eight moves.  Each move consists of moving a disk
to a position where it is adjacent to two other disks.  As
you can see, initially the 1, 4, and 6 are each adjacent to two
other disks, while the 2, 3, and 5 are each adjacent to four
other disks.


                     1
                   2   3    --->   1  2  3  4  5  6
                4    5    6
'''

from __future__ import print_function

#Required: graphics.py module, written by John Zelle
#   http://mcsp.wartburg.edu/zelle/python/graphics.py
try:
  import graphics as g
except:
  print('This program depends on graphics.py by John Zelle at\n' \
        + 'http://mcsp.wartburg.edu/zelle/python/graphics.py\n'\
        + 'Please load and try again.')
  exit()

class LabeledCircle:
  '''
  A LabeledCircle is a graphics.py circle with a text label to uniquely
  identify it.  It location is defined by (x, y) which refers to the grid
  whose x and y ar multiples of the size of the circle.  Grid locations
  are converted to pixel values in a method below.
  '''
  
  def __init__(self, x, y, label):
    '''
    Create the LabeledCircle which is a graphics.py Circle with a Text
    label identifier.  The coodinates are its position on a grid where
    the cell sizes are the size of each circle with the circle centered
    in the cell.
    '''
    self.x = x                          #Grid x value of circle
    self.y = y                          #Grid y value of circle

    (posX, posY)= self.loc2pixel(x, y)  #Get pixel values for x and y

    #Create the object
    self.circle = g.Circle(g.Point(posX, posY), circleSize)
    self.label = g.Text(g.Point(posX, posY),label)


  def draw(self, window):
    '''
    Draw the object (the circle and its label) onto the window
    '''
    self.circle.draw(window)
    self.label.draw(window)


  def move(self, x, y):
    '''
    Move the object to (x, y).  Calculate the original pixel location and
    the new position's pixel location from the grid locations.  Calcuate
    the pixel displacement.  Then move the circle and its label.
    '''
    (posX, posY) = self.loc2pixel(self.x, self.y) #Current pixel position
    (newPosX, newPosY) = self.loc2pixel(x,y)      #New pixel position
    xDist = newPosX - posX                        #Calculate the displacement
    yDist = newPosY - posY
    self.circle.move(xDist, yDist)                #Do the move
    self.label.move(xDist, yDist)
    self.x = x                                    #Update the new grid position
    self.y = y

    
  def loc2pixel(self, x, y):
    '''
    Convert the grid location to a pixel location on the window
    '''
    pixelX = x * circleSize + 15
    pixelY = windowSizeY - (y * circleSize) - 15
    return (pixelX, pixelY)



######Main##########################
circleSize = 10
windowSizeX = 300
windowSizeY = 100


def doMove(cir):
  '''
  Display the move of the LabeledCircle object cir by
  calling the LabeledCircle's move method and displaying
  a text string describing which move.
  '''
  global counter, move
  (circle, x, y) = cir
  circles[circle-1].move(x, y)  # and move it
  
  #Display which move just took place
  counter.undraw()              #Remove previous text
  counter = g.Text(g.Point(30,20),"Move " + str(move))
  counter.draw(win)


if __name__ == "__main__":
  win = g.GraphWin("November 2016 Puzzler", windowSizeX, windowSizeY)

  #Initial position of the circles
  # Circle positions are a grid with each grid element the size of the
  # circles.  For example, the final position of circle 1 is (0, 0).
  # The initial position of 1 is (8, 4).  The initial position of
  # circle 4 is (6, 0).
  circles = []                              #List containing the LabeledCircles
  circles.append(LabeledCircle(8, 4, "1"))  #Circle 1 is list element 0
  circles.append(LabeledCircle(7, 2, "2"))  #Circle 2 is ... 1, etc.
  circles.append(LabeledCircle(9, 2, "3"))
  circles.append(LabeledCircle(6, 0, "4"))
  circles.append(LabeledCircle(8, 0, "5"))
  circles.append(LabeledCircle(10, 0, "6"))

  #Display the forward/backward/exit option 'buttons'
  g.Circle(g.Point(200, 40), 20).draw(win)                #Go back 'button'
  g.Text(g.Point(200, 40), "<").draw(win)
  g.Circle(g.Point(260, 40), 20).draw(win)                #Go forward 'button'
  g.Text(g.Point(260, 40), ">").draw(win)
  g.Rectangle(g.Point(180,70), g.Point(280,95)).draw(win) #Exit 'button'
  g.Text(g.Point(230,83),"Exit").draw(win)

  #The eight moves forward and in reverse.  Each move is a tuple as follows:
  # (circleToMove, xDestinationPosition, yDestinationPosition)
  moves = ((0,0,0),(5,5,2),(3,4,0),(1,3,2),(2,2,0),(3,1,2),(1,0,0),(3,4,0),(5,8,0))
  backmoves = ((5,8,0),(3,9,2),(1,8,4),(2,7,2),(3,4,0),(1,3,2),(3,1,2),(5,5,2))
  
  #Draw the initial position of the puzzle
  for n in range(6):
    circles[n].draw(win)

  #Draw the instructions
  counter = g.Text(g.Point(130,10),"Click mouse on < or > to move back or forward")
  counter.setSize(8)
  counter.draw(win)


  #Check mouse click for forward (>), backward (<) move
  #  or Exit
  move = 0
  while True:
    click = win.getMouse()        #Get position of mouse click
    clickX = click.getX()         # check for forward or back
    clickY = click.getY()         # check for exit

    #Exit was clicked?
    if clickY > 70:
      break                       #Leave this loop and quit
    
    #Forward was clicked?
    elif clickX > 230:
      if move < 8:                #Only 8 moves, ignore past 8
        move += 1
        doMove(moves[move])       #Display this move
        
    #Backward was clicked?       
    else:
      if move != 0:               #Ignore moves before the beginning
        move -= 1
        doMove(backmoves[move])   #Move circle back

  #Close the window and quit
  win.close()