import pygame
import math
 
COLORS = [ 'crimson', 'forestgreen', 'yellow', 'royalblue',
           'saddlebrown', 'hotpink', 'darkorange', 'darkmagenta' ]
NUM_OF_DISKS = len(COLORS)
BASE_LENGTH = 200
C_WIDTH  = 3.732 * BASE_LENGTH
C_HEIGHT = 3.500 * BASE_LENGTH
DISK_R = 0.9 * BASE_LENGTH
POLE_R = 15
POSITIONS = { 'Source'      : [0.268, 0.714],
              'Auxiliary'   : [0.500, 0.286],
              'Destination' : [0.732, 0.714] }
FLASHING_COUNTER = 20
STEPS = 30
 
class Vector:
  def __init__(self, x, y):
    self.x = x
    self.y = y
class Position(Vector):
  def __init__(self, x, y):
    super().__init__(x, y)
  def move(self, vec):
    self.x += vec.x
    self.y += vec.y
class Disk:
  def __init__(self, level):
    self.level = level
    self.color = COLORS[level]
    self.r = (DISK_R-POLE_R)*(NUM_OF_DISKS-level)/NUM_OF_DISKS + POLE_R
class MovingDisk(Disk):
  def __init__(self, level, frm, to):
    super().__init__(level) 
    [sx, sy] = [frm.pos.x, frm.pos.y]
    [dx, dy] = [to.pos.x,  to.pos.y]
    self.pos = Position(sx,sy)
    self.mvec = Vector((dx-sx)/STEPS,(dy-sy)/STEPS)
    self.move_ctr = 0
    self.frm = frm
    self.to = to
  def step_forward(self):
    self.pos.move(self.mvec)
    self.move_ctr += 1
  def finish_p(self):
    ret_flag = (self.move_ctr == STEPS)
    if ret_flag:
      self.to.disks.append(Disk(self.level))
    return ret_flag
class Tower:
  def __init__(self, name, disks, direction=None):
    self.name = name
    self.disks = []
    for i in range(disks):
      self.disks.append(Disk(i))
    self.direction = direction
    self.moving = False
    self.flash_ctr = 0
  def toplevel(self):
    l = len(self.disks)
    # '-1' means there is no disk.
    return self.disks[l-1].level if l > 0 else -1
def setup():
  pygame.init()
  screen = pygame.display.set_mode((C_WIDTH, C_HEIGHT))
  pygame.display.set_caption('GDHP')
  for t in [src,aux,dst]:
    [rx, ry] = POSITIONS[t.name]
    t.pos = Position(rx * C_WIDTH, ry * C_HEIGHT)
  return screen
def base_drawing():
  screen.fill('beige')
  for t in [src,aux,dst]:
    # draw disks
    for d in t.disks:
      pygame.draw.circle(screen, d.color, (t.pos.x,t.pos.y), d.r)
      pygame.draw.circle(screen, 'black', (t.pos.x,t.pos.y), d.r, 1)
    # draw a pole
    fillcolor = 'gold' \
      if t.moving and t.flash_ctr < FLASHING_COUNTER/2 else 'white' 
    pygame.draw.circle(screen, fillcolor, (t.pos.x,t.pos.y), POLE_R)
    pygame.draw.circle(screen, 'brown', (t.pos.x,t.pos.y), POLE_R, 1)
    # draw a direction
    [sx, sy] = [t.pos.x, t.pos.y]
    [dx, dy] = [t.direction.pos.x, t.direction.pos.y]
    r = POLE_R / math.sqrt((dx-sx)*(dx-sx)+(dy-sy)*(dy-sy))
    [dx, dy] = [(dx-sx)*r+sx, (dy-sy)*r+sy]
    pygame.draw.line(screen, (0,0,128), (sx,sy), (dx,dy), 3)
def flash_poles():
  for t in [src,aux,dst]:
    t.moving = (t.direction.direction == t)
    t.flash_ctr += 1
    t.flash_ctr %= FLASHING_COUNTER
def pop_disk(src,aux,dst):
  towers = list(filter(lambda x: x.moving, [src,aux,dst]))
  idx = 0 if towers[0].toplevel() > towers[1].toplevel() else 1
  [frm, to] = [towers[idx], towers[1-idx]]
  return MovingDisk(frm.disks.pop().level,frm,to) \
           if len(frm.disks) > 0 else None
def draw_moving_disk():
  d = moving_disk
  d.step_forward()
  pygame.draw.circle(screen, d.color, (d.pos.x,d.pos.y), d.r)
  pygame.draw.circle(screen, 'black', (d.pos.x,d.pos.y), d.r, 1)
  return d.finish_p()
def turn():
  for t in [moving_disk.frm,moving_disk.to]:
    t.direction = list(filter(
      lambda x: (x != t) and (x != t.direction), [src,aux,dst]))[0]
    t.moving = False
def draw():
  # base drawing
  base_drawing()
  # find two exchange-towers out of three
  flash_poles()
  # start moving
  finish_p = False
  mdisk = moving_disk
  if mdisk == None:
    mdisk = pop_disk(src,aux,dst)
    if mdisk == None:
      finish_p = True
  else:
    if draw_moving_disk():
      turn()
      mdisk = None
  return mdisk, finish_p
# main routine
if __name__ == '__main__':
  src = Tower('Source', NUM_OF_DISKS)
  aux = Tower('Auxiliary',   0, src)
  dst = Tower('Destination', 0, src)
  # In the case of NUM_OF_DISKS is odd, 
  # the src must face the src.
  # Otherwise, the src faces the aux.
  src.direction = dst if len(COLORS) % 2 == 1 else aux
  # the reference to moving disk is stored to this variable.
  moving_disk = None
  screen = setup()
  while True:
    for event in pygame.event.get():
      if event.type == pygame.TEXTINPUT and event.text == 'q':
        pygame.quit()
        exit()
    moving_disk, finish_p = draw()
    if finish_p:
      pygame.quit()
      exit()
    pygame.display.flip()
    pygame.time.delay(30)