Honeycombs/Python

From Rosetta Code
#!/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

--LambertDW 17:19, 27 March 2012 (UTC)