Honeycombs/Python

From Rosetta Code
(Redirected from HoneyCombPython)

<lang Python>

  1. !/some/path/python3

   This program, not a translation from tcl, includes doctests.  Suggested program use:
   $ python3 -m doctest hexagons.py
   Rotations support comes in name only.  A full blown 3D system
   with homogeneous coordinates seemed overboard.

import time import math import pprint import random import string import tkinter import numbers

Tau = 2*math.pi # pi is wrong. http://www.youtube.com/watch?v=jG7vhMMXagQ

cdr = lambda a: a[1:] car = lambda a: a[0]

def flatten(a):

   
       provide lisp-style nested list flattening
       >>> flatten((((2,4),),[1,2,3],[[5,2,],88]))
       [2, 4, 1, 2, 3, 5, 2, 88]
   
   try:
       if not len(a):
           return []
   except:
       return [a]
   else:
       return flatten(car(a))+flatten(cdr(a))

class Base:

   
       Base provides default __init__ and __repr__ methods
       for simple classes.  The subclass includes a set of
       mandatory_arguments .  See examples.
       The logic---yes, I have a reason---for using Base:
       It has been said that using attribute names is
       "good style" making for "clearly written code" and so forth.
       The Base constructor REQUIRES keyword arguments when used.
       You can get around this using a factory function to wrap
       object construction.  (Because it is, in my opinion, also
       ridiculous to write the keywords over and over when you
       have many of the same objects/function to create/call.)
       See example in doctest:
       >>> Base(a=3)   # support doctest with command $ python3 -m doctest -v this.file
       Base(**{'a': 3})
       >>> f = lambda a: Base(a=a)
       >>> f(1)
       Base(**{'a': 1})
       >>> f('alpha')
       Base(**{'a': 'alpha'})
   
   class BaseClassException(Exception):
       pass
   mandatory_arguments = set()
   def __init__(self,**kwargs):
       mandatory_arguments = self.__class__.mandatory_arguments
       if not mandatory_arguments.issubset(kwargs):
           raise Base.BaseClassException(
               self.__class__.__name__ +
               ' requires keyword arguments ' +
               str(mandatory_arguments))
       self.kwargs = kwargs
       for kv in kwargs.items():
           setattr(self,*kv)
   def __repr__(self):
       return self.__class__.__name__+'(**'+pprint.pformat(self.kwargs)+')'

class Ngon(Base):

   
       >>> square = Ngon(n = 4, center = (0,0), radius = 1, rotation = 0,)
       >>> square.center
       (0, 0)
   
   mandatory_arguments = set('n center radius rotation'.split())
   def __init__(self,**args):
       self._coordinates = False
       super().__init__(**args)
   def __call__(self):
       if not self._coordinates:
           xi = self.rotation
           step = Tau/self.n
           self._coordinates = []
           (x0,y0,) = self.center
           r = self.radius
           for i in range(self.n):
               self._coordinates.append((r*math.cos(xi)+x0,r*math.sin(xi)+y0,))
               xi += step
       return self._coordinates
   @property
   def flat(self):
       return flatten(self())
   @property
   def inner_radius(self):
       return self.radius*math.sqrt(1**2-(1/2)**2)

class Hexagon(Ngon):

   
       >>> h = Hexagon(center = (1,0), radius = 1, rotation = 0,)
       >>> h.center
       (1, 0)
       >>> h.flat[2]
       1.5
   
   mandatory_arguments = set('center radius rotation'.split())
   def __init__(self,**args):
       args['n'] = 6
       super().__init__(**args)

class Vector(Base):

   
       Of course I could have used numpy.  scipy is unavailable
       from the standard library, so I wrote Vector.
       >>> P = Vector(P=(1,2,))
       >>> len(P)
       2
       >>> P[0]
       1
       >>> (P+P)[1]
       4
       >>> len(P+P)
       2
       >>> (P*3)[0]
       3
       >>> (P-P)[1]
       0
       >>> P.dot((2,3,))
       8
   
   mandatory_arguments = set('P')
   def __len__(self):
       return len(self.P)
   def __getitem__(self,ITEM):
       return self.P[ITEM]
   def __add__(a,b): # I find (self, other) silly for dyadic operator methods
       if a.__class__ != b.__class__:
           raise ValueError('Adding Vectors works.  You did something else.')
       if len(a) != len(b):
           raise ValueError('Incommensurate dimensionality')
       return Vector(P=tuple(a[i]+b[i] for i in range(len(a))))
   def __neg__(self):
       return self*(-1)
   def __sub__(a,b):
       return a+(-b)
   def __mul__(a,b):
       if not isinstance(b,numbers.Number):
           raise ValueError('not a dot or cross product, honey.  Scalars only')
       return Vector(P=tuple(p*b for p in a))
   def dot(a,b=None):
       b = b or a
       try:
           if (len(a) == len(b)) and isinstance(b[0],numbers.Number):
               return sum(A*B for (A,B,) in zip(a,b,))
       except:
           pass
       raise ValueError('Incommensurate lengths or types')

