Honeycombs/Python
< Honeycombs(Redirected from HoneyCombPython)
<lang Python>
- !/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
- ha ha, Turns out I always wanted to invoke main during tests.
- (module name is not __main__ when invoked from doctest)
if '__main__' == __name__:
main()
else:
main()
- picture facilitates the "comb" hexagon positioning logic
- ........
- .
- .
- x .........
- .
- .
- ........ x
</lang> --LambertDW 17:19, 27 March 2012 (UTC)