Monday, July 23, 2012

Tkinter 15 Puzzle Demo (Placer Geometry Manager)

This code is based on the Tcl puzzle.tcl demo which creates a square with 15 numbered buttons and an empty space which allows the numbers to be moved around one at a time; the goal being to get all the button tiles into proper numerical order.


It's actually a pretty good demonstration of the Placer Geometry Manager which allows widgets to be precisely placed relative to the top, left corner of their containing, or ancestor, frame.

# File: puzzle.py
# References:
#    'Python and Tkinter Programming' by John Grayson, p90
#    http://www.tcl.tk/man/tcl8.5/TkCmd/place.htm
#    http://www.tcl.tk/man/tcl8.5/TkCmd/colors.htm

from tkinter import *
from tkinter import ttk
from demopanels import MsgPanel, SeeDismissPanel

class PuzzleDemo(Frame):
    
    def __init__(self, isapp=True, name='puzzledemo'):
        Frame.__init__(self, name=name)
        self.pack(expand=Y, fill=BOTH)
        self.master.title('15 Puzzle Demo')
        self.isapp = isapp
        self._create_widgets()
        
    def _create_widgets(self):
        if self.isapp:
            MsgPanel(self, ["A 15-puzzle appears below as a collection of buttons.  ",
                            "Click on any of the pieces next to the space and that ",
                            "piece will slide over the space.\n\n",
                            "Continue this until the pieces are arranged in numerical ",
                            "order from upper-left to lower-right."])
            
            SeeDismissPanel(self)
        
        self._create_demo_panel()
        
    def _create_demo_panel(self):
        bgColor = 'gray80'  # colour for panel background and empty space
        
        # if width and height are not specifically set buttons are positioned
        # in a 0 size window and do not show up
        demoPanel = Frame(self, borderwidth=2, relief=SUNKEN, background=bgColor,
                          width=120, height=120)
        demoPanel.pack(side=TOP, pady=1, padx=1)
        
        # buttons are placed relative to the top, left corner of demoPanel
        # with relations expressed as a value between 0.0 and 1.0
        # top, left corner = (x,y) = (0,0)
        # bottom, right corner = (x,y) = (1,1)
        self.xypos = {}
        self.xypos['space'] = (.75, .75)
        order = [3, 1, 6, 2, 5, 7, 15, 13, 4, 11, 8, 9, 14, 10, 12]
        
        for i in range(15):
            num = order[i]
            self.xypos[num] = ( i%4 * .25, i//4 * .25)
            b = ttk.Button(text=num, style='Puzzle.TButton')
            b['command'] =lambda b=b: self._puzzle_switch(b)
            b.place(in_=demoPanel, relx=self.xypos[num][0], rely=self.xypos[num][1],
                    relwidth=.25, relheight=.25)
        
        # set button background to demoPanel background
        ttk.Style().configure('Puzzle.TButton', background=bgColor)
        
    def _puzzle_switch(self, button):
        num = button['text']
        sx = self.xypos['space'][0]     # position of 'space'
        sy = self.xypos['space'][1]
        x = self.xypos[num][0]          # position of selected button
        y = self.xypos[num][1]
        
        # is the selected button next to the space?
        if(    sy-.01 <= y <= sy+.01 and sx-.26 <= x <= sx+.26
            or sx-.01 <= x <= sx+.01 and sy-.26 <= y <= sy+.26):
            
            # swap button with space
            self.xypos['space'], self.xypos[num] = self.xypos[num], self.xypos['space']
            
            # re-position button
            button.place(relx=self.xypos[num][0], rely=self.xypos[num][1])
        
if __name__ == '__main__':
    PuzzleDemo().mainloop()