class EnhancedCanvas(tkinter.Canvas):

   def create_loop(self,*args,**kwargs):
       
           draw a poly-line including a connection between the first and last points.
       
       LOOP = tuple(args) + (args[0],args[1],)
       self.create_line(*LOOP,**kwargs)

class HoneyComb:

   def __init__(self,s,radius=20,rotation=0):
       self.comb(radius,rotation)
       tk = tkinter.Tk()
       tk.geometry('300x320')
       canvas = EnhancedCanvas(tk)
       canvas.bind('<Button>',self.button) # on mouse button, call the HoneyComb button method
       canvas.bind('<Key>',self.key) # on key event, call the HoneyComb key method
       canvas.pack(expand=True,fill=tkinter.BOTH,)
       canvas.focus_set()              # window must have focus to capture keys!
       self.SELECTED = [False,]*len(self.HEXAGONS)
       self.SELECTIONS = []
       self.canvas = canvas
       self.texts = s[:len(self.HEXAGONS)] # :-(  The set of letters in CUB SCOUTS might not be available.
       self.tk = tk
       self.paint()
       tk.mainloop()
   def paint(self):
       canvas = self.canvas
       s = self.texts
       for (I,H,) in enumerate(self.HEXAGONS):
           # use subtle color change.  Can you spell CUB SCOUT ?
           canvas.create_polygon(*H.flat,fill=('yellow','gold')[self.SELECTED[I]])
           canvas.create_text(*H.center,text=s[I],fill='blue')
       for H in self.HEXAGONS:
           canvas.create_loop(*H.flat,width=3)
       self.tk.update_idletasks()
   def repaint(self,I):
       SELECTED = self.SELECTED
       SELECTIONS = self.SELECTIONS
       SELECTED[I] = True
       SELECTIONS.append(I)
       self.paint()
       #code to display self.texts[I] fits here
       # could either pack a text box into tk and use that,
       # or erase previous character and post new char with create_text
       # using xor mode or color change directly onto the canvas.
       if all(SELECTED):               # finished
           print('sleep 2 seconds--->then gone')
           time.sleep(2)
           self.tk.destroy()
   def __call__(self):
       return ' '.join(self.texts[J] for J in self.SELECTIONS)
   def key(self,EVENT,):
        key board activity trap comes to this function 
       s = self.texts
       try:
           I = s.index(EVENT.char.upper())
       except ValueError:
           pass
       else:
           self.repaint(I)
   def button(self,EVENT,):
        mouse button activity trap comes to this function 
       # I don't recall how to use or if possible "nearest" or tagging create_... figures
       A = Vector(P=(EVENT.x,EVENT.y))
       (BEST, SHORTEST,) = (0, 9e44,)
       # could search the set of yet unchosen HEXAGONS
       # However, CUB SCOUTS has repeat letters.
       for (I,H,) in enumerate(self.HEXAGONS):
           # use dot product to stand in for the length of the vector
           # between the mouse event and the hexagon centers.
           L = (A-Vector(P=H.center)).dot()
           if L < SHORTEST:
               (BEST, SHORTEST,) = (I, L,)
       self.repaint(BEST)
   def comb(self,radius=20,rotation=0):
       C = Vector(P=(radius*1.25,radius*1.25,))
       H = Hexagon(center = C, radius = radius, rotation = rotation,)
       IR = H.inner_radius
       OFFSET = Vector(P=(0,IR*2,))
       HEXAGONS = [H,]
       for i in range(3):
           C += OFFSET
           HEXAGONS.append(Hexagon(center = C, radius = radius, rotation = rotation))
       OFFSET = Vector(P=((IR*2)*math.cos(Tau/(3*4)),(IR*2)*math.sin(Tau/(3*4))))
       for i in range(4):
           C = Vector(P=HEXAGONS[i].center)
           HEXAGONS.append(Hexagon(center = C+OFFSET, radius = radius, rotation = rotation))
       OFFSET = Vector(P=(OFFSET[0]*2,0))
       for i in range(8):
           C = Vector(P=HEXAGONS[i].center)
           HEXAGONS.append(Hexagon(center = C+OFFSET, radius = radius, rotation = rotation))
       for i in range(8,12):
           C = Vector(P=HEXAGONS[i].center)
           HEXAGONS.append(Hexagon(center = C+OFFSET, radius = radius, rotation = rotation))
       self.HEXAGONS = HEXAGONS

def main():

   UC = list(string.ascii_uppercase)
   random.shuffle(UC)
   HC = HoneyComb(s=UC,rotation=0 and not 0.52)
   print(HC())             # HC object retains selection order record
  1. ha ha, Turns out I always wanted to invoke main during tests.
  2. (module name is not __main__ when invoked from doctest)

if '__main__' == __name__:

   main()

else:

   main()
  1. picture facilitates the "comb" hexagon positioning logic
  2. ........
  3. .
  4. .
  5. x .........
  6. .
  7. .
  8. ........ x

</lang> --LambertDW 17:19, 27 March 2012 (UTC)