A Letter from Guido (creator of Python) to you

Guido van Rossum has posted a letter to all you young programmers out there.

Read it here.

 

Comprehending Lists and Tuples

SUPERIMPOSED CAPTION: ‘SUBURBAN LOUNGE NEAR ESHER’
Elderly couple, Mr A and Mrs B are staring through french windows at a cat that is sitting in the middle of their lawn motionless and facing away from them. A car is heard drawing up.

The last couple of tutorials have been a bit heavy, so this week’s tutorials are going to balance that out by being a little light. We’re having a look at a couple of different aspects of the Python language.

List Comprehensions
The first is comprehensions:

>>> a_list = [1,2,3,4,5]
>>> b_list= [element*2 for element in a_list]
>>> b_list
[2, 4, 6, 8, 10]

In this example, we have automatically generated a new list b_list from an existing list* a_list by using a list comprehension. The structure of the comprehension is, I hope comprehensible. If not, it is a little hard to explain in steps. The comprehension above is equivalent to:

>>> b_list = []
>>> for element in a_list:
...    b_list.append(element*2)
... 
>>> b_list
[2, 4, 6, 8, 10]

We could call the variable element anything we liked – it is just another variable:

 
>>> b_list = [baloney*2 for baloney in a_list]
>>> b_list
[2, 4, 6, 8, 10]
>>> 

You can also add conditions. Let’s say you only wanted the even elements in a_list:

 
>>> b_list = [element for element in a_list if element%2 ==0] #ie remainder is 0 when dividing by two
>>> b_list
[2, 4]
>>> 

The condition here is if element%2 ==0, but you can substitute other conditions (as long as they resolve to either True or False). You can have a comprehension which relies on multiple variables (this example is from the Python documentation):

 
>>> [(x, y) for x in [1,2,3] for y in [3,1,4] if x != y]
[(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)]

Unwinding this is a little tricky – for each element in [1,2,3] it runs through each element in [3,1,4] (ie 9 comparisons in all) and appends the tuple if the elements are not equal.

List comprehensions provide an easy shorthand for constructing lists. Moreover, they are often more readable than writing out the for loops explicitly.

Tuples
The second topic is tuples.
The other thing we’re going to look at is tuples (pronounced, variously, “tupp-lls”, “two-pls” and, to some people, “tyou-pls” – my preference is tupp-lls, rhymes with couples). Tuples are a little like lists, except that they are immutable. That is, once they are made they cannot be changed. Tuples are made by putting the elements in round braces [actually, adding commas between them and, sometimes, also adding braces]:

 
>>> my_tuple = (1,2,3)
>>> my_tuple
(1, 2, 3)
>>> my_list = [1,2,3]
>>> my_list
[1, 2, 3]

Elements of the tuple are referenced in the same way as you’d reference a list but, unlike lists, these elements cannot be changed (tuples are “immutable”):

 
>>>my_tuple[0]
1               
>>>my_tuple[0]=2
Traceback (most recent call last):                                         
  File "<stdin>", line 1, in <module>                                      
TypeError: 'tuple' object does not support item assignment    

The TypeError is telling us that you can’t change the elements of a tuple. To make a tuple with a single element is a little difficult because the interpreter could just interpret the parentheses as determining order of operation (think (1+2)*3). Instead, we put a comma after the single element to indicate that we are creating a tuple:

 
>>> my_tuple = (1)  # no comma, so Python just thinks it's a number
>>> my_tuple  
1             
>>> my_tuple[0]  # just a number, so has no elements
Traceback (most recent call last):                                                                           
  File "<stdin>", line 1, in <module> 
TypeError: 'int' object has no attribute '__getitem__'
>>> my_tuple= (1,)
>>> my_tuple
(1,)                                                                                                                 
>>>my_tuple[0]                                                                                                       
1              

So, if a tuple does what a list can do, only less, why bother with a tuple? Why not use a list? Well, tuples are easier for the Python interpreter to deal with and therefore might end up being faster. Tuples might also indicate that you have an ordered list where the ordering has some meaning (for example, a date tuple might store year, month and day (in that order) in tuple). The fact that you’re using a tuple then flags that each of the entries has a distinct meaning or use and that their order is significant. Another pragmatic reason to use a tuple is when you have data which you know shouldn’t change (like a constant). So, if you accidentally try to change one of the tuple’s entries, you will have an error thrown.

Finally, since tuples are immutable they can be used as keys in dictionaries, unlike lists:

 
>>> my_dict={}
>>> my_tuple=("A Name",23,"A location")
>>> my_list= list(my_tuple)
>>> my_dict[my_tuple]="Client 1"
>>> my_dict[my_list]="Client 2"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'

Notes:
* as per Don’s comment, you can use any iterator, but we haven’t talked about them yet, so they don’t yet exist.

Yet Another View of Chess

One of the women goes over to the set and switches it over. As she goes back to her seat from the radio we hear the theme music again, fading out as the sounds of [comic] violence and screaming start again and continue unabated in vigour.
Man’s Voice: I think she’s dead.
Woman’s Voice: No I’m not!

In our last tutorial we substituted a graphical representation of a chess board for a, rather more boring, textual one. This tutorial, we’re going to move our representation into three dimensions with an isometric view of the same Model. You are probably familiar with isometric views as they are often used in video games. Isometric representations attempt to give a three dimensional view of a subject, but without a perspective distortion (when there is a perspective parallel lines meet somewhere).

In the 2d representation of a chess board, you can think of the board on a table, with yourself leaning over it, viewing it directly from above. Here, both the columns and rows look the same size. However, if you think of yourself now sitting down your view of the board will change. The columns will appear shorter than the rows – and if you keep lowering your eyes until they are level with the table, each column disappears entirely behind the first row. With an isometric view we are simulating someone looking from, perhaps, a normal sitting height, but with the board turned. In order to do this we will shorten the length of the tiles and skew them. This is what our new tiles look like (err.. you can’t see the white one):

chess_isometric_white_tilechess_isometric_black_tilechess_isometric_black_tile_demo

 

 

These tiles measure 120×60 pixels (and are twice as wide as they are tall).  Also shown is the actual size of the black tile (they are rectangles with transparent triangles in the corners).  We have an additional problem with an isometric board in that before we used symbolic representations of the pieces, but for a 3d board, we will use a visual representation.  Compare an old pawn with a new pawn:

Chess_P45chess_isometric_white_pawn

 

This, in turn, will mean that our pieces are of different sizes.  In fact, we have lots of problems.  Simply substituting these new images into the previous view code doesn’t look pretty:

chess_mvc3_B

Some things to note include:

The board is totally wacky.  For each row, each successive tile needs to be lowered by 30 pixels below the previous one.  For each column, the tiles need to be moved to the right by 60 pixels, and shifted down a bit.  The chess pieces are aligned incorrectly – their tops match, but their bases do not (this is because we used anchor = NW in the previous example).  The pieces also don’t line up with the centre[sic] of their tiles.  The pieces themselves also take up more than one tile on the board.  So, we need to allow more space in the canvas for the board to fit into and we also need to change the way tiles are laid out on it, as well as sorting out how the pieces match the tiles.

The newly laid out board looks like this:

chess_mvc3_C

The numbers printed on the board (eg 1:420,0) show: The order in which the tile is drawn, then the x location, then the y location.  You will notice that all of the arrows point to a position outside the tile itself.  That is because in all of the tiles there is a triangle of transparency in every corner.  The tiles really are positioned with their top left corner at the arrow’s location.  Moreover, the tiles at the bottom (the row apparently closer to us, used for the white pieces) were drawn last.   Those at the top were drawn first.

The next problem that we have to deal with is the fact that the pieces are of differing sizes.  This, of itself, means that we can’t place them on the canvas by reference to their top left corners (this would mean that their tops would be aligned, but their bases would be out of alignment).  Instead we have to place them by reference to their base.  Each of the images for the pieces has been specially designed so that there is a transparent area (of 21 pixels – you get this number by trial and error or mathematically*) from the bottom of the gif.  The images are also 120 pixels wide (ie the width of the isometric tiles), with the piece centered in that space.  The tallest piece is the king, weighing in at 150 pixels (including the transparent area).  However, the base of the king aligns with the base of the tiles, so there is an overlap of 60 pixels.  This means that there needs to be another 90 pixels of headroom above the board to accommodate the height of the king.  This is calculated automatically in the view.  In the code, we now calculate the location of the bottom lower left hand corner and use it to to draw both the tiles and the pieces.

We now need to connect the view to the model – that is, to convert mouse clicks to board coordinates and to instructions to the Model.  However, unlike in our earlier view, there are places on the screen which are not part of the board at all.  Also, finding the row and column requires us to count diagonally in both axes.  This turns out to be a little complex so I’m glossing over it here.  If you’re interested the equations are in the controller’s source code.**

Our new board looks like this:

chess_mvc3_E

I have commented out the  Model entirely, importing it from the previous tutorial’s code.  You will need either need to change last tute’s file to be mvc2.py or the import line to refer to the file you saved it as (or uncomment the Model code).

Code:

#/usr/bin/python
'''
Representing a chess set in Python
Part 3 (Isometric tiles)
Brendan Scott
4 May 2013

Dark square on a1
Requires there to be a directory called
chess_data in the current directory, and for that
data directory to have a copy of all the images

'''

import Tkinter as tk
from Tkinter import PhotoImage
import os.path
import os

from mvc2 import Model,  BoardLocation,  View
# Use the Model class from the previous tutorial.
# rename mvc2 to whatever name you gave the script from that tutorial
# if you can't find the previous tutorial just uncomment the definition below.

column_reference = "a b c d e f g h".split(" ")
EMPTY_SQUARE = " "

TILE_WIDTH = 60
'''We have used a tile width of 60 because the images we are used are 60x60 pixels
The original svg files were obtained from

http://commons.wikimedia.org/wiki/Category:SVG_chess_pieces/Standard_transparent

after downloading they were batch converted to png, then gif files.  Bash one liners
(Unix) to do this:
for i in $(ls *.svg); do inkscape -e ${i%.svg}.png -w 60 -h 60 $i ; done
for i in $(ls *.png); do convert $i  ${i%.png}.gif ; done
white and black tiles were created in inkscape

Isometric tiles were created in inkscape
Isometric pieces were created with povray using ChessSets Version 1.2
by James Garner ( jkgarner@charter.net )
then post processed in GIMP.

'''

ISOMETRIC_TILE_WIDTH = 120
ISOMETRIC_TILE_HEIGHT = 60
# to display these tiles many locations in the code rely on integer division by 2,
# so this width and height should both be even numbers (otherwise rounding errors will accumulate)

BOARD_WIDTH = 8*TILE_WIDTH
BOARD_HEIGHT = BOARD_WIDTH

ISOMETRIC_BOARD_WIDTH = 8 * ISOMETRIC_TILE_WIDTH
ISOMETRIC_BOARD_HEIGHT= 8*ISOMETRIC_TILE_HEIGHT

DATA_DIR = "chess_data"
ISOMETRIC_DATA_DIR = "chess_data"

ISOMETRIC_TILES = {"black_tile":"chess_isometric_black_tile.gif",
    "B":"chess_isometric_white_bishop1.gif",
    "b":"chess_isometric_black_bishop1.gif",
    "k":"chess_isometric_black_king1.gif",
    "K":"chess_isometric_white_king1.gif",
    "n":"chess_isometric_black_knight1.gif",
    "N":"chess_isometric_white_knight1.gif",
    "p":"chess_isometric_black_pawn1.gif",
    "P":"chess_isometric_white_pawn1.gif",
    "q":"chess_isometric_black_queen1.gif",
    "Q":"chess_isometric_white_queen1.gif",
    "r":"chess_isometric_black_rook1.gif",
    "R":"chess_isometric_white_rook1.gif",
    "white_tile":"chess_isometric_white_tile.gif"
    }

#class Model(object):
#    def __init__(self):
#        '''create a chess board with pieces positioned for a new game
#        row ordering is reversed from normal chess representations
#        but corresponds to a top left screen coordinate
#        '''
#
#        self.board = []
#        pawn_base = "P "*8
#        white_pieces =  "R N B Q K B N R"
#        white_pawns = pawn_base.strip()
#        black_pieces = white_pieces.lower()
#        black_pawns = white_pawns.lower()
#        self.board.append(black_pieces.split(" "))
#        self.board.append(black_pawns.split(" "))
#        for i in range(4):
#            self.board.append([EMPTY_SQUARE]*8)
#        self.board.append(white_pawns.split(" "))
#        self.board.append(white_pieces.split(" "))
#
#
#    def move(self, start,  destination):
#        ''' move a piece located at the start location to destination
#        (each an instance of BoardLocation)
#        Does not check whether the move is valid for the piece
#        '''
#        # error checking
#        for c in [start, destination]:  # check coordinates are valid
#            if c.i > 7 or c.j > 7 or c.i <0 or c.j <0:
#                return
#        if start.i == destination.i and start.j == destination.j: # don't move to same location
#            return
#
#        if self.board[start.i][start.j] == EMPTY_SQUARE:  #nothing to move
#            return
#
#        f = self.board[start.i][start.j]
#        self.board[destination.i][destination.j] = f
#        self.board[start.i][start.j] = EMPTY_SQUARE
#
#
#class BoardLocation(object):
#    def __init__(self, i, j):
#        self.i = i
#        self.j = j

class Isometric_View(tk.Frame):
    def __init__(self,  parent = None):
        tk.Frame.__init__(self, parent)
        self.parent = parent
        self.preload_images()
        self.canvas_height = 7*ISOMETRIC_TILE_HEIGHT+self.board_y_offset
        self.canvas = tk.Canvas(self, width=ISOMETRIC_BOARD_WIDTH, height=self.canvas_height)
        self.canvas.pack()
        self.parent.title("Python4Kids")
        self.pack()

    def preload_images(self):
        self.images = {}
        for image_file_name in ISOMETRIC_TILES:
            f = os.path.join(ISOMETRIC_DATA_DIR, ISOMETRIC_TILES[image_file_name])
            if not os.path.exists(f):
                print("Error: Cannot find image file: %s at %s - aborting"%(ISOMETRIC_TILES[image_file_name], f))
                exit(-1)
            self.images[image_file_name]= PhotoImage(file=f)
        tallest = 0
        for k, im in self.images.items():
            h = im.height()
            if h > tallest:
                tallest = h

        self.board_y_offset = tallest

    def clear_canvas(self):
        ''' delete everything from the canvas'''
        items = self.canvas.find_all()
        for i in items:
            self.canvas.delete(i)

    def draw_empty_board(self,  debug_board = False):
        ''' draw an empty board on the canvas
        if debug_board is set  show the coordinates of each of the tile corners'''

        for j in range(8): # rows, or y coordinates
            for i in range(8): # columns, or x coordinates
                x, y = self.get_tile_sw(i, j)
                drawing_order = j*8 + i
                tile_white = (j+i)%2
                if tile_white == 0:
                    tile = self.images['white_tile']
                else:
                    tile = self.images['black_tile']
                self.canvas.create_image(x, y, anchor = tk.SW,  image=tile)

                if debug_board:  # implicitly this means if debug_board == True.
                    ''' If we are drawing a debug board, draw an arrow showing top left
                    and its coordinates. '''
                    current_tile = drawing_order +1 # (start from 1)

                    text_pos =  (x+ISOMETRIC_TILE_WIDTH/2, y-ISOMETRIC_TILE_HEIGHT/2)
                    line_end = (x+ISOMETRIC_TILE_WIDTH/4,  y -ISOMETRIC_TILE_HEIGHT/4)
                    self.canvas.create_line((x, y), line_end,  arrow = tk.FIRST)
                    text_content = "(%s: %s,%s)"%(current_tile, x, y)
                    self.canvas.create_text(text_pos, text=text_content)

    def get_tile_sw(self, i,  j):
        ''' given a row and column location for a piece return the x,y coordinates of the bottom left hand corner of
        the tile '''

        y_start = (j*ISOMETRIC_TILE_HEIGHT/2)+self.board_y_offset
        x_start = (7-j)*ISOMETRIC_TILE_WIDTH/2
        x = x_start+(i*ISOMETRIC_TILE_WIDTH/2)
        y = y_start +(i*ISOMETRIC_TILE_HEIGHT/2)

        return (x, y)

    def draw_pieces(self, board):
        for j, row in enumerate(board):  # this is the rows = y axis
            # using enumerate we get an integer index
            # for each row which we can use to calculate y
            # because rows run down the screen, they correspond to the y axis
            # and the columns correspond to the x axis
            # isometric pieces need to be drawn by reference to a bottom corner of the tile,  We are using
            # SW  (ie bottom left).

            for i,  piece in enumerate(row): # columns = x axis
                if piece == EMPTY_SQUARE:
                    continue  # skip empty tiles
                tile = self.images[piece]
                x, y = self.get_tile_sw(i, j)
                self.canvas.create_image(x, y, anchor=tk.SW,  image = tile)

    def display(self, board,  debug_board= False):
        ''' draw an empty board then draw each of the
        pieces in the board over the top'''

        self.clear_canvas()
        self.draw_empty_board(debug_board=debug_board)
        if not debug_board:
            self.draw_pieces(board)

        # first draw the empty board
        # then draw the pieces
        # if the order was reversed, the board would be drawn over the pieces
        # so we couldn't see them

    def display_debug_board(self):
        self.clear_canvas()
        self.draw_empty_board()

class Controller(object):
    def __init__(self,  parent = None,  model = None):
        if model is None:
            self.m = Model()
        else:
            self.m = model
        self.v = Isometric_View(parent)
        ''' we have created both a model and a view within the controller
        the controller doesn't inherit from either model or view
        '''
        self.v.canvas.bind("<Button-1>",  self.handle_click_isometric)
        # this binds the handle_click method to the view's canvas for left button down

        self.clickList = []
        # I have kept clickList here, and not in the model, because it is a record of what is happening
        # in the view (ie click events) rather than something that the model deals with (eg moves).

    def run(self,  debug_mode = False):
        self.update_display(debug_board=debug_mode)
        tk.mainloop()

    def handle_click_isometric(self,  event):
        ''' Handle a click received.  The x,y location of the click on the canvas is at
        (event.x, event.y)
        First, we need to translate the event coordinates (ie the x,y of where the click occurred)
        into a position on the chess board
        add this to a list of clicked positions
        every first click is treated as a "from" and every second click as a"to"
        so, whenever there are an even number of clicks, use the most recent to two to perform a move
        then update the display
        '''
        i, j = self.xy_to_ij(event.x,  event.y)
        self.clickList.append(BoardLocation(7-i, j))  # 7-i because the Model stores the board in reverse row order.
        # just maintain a list of all of the moves
        # this list shouldn't be used to replay a series of moves because that is something
        # which should be stored in the model - but it wouldn't be much trouble to
        # keep a record of moves in the model.
        if len(self.clickList)%2 ==0:
            # move complete, execute the move
            self.m.move(self.clickList[-2], self.clickList[-1])
            # use the second last entry in the clickList and the last entry in the clickList
            self.update_display()

    def xy_to_ij(self, x, y):
        ''' given x,y coordinates on the screen, convert to a location on
        the virtual board.
        Involves non trivial mathematics
        The tiles have, in effect, two edges. One leading down to the right,
        and one leading up to the right. These define a (non-orthogonal) basis
        (look it up) for describing the screen.
        The first vector V1 is (60,-30), the second V2= (60,30), and the
        coordinates were given XY=(x,y)
        so we want to find two numbers a and b such that:
        aV1+bV2 = XY
        Where a represents the column and b represents the row
        or, in other words:
        a(60,-30)+b(60,30) = (x,y)
        60a +60b = x
        -30a +30b = y
        so
        b = (y+30a)/30.0
        and
        60a+60*(y+30a)/30 = x
        => 60a +2y+60a = x
        => 120a = x-2y
        a = (x-2y)/120

        HOWEVER, this is calculated by reference to the a1 corner of the board
        AND assumes that y increases going upwards not downwards.

        This corner is located at 8* ISOMETRIC_TILE_HEIGHT/2  from the bottom of the canvas
        (count them)
        so first translate the x,y coordinates we have received
        x stays the same
        '''

        y = self.v.canvas_height-y # invert it
        y = y - 4*ISOMETRIC_TILE_HEIGHT # Get y relative to the height of the corner

        a = (x-2*y)/120.0
        b = (y+30*a)/30.0
        # if either of these is <0 this means that the click is off the board (to the left or below)
        # if the number is greater than -1, but less than 0, int() will round it up to 0
        # so we need to explicitly return -1 rather than just int(a) etc.

        return (int(b) if b>= 0 else -1, int(a) if a >= 0 else -1)

    def update_display(self,  debug_board= False):
        self.v.display(self.m.board,  debug_board = debug_board)

    def parse_move(self, move):
        ''' Very basic move parsing
        given a move in the form ab-cd where a and c are in [a,b,c,d,e,f,g,h]
        and b and d are numbers from 1 to 8 convert into BoardLocation instances
        for start (ab) and destination (cd)
        Does not deal with castling (ie 0-0 or 0-0-0) or bare pawn moves (e4)
        or capture d4xe5 etc
        No error checking! very fragile
        '''

        s, d = move.split("-")

        i = 8- int(s[-1]) # board is "upside down" with reference to the representation
        j = column_reference.index(s[0])
        start = BoardLocation(i, j)

        i =  8- int(d[-1])
        j= column_reference.index(d[0])
        destination = BoardLocation(i, j)

        return start,  destination

if __name__=="__main__":
    missing_files = False
    if not os.path.exists(ISOMETRIC_DATA_DIR):
        missing_files = True
        print ("Cannot find data directory")

    if not missing_files:
        missing_list = []
        for k, v in ISOMETRIC_TILES.items():
            fn = os.path.join(ISOMETRIC_DATA_DIR,  v)
            if not os. path.exists(fn):
                missing_files = True
                print ("Cannot find file: %s"%fn)
                missing_list.append(v)
    else: # whole directory missing
        missing_list= ISOMETRIC_TILES.values()

    if missing_files:
        ''' basic check - if there are files missing from the data directory, the
        program will still fail '''
        dl = raw_input("Cannot find chess images directory.  Download from website? (Y/n)")
        if dl.lower() == "n":
            print("Some image files not found, quitting.")
            exit(0)
        if not os.path.exists(ISOMETRIC_DATA_DIR):
            print("Creating directory: %s"%os.path.join(os.getcwd(), ISOMETRIC_DATA_DIR))
            os.mkdir(ISOMETRIC_DATA_DIR)

        import urllib
        url_format="http://python4kids.files.wordpress.com/2013/05/chess_isometric_black_tile.gif"
        url_format= "http://python4kids.files.wordpress.com/2013/05/%s"
#        for k, v in ISOMETRIC_TILES.items():
        for v in missing_list:
            url = url_format%v
            target_filename = os.path.join(ISOMETRIC_DATA_DIR, v)
            print("Downloading file: %s"%v)
            urllib.urlretrieve(url, target_filename)

    parent = tk.Tk()
    c = Controller(parent)
    c.run(debug_mode= False)

But that’s not all.  Now we can start using the full power that separating the controller, model and views gives us.  By defining a new (2d) view and reinstating the click handler from the previous tutorial we can have two views running at the same time with moves in one automatically reflected in the other:

chess_mvc3_F

Both boards are kept in sync – so, if you make a move on either of the board, it is shown on both.  In fact, you can click a starting square on one board, and a finishing square on the other and it will do the move for you.  Having two boards like this on the same computer screen may not be of much practical value, but it does demonstrate that a controller can administer more than one view at a time.  Each of these views might be on a different person’s computer, for example.

Code with the two views:

#/usr/bin/python
'''
Representing a chess set in Python
Part 3 (Isometric tiles)
Brendan Scott
4 May 2013

Dark square on a1
Requires there to be a directory called
chess_data in the current directory, and for that
data directory to have a copy of all the images

'''

import Tkinter as tk
from Tkinter import PhotoImage
import os.path
import os

from mvc2 import Model,  BoardLocation,  View,  TILES
# Use the Model class from the previous tutorial.
# rename mvc2 to whatever name you gave the script from that tutorial
# if you can't find the previous tutorial just uncomment the definition below.

column_reference = "a b c d e f g h".split(" ")
EMPTY_SQUARE = " "

TILE_WIDTH = 60
'''We have used a tile width of 60 because the images we are used are 60x60 pixels
The original svg files were obtained from

http://commons.wikimedia.org/wiki/Category:SVG_chess_pieces/Standard_transparent

after downloading they were batch converted to png, then gif files.  Bash one liners
(Unix) to do this:
for i in $(ls *.svg); do inkscape -e ${i%.svg}.png -w 60 -h 60 $i ; done
for i in $(ls *.png); do convert $i  ${i%.png}.gif ; done
white and black tiles were created in inkscape

Isometric tiles were created in inkscape
Isometric pieces were created with povray using ChessSets Version 1.2
by James Garner ( jkgarner@charter.net )
then post processed in GIMP.

'''

ISOMETRIC_TILE_WIDTH = 120
ISOMETRIC_TILE_HEIGHT = 60
# to display these tiles many locations in the code rely on integer division by 2,
# so this width and height should both be even numbers (otherwise rounding errors will accumulate)

BOARD_WIDTH = 8*TILE_WIDTH
BOARD_HEIGHT = BOARD_WIDTH

ISOMETRIC_BOARD_WIDTH = 8 * ISOMETRIC_TILE_WIDTH
ISOMETRIC_BOARD_HEIGHT= 8*ISOMETRIC_TILE_HEIGHT

DATA_DIR = "chess_data"
ISOMETRIC_DATA_DIR = "chess_data"

ISOMETRIC_TILES = {"black_tile":"chess_isometric_black_tile.gif",
    "B":"chess_isometric_white_bishop1.gif",
    "b":"chess_isometric_black_bishop1.gif",
    "k":"chess_isometric_black_king1.gif",
    "K":"chess_isometric_white_king1.gif",
    "n":"chess_isometric_black_knight1.gif",
    "N":"chess_isometric_white_knight1.gif",
    "p":"chess_isometric_black_pawn1.gif",
    "P":"chess_isometric_white_pawn1.gif",
    "q":"chess_isometric_black_queen1.gif",
    "Q":"chess_isometric_white_queen1.gif",
    "r":"chess_isometric_black_rook1.gif",
    "R":"chess_isometric_white_rook1.gif",
    "white_tile":"chess_isometric_white_tile.gif"
    }

#class Model(object):
#    def __init__(self):
#        '''create a chess board with pieces positioned for a new game
#        row ordering is reversed from normal chess representations
#        but corresponds to a top left screen coordinate
#        '''
#
#        self.board = []
#        pawn_base = "P "*8
#        white_pieces =  "R N B Q K B N R"
#        white_pawns = pawn_base.strip()
#        black_pieces = white_pieces.lower()
#        black_pawns = white_pawns.lower()
#        self.board.append(black_pieces.split(" "))
#        self.board.append(black_pawns.split(" "))
#        for i in range(4):
#            self.board.append([EMPTY_SQUARE]*8)
#        self.board.append(white_pawns.split(" "))
#        self.board.append(white_pieces.split(" "))
#
#
#    def move(self, start,  destination):
#        ''' move a piece located at the start location to destination
#        (each an instance of BoardLocation)
#        Does not check whether the move is valid for the piece
#        '''
#        # error checking
#        for c in [start, destination]:  # check coordinates are valid
#            if c.i > 7 or c.j > 7 or c.i <0 or c.j <0:
#                return
#        if start.i == destination.i and start.j == destination.j: # don't move to same location
#            return
#
#        if self.board[start.i][start.j] == EMPTY_SQUARE:  #nothing to move
#            return
#
#        f = self.board[start.i][start.j]
#        self.board[destination.i][destination.j] = f
#        self.board[start.i][start.j] = EMPTY_SQUARE
#
#
#class BoardLocation(object):
#    def __init__(self, i, j):
#        self.i = i
#        self.j = j

class Isometric_View(tk.Frame):
    def __init__(self,  parent = None):
        tk.Frame.__init__(self, parent)
        self.parent = parent
        self.preload_images()
        self.canvas_height = 7*ISOMETRIC_TILE_HEIGHT+self.board_y_offset
        self.canvas = tk.Canvas(self, width=ISOMETRIC_BOARD_WIDTH, height=self.canvas_height)
        self.canvas.pack()
        self.parent.title("Python4Kids")
        self.pack()

    def preload_images(self):
        self.images = {}
        for image_file_name in ISOMETRIC_TILES:
            f = os.path.join(ISOMETRIC_DATA_DIR, ISOMETRIC_TILES[image_file_name])
            if not os.path.exists(f):
                print("Error: Cannot find image file: %s at %s - aborting"%(ISOMETRIC_TILES[image_file_name], f))
                exit(-1)
            self.images[image_file_name]= PhotoImage(file=f)
        tallest = 0
        for k, im in self.images.items():
            h = im.height()
            if h > tallest:
                tallest = h

        self.board_y_offset = tallest

    def clear_canvas(self):
        ''' delete everything from the canvas'''
        items = self.canvas.find_all()
        for i in items:
            self.canvas.delete(i)

    def draw_empty_board(self,  debug_board = False):
        ''' draw an empty board on the canvas
        if debug_board is set  show the coordinates of each of the tile corners'''

        for j in range(8): # rows, or y coordinates
            for i in range(8): # columns, or x coordinates
                x, y = self.get_tile_sw(i, j)
                drawing_order = j*8 + i
                tile_white = (j+i)%2
                if tile_white == 0:
                    tile = self.images['white_tile']
                else:
                    tile = self.images['black_tile']
                self.canvas.create_image(x, y, anchor = tk.SW,  image=tile)

                if debug_board:  # implicitly this means if debug_board == True.
                    ''' If we are drawing a debug board, draw an arrow showing top left
                    and its coordinates. '''
                    current_tile = drawing_order +1 # (start from 1)

                    text_pos =  (x+ISOMETRIC_TILE_WIDTH/2, y-ISOMETRIC_TILE_HEIGHT/2)
                    line_end = (x+ISOMETRIC_TILE_WIDTH/4,  y -ISOMETRIC_TILE_HEIGHT/4)
                    self.canvas.create_line((x, y), line_end,  arrow = tk.FIRST)
                    text_content = "(%s: %s,%s)"%(current_tile, x, y)
                    self.canvas.create_text(text_pos, text=text_content)

    def get_tile_sw(self, i,  j):
        ''' given a row and column location for a piece return the x,y coordinates of the bottom left hand corner of
        the tile '''

        y_start = (j*ISOMETRIC_TILE_HEIGHT/2)+self.board_y_offset
        x_start = (7-j)*ISOMETRIC_TILE_WIDTH/2
        x = x_start+(i*ISOMETRIC_TILE_WIDTH/2)
        y = y_start +(i*ISOMETRIC_TILE_HEIGHT/2)

        return (x, y)

    def draw_pieces(self, board):
        for j, row in enumerate(board):  # this is the rows = y axis
            # using enumerate we get an integer index
            # for each row which we can use to calculate y
            # because rows run down the screen, they correspond to the y axis
            # and the columns correspond to the x axis
            # isometric pieces need to be drawn by reference to a bottom corner of the tile,  We are using
            # SW  (ie bottom left).

            for i,  piece in enumerate(row): # columns = x axis
                if piece == EMPTY_SQUARE:
                    continue  # skip empty tiles
                tile = self.images[piece]
                x, y = self.get_tile_sw(i, j)
                self.canvas.create_image(x, y, anchor=tk.SW,  image = tile)

    def display(self, board,  debug_board= False):
        ''' draw an empty board then draw each of the
        pieces in the board over the top'''

        self.clear_canvas()
        self.draw_empty_board(debug_board=debug_board)
        if not debug_board:
            self.draw_pieces(board)

        # first draw the empty board
        # then draw the pieces
        # if the order was reversed, the board would be drawn over the pieces
        # so we couldn't see them

    def display_debug_board(self):
        self.clear_canvas()
        self.draw_empty_board()

class Controller(object):
    def __init__(self,  parent = None,  model = None):
        if model is None:
            self.m = Model()
        else:
            self.m = model
        self.v = Isometric_View(parent)
        self.v2 = View(parent)

        ''' we have created both a model and a view within the controller
        the controller doesn't inherit from either model or view
        '''
        self.v.canvas.bind("<Button-1>",  self.handle_click_isometric)
        # this binds the handle_click method to the view's canvas for left button down
        self.v2.canvas.bind("<Button-1>",  self.handle_click)
        self.clickList = []
        # I have kept clickList here, and not in the model, because it is a record of what is happening
        # in the view (ie click events) rather than something that the model deals with (eg moves).

    def run(self,  debug_mode = False):
        self.update_display(debug_board=debug_mode)
        tk.mainloop()

    def handle_click(self,  event):
        ''' Handle a click received.  The x,y location of the click on the canvas is at
        (event.x, event.y)
        First, we need to translate the event coordinates (ie the x,y of where the click occurred)
        into a position on the chess board
        add this to a list of clicked positions
        every first click is treated as a "from" and every second click as a"to"
        so, whenever there are an even number of clicks, use the most recent to two to perform a move
        then update the display
        '''
        j = event.x/TILE_WIDTH
        #  the / operator is called integer division
        # it returns the number of times TILE_WIDTH goes into event.x ignoring any remainder
        # eg: 2/2 = 1, 3/2 = 1, 11/5 = 2 and so on
        # so, it should return a number between 0 (if x < TILE_WIDTH) though to 7
        i = event.y/TILE_WIDTH

        self.clickList.append(BoardLocation(i, j))
        # just maintain a list of all of the moves
        # this list shouldn't be used to replay a series of moves because that is something
        # which should be stored in the model - but it wouldn't be much trouble to
        # keep a record of moves in the model.
        if len(self.clickList)%2 ==0:
            # move complete, execute the move
            self.m.move(self.clickList[-2], self.clickList[-1])
            # use the second last entry in the clickList and the last entry in the clickList
            self.update_display()

    def handle_click_isometric(self,  event):
        ''' Handle a click received.  The x,y location of the click on the canvas is at
        (event.x, event.y)
        First, we need to translate the event coordinates (ie the x,y of where the click occurred)
        into a position on the chess board
        add this to a list of clicked positions
        every first click is treated as a "from" and every second click as a"to"
        so, whenever there are an even number of clicks, use the most recent to two to perform a move
        then update the display
        '''
        i, j = self.xy_to_ij(event.x,  event.y)
        self.clickList.append(BoardLocation(7-i, j))  # 7-i because the Model stores the board in reverse row order.
        # just maintain a list of all of the moves
        # this list shouldn't be used to replay a series of moves because that is something
        # which should be stored in the model - but it wouldn't be much trouble to
        # keep a record of moves in the model.
        if len(self.clickList)%2 ==0:
            # move complete, execute the move
            self.m.move(self.clickList[-2], self.clickList[-1])
            # use the second last entry in the clickList and the last entry in the clickList
            self.update_display()

    def xy_to_ij(self, x, y):
        ''' given x,y coordinates on the screen, convert to a location on
        the virtual board.
        Involves non trivial mathematics
        The tiles have, in effect, two edges. One leading down to the right,
        and one leading up to the right. These define a (non-orthogonal) basis
        (look it up) for describing the screen.
        The first vector V1 is (60,-30), the second V2= (60,30), and the
        coordinates were given XY=(x,y)
        so we want to find two numbers a and b such that:
        aV1+bV2 = XY
        Where a represents the column and b represents the row
        or, in other words:
        a(60,-30)+b(60,30) = (x,y)
        60a +60b = x
        -30a +30b = y
        so
        b = (y+30a)/30.0
        and
        60a+60*(y+30a)/30 = x
        => 60a +2y+60a = x
        => 120a = x-2y
        a = (x-2y)/120

        HOWEVER, this is calculated by reference to the a1 corner of the board
        AND assumes that y increases going upwards not downwards.

        This corner is located at 8* ISOMETRIC_TILE_HEIGHT/2  from the bottom of the canvas
        (count them)
        so first translate the x,y coordinates we have received
        x stays the same
        '''

        y = self.v.canvas_height-y # invert it
        y = y - 4*ISOMETRIC_TILE_HEIGHT # Get y relative to the height of the corner

        a = (x-2*y)/120.0
        b = (y+30*a)/30.0
        # if either of these is <0 this means that the click is off the board (to the left or below)
        # if the number is greater than -1, but less than 0, int() will round it up to 0
        # so we need to explicitly return -1 rather than just int(a) etc.

        return (int(b) if b>= 0 else -1, int(a) if a >= 0 else -1)

    def update_display(self,  debug_board= False):
        self.v.display(self.m.board,  debug_board = debug_board)
        self.v2.display(self.m.board, debug_board= debug_board)

    def parse_move(self, move):
        ''' Very basic move parsing
        given a move in the form ab-cd where a and c are in [a,b,c,d,e,f,g,h]
        and b and d are numbers from 1 to 8 convert into BoardLocation instances
        for start (ab) and destination (cd)
        Does not deal with castling (ie 0-0 or 0-0-0) or bare pawn moves (e4)
        or capture d4xe5 etc
        No error checking! very fragile
        '''

        s, d = move.split("-")

        i = 8- int(s[-1]) # board is "upside down" with reference to the representation
        j = column_reference.index(s[0])
        start = BoardLocation(i, j)

        i =  8- int(d[-1])
        j= column_reference.index(d[0])
        destination = BoardLocation(i, j)

        return start,  destination

if __name__=="__main__":
    missing_files = False
    if not os.path.exists(ISOMETRIC_DATA_DIR):
        missing_files = True
        print ("Cannot find data directory")

    if not missing_files:
        missing_list = []
        missing_2dlist=[]
        for k, v in ISOMETRIC_TILES.items():
            fn = os.path.join(ISOMETRIC_DATA_DIR,  v)
            if not os. path.exists(fn):
                missing_files = True
                print ("Cannot find file: %s"%fn)
                missing_list.append(v)
        for k, v in TILES.items():
            fn = os.path.join(ISOMETRIC_DATA_DIR,  v)
            if not os. path.exists(fn):
                missing_files = True
                print ("Cannot find file: %s"%fn)
                missing_2dlist.append(v)

    else: # whole directory missing
        missing_list= ISOMETRIC_TILES.values()
        missing_2dlist = TILES.values()

    if missing_files:
        ''' basic check - if there are files missing from the data directory, the
        program will still fail '''
        dl = raw_input("Cannot find chess images directory.  Download from website? (Y/n)")
        if dl.lower() == "n":
            print("Some image files not found, quitting.")
            exit(0)
        if not os.path.exists(ISOMETRIC_DATA_DIR):
            print("Creating directory: %s"%os.path.join(os.getcwd(), ISOMETRIC_DATA_DIR))
            os.mkdir(ISOMETRIC_DATA_DIR)

        import urllib
        url_format="http://python4kids.files.wordpress.com/2013/05/chess_isometric_black_tile.gif"
        url_format= "http://python4kids.files.wordpress.com/2013/05/%s"
#        for k, v in ISOMETRIC_TILES.items():
        for v in missing_list:
            url = url_format%v
            target_filename = os.path.join(ISOMETRIC_DATA_DIR, v)
            print("Downloading file: %s"%v)
            urllib.urlretrieve(url, target_filename)

        url_format= "http://python4kids.files.wordpress.com/2013/04/%s"
        for v in missing_2dlist:
            url = url_format%v
            target_filename = os.path.join(ISOMETRIC_DATA_DIR, v)
            print("Downloading file: %s"%v)
            urllib.urlretrieve(url, target_filename)
    parent = tk.Tk()
    c = Controller(parent)
    c.run(debug_mode= False)

Notes:

* the approximate “y” dimension of the base of each of the pieces (if just the base was drawn) is roughly 18 pixels, so the vertical centre is about 9 pixels from the bottom (non-transparent) pixel of the piece.  The tiles are 60 pixels high, so their vertical centre is at 30 pixels high.  If the vertical centre of the base is at the vertical centre of the tile then the piece needs to be padded with 21 pixels below it (21+9=30).

** The Tkinter canvas does have a mechanism for tagging things drawn on it.  Instead of calculating the location of the tiles they (and the pieces) could, instead, be tagged (with eg, the coordinates of the tile/piece)  and those tags read and parsed by the handler.  However, that brings with it other problems, such as overlapping of the tiles and pieces.

A Different View on Our Chess Model

Cut to a polite, well dressed assistant at a counter with a big sign saying ‘End of Show Department’ behind him.
Assistant Well it is one of our cheapest, sir.
Chris What else have you got?
Assistant Well, there’s the long slow pull-out, sir, you know, the camera tracks back and back and mixes…
As he speaks we pull out and mix through to the exterior of the store. Mix through to even wider zoom ending up in aerial view of London. It stops abruptly and we cut back to Chris.

In the last tutorial we saw how to model the position on a chess board. However, the interface was pretty basic. It looked like this:

 : ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
--------------------------------------------------
8: ['r', 'n', 'b', 'q', 'k', 'b', 'n', 'r']
7: ['p', 'p', 'p', 'p', 'p', 'p', 'p', 'p']
6: [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ']
5: [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ']
4: [' ', ' ', ' ', ' ', 'P', ' ', ' ', ' ']
3: [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ']
2: ['P', 'P', 'P', 'P', ' ', 'P', 'P', 'P']
1: ['R', 'N', 'B', 'Q', 'K', 'B', 'N', 'R']
move (eg e2-e4)

but it’s not too hard to update this interface to something that looks much, much better but has the same functionality (which, admittedly wasn’t that extensive in this case).  Like this, for example:

chess_mvc2That’s because we’ve kept the data separate from the way of presenting the data.   At the end of the last tutorial I left you with the question: “Why did we go to all this trouble to separate the model from the view?.  What we covered in the last tutorial is known as a model-view-controller (or MVC) pattern.  To quote Wikipedia:

Model–view–controller (MVC) is a software architecture pattern which separates the representation of information from the user’s interaction with it.[1][2] The model consists of application data, business rules, logic, and functions. A view can be any output representation of data, such as a chart or a diagram. Multiple views of the same data are possible, …. The controller mediates input, converting it to commands for the model or view.[3] The central ideas behind MVC are code reusability and separation of concerns.”

If your program is small, then these distinctions are unnecessarily cloying.  Further, the actual distinction between controllers and views is a little blurred for modern desktop applications, but the MVC pattern is quite important for programming web based applications.

Ideally, if the model, view and controller are all separated, designing and coding your application will be easier as it grows.  It also allows you to vary these parts independently of each other.  It is not at all unusual to want to update the look and feel of a program, without changing the underlying data on which it relies.  However, if the view is entangled with the model (that is, the data), you need to understand both the view and the model before you start changing the view – it will all end in tears.  If something is data (eg if it would be something you might want to write to a save file), you should put it in the model.  If it has to do with what the user sees, put it in the view.  Everything else (and this should just be coordination between the model and the view), put in the controller.

In this tutorial rather than building the code incrementally I am, instead, going to focus on the concepts and let you read through the source code (at the end of this tutorial) and the comments at your own pace.

MVC and Interfaces

The code defines a Model, a View and a Controller. The code for the Model is the same as in our previous tutorial. the view and controller on the other hand are different. However, the view still has a display method – and this method performs the same role as in the previous tutorial (that is, it draws the whole board and all of the pieces). The methods are the interface between the class and the outside world. If the names are changed or they perform a different role, that interface gets broken (which is pretty sad). Keeping the same method names performing the same roles allows a single controller to be used with different views. This makes developing the views and controllers easier in the future. When you make a replacement view (or controller or model) within a program, try to keep methods with the same name doing the same thing.  If you are writing a new program you are free to change them, but you might want to keep some of them the same for consistency.

The New View

The chess board is made out of 64 (8 rows of 8) squares, with pieces in 32 of those squares (at the start of the game).  The main thing the view does is to calculate where to put these tiles (and pieces).  All of these locations are determined by the length of the tiles.  All of the tiles are 60×60 pixel gif images.*  So, we have set a variable TILE_WIDTH = 60, then determined the size of the board from this (a square of side 8*60 pixels).  The top left tile is at location 0,0 (its bottom right corner is at (60,60).  The tile to the right of it is starts at (60,0), and has a bottom right corner at (60,120).  Here are all the tile locations:

chess_mvc2_BThe last tiles in each row are at 420, not 480, as you might expect (from 8*60).  This is because the 60 pixel width of the tile needs to fit into 480, so, needs to start 60 pixels earlier – at 420.

Once the program has worked out where to put the tile, it gets the appropriate tile to place and draws it there.  Because the pieces fit wholly within the squares, there is no need to worry about overlaps in the tiles.

The New Controller

The run method is much simpler in the new controller, largely because Tkinter is doing a lot of stuff in its mainloop() method.  The new controller has kept the parse_move method (although it is not used).  I have kept it just in case you want to add a place for the user to type their moves later.

The controller binds a callback to mouse clicks on the canvas. It reverses the computation to draw the grid in order to translate the click into a board position (i,j). It records these board positions and, every second click, sends the last two off to the model’s move method, then updates the view.

The controller also has an attribute called clickList.  This is here as a convenience variable to track the current state of the interface (it is would have been more work to set and track a flag first click etc).  Ideally, if you wanted to (eg) implement a replay (or undo) function you would create a move_list variable in the model and have the controller query the model for this data to pass on to the view.

New Print Syntax, Widget Methods

I have started using the new Python 3 print syntax.  In Python 3 print is a function (the arguments of which will be printed).  This new syntax print(something) should be backported into (that is, work in) the Python you are using.  If not, remove the brackets ().

I have also not gone into any detail on the methods of the canvas widget. You will need to start looking these things up.

Auto Downloader

Finally, the program will not work unless you have data files for the images of the pieces and the board. So, the program checks for them at start up. If they are not there, all of them are first downloaded from this site.

Notes:

* If you want to make a board at a different size, check the source code for the URL of the svg files I used, as well as some Unix shell commands for batching creation of the gifs from them.  Then, just make sure TILE_WIDTH matches your new tile size.

Code

#/usr/bin/python
'''
Representing a chess set in Python
Part 2
Brendan Scott
27 April 2013

Dark square on a1
Requires there to be a directory called
chess_data in the current directory, and for that 
data directory to have a copy of all the images

'''

import Tkinter as tk
from Tkinter import PhotoImage
import os.path
import os

#column_reference = "1 2 3 4 5 6 7 8".split(" ")
column_reference = "a b c d e f g h".split(" ")
EMPTY_SQUARE = " "

TILE_WIDTH = 60
'''We have used a tile width of 60 because the images we are used are 60x60 pixels
The original svg files were obtained from 

http://commons.wikimedia.org/wiki/Category:SVG_chess_pieces/Standard_transparent

after downloading they were batch converted to png, then gif files.  Bash one liners
to do this:
for i in $(ls *.svg); do inkscape -e ${i%.svg}.png -w 60 -h 60 $i ; done
for i in $(ls *.png); do convert $i  ${i%.png}.gif ; done
white and black tiles were created in inkscape
'''

BOARD_WIDTH = 8*TILE_WIDTH
BOARD_HEIGHT = BOARD_WIDTH
DATA_DIR = "chess_data"
TILES = {"black_tile":"black_tile.gif",
    "B":"chess_b451.gif",
    "b":"chess_b45.gif",
    "k":"chess_k45.gif",
    "K":"chess_k451.gif",
    "n":"chess_n45.gif",
    "N":"chess_n451.gif",
    "p":"chess_p45.gif",
    "P":"chess_p451.gif",
    "q":"chess_q45.gif",
    "Q":"chess_q451.gif",
    "r":"chess_r45.gif",
    "R":"chess_r451.gif",
    "white_tile":"white_tile.gif"
    }

class Model(object):
    def __init__(self):
        '''create a chess board with pieces positioned for a new game
        row ordering is reversed from normal chess representations
        but corresponds to a top left screen coordinate 
        '''
        
        self.board = []
        pawn_base = "P "*8
        white_pieces =  "R N B Q K B N R"
        white_pawns = pawn_base.strip() 
        black_pieces = white_pieces.lower()
        black_pawns = white_pawns.lower()
        self.board.append(black_pieces.split(" "))
        self.board.append(black_pawns.split(" "))
        for i in range(4):
            self.board.append([EMPTY_SQUARE]*8)
        self.board.append(white_pawns.split(" "))
        self.board.append(white_pieces.split(" "))


    def move(self, start,  destination):
        ''' move a piece located at the start location to destination
        (each an instance of BoardLocation)
        Does not check whether the move is valid for the piece
        '''
        # error checking
        for c in [start, destination]:  # check coordinates are valid
            if c.i > 7 or c.j > 7 or c.i <0 or c.j <0:
                return 
        if start.i == destination.i and start.j == destination.j: # don't move to same location
            return

        if self.board[start.i][start.j] == EMPTY_SQUARE:  #nothing to move
            return 
        
        f = self.board[start.i][start.j]
        self.board[destination.i][destination.j] = f
        self.board[start.i][start.j] = EMPTY_SQUARE


class BoardLocation(object):
    def __init__(self, i, j):
        self.i = i
        self.j = j

class View(tk.Frame):
    def __init__(self,  parent = None):
        tk.Frame.__init__(self, parent)
        self.canvas = tk.Canvas(self, width=BOARD_WIDTH, height=BOARD_HEIGHT)
        self.canvas.pack()
        self.images = {}
        for image_file_name in TILES:
            f = os.path.join(DATA_DIR, TILES[image_file_name])
            if not os.path.exists(f):
                print("Error: Cannot find image file: %s at %s - aborting"%(TILES[image_file_name], f))
                exit(-1)
            self.images[image_file_name]= PhotoImage(file=f)
            '''This opens each of the image files, converts the data into a form that Tkinter
            can use, then stores that converted form in the attribute self.images
            self.images is a dictionary, keyed by the letters we used in our model to
            represent the pieces - ie PRNBKQ for white and prnbkq for black
            eg self.images['N'] is a PhotoImage of a white knight
            this means we can directly translate a board entry from the model into a picture
            '''
        self.pack()
        

    def clear_canvas(self):
        ''' delete everything from the canvas'''
        items = self.canvas.find_all()
        for i in items:
            self.canvas.delete(i)

    def draw_row(self, y,  first_tile_white=True,  debug_board = False):
        ''' draw a single row of alternating black and white tiles, 
        the colour of the first tile is determined by first_tile_white
        if debug_board is set  show the coordinates of each of the tile corners
        '''

        if first_tile_white:
            remainder = 1
        else:
            remainder = 0
        for i in range(8):
            x = i*TILE_WIDTH
            if i%2 == remainder:  
                # i %2 is the remainder after dividing i by 2
                # so i%2 will always be either 0 (no remainder- even numbers) or 
                # 1 (remainder 1 - odd numbers)
                # this tests whether the number i is even or odd
                tile = self.images['black_tile']
            else:
                tile = self.images['white_tile']
            self.canvas.create_image(x, y, anchor = tk.NW,  image=tile)
            # NW is a constant in the Tkinter module.  It stands for "north west" 
            # that is, the top left corner of the picture is to be located at x,y
            # if we used another anchor, the grid would not line up properly with 
            # the canvas size 
            if debug_board:  # implicitly this means if debug_board == True.
                ''' If we are drawing a debug board, draw an arrow showing top left
                and its coordinates. '''
                text_pos =  (x+TILE_WIDTH/2, y+TILE_WIDTH/2)
                line_end = (x+TILE_WIDTH/4,  y +TILE_WIDTH/4)
                self.canvas.create_line((x, y), line_end,  arrow = tk.FIRST)
                text_content = "(%s,%s)"%(x, y)
                self.canvas.create_text(text_pos, text=text_content)
            

    def draw_empty_board(self,  debug_board = False):
        ''' draw an empty board on the canvas
        if debug_board is set  show the coordinates of each of the tile corners'''
        y = 0
        for i in range(8): # draw 8 rows
            y = i*TILE_WIDTH  
            # each time, advance the y value at which the row is drawn
            # by the length of the tile
            first_tile_white =  not (i%2)
            self.draw_row(y, first_tile_white,  debug_board )
    
    def draw_pieces(self, board):
        for i, row in enumerate(board): 
            # using enumerate we get an integer index
            # for each row which we can use to calculate y
            # because rows run down the screen, they correspond to the y axis
            # and the columns correspond to the x axis
            for j,  piece in enumerate(row):
                if piece == EMPTY_SQUARE:
                    continue  # skip empty tiles
                tile = self.images[piece]
                x = j*TILE_WIDTH
                y = i*TILE_WIDTH
                self.canvas.create_image(x, y, anchor=tk.NW,  image = tile)
    
    def display(self, board,  debug_board= False):
        ''' draw an empty board then draw each of the
        pieces in the board over the top'''
        
        self.clear_canvas()
        self.draw_empty_board(debug_board=debug_board)
        if not debug_board: 
            self.draw_pieces(board)

        # first draw the empty board
        # then draw the pieces
        # if the order was reversed, the board would be drawn over the pieces
        # so we couldn't see them
    
    def display_debug_board(self):
        self.clear_canvas()
        self.draw_empty_board()
        
                
class Controller(object):
    def __init__(self,  parent = None,  model = None):
        if model is None:
            self.m = Model()
        else:
            self.m = model
        self.v = View(parent)
        ''' we have created both a model and a view within the controller
        the controller doesn't inherit from either model or view
        '''
        self.v.canvas.bind("<Button-1>",  self.handle_click)
        # this binds the handle_click method to the view's canvas for left button down
        
        self.clickList = []
        # I have kept clickList here, and not in the model, because it is a record of what is happening
        # in the view (ie click events) rather than something that the model deals with (eg moves).
    
    def run(self,  debug_mode = False):
        self.update_display(debug_board=debug_mode)
        tk.mainloop()
        
    def handle_click(self,  event):
        ''' Handle a click received.  The x,y location of the click on the canvas is at 
        (event.x, event.y)
        First, we need to translate the event coordinates (ie the x,y of where the click occurred) 
        into a position on the chess board
        add this to a list of clicked positions
        every first click is treated as a "from" and every second click as a"to"
        so, whenever there are an even number of clicks, use the most recent to two to perform a move
        then update the display
        '''
        j = event.x/TILE_WIDTH  
        #  the / operator is called integer division
        # it returns the number of times TILE_WIDTH goes into event.x ignoring any remainder
        # eg: 2/2 = 1, 3/2 = 1, 11/5 = 2 and so on
        # so, it should return a number between 0 (if x < TILE_WIDTH) though to 7
        i = event.y/TILE_WIDTH  
        
        self.clickList.append(BoardLocation(i, j))  
        # just maintain a list of all of the moves
        # this list shouldn't be used to replay a series of moves because that is something
        # which should be stored in the model - but it wouldn't be much trouble to 
        # keep a record of moves in the model. 
        if len(self.clickList)%2 ==0:
            # move complete, execute the move
            self.m.move(self.clickList[-2], self.clickList[-1])
            # use the second last entry in the clickList and the last entry in the clickList
            self.update_display()
    
    def update_display(self,  debug_board= False):
        self.v.display(self.m.board,  debug_board = debug_board)
    

    def parse_move(self, move):
        ''' Very basic move parsing 
        given a move in the form ab-cd where a and c are in [a,b,c,d,e,f,g,h]
        and b and d are numbers from 1 to 8 convert into BoardLocation instances
        for start (ab) and destination (cd)
        Does not deal with castling (ie 0-0 or 0-0-0) or bare pawn moves (e4)
        or capture d4xe5 etc
        No error checking! very fragile
        '''
        
        s, d = move.split("-")

        i = 8- int(s[-1]) # board is "upside down" with reference to the representation
        j = column_reference.index(s[0])
        start = BoardLocation(i, j)
        
        i =  8- int(d[-1])
        j= column_reference.index(d[0])
        destination = BoardLocation(i, j)

        return start,  destination

if __name__=="__main__":
    if not os.path.exists(DATA_DIR):
        ''' basic check - if there are files missing from the data directory, the
        program will still fail '''
        dl = raw_input("Cannot find chess images directory.  Download from website? (Y/n)")
        if dl.lower() == "n":
            print("No image files found, quitting.")
            exit(0)
        print("Creating directory: %s"%os.path.join(os.getcwd(), DATA_DIR))
        import urllib

        os.mkdir(DATA_DIR)
        url_format= "http://python4kids.files.wordpress.com/2013/04/%s"       
        for k, v in TILES.items():
            url = url_format%v
            target_filename = os.path.join(DATA_DIR, v)
            print("Downloading file: %s"%v)
            urllib.urlretrieve(url, target_filename)
            
    parent = tk.Tk()
    c = Controller(parent)
    c.run(debug_mode= False)

Modelling Chess Positions

The knight is carrying a raw chicken. The man apprehensively covers his head and the knight slams him in the stomach with the chicken.
Woman I think it’s silly to ask a lizard what it thinks, anyway.
Chairman (off) Why?
Woman I mean they should have asked Margaret Drabble.
Young Man (very reasonably) Well I think, er, customs people are quite necessary, and I think they’re doing quite a good job really. Check.
We now see that he is playing chess with another young man. They are in an ordinary flat. There is a tremendous battering, banging, hammering and clattering at the door.

Practically everything in programming involves making a representation of something from real life or someone’s imagination and visualising that  model to the user of the program.  In this tutorial we are going to put together a very basic model of the pieces on a chess board.  A chess board looks like this:

(source)

Which is to say, the board itself has 8 rows (which run horizontally) and 8 columns (which run vertically).  There are 8 pawns for each of white and black and each of white and black have an additional 8 pieces (capital pieces).  A prime contender for representing the board is a list of lists, with each list corresponding to a row and column respectively.  The usual way to represent the pieces is to use capital letters for white pieces (RNBKQP – Rook, kNight, Bishop, King, Queen, Pawn)  and corresponding lower case letters for the black pieces.  Thus, to represent the white rook in the lower left hand corner one would have the letter R in the first column of the first row of your list of lists.

Aside: Lists of lists

Lists can have any elements in them.  In particular, a list can contain lists as its elements.

>>> a = []
>>> a.append(range(8))  # range returns a list
>>> a
[[0, 1, 2, 3, 4, 5, 6, 7]]

Here, the double square brackets [[ ]] indicate two levels of list. This is clearer if we add another element:

>>> a.append(range(4))
>>> a
[[0, 1, 2, 3, 4, 5, 6, 7], [0, 1, 2, 3]]

The two elements in the list are separated by a comma:

>>> a[0]  # remember the first element of a list is at index 0 not 1.
[0, 1, 2, 3, 4, 5, 6, 7]
>>> a[1]
[0, 1, 2, 3]

There is a shortcut notation for accessing an element of an element of a list of lists [sic]:

>>> a[0][7]
7
>>> a[1][3]
3

So, a[0] refers to the first element of the list a, which is, itself, a list. But a[0][7] refers to the eighth element of that list.

Making our Model
With this knowledge we can make a model of the initial set up of a chess board using a 8×8 list of lists, with each element representing a square on the chess board.* The first row is the black pieces: ['r', 'n', 'b', 'q', 'k', 'b', 'n', 'r'], the second row is the black pawns: ['p', 'p', 'p', 'p', 'p', 'p', 'p', 'p'], then there are four rows of empty squares [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '] followed by white pawns ['P', 'P', 'P', 'P', 'P', 'P', 'P', 'P'], then white pieces ['R', 'N', 'B', 'Q', 'K', 'B', 'N', 'R'].

Exercise: check there are 8 rows, each with 8 elements.

So, let’s make a class to hold this representation:

EMPTY_SQUARE = " "

class Model(object):
    def __init__(self):
        '''create a chess board with pieces positioned for a new game
        row ordering is reversed from normal chess representations
        but corresponds to a top left screen coordinate 
        '''
        
        self.board = []
        pawn_base = "P "*8
        white_pieces =  "R N B Q K B N R"
        white_pawns = pawn_base.strip() 
        black_pieces = white_pieces.lower()
        black_pawns = white_pawns.lower()
        self.board.append(black_pieces.split(" "))
        self.board.append(black_pawns.split(" "))
        for i in range(4):
            self.board.append([EMPTY_SQUARE]*8)
        self.board.append(white_pawns.split(" "))
        self.board.append(white_pieces.split(" "))

Each time the Model class is instantiated (that is whenever you see something like a = Model()) the instance will be created with an attribute called self.board which has an initial chess position represented in it.

Exercise: make an instance of Model() and print its attribute board.

Viewing the Model
At the moment it is hard to know whether our Model is properly representing a chess board. What we need is a way to view a given board. This could just be a view function, but, since there aren’t enough classes in the world already, I am going to make it a class:

column_reference = "a b c d e f g h".split(" ")
class View(object):
    def __init__(self):
        pass
    def display(self,  board):
        print("%s: %s"%(" ", column_reference))
        print("-"*50)
        for i, row in enumerate(board):
            row_marker = 8-i
            print("%s: %s"%(row_marker,  row))

So, let’s create a model and a view, then pass data from the model to the display method of the view:

>>> m = Model() #instantiate a model
>>> v = View()  #instantiate a view
>>> v.display(m.board)  # pass the model's data to the view's display method
 : ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
--------------------------------------------------
8: ['r', 'n', 'b', 'q', 'k', 'b', 'n', 'r']
7: ['p', 'p', 'p', 'p', 'p', 'p', 'p', 'p']
6: [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ']
5: [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ']
4: [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ']
3: [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ']
2: ['P', 'P', 'P', 'P', 'P', 'P', 'P', 'P']
1: ['R', 'N', 'B', 'Q', 'K', 'B', 'N', 'R']

I have prettied it up a little by adding coordinate headings (a-h and 1-8). This is a recognised chess notation for identifying board positions and making moves. So, to move the white pawn in front of the king two spaces forward, one writes e2-e4 – that is, e2 (the piece’s starting square) – (to) e4 (the destination square). These coordinates have the same concept as the ones we met in our earlier tutorials (eg here) except that they only have 8 positions in each axis (as opposed to hundreds of pixels in a window), and they start from the lower, rather than the upper left.

Controlling the View and Model
All we need now is something which allows you to feed back information to the model, so that it can change, and for those changes to be displayed back to us. The bit which fits between the model and view, controlling the interactions between them is called (not surprisingly) the Controller. First though, I want to add something for the Model to do – a move piece method, along with a position class (which is actually superfluous – I have only introduced because I wanted to refer to coordinates as .i and .j)

#/usr/bin/python2.7
'''
Representing a chess set in Python
Brendan Scott
19 April 2013

Dark square on a1

'''

#column_reference = "1 2 3 4 5 6 7 8".split(" ")
column_reference = "a b c d e f g h".split(" ")
EMPTY_SQUARE = " "

class Model(object):
    def __init__(self):
        '''create a chess board with pieces positioned for a new game
        row ordering is reversed from normal chess representations
        but corresponds to a top left screen coordinate 
        '''
        
        self.board = []
        pawn_base = "P "*8
        white_pieces =  "R N B Q K B N R"
        white_pawns = pawn_base.strip() 
        black_pieces = white_pieces.lower()
        black_pawns = white_pawns.lower()
        self.board.append(black_pieces.split(" "))
        self.board.append(black_pawns.split(" "))
        for i in range(4):
            self.board.append([EMPTY_SQUARE]*8)
        self.board.append(white_pawns.split(" "))
        self.board.append(white_pieces.split(" "))

    def move(self, start,  destination):
        ''' move a piece located at the start location to destination
        (each an instance of BoardLocation)
        Does not check whether the move is valid for the piece
        '''
        # error checking
        for c in [start, destination]:  # check coordinates are valid
            if c.i > 7 or c.j > 7 or c.i <0 or c.j <0:
                return 
        if start.i == destination.i and start.j == destination.j: # don't move to same location
            return

        if self.board[start.i][start.j] == EMPTY_SQUARE:  #nothing to move
            return 
            
        f = self.board[start.i][start.j]
        self.board[destination.i][destination.j] = f
        self.board[start.i][start.j] = EMPTY_SQUARE


class BoardLocation(object):
    def __init__(self, i, j):
        self.i = i
        self.j = j
        

class View(object):
    def __init__(self):
        pass
    def display(self,  board):
        print("%s: %s"%(" ", column_reference))
        print("-"*50)
        for i, row in enumerate(board):
            row_marker = 8-i
            print("%s: %s"%(row_marker,  row))
        

class Controller(object):
    def __init__(self):
        self.model = Model()
        self.view = View()
    
    def run(self):
        ''' main loop'''
        while True:
            self.view.display(self.model.board)
            move = raw_input("move (eg e2-e4) ")
            move = move.lower()
            if move =="q":
                break
            if move =="":
                move = "e2-e4"
            start,  destination = self.parse_move(move)
            self.model.move(start, destination)
            
    def parse_move(self, move):
        ''' Very basic move parsing 
        given a move in the form ab-cd where a and c are in [a,b,c,d,e,f,g,h]
        and b and d are numbers from 1 to 8 convert into BoardLocation instances
        for start (ab) and destination (cd)
        Does not deal with castling (ie 0-0 or 0-0-0) or bare pawn moves (e4)
        or capture d4xe5 etc
        No error checking! very fragile
        '''
        
        s, d = move.split("-")

        i = 8- int(s[-1]) # board is "upside down" with reference to the representation
        j = column_reference.index(s[0])
        start = BoardLocation(i, j)
        
        i =  8- int(d[-1])
        j= column_reference.index(d[0])
        destination = BoardLocation(i, j)

        return start,  destination
        

if __name__=="__main__":
    C = Controller()
    C.run()

Now, if you run this from a command line, it should allow you to move the pieces around on the board using the e2-e4 notation. It doesn’t play chess – or even checks that the moves are valid, but it does record the result of them.

Homework: Why did we go to all this trouble to separate the model from the view?

* Note: another way of doing this is to keep a dictionary of pieces and their locations…

Release of Space Fighto!

Captain     Oh. All right. (into the PA) Women, children and Red Indians…
Cut to another officer in astronaut’s kit.
Second Officer     And spacemen!

Summary: The game can be downloaded here.  The main project page is here.

A couple of years ago this blog had a hiatus for a few months.  In that time, I wrote a short lunar lander game using 2d vector graphics (see this screen shot for an example of what vector graphics used to look like).   Well, it never got finished, but it did serve as the inspiration of a space fighting game which I did finish – well, except for a title screen.  Over the following 18 months I put off adding a title screen (and some other stuff, but mostly a title screen) before finally releasing it in late January this year (2013).   The game is inspired by (but is not a clone of) an old arcade game called space wars.

Start Screen

Each of the ships flys around a central sun, which draws them in with its gravity.  Gravity also affects the bullets, which makes the game very unpredictable.

Game play shot – see the wreckage?

Each ship is made of a number of little lines.  When the ship is hit, the lines blow apart.  When I play the game with my son, we usually end up in hysterics.

To play the game you will need python 2.6/2.7 (obviously) and pygame installed.   The game doesn’t have any sound (oops! – I don’t tend to use speakers :(   There are plenty of improvements that could be made (such as different geometry for the edges of the screen, a glancing blow feature where your ship is damaged by a glancing hit rather than destroyed, a hyperspace button, different terrain features etc.   Maybe these will be added in the future…

The game can be downloaded here.  The main project page is here.

My source code is licensed under GPL v 3 and is included in the tarball, so stick your head in and have a look at how the code is structured.

While the game is too complex for a direct explanation in this blog, I hope to make a few comments about games and programming in general in future tutorials, using it as an example.  In the meantime, have fun!

200,000 page views

Dear Everyone,

Sometime in the last hour (it is 12:22 London time on 16 December) the blog had its 200,000th page view.   Sorry for no tutorials recently, but thank you all for your support.

Cheers,

Brendan

 

 

I Wanted to be a Lumberjack (logging)

I Wanted to be a Lumberjack (logging)

Barber:     All right … I confess I haven’t cut your hair …  I didn’t want to be a barber anyway. I wanted to be a lumberjack. Leaping from tree to tree as they float down the mighty rivers of British Columbia . . . (he is gradually straightening up with a visionary gleam in his eyes). The giant redwood, the larch, the fir, the mighty Scots pine. (he tears off his barber’s jacket, to reveal tartan shirt and lumberjack trousers underneath; as he speaks the lights dim behind him and a choir of Mounties is heard, faintly in the distance)

When something goes wrong with a program, it is natural to try to diagnose the problem by using the print statement to check the value of variables.  As your program grows in size however, this begins to get unwieldy, if only because the output of print ends up in the console and you lose it to the autoscrolling.  Python offers another way to record these messages – the logging module.  In its most most basic form:

import logging

fn  ="test.log"  # Warning! Will delete any file called test.log!
logging.basicConfig(filename= fn, filemode='w', level = logging.DEBUG)

if __name__=="__main__":
  logging.debug("This is a test entry")

If you save this to a file (p4kLogging.py) and run it, it should:

  • create a file called “test.log” (we could have called it something else if we wished (this is filename =fn)
  • if such a file already exists, that file is deleted and a new, empty one created on top of it (this is the filemode = “w” part)

Let’s read the file now from the Python console:

>>> fn  ="test.log"
>>> f = open(fn,'r')
>>> print f.read()
DEBUG:root:This is a test entry

>>> f.close()

This gives us the message (“This is a test entry”), as well as what logging status it has been given (DEBUG) and what function made the log (root – or the main part of the program)

Let’s try the same program again, but this time we’ll set the logging level to WARNING:

import logging

fn  ="test.log"  # Warning! Will delete any file called test.log!
logging.basicConfig(filename= fn, filemode='w', level = logging.WARNING)

if __name__=="__main__":
  logging.debug("This is a test entry")

This time, when we print the contents of the file:

>>> f = open(fn,'r')
>>> print f.read()

>>> f.close()

It’s empty! This is because we configured the logging to only show messages which were of WARNING or higher priority.  Since DEBUG is of lower priority it was filtered out.  Most excellent! Imagine if these were print statements instead.  Once the program was finished/or the problem debugged, you need to go back and either remove or comment out all of the print statements (leaving important ones alone – so you can’t necessarily just use search and replace). Now, instead, we simply turn all of them off by a simple change of the logger configuration.  However, it gets better.

The output we initially saw was a little pedestrian. We can configure the logger to provide heaps more information.  Here’s a slightly longer program with a more involved logging parameter:

import logging

fn  ="test.log"  # Warning! Will delete any file called test.log!
logging.basicConfig(filename= fn, filemode='w', level = logging.DEBUG, format= "%(levelname)s %(asctime)s %(funcName)s @%(lineno)d %(message)s")

def functionA():
  logging.debug("We're in function A")

def functionB():
  logging.debug("We're in function B")

if __name__=="__main__":
  logging.debug("This is a test entry")
  functionA()
  functionB()

Look at what we get now when we run the program:

>>> f = open(fn,'r')
>>> print f.read()
DEBUG 2012-10-22 16:26:15,789 <module> @13 This is a test entry
DEBUG 2012-10-22 16:26:15,789 functionA @7 We're in function A
DEBUG 2012-10-22 16:26:15,789 functionB @10 We're in function B

>>> f.close()

Now, we get a bunch more info, including the time and date of the logging message (down to milliseconds).  Not only that, we get the name of the function from which the function call was made and the line number in the program where the function call was made.

Advanced stuff

Knowing the line number in particular is pretty useful.  For example, you can filter the log file for references to that specific line number eg (this only works on systems with grep installed of course):

>grep \@7 test.log
DEBUG 2012-10-22 16:26:15,789 functionA @7 We're in function A

You can even use it to open up your file at the exact line number:

>vi p4kLogging121022C.py +7

This, of course, is only if you’re using the vi editor, but other editors should have “go to line” functions which can also be used.   This is much easier than hunting through your text file trying to find where the logging call was made.

Final comments

Different levels you will typically use are logging.debug, logging.warning and logging.error.  The logging function you use will log whatever string is passed as an argument:

logging.debug("It's this bit which will print")

You can use string formatting here as well to log variable values:

logging.warning("The value of x is %s"%(x))

It is good practice to use a logger for your debugging messages.  If you have a short program it may be easier to just use print statements.  However, as your programs grow, you will definitely need to do logging instead.

More info: Logging how to, logging docs

Slider Spliner

Back to the photo sequence and music. Each photo is on the screen for only two seconds, and in between each there is a click as of a slide projector changing or even the sound of the shutter of a camera. The photos show in sequence: Anthony Barber, Katy Boyle, Edgar Allan Poe, a loony head and shoulders. He has ping-pong ball eyes, several teeth blocked out, a fright wig and his chest is bare but across it is written ‘A Loony’, Reginald Maudling, Tony Jacklin. A buzzer sounds.

In the last tutorial I used the term “quadradic Bezier curve” (which is a particular sort of way of drawing a curved line through three points).  A more generic name for this way of drawing curves is “spline”, named after a strip of flexible material used by draftsmen to draw curves, also now called flexicurves).   In this tutorial we’re going to learn about the Scale widget (but it looks like a slider) and use it to show how a computer draws those smooth curves.

Our starting point is the canvasCurve program from the previous tutorial, but we’ll jettison the mouse motion stuff, make it a bit larger and just draw a fixed, rather large, unhappy looking spline:

 

# -*- coding: utf-8 -*-
from Tkinter import *

TITLE = "Drawing a Curve"
WIDTH = 400
HEIGHT = 400
CENTREX = WIDTH/2
CENTREY = HEIGHT/2
NODE_RADIUS = 3
NODE_COLOUR = "red"
LINE_COLOUR= "yellow"

formatString = "x: %03d, y: %03d"

class Canvassing():
  def __init__(self, parent = None):
    self.canvas = Canvas(width=WIDTH,height=HEIGHT, bg = "blue")
    self.canvas.pack()
    self.readout = Label(text="This is a label")
    self.readout.pack()

    self.line = None
    self.canvas.master.wm_title(string = TITLE)
    self.points = [ (0,HEIGHT-10), (CENTREX,0),(WIDTH,HEIGHT-10) ]
    self.splinesteps = 12
    self.drawTheSpline()

  def drawTheSpline(self):
    allItems = self.canvas.find_all()
    for i in allItems:  # delete all the items on the canvas
      self.canvas.delete(i)

    self.line = self.canvas.create_line(self.points,  width=2, fill = LINE_COLOUR, smooth = True, splinesteps = self.splinesteps)
    for p in self.points:
      self.drawNode(p)
    self.readout.config(text="Steps: %s"%self.splinesteps)

  def drawNode(self, p):
      boundingBox = (p[0]-NODE_RADIUS, p[1]+NODE_RADIUS, p[0]+NODE_RADIUS,p[1]-NODE_RADIUS)
      # mixed + and - because y runs from top to bottom not bottom to top
      self.canvas.create_oval(boundingBox, fill=NODE_COLOUR)

Canvassing()
mainloop()

To this we will add our Scale widget/slider bar.  The Scale widget can be configured in a variety of ways.  In this case we’ve passed it from_ (note the trailing underscore) and to parameters (self.slider = Scale(from_=1, to =20, orient = HORIZONTAL)).  These are the numbers that the widget will range from and to respectively.  If you grab and move the slider you will see its values change, but nothing happens to the curve:

# -*- coding: utf-8 -*-
from Tkinter import *

TITLE = "Drawing a Curve"
WIDTH = 400
HEIGHT = 400
CENTREX = WIDTH/2
CENTREY = HEIGHT/2
NODE_RADIUS = 3
NODE_COLOUR = "red"
LINE_COLOUR= "yellow"

formatString = "x: %03d, y: %03d"

class Canvassing():
  def __init__(self, parent = None):
    self.canvas = Canvas(width=WIDTH,height=HEIGHT, bg = "blue")
    self.canvas.pack()
    self.slider = Scale(from_=1, to =20, orient = HORIZONTAL)
    self.slider.pack()
    self.readout = Label(text="This is a label")
    self.readout.pack()

    self.line = None
    self.canvas.master.wm_title(string = TITLE)
    self.points = [ (0,HEIGHT-10), (CENTREX,0),(WIDTH,HEIGHT-10) ]
    self.splinesteps = 12

    self.drawTheSpline()

  def drawTheSpline(self):
    allItems = self.canvas.find_all()
    for i in allItems:  # delete all the items on the canvas
      self.canvas.delete(i)

    self.line = self.canvas.create_line(self.points,  width=2, fill = LINE_COLOUR, smooth = True, splinesteps = self.splinesteps)
    for p in self.points:
      self.drawNode(p)
    self.readout.config(text="Steps: %s"%self.splinesteps)

  def drawNode(self, p):
    boundingBox = (p[0]-NODE_RADIUS, p[1]+NODE_RADIUS, p[0]+NODE_RADIUS,p[1]-NODE_RADIUS)
    # mixed + and - because y runs from top to bottom not bottom to top
    self.canvas.create_oval(boundingBox, fill=NODE_COLOUR)

Canvassing()
mainloop()

Exercise:  slide the slider from left to right and confirm that it runs over all of the values from from_ to to.

Exercise: try different values for from and to.  Does the slider change its appearance?  If to is much greater than from_  does the slider miss some values in between when you slide it?

I could hook up a listener here to track whether the slider has changed, but I’m just going to add a button.  When you press the button it gets the slider’s value and updates the drawing based on that value:

# -*- coding: utf-8 -*-
from Tkinter import *

TITLE = "Drawing a Curve"
WIDTH = 400
HEIGHT = 400
CENTREX = WIDTH/2
CENTREY = HEIGHT/2
NODE_RADIUS = 3
NODE_COLOUR = "red"
LINE_COLOUR= "yellow"

formatString = "x: %03d, y: %03d"

class Canvassing():
  def __init__(self, parent = None):
    self.canvas = Canvas(width=WIDTH,height=HEIGHT, bg = "blue")
    self.canvas.pack()
    self.slider = Scale(from_=1, to =20, orient = HORIZONTAL)
    self.slider.pack()
    self.updateButton = Button(text="Update!",command = self.update)
    self.updateButton.pack()
    self.readout = Label(text="This is a label")
    self.readout.pack()

    self.line = None
    self.canvas.master.wm_title(string = TITLE)
    self.points = [ (0,HEIGHT-10), (CENTREX,0),(WIDTH,HEIGHT-10) ]
    self.splinesteps = 12

    self.drawTheSpline()

  def drawTheSpline(self):
    allItems = self.canvas.find_all()
    for i in allItems:  # delete all the items on the canvas
      self.canvas.delete(i)

    self.line = self.canvas.create_line(self.points,  width=2, fill = LINE_COLOUR, smooth = True, splinesteps = self.splinesteps)
    for p in self.points:
      self.drawNode(p)
    self.readout.config(text="Steps: %s"%self.splinesteps)

  def drawNode(self, p):
    boundingBox = (p[0]-NODE_RADIUS, p[1]+NODE_RADIUS, p[0]+NODE_RADIUS,p[1]-NODE_RADIUS)
    # mixed + and - because y runs from top to bottom not bottom to top
    self.canvas.create_oval(boundingBox, fill=NODE_COLOUR)

  def update(self):
    # important bit here is to "get" the value in the slider
    self.splinesteps = self.slider.get()
    self.drawTheSpline()

Canvassing()
mainloop()

Exercise: Click the button for different values of the slider.

So, what’s happening here?  When Tkinter draws a smooth line (using create_line) it doesn’t actually draw a curved line.  Rather, it draws a number of much shorter straight lines.  You will notice that the create_line entry has a parameter called splinesteps.  This parameter tells Tkinter how many of these shorter straight lines should be drawn when approximating the curve.  If no number is passed to this parameter, it defaults to 12.  You will see that the spline becomes increasingly more refined as this number is increased.  You should also note that there is a great deal of improvement in the curve when these numbers are small, but much less improvement when the numbers are large (do the next exercise).

Exercise:  See that there is a big change in the curve when this number changes from 1 to 2, and 2 to 3, but very little improvement from 11 to 12 (or even from 12 to 20).

Finally, I want to point out that, since self.slider is a widget, it doesn’t have a value as such so an assignment such as someVariable = self.slider doesn’t make sense to Python, although it is perfectly logical for us. Rather, since self.slider is a Tkinter Scale Widget, it is really an instance of the Scale Class.  This means it has a lot of different attributes and methods (try print dir(self.slider)).  To get the value of the slider at any point, you need to use its get() method.  This is a common pattern for Tkinter widgets so you’ll need to get() used to it.

Quadratic Bezier Curves

An open field. A large hamper, with an attendant in a brown coat standing behind it. The attendant opens the hamper and three pigeon fanciers, (in very fast motion) leap out and run off across the field, wheeling in a curve as birds do.

Or, in other words, smooth lines.  This is not the tutorial I had intended to write, but the tutorial I had intended to write is too long in coming.  I have been working on a stick figure program in Python and need to prototype drawing curved lines using Tkinter. I figured I may as well incorporate it into a tutorial.

A Loose End

At the end of the Being Animated tutorial I set some exercises – I hope you’ve done them by now.  One of the questions I asked was why I chose a width of 21 pixels and not 20?  There is no magic in the number 21 per se.  Rather, the issue is whether or not the number is an even or an odd number.  Harking back to our discussion about pixels you should realise [sic] that pixels on the screen are not like marks on a ruler.  Marks on a ruler take up no space pixels, on the other hand, do.  If you chose an even number of pixels for something you want to centre [sic] somewhere the centre pixel will be at the centre.  Then you will have a different number of pixels on either side.  To take an extreme example, if it was two pixels wide, then one pixel would be at the mouse’s location, and one would be left hanging.  There would not be a way to balance it.  However, if it was three pixels wide, one could be at the mouse position, and one could be on either side.

Introduction

In the Being Animated tutorial we also saw how to draw a line on a Tkinter Canvas Widget.  In this tutorial we’re going to look at how to draw a curved line. I have used the code from canvasLine2B.py in that tutorial as a starting point for the code below – save this to canvasCurve1.py:

# -*- coding: utf-8 -*-
from Tkinter import *

TITLE = "Drawing a Curve"
WIDTH = 200
HEIGHT = 200
CENTREX = WIDTH/2
CENTREY = HEIGHT/2

formatString = "x: %03d, y: %03d"

class Canvassing():
  def __init__(self, parent = None):
    self.canvas = Canvas(width=WIDTH,height=HEIGHT, bg = "blue")
    self.canvas.pack()
    self.readout = Label(text="This is a label")
    self.readout.pack()
    self.canvas.bind("<Motion>",self.onMouseMotion)
    self.line = None
    self.canvas.master.wm_title(string = TITLE)
    self.points = [(CENTREX-WIDTH/4, CENTREY-HEIGHT/4),
		    (CENTREX, CENTREY)
		    ]

  def onMouseMotion(self,event):  # really should rename this as we're doing something different now
    self.readout.config(text = formatString%(event.x, event.y))
    self.canvas.delete(self.line) # first time through this will be None
    # but Tkinter is ok with that
    self.line = self.canvas.create_line(self.points, (event.x,event.y), width=2, fill = "yellow")

Canvassing()
mainloop()

You can see that I’ve:

  • removed the magic number 100 from the code, and replaced it by WIDTH and HEIGHT – capitalised variable names to indicate that they are (or should be) global constants;
  • added a list called self.points which, at the moment, contains two points;
  • change the name of the second method to onMouseMotion (because it’s what is done when there’s a movement of the mouse);
  • increased the width of the line so it is a little easier to see;
  • changed the drawing code.  Now the two points above and the current position of the mouse (event.x, event.y) are fed into create_line;
    and
  • added a title to the window.

This gives us something that looks like this (except that the second line segment follows the mouse, something this still image doesn’t do justice to):

Exercise: Try some other values of WIDTH and HEIGHT to verify that the program still works.

Exercise: Try adding more entries to self.points (in the form (x,y) with commas between them all, but none after the last one)

Drawing nodes

To demonstrate that there are three points involved I am going to add some code to draw a small circle around each point, and moving the one under the mouse pointer with the mouse:

# -*- coding: utf-8 -*-
from Tkinter import *

TITLE = "Drawing a Curve"
WIDTH = 200
HEIGHT = 200
CENTREX = WIDTH/2
CENTREY = HEIGHT/2
NODE_RADIUS = 3
NODE_COLOUR = "red"
LINE_COLOUR= "yellow"

formatString = "x: %03d, y: %03d"

class Canvassing():
  def __init__(self, parent = None):
    self.canvas = Canvas(width=WIDTH,height=HEIGHT, bg = "blue")
    self.canvas.pack()
    self.readout = Label(text="This is a label")
    self.readout.pack()
    self.canvas.bind("<Motion>",self.onMouseMotion)
    self.line = None
    self.canvas.master.wm_title(string = TITLE)
    self.points = [(CENTREX-WIDTH/4, CENTREY-HEIGHT/4),
		    (CENTREX, CENTREY)
		    ]

  def onMouseMotion(self,event):  # really should rename this as we're doing something different now
    self.readout.config(text = formatString%(event.x, event.y))
    allItems = self.canvas.find_all()
    for i in allItems:  # delete all the items on the canvas
      self.canvas.delete(i)
    # deleting everything every time is inefficient, but it doesn't matter for our purposes.
    for p in self.points:
      self.drawNode(p)
    p = (event.x, event.y)  # now repurpose p to be the point under the mouse
    self.line = self.canvas.create_line(self.points, p, width=2, fill = LINE_COLOUR)
    self.drawNode(p)

  def drawNode(self, p):
      boundingBox = (p[0]-NODE_RADIUS, p[1]+NODE_RADIUS, p[0]+NODE_RADIUS,p[1]-NODE_RADIUS)
      # mixed + and - because y runs from top to bottom not bottom to top
      self.canvas.create_oval(boundingBox, fill=NODE_COLOUR)

Canvassing()
mainloop()

This gives something similar to the picture above, but with the nodes clearly shown:

Magic Pixie Dust

Once we’ve done this, drawing a curved line is a snap.  We simply add a new parameter, smooth, to the create_line method call:

    self.line = self.canvas.create_line(self.points, p, width=2, fill = LINE_COLOUR, smooth = True)

Now you can see the curve clearly shown as well as where the points are:

A Short Aside – Some Python Minecraft Stuff

The blog has been on holiday for many weeks now, following my own holiday. I am looking to get back into it over the next little while. In the meantime I have done some Google searches for Minecraft related programs written in Python that might be of interest/use.  I have not run any of them myself though, so reviews welcome.
Minecraft Screenshot Viewer in wxPython by Davy Mitchell. This viewer is written using wxPython, which is a Tkinter substitute.  You may need to install wxPython separately for it to work.  The code itself is here.

A variety of Minecraft scripts for editing save files. The site’s maintainer, Paul Spooner has asked for feedback on the scripts (see comments below) so get in contact if you have any.

MCSuperServer, which will automatically start your server for you when someone tries to connect.

Handle - a front end for bukkit it allows you to easily automate mundane tasks associated with running a server.

Homework: Open at least one of these in a text editor/online and try to understand how the code works/does what it does.

Being Animated

CAPTION: ‘AN APOLOGY’
Voice Over     The BBC would like to apologize for the constant repetition in this show.
    DIFFERENT CAPTION READING: ‘AN APOLOGY’
Voice Over     The BBC would like to apologize for the constant repetition in this show.
    ANIMATION: the ‘five frog curse’.
Cut to the five Gumbys standing in a tight group.

In the last tutorial we looked at the Canvas widget from Tkinter and we also had a look at the concept of cartesian coordinate systems. In our example we could identify specific pixels by counting across and down from the top left of the canvas (ordinarily cartesian systems count left and up).  We also bound the motion event to the canvas and used it to display the current coordinates of the mouse in the canvas.  The reason we did this is because to draw anything on the canvas you need to know the coordinates of where it is to be drawn.

Part of the homework was to identify what it was about the readout which didn’t make sense.   I didn’t see any comments on the tutorial, so I assume you’re baffled by what didn’t make sense.

Answer: if you move the mouse around enough, you can get pixel positions of (0,0) and (101,101).  This means that there are 102 pixels (from 1 to 100 is 100 plus one for each of 0 and 101 gives 102).  This is odd because when we set up the program we specifically said we wanted both the height and width to be 100 pixels.   Where are these two extra pixels in each direction coming from?  If you have a screen magnifier you can see it yourself, but you can also see it on the enlarged picture from the previous tutorial.  There is a white border around the blue canvas area:

It is this border where the extra pixels come from.

The canvas widget has a variety of methods for drawing stuff on the canvas area.  To use these methods you need to provide start and end coordinates.  Here is a yellow line drawn in the centre of the canvas from top to bottom:

To create this I added:

    start = (50,0)
    end = (50,100)
    self.canvas.create_line(start, end, fill = "yellow")

to the earlier code after the self.canvas.bind line.  The thing to note here is the use of the coordinate system we discussed.

Exercise 1: swap the values of start and end.  What difference does this make?  Keep end static and try some different combinations for start.  Try to predict what it will look like before you run the program.

Exercise 2: change the code so that the line runs left to right through the middle rather than up and down.

Our next task is to try to animate this line.  We already have code to track mouse movements over the canvas, so let’s repurpose it to draw a line where the mouse pointer is (removing the code for the line in the middle):

# -*- coding: utf-8 -*-
# canvasLine2.py
from Tkinter import *

formatString = "x: %03d, y: %03d"

class Canvassing():
  def __init__(self, parent = None):
    self.canvas = Canvas(width=100,height=100, bg = "blue")
    self.canvas.pack()
    self.readout = Label(text="This is a label")
    self.readout.pack()
    self.canvas.bind("<Motion>",self.updateCoordinates)

  def updateCoordinates(self,event):
    self.readout.config(text = formatString%(event.x, event.y))
    start = (event.x, 0)
    end = (event.x, 100)
    self.canvas.create_line(start, end, fill = "yellow")

Canvassing()
mainloop()

If you run this and move the mouse around you will see something like this:

What’s going on here is that we are drawing new lines, but we aren’t erasing the old ones.  The computer, being “incredibly fast, accurate and stupid” doesn’t realise that we’re only interested in having one of the lines we’ve drawn present at any one time. We could recycle a single line moving it around, but that would require me to explain the concept of deltas and offsets, which you’ll need to look up for yourself.  Instead, we’re going to delete each line and draw a new one each time the mouse is moved.

# -*- coding: utf-8 -*-
#canvasLine2B.py
from Tkinter import *

formatString = "x: %03d, y: %03d"

class Canvassing():
  def __init__(self, parent = None):
    self.canvas = Canvas(width=100,height=100, bg = "blue")
    self.canvas.pack()
    self.readout = Label(text="This is a label")
    self.readout.pack()
    self.canvas.bind("<Motion>",self.updateCoordinates)
    self.line = None

  def updateCoordinates(self,event):  # really should rename this as we're doing something different now
    self.readout.config(text = formatString%(event.x, event.y))
    start = (event.x, 0)
    end = (event.x, 100)
    self.canvas.delete(self.line) # first time through this will be None
    # but Tkinter is ok with that
    self.line = self.canvas.create_line(start, end, fill = "yellow")

Canvassing()
mainloop()

Now when you run the code you should get a single vertical line which follows your mouse pointer as it moves over the canvas.  Hey presto! You’ve just done your first animation.  Animation on computers involves deleting stuff one the screen which is out of date (or “painting” over it) and then replacing it with new stuff.  The value of self.line here is just an integer.  This is a reference that Tkinter uses to identify the objects it has drawn on the screen.

Exercise: add a print statement to print out the value of self.line for each call to updateCoordinates()

Exercise: change the code so that instead of a vertical line there is a cross hair (with a horizontal line running across the canvas) following the mouse pointer.  Hint: you need to draw two lines

Extra points: make the cross hair only 21 pixels wide centred on the mouse pointer.

Extra extra points: why 21 pixels and not 20?

Canvassing

Cut to Art Gallery. A large sign says: ‘Italian Masters of the Renaissance’. Two art critics wandering through. They stop in front of a large Titian canvas. The canvas is about ten foot high by six foot wide.
First Critic     Aren’t they marvelous? The strength and boldness… life and power in those colours.
Second Critic     This must be Titian’s masterpiece.
First Critic     Oh indeed – if only for the composition alone. The strength of those foreground figures … the firmness of the line…
Second Critic     Yes, the confidence of the master at the height of his powers.

In this tutorial we are going to have a look at a new Tkinter widget called the Canvas.  A canvas widget, is, somewhat like a real canvas, something that you can throw paint or other stuff all over.  If you want to program games or animation you need to have an understanding of coordinate systems and computer animation.  A basic understanding of coordinates is the main aim of this tutorial.  In the process we will also incidentally cover another type of event – Exercise – find the new event. What is it called?

For this tutorial I want you to create a new file in your python4kids directory called canvassing.py

# -*- coding: utf-8 -*-
from Tkinter import *

class Canvassing():
  def __init__(self, parent = None):
    self.canvas = Canvas(width=300, height=200, bg = "blue")
    self.canvas.pack()

Canvassing()
mainloop()

This code, when run, should give you something that looks like this (click the close widget to close):

The default background is a sort of white, and I thought you might not be able to tell against the WordPress site, so I used blue instead.  Isn’t Python clever to know what blue is?  I hope you can also tell that if you put different numbers in for width and height you’d get a different looking rectangle.

Exercise: Change the height or width parameters (but not both) and see what you get, try a couple of different values.  Now try changing both the height and width parameters.

It’s important that you do the exercise as it demonstrates a relationship between the numbers and the size of the grid.  In our example, we have a rectangle which is wider (300) than it is tall (200).  In fact, if you were to look closely and count carefully, you would find that there are literally 300 blue dots along the bottom of the rectangle and 200 blue dots running up each side.

Exercise: Change width to be 10 and the height to be 1:

    self.canvas = Canvas(width=10,height=1, bg = "blue")

now, go find a magnifying glass and count the dots in the canvas.

Each of those dots is called a pixel (short for picture element).  Our original canvas was 300 pixels wide and 200 pixels high.  It had a total of 60,000 (ie 300×200) individual pixels!  Computers display their information by changing each of those individual pixels. Now, let’s make another tiny canvas and draw two tiny (1 pixel wide) lines on our new tiny canvas (save this to canvassing10x2.py):

# -*- coding: utf-8 -*-
from Tkinter import *

class Canvassing():
  def __init__(self, parent = None):
    self.canvas = Canvas(width=10,height=2, bg = "blue")
    self.canvas.pack()
    self.canvas.create_line(1,1,2,1,fill="red")
    self.canvas.create_line(5,2,6,2,fill="yellow")

Canvassing()
mainloop()

This should give you a window that looks something like this (grab an edge and expand it to get a close widget):

With this you should just about be able to see the two pixels we coloured in (don’t do this generally by the way, Tkinter is not built for doing pixel operations).  The first is the red pixel in the top left corner.  The second is the yellow pixel on the bottom in the middle.  In case you don’t have a magnifying glass, here’s a photo I took of my screen which shows it a little better:

The pixel boundaries are obvious in this photo (see the grid lines?).  If you look hard you can count the pixels (the photo is a little too good because it shows the subpixel array* used by my monitor to create the colours). For good measure here is the original picture blown up 4x:

So, why is the red in the top left and the yellow in the bottom middle?  Well, the canvas is  10 pixels wide and is 2 pixels high. So you can identify each of the pixels by whether they’re on the top row or bottom row, and how far along they are.  In this case, we created a red line starting at 1,1 and ending at 2, 1.  In each case the first number is how far along from the left and the second number is how far down from the top.  So 1,1 is the first pixel on the first line while 2,1 is pixel two on line 1 (if you look closely we’ve actually ended up with a black pixel at pixel 2.

In the second case (5,2,6,2) we drew a line from pixel 5 on line 2 (5,2) to pixel 6 on line 2 (6,2).  Since 5 is midway between 1 and 10 it looks like it’s in the middle.

So, what’s all this about?  Well anything to do with manipulating a canvas is about coordinates. That is, what pixel on what line.  Larger canvases just mean more pixels to play with.  Here we had 20 pixels (ten pixels each line on two lines) but our original example had 60,000.  A computer screen with a resolution of 1024 x 768 (which, as at June 2012, Wikipedia alleges represents about 20% of web users) has three quarters of a million pixels (786,432).   The bottom line is that if you understand how coordinates work, then you’re already a good way there to doing computer graphics and animations.

In the last example we use an event to print out the coordinates of the mouse as you move it around the canvas (save this to canvassingCoordinates.py):

# -*- coding: utf-8 -*-
from Tkinter import *

formatString = "x: %03d, y: %03d"

class Canvassing():
  def __init__(self, parent = None):
    self.canvas = Canvas(width=100,height=100, bg = "blue")
    self.canvas.pack()
    self.readout = Label(text="This is a label")
    self.readout.pack()
    self.canvas.bind("<Motion>",self.updateCoordinates)

  def updateCoordinates(self,event):
    self.readout.config(text = formatString%(event.x, event.y))

Canvassing()
mainloop()

Homework: 1. There’s something about the readout which doesn’t make sense.  What is it?

2. Explain what is happening here.  How does the label readout work?

* On my LCD each “pixel” is actually a group of three smaller pixels, one red, one blue, one green.

Being Exceptional

Lady     Yes this looks the sort of thing. May I just try it?
Assistant     Certainly, madam.
    The lady presses button and a sheet of flame shoots out across the hall.
Lady     Oh! Sorry! So sorry! (she is happy though) Yes that’s fine.
Assistant     Is that on account, madam?
Lady     Yes.

Apparently, in Python, it is easier to ask for forgiveness rather than seek permission.   That is to say, the normal approach when writing Python code is to assume that what you are trying to do will work properly.  If something exceptional happens and the code doesn’t work the way you were hoping, then the Python interpreter will tell you of the error so that you can handle that exceptional circumstance.  This general approach, of trying to do something, then cleaning up if something goes wrong is acronymically called EAFP (“easier to ask for forgiveness than permission.  Here is a (somewhat silly) example:

>>> a= 1
>>> b="2"
>>> a+b
Traceback (most recent call last):
File "", line 1, in
TypeError: unsupported operand type(s) for +: 'int' and 'str'

What has happened is that we have tried to add a word (ie the string “2″) to a number (the integer 1).  Addition doesn’t make any sense in this circumstance, so Python “throws” an “exception”.  In this case the exception is called a TypeError.   The problem with this is that when an exception is raised, then unless the program deals with the exception, the Python interpreter will step in, stop the program and print an error message like the one above.

Python deals with this by the try/except structure. You try to do the statements in the first block of code, and if there is an exception (that is, you failed when you tried), then you do the statements in the second block of code.

>>> try:
...    print a+b      # this is the first block of code and can be multiple statements
... except TypeError:
...    print "Can't add those things together!" # this is the second block of code
...
Can't add those things together!

Can you see that no exception was raised here?  As far as the Python interpreter is concerned, everything ran smoothly.  The program tried to print a+b, and, in doing so, tried to work out what a +b is.  However, it failed because a is a number and b is a string.  Because it failed nothing got printed in the first block of code.  Also because the specific failure was a TypeError, the second block of code was run.

A short code snippet can show you how this works:

>>> c = [1,"2",2,3,4,5]
>>> sumOfc = 0
>>> # Try Number 1 - this will fail
>>> for number in c:
...     sumOfc += number
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
TypeError: unsupported operand type(s) for +=: 'int' and 'str'
>>> # Try Number 2 - this will work
>>> sumOfc = 0  # need to reset the sum
>>> for number in c:
...     try:
...        sumOfc += number
...     except TypeError:
...        pass
...
>>> sumOfc
15

In the first try, the interpreter ran into the string “2″ and didn’t know what to do, so gave up.  In the second try, the interpreter knew exactly what to do if there was a problem with the addition (pass – or, in other words “do nothing”[see note 1]) so it went through the whole of the array adding up the things that were numbers without complaining.

Of course, you don’t use try/except blocks around everything.  You will usually have an idea about the parts of the code where something goes wrong.  You can put that code in a try/except block, with code to deal with a failure.  What you shouldn’t ever do is this:

>>> sumOfc = 0
>>> c = [1,"2",2,3,4,5]
>>> for number in c:
...     try:
...        sumOfc += number
...     except:   # <- Don't do this!
...        pass
...
>>> sumOfc
15

It is not entirely obvious what is wrong here – the code works just as well as before.  The only difference is in the except line – there is no exception specified.   The reason that this is bad isn’t that the code doesn’t work well, but, rather, that it works too well.  Every exception will be caught here, not just the one that you were expecting.  This is bad because if some other problem arises in your try block, you’ll never learn about it and your exception code will probably deal with it incorrectly.  You won’t know why your program doesn’t work because the interpreter won’t tell you.

The other approach to dealing with possible errors is called Look Before You Leap (LBYL).  LBYL involves making sure everything is right before you actually do the thing you do the problematic operation.  So, for example, you might check that what you were trying to add was an integer:

>>> c = [1,"2",2,3,4,5]
>>> sumOfc = 0
>>> for number in c:
...    if isinstance(number,int):
...       sumOfc += number
...
>>> sumOfc
15

Here the isinstance() function is another feature of Python introspection.  It tests whether the variable number is an integer (“int”).  So this code doesn’t even bother trying to add the number unless its an integer.  It first checks “is this an integer I see before me?”  If so, it adds it, if not, it ignores it.   Which is fine as far as it goes…

The problem with this is that it’s the wrong way around.  You’re not really interested in whether or not the thing you’re adding is an integer.  Rather, you’re interested in knowing whether addition is meaningful [see note 2].  In fact, this way of approaching things is broken:

>>> c = [1,"2",2,3,4,5.2]
>>> sumOfc = 0
>>> for number in c:
...    if isinstance(number,int):
...       sumOfc += number
...
>>> sumOfc
10

We’ve changed the last number in the array to be 5.2, but this causes it to be ignored because it isn’t an integer – ooops!  Applying the earlier try/except code gives the right result though:

>>> c = [1,"2",2,3,4,5.2]
>>> sumOfc = 0
>>> for number in c:
...     try:
...         sumOfc += number
...     except TypeError:
...         pass
...
>>> sumOfc
15.199999999999999

Well, accurate up to the rounding error…  Our try/except code gave us floating point addition for “free”, where the LBYL failed.  In fact, it’s even more interesting because how it works is dependent on what we define sumOfc to be:

>>> sumOfc = ''
>>> for number in c:
...     try:
...         sumOfc += number
...     except TypeError:
...         pass
...
>>> sumOfc
'2'

Wow! If that didn’t make you giddy with excitement, I don’t know what will.  The same code works if we make sumOfc a string.  Implicitly then we’re asking Python  to join the strings (which is what + means for a string) together and ignore stuff that can’t be joined.   We got this entirely for free from our try/except structure, something that would have needed reworking in the LBYL.  In fact, we could use this structure for any object that has addition defined for it.  It’s this sort of clever which makes Python so good.

Caveat:

Normally the place to use exceptions is where, every once in a while, something out of the ordinary will happen and you have some idea about how it will be out of the ordinary.  In the first case, if it’s not unusual then it’s not exceptional – don’t handle it with exceptions.  In the second case, if you can’t identify how it will be out of the ordinary you can’t deal with it in your except block.

Notes:

1. The point of the except block is to deal with any problems in the try block.  In this case you might try to convert the value to an integer in order that it could be added.

2. Implicitly we’re talking here about breaking interfaces. You, as a programmer, shouldn’t be concerned with what is going on under the hood.  You should be able to rely on the fact that an addition method is defined for an object.  If it is, then  you ought to be able to use it without having to know the internal details of the object.  So in the example here, addition is meaningful between integers and floating point numbers, but addition is also meaningful between two strings.

Weird Binding Stuff

Voice Over     This man is Ernest Scribbler… writer of jokes. In a few moments, he will have written the funniest joke in the world… and, as a consequence, he will die … laughing.
    Ernest stops writing, pauses to look at what he has written… a smile slowly spreads across his face, turning very, very slowly to uncontrolled hysterical laughter… he staggers to his feet and reels across room helpless with mounting mirth and eventually collapses and dies on the floor.

Summary: id(), copy, copy.copy(), copy.deepcopy()

A short tutorial this week on some oddities with the way Python stores and references (“binds to”) data.   You might remember, a long time ago, we talked about how, when we store data in a variable, it’s like putting your stuff in a bucket so that you can access it later.  Variables in Python actually turn out to be references to objects.   A side effect of this is that, in some cases, Python doesn’t work out how you think it will – typically this is where your object is a list or dictionary (actually any object, but you only notice this effect with compound objects) that you think you have copied, but you actually haven’t.

In particular, you can do this with ‘plain’ variables:

 >>> a = 5
 >>> b = a
 >>> a = 6
 >>> b
 5

You can also do this with lists:

 >>> c = [1,2]
 >>> d= c
 >>> c =[5,6]
 >>> d
 [1, 2]

But there’s a gotcha with lists where you change one of the list’s entries:

 >>> c= [1,2]
 >>> d = c
 >>> d
 [1, 2]
 >>> c[0]=3
 >>> d
 [3, 2]

Can you see that, even though we only changed the first entry in the list c (that is, c[0]), the first entry of d has also changed?  That’s because there is an underlying list object that both c and d are pointing to.  That is, they are both pointing to the same thing.  In a sense they are both windows to the same room (the list object).  Looking in either window allows you to “see” the changes made in the room.   You can see that the objects are the same because you can check their location in memory using the id() function which is built in to Python (try help(id)):

 >>> id(c)
 139636641421288
 >>> id(d)
 139636641421288
 >>> id(c) == id(d)
 True

The number (139636641421288) is where in the computer’s memory the object is stored.  It will change, probably each time you run the program.  If we assign a different list to d, it will have a different id, even though the values in the list are the same:

 >>> d = [3,2]       # note this new list has the same values as the old one
 >>> id(c) == id(d)
 False
 >>> id(d)
 139636640593824
 >>>

We can see that this other list is stored in a different location because the id() of the lists is different.  It turns out that this referencing behaviour is actually what you want to happen in most cases.  However, every so often you want your lists to be separate.  For that there is a special module called copy.  The copy module has a method (also called copy) which allows you to copy across the values of an object, rather than simply referencing (called “binding“) to an existing object:

>>> import copy
>>> d = copy.copy(c)
>>> d
[3, 2]
>>> c[0]=1
>>> d
[3, 2]
>>> c
[1, 2]

When you use copy.copy() the two objects will be separate and can be used independently.  Changes to one won’t show up in the other.  Where a compound object like a list or a dictionary has values which themselves are compound objects – for example a list where each entry in the list is itself a list – use the copy.deepcopy() method.   Depending on the complexity of your objects deepcopy() is not guaranteed to work (objects which refer to themselves somehow can cause a problem), but generally you will be fine.

Recap on Progress

Interviewer     Well… lets move on to our guest who not only lives in Essex but also speaks only the ends of words. Mr Ohn Ith. Mr Ith, good evening.
Enter from back of set as per Eamonn Andrews show Mr Ohn Ith. He sits at the desk
Mr Ith     … ood … ing.
Interviewer     Nice to have you on the show.
Mr Ith     … ice … o … e … ere.
Interviewer     Mr Ith, don’t you find it very difficult to make yourself understood?
Mr Ith     Yes, it is extremely difficult.
Interviewer     Just a minute, you’re a fraud
Mr Ith     Oh no. I can speak the third and fourth sentences perfectly normally.
Interviewer     Oh I see. So your next sentence will be only the ends of words again?
Mr Ith     T’s… ight.

Intermezzo

Ooops, sorry.  What was going to be a short respite after Easter became a rather extended vacation.

The good news is that, we’ve finally done enough to pretty much do any application you want (excepting, perhaps, graphical games).  The bad news is that I’m in a quandry about whether to fill out a few holes in the basics (which can be done in bite size chunks), or move onto more advanced stuff (like pygame, which is definitely on the menu).   Not so much a problem for this lesson though – it’s a review of where we’re up to.

I have also started putting together an index, which I am slowly catching up on. If you’re looking for a particular topic that I’ve covered, try the Index (although it’s not yet complete).

Recap

So, what’s the recap on what we’ve done recently?

In our past few tutorials we have created our own GUI application which actually does something useful (more or less).  Some of the things we covered are:

  • configuration files
  • parsing (that is, breaking data down into pieces that our program can make sense of)
  • we had a specific example of using instances of a class to store each configuration item
  • we read a configuration file and parsed it
  • we got to see some new widgets: Tkinter TextEdit, Entry and Frame widgets
  • we saw how to use a Tkinter Frame Widget as a way of collecting a group of related widgets together (don’t be scared of using Frames to help layout things correctly.  In some layouts you can have heaps of Frames)
  • we have tested a layout with a single row of widgets before generalising it for every widget[* see the process point below]
  • subclassing our original widget to add some GUI related functionality
  • why widgets are not like variables and how to get the letters currently in the Entry widget
  • a little bit of data verification – that true/false values remain true or false (extra points would be to use a RadioButton widget)
  • backing up the existing file and
  • writing data that we’ve collected to the file

Some Notes

Of these, I wanted to point out the process item.  that is, testing a layout with a single row of widgets first.  It is generally a good idea to break down any problem that you’re faced with into a number of smaller subtasks before solving the subtasks one after another.  At the time, this will seem tedious because in your mind you want to solve the big issue, not some small issue.  However, it’s the best way to (not only) actually reach a solution, (but also) to reach a solution which you can reuse in the future.  Often, trying to solve the larger task in one go will end up either in failure because it’s too complex (at which time you’re forced to break it up into subtasks anyway), or with a solution which is so specific to your circumstances that it can’t be reused.  Take the extra time to break down the task.

In the meantime I will think about the tutorials to come.

Minecraft config editor – Part, the Ultimate

Boss     (unfolding big map across table; talking carefully) Right … this is the plan then. … At 10:52, I shall approach the counter and purchase a watch costing £5.18.3d. I shall then give the watch to you, Vic. You’ll go straight to Norman’s Garage in East Street. You lads continue back up here at 10:56 and we rendezvous in the back room at the Cow and Sickle, at 11:15. All right, any questions?
Larry     We don’t seem to be doing anything illegal.
Boss     What do you mean?
Larry     Well … we’re paying for the watch.
Boss     (patiently) Yes…
Larry     (hesitating) Well… why are we paying for the watch?
Boss     (heavily) They wouldn’t give it to us if we didn’t pay for it, would they… eh?

This is our final instalment [sic] of our Minecraft config editor.   In the earlier tutorials we have done everything except actually updating the file.  Before we do update the file though, we need to make a backup of it, so it’s these two things that we’re going to do now.

All of the action will be in changing the behaviour of the ‘Ok’ button so that it makes a copy of server.properties into a new file called server.properties.bup and then writes the updated data to the server.properties file.  There is a small amount of work in making a copy of the file so we are going to do it in a separate function.  The function is not very smart.  It reads all of the data from the existing file and then just writes it out to the new file:

def backupFile(fileName):
  ''' Quick and dirty copy of file to fileName+".bup" - might also use os.rename(), but behaviour of os.rename is platform dependent '''
  fileObject = open(fileName,'rb')
  fileData = fileObject.read()
  fileObject.close()
  fileObject= open(fileName+".bup",'wb')  # will overwrite if it exists
  fileObject.write(fileData)
  fileObject.close()

The function could also use the rename() method from the os module.  Unfortunately, this behaves differently depending on the operating system you are using, so to keep it simple I have avoided it.

Exercise: Work out what this function is supposed to do, then confirm that it does it eg: start up a Python console, paste the definition in and then call the function with “server.properties” as a parameter.

Extra Points: If you are on a Unix based system, use the diff command to show the differences between the original and .bup (there shouldn’t be any).

With that done we can hook up the backup to the ‘Ok’ callback, and also write the new data:

def okClicked():
  '''Get the edited values and write them to the file then quit'''
  #TODO: add a confirmation dialog
  global fileName
  backupFile(fileName)
  dataToWrite = []
  for c in configLines:
    c.update()
    dataToWrite.append(c.item2ConfigLine())
  # have updated and printed each line, now exit
  fileObject = open(fileName,'wt')
  fileObject.write('\n'.join(dataToWrite)+'\n')
  # '\n' is technically not the newline character on Windows
  # but by default Python converts \n to the correct character on write
  # not sure if minecraft needs a final '\n', so included one just in case
  fileObject.close()
  exit()

Here we have introduced an array called dataToWrite.  Where, in the last tute we just printed out the line, in this tute we are appending those lines to the dataToWrite array.  Then, once they have been accumulated, we open the server.properties file (clearing it) then write our new data into it.  Only one generation of backup is saved.

We have used the global statement here to use the value of the fileName variable.  This is a little messy, but is a consequence of how the program has evolved.

One of the things that you might include here is a confirmation step.  Before the data gets overwritten we might ask the user to confirm that they are going to write over their data, giving them a second chance if they clicked “Ok” by mistake.

Complete code here:

# -*- coding: utf-8 -*-

'''Minecraft config editor:
This is an editor for the Minecraft server.properties file.  It:
* opens the file server.properties
* reads, then closes the file
* parses each line by
-- stripping leading and trailing whitespace
-- if the line starts with "#", marks it as a comment
-- splits the line into a key, value pair, with the pair separated by a "=" sign
-- if the value of the pair is either "true" or "false", the entry is marked as a boolean (ie its only values are either true or false)
* displays each key, value entry on the screen allowing you to edit it
* renames the server.properties file to server.properties.bup (overwriting any existing file of that name from earlier edits)
* opens a new file called server.properties
* writes each of the entries to that new file
* closes the server.properties file.
'''

from Tkinter import *


class configItem(object):  # name of the class, it is based on an object called 'object'
  def __init__(self, line):# this is called each time an instance of the class is created
    line = line.strip()  # this removes any white space at the start or end of the line
    # if it starts with a # it's a comment so check for it
    if line[:1] == "#":
       self.configKey = "#"
       self.configVal = line[1:]
    else:  # otherwise assume it's of the form x = y
       spam = line.split("=")
       self.configKey = spam[0]
       self.configVal = spam[1]
    # now check to see whether the config item takes only the values "true" and "false" 
    if self.configVal.lower() in ["true","false"]:
       self.isTrueFalse = True
    else:
       self.isTrueFalse = False
       
  def item2ConfigLine(self):
    if self.configKey=="#":
      '''If the key is '#' then this is a comment, so don't include an '=' sign'''
      return "%s%s"%(self.configKey, self.configVal)
    else:
      '''otherwise, it has the form key=value'''
      return "%s=%s"%(self.configKey,self.configVal)
      
   

class guiConfigItem(configItem):
  def __init__(self,line):
    super(guiConfigItem,self).__init__(line)  # run configItem's __init__ method
    self.frame = Frame()
    self.keyLabel = Label(self.frame, text = self.configKey)
    self.valueEntry = Entry(self.frame, width="60")
    self.valueEntry.insert("0",self.configVal)
    self.keyLabel.pack(side=LEFT )
    self.valueEntry.pack(side=RIGHT)
    self.frame.pack(side=TOP, fill="x")
 
  def update(self):
    ''' Get the value which is currently in the Entry widget and save it to configVal'''
    if self.isTrueFalse:
      '''if isTrueFalse is True, then we should only have the values 'true' and 'false' in this
      Entry.  So, only update the configuration value if it is one of these two.  Otherwise, ignore it. '''
      spam = self.valueEntry.get()
      if spam in ['true','false']:
	self.configVal = spam
    else:
      '''this is not a variable which is limited to 'true' and 'false', so store the whole text'''
      self.configVal = self.valueEntry.get()
    
    
# get data from the file

fileName = "ser_ver.properties"
fileObject = open(fileName,'rb')
fileData = fileObject.read()
fileObject.close()



root = Tk()
configLines = []

for line in fileData.split('\n'):  # this splits it into individual lines
    if line.strip()=='':
      continue
    configLines.append(guiConfigItem(line=line))

# 1. create callbacks for each of the buttons, 
def okClicked():
  '''Get the edited values and write them to the file then quit'''
  #TODO: add a confirmation dialog
  global fileName
  backupFile(fileName)
  dataToWrite = []
  for c in configLines:
    c.update()
    dataToWrite.append(c.item2ConfigLine())
  # have updated and printed each line, now exit
  fileObject = open(fileName,'wt')
  fileObject.write('\n'.join(dataToWrite)+'\n')
  # '\n' is technically not the newline character on Windows
  # but by default Python converts \n to the correct character on write
  # not sure if minecraft needs a final '\n', so included one just in case
  fileObject.close()
  exit()

def backupFile(fileName):
  ''' Quick and dirty copy of file to fileName+".bup" - might also use os.rename(), but behaviour of os.rename is platform dependent '''
  fileObject = open(fileName,'rb')
  fileData = fileObject.read()
  fileObject.close()
  fileObject= open(fileName+".bup",'wb')  # will overwrite if it exists
  fileObject.write(fileData)
  fileObject.close()

def cancelClicked():
  '''Cancel edits and quit'''
  exit()

# 2. create a frame for the buttons to go in
bottomFrame = Frame(root)

# 3. create the buttons, hooking up each of the buttons up to the callback
okWidget = Button(bottomFrame, text= "Ok", command = okClicked)
cancelWidget = Button(bottomFrame, text="Cancel", command = cancelClicked)

# 4. pack the buttons, then, finally,
okWidget.pack(side=LEFT)
cancelWidget.pack(side=RIGHT) 
# 5. then pack the frame:
bottomFrame.pack(side=BOTTOM)

root.mainloop()


Exercise: confirm that “Ok” saves your edits (open server.properties in a text editor or (extra points) write some Python to read and print the contents of the file) and that “Cancel” doesn’t.

Comments:

The code is a little messy because of how it has evolved in the course of explaining it.  Having code growing organically and getting messy is not unusual.  Every once in a while you need to stop and clean it up.  Cleaning it up can also allow you to restructure your code in ways you didn’t realise when you were writing it in the first place.

PS

My class names are naughty.  They should start with a capital letter.

Almost There! Adding Methods to Our Classes

Mr Mann     Ee ecky thump! (indicates more power)
Third Booth     Ee ecky thump!
Mr Mann     Excellent.
Third Booth     Thank you, sir. (puts earphones on, listens)
Mr Mann     It’s a really quick method of learning.

There are two more things to do with our Minecraft config file editor before we’ve got the main part of it working (we may do some tweaking later).  We need to:

  • add the Ok and Cancel buttons back; and
  • when someone clicks Ok, we need to update the server.properties file

We’re doing the first of these today. We saw earlier how to do the Ok and Cancel buttons, although at the time we didn’t actually put any meat in the functions they called.  So, let’s fill that out now.  For Cancel, we are just going to quit the editor without making any changes – that’s pretty easy.  For the Ok button though, we’re going to have to:

  1. somehow read all of the values from the screen (since we don’t know which ones have been changed we need to read them all);
  2. make a backup of the server.properties file
  3. write all of the key:value pairs to the new server.properties file.

Unlike variables, widgets are not the same as what is stored in them.  If we have an Entry widget called E and we want to store what has been typed there in a variable called text we can’t just write text = E.   This is because E is not a variable as we understand it.  Actually, E is an instance of a class.  This would just make another reference to the same Entry widget with the name text.  Rather, we want to “get” the current value of the text entered into E.  It turns out that the Entry widget has a method (called get()) which gets that text for you.

>>> from Tkinter import *
>>> E = Entry()  # this should pop up a Tkinter window
>>> E.pack()  # the widget should appear in your Tkinter window now
>>> type(E)
<type 'instance'>
>>> type(Entry)
<type 'classobj'>
>>> text = E
>>> type(text)
<type 'instance'>
>>> print text
.140543131462184
>>> # now type "Hi P4K!" in the entry widget
...
>>> text = E.get()
>>> print text
Hi P4K!
>>> # now add " - Again" to the end of the entry widget (leave the "Hi P4K!" there)
...
>>> text = E.get()
>>> print text
Hi P4K! - Again
>>> # you can also print the value which you get() without storing it first:
...
>>> print E.get()
Hi P4K! - Again

So what we’re going to do in our code is get() all these edited values when someone clicks “Ok”.  We could do that directly, for example by finding the relevant guiConfigItem and calling the get() method on the valueEntry attribute of that item.   That would also mean we’d have to make a copy of the key for that item and then combine them together with “=” before we wrote them to the server.properties file.  This would mean that logic which is relevant to the configItem class would be stored somewhere other than inside the class - which rather defeats the purpose of having a class to keep track of these things.  Instead, we’re going to add a method to the guiConfigItem class which updates the values it has stored.  That turns out to be pretty easy:

  def update(self):
    ''' Get the value which is currently in the Entry widget and save it to configVal'''
    if self.isTrueFalse:
      '''if isTrueFalse is True, then we should only have the values 'true' and 'false' in this
      Entry.  So, only update the configuration value if it is one of these two.  Otherwise, ignore it. '''
      spam = self.valueEntry.get()
      if spam in ['true','false']:
	self.configVal = spam
    else:
      '''this is not a variable which is limited to 'true' and 'false', so store the whole text'''
      self.configVal = self.valueEntry.get()

Note here that we are referencing the attribute configVal which is defined in the parent class.  Also note that we’ve included a bit of logic here to ensure that those configuration values which start as ‘true’ or ‘false’ can only be ‘true’ or ‘false’.  If you type something else into them it will be ignored.  It is sufficient here to just say if self.isTrueFalse rather than if self.isTrueFalse is True (the “is True” is redundant).

We also need a way to prepare the lines of the server.properties file to be printed or written to the file.  We do this by adding a method to the configItem class (since it doesn’t have anything to do with the graphical interface we don’t add it to the subclass):

  def item2ConfigLine(self):
    if self.configKey=="#":
      '''If the key is '#' then this is a comment, so don't include an '=' sign'''
      return "%s%s"%(self.configKey, self.configVal)
    else:
      '''otherwise, it has the form key=value'''
      return "%s=%s"%(self.configKey,self.configVal)

See this tutorial for an explanation of the %s stuff…

We don’t have a way to test these methods out yet. So let’s hook up the Ok and Cancel buttons. The Ok button will run through each of the guiConfigItems and update it, then print out the configuration line. After all items have been processed this way, the program will exit. The cancel button will just exit without doing anything. So, we need to:
1. create callbacks for each of the buttons,
2. create a frame for the buttons to go in
3. create the buttons, hooking up each of the buttons up to the callback
4. pack the buttons, then, finally,
5. pack the frame.
These go before the root.mainloop() line.

# 1. create callbacks for each of the buttons,
def okClicked():
  '''Get the edited values and write them to the file then quit'''
  for c in configLines:
    c.update()
    print c.item2ConfigLine()
  # have updated and printed each line, now exit
  exit()

def cancelClicked():
  '''Cancel edits and quit'''
  exit()

# 2. create a frame for the buttons to go in
bottomFrame = Frame(root)

# 3. create the buttons, hooking up each of the buttons up to the callback
okWidget = Button(bottomFrame, text= "Ok", command = okClicked)
cancelWidget = Button(bottomFrame, text="Cancel", command = cancelClicked)

# 4. pack the buttons, then, finally,
okWidget.pack(side=LEFT)
cancelWidget.pack(side=RIGHT)
# 5. then pack the frame:
bottomFrame.pack(side=BOTTOM)

At the moment, the Ok button just prints out the values of the items. This is so that we can test how it is working before we let it go editing the actual file.

Here is the complete source code:

# -*- coding: utf-8 -*-

'''Minecraft config editor:
This is an editor for the Minecraft server.properties file.  It:
* opens the file server.properties
* reads, then closes the file
* parses each line by
-- stripping leading and trailing whitespace
-- if the line starts with "#", marks it as a comment
-- splits the line into a key, value pair, with the pair separated by a "=" sign
-- if the value of the pair is either "true" or "false", the entry is marked as a boolean (ie its only values are either true or false)
* displays each key, value entry on the screen allowing you to edit it
* renames the server.properties file to server.properties.bup (overwriting any existing file of that name from earlier edits)
* opens a new file called server.properties
* writes each of the entries to that new file
* closes the server.properties file.
'''

from Tkinter import *

class configItem(object):  # name of the class, it is based on an object called 'object'
  def __init__(self, line):# this is called each time an instance of the class is created
    line = line.strip()  # this removes any white space at the start or end of the line
    # if it starts with a # it's a comment so check for it
    if line[:1] == "#":
       self.configKey = "#"
       self.configVal = line[1:]
    else:  # otherwise assume it's of the form x = y
       spam = line.split("=")
       self.configKey = spam[0]
       self.configVal = spam[1]
    # now check to see whether the config item takes only the values "true" and "false"
    if self.configVal.lower() in ["true","false"]:
       self.isTrueFalse = True
    else:
       self.isTrueFalse = False

  def item2ConfigLine(self):
    if self.configKey=="#":
      '''If the key is '#' then this is a comment, so don't include an '=' sign'''
      return "%s%s"%(self.configKey, self.configVal)
    else:
      '''otherwise, it has the form key=value'''
      return "%s=%s"%(self.configKey,self.configVal)

class guiConfigItem(configItem):
  def __init__(self,line):
    super(guiConfigItem,self).__init__(line)  # run configItem's __init__ method
    self.frame = Frame()
    self.keyLabel = Label(self.frame, text = self.configKey)
    self.valueEntry = Entry(self.frame, width="60")
    self.valueEntry.insert("0",self.configVal)
    self.keyLabel.pack(side=LEFT )
    self.valueEntry.pack(side=RIGHT)
    self.frame.pack(side=TOP, fill="x")

  def update(self):
    ''' Get the value which is currently in the Entry widget and save it to configVal'''
    if self.isTrueFalse:
      '''if isTrueFalse is True, then we should only have the values 'true' and 'false' in this
      Entry.  So, only update the configuration value if it is one of these two.  Otherwise, ignore it. '''
      spam = self.valueEntry.get()
      if spam in ['true','false']:
	self.configVal = spam
    else:
      '''this is not a variable which is limited to 'true' and 'false', so store the whole text'''
      self.configVal = self.valueEntry.get()

# get data from the file

fileName = "server.properties"
fileObject = open(fileName,'rb')
fileData = fileObject.read()
fileObject.close()

root = Tk()
configLines = []

for line in fileData.split('\n'):  # this splits it into individual lines
    if line.strip()=='':
      continue
    configLines.append(guiConfigItem(line=line))

# 1. create callbacks for each of the buttons,
def okClicked():
  '''Get the edited values and write them to the file then quit'''
  for c in configLines:
    c.update()
    print c.item2ConfigLine()
  # have updated and printed each line, now exit
  exit()

def cancelClicked():
  '''Cancel edits and quit'''
  exit()

# 2. create a frame for the buttons to go in
bottomFrame = Frame(root)

# 3. create the buttons, hooking up each of the buttons up to the callback
okWidget = Button(bottomFrame, text= "Ok", command = okClicked)
cancelWidget = Button(bottomFrame, text="Cancel", command = cancelClicked)

# 4. pack the buttons, then, finally,
okWidget.pack(side=LEFT)
cancelWidget.pack(side=RIGHT)
# 5. then pack the frame:
bottomFrame.pack(side=BOTTOM)

root.mainloop()

Exercise: run the code and confirm that: (a) your edits are captured and printed out; (b) if you enter anything but “true” or “false” for an item that takes only true and false, then the edit is ignored; and (c) that if you change a true to a false or vice versa, that that edit is captured.

Minecraft Config: Subclassing and Inheritance, Editing all config items

Mr. Simpson:     Good. Well I have this large quantity of string, a hundred and twenty-two thousand miles of it to be exact, which I inherited, and I thought if I advertised it–
Wapcaplet:     Of course! A national campaign. Useful stuff, string, no trouble there.
Mr. Simpson:     Ah, but there’s a snag, you see. Due to bad planning, the hundred and twenty-two thousand miles is in three inch lengths. So it’s not very useful.

In the previous tutorial we learnt that when we pack Tkinter objects, the order in which we pack them affects how they are displayed in the GUI.  We used a Text widget to enable the user to edit text in the GUI.  We also learnt to use a Frame widget to help with the layout of our GUI.  In particular, we put a Label and a Text widget together into one Frame, and put an ok and cancel Button into another.   For homework you needed to get data from the Text widget.

In order to display all of the configuration options we are going to go a bit nutty using Frames.   We will eventually (but not today) use one Frame to hold all of the configuration options, and another Frame to hold the Ok and Cancel buttons.  But that’s not all!  We will also use a Frame to house each configuration option (ie key and value pair).  Before we do that though, we need to remember where we were up to reading and parsing the server.properties file.

Here is the code we finished with two tutorials ago, excluding the last couple of lines (which printed out the results) for your reference if you need it (click to expand):

'''Minecraft config editor:
This is an editor for the Minecraft server.properties file.  It:
* opens the file server.properties
* reads, then closes the file
* parses each line by
-- stripping leading and trailing whitespace
-- if the line starts with "#", marks it as a comment
-- splits the line into a key, value pair, with the pair separated by a "=" sign
-- if the value of the pair is either "true" or "false", the entry is marked as a boolean (ie its only values are either true or false)
* displays each key, value entry on the screen allowing you to edit it
* renames the server.properties file to server.properties.bup (overwriting any existing file of that name from earlier edits)
* opens a new file called server.properties
* writes each of the entries to that new file
* closes the server.properties file.
'''

class configItem(object):  # name of the class, it is based on an object called 'object'
  def __init__(self, line):# this is called each time an instance of the class is created
    line = line.strip()  # this removes any white space at the start or end of the line
    # if it starts with a # it's a comment so check for it
    if line[:1] == "#":
       self.configKey = "#"
       self.configVal = line[1:]
    else:  # otherwise assume it's of the form x = y
       spam = line.split("=")
       self.configKey = spam[0]
       self.configVal = spam[1]
    # now check to see whether the config item takes only the values "true" and "false"
    if self.configVal.lower() in ["true","false"]:
       self.isTrueFalse = True
    else:
       self.isTrueFalse = False

# get data from the file

fileName = "server.properties"
fileObject = open(fileName,'rb')
fileData = fileObject.read()
fileObject.close()

configLines = []

for line in fileData.split('\n'):  # this splits it into individual lines
    if line.strip()=='':
      continue
    configLines.append(configItem(line))

If you remember we defined a class called configItem.  We read the lines from the config file and used each line to create instances of configItem.  We stored those in an array called configLines.  Each instance has two attributesconfigKey and configVal (that is, the things on the left and right hand side of the equals respectively).  In the last tutorial for one key, value pair we:

  • created a label and set it equal to configKey;
  • created a text widget and set its value to configVal; and, finally,
  • created a frame in which to pack each of these.

Now we have to do that for each and every entry in the array.  There are plenty of ways to do this.  However, I am going to do it by “subclassing” the configItem class.  That is, I am going to create a new class which is based on (“inherits from” or “is a subclass of”) the configItem class.  It has the features of the configItem class but will also store some stuff relating to the Tkinter widgets that we will need.  This is the new class which I’ve called guiConfigItem:

class guiConfigItem(configItem):
  def __init__(self,line):
    super(guiConfigItem,self).__init__(line)  # run configItem's __init__ method
    self.frame = Frame()
    self.keyLabel = Label(self.frame, text = self.configKey)
    self.valueEntry = Entry(self.frame, width="60")
    self.valueEntry.insert("0",self.configVal)
    self.keyLabel.pack(side=LEFT )
    self.valueEntry.pack(side=RIGHT)
    self.frame.pack(side=TOP)

Some things to note about this class:

  • instead of the first line ending “(object):” like the other classes we’ve seen, this one ends “(configItem):”.  This means that guiConfigItem’s immediate parent is configItem.  However, since configItem is based on object, in the end, so is guiConfigItem.
  • it takes the same initialisation parameters as configItem (that is, self and line)
  • the first thing it does in initialising stuff is to call super(guiConfigItem,self).__init__(line).  This runs configItem’s __init__ method, so every guiConfigItem starts with the same initialisation that configItem would have
  • it starts by creating a Frame, stores it in self.frame, then, inside the frame, it creates a label and an Entry widget.  An Entry widget is the single line version of the Text widget we used last time, and it should be good enough for our purposes.
  • you can tell that the Label and Entry widgets are created inside the frame which has been created because the first parameter passed to them is self.frame.
  • the Label widget is packed to the LEFT, and the Entry widget to the RIGHT.  the frame is also packed, but it is packed to TOP (ie it will make a list from top to bottom)

This class is added after the definition of the configItem class.  In order to get it working we just have to make a four changes to the program.  We will:

  • import Tkinter – from Tkinter import *;
  • create a root window in which to pack things - root =Tk();
  • change the loop to create guiConfigItems rather than configItems – configLines.append(guiConfigItem(line=line)); and
  • we will start the gui with a mainloop() – root.mainloop()

Here is the updated source code:


'''Minecraft config editor:
This is an editor for the Minecraft server.properties file.  It:
* opens the file server.properties
* reads, then closes the file
* parses each line by
-- stripping leading and trailing whitespace
-- if the line starts with "#", marks it as a comment
-- splits the line into a key, value pair, with the pair separated by a "=" sign
-- if the value of the pair is either "true" or "false", the entry is marked as a boolean (ie its only values are either true or false)
* displays each key, value entry on the screen allowing you to edit it
* renames the server.properties file to server.properties.bup (overwriting any existing file of that name from earlier edits)
* opens a new file called server.properties
* writes each of the entries to that new file
* closes the server.properties file.
'''

from Tkinter import *

class configItem(object):  # name of the class, it is based on an object called 'object'
  def __init__(self, line):# this is called each time an instance of the class is created
    line = line.strip()  # this removes any white space at the start or end of the line
    # if it starts with a # it's a comment so check for it
    if line[:1] == "#":
       self.configKey = "#"
       self.configVal = line[1:]
    else:  # otherwise assume it's of the form x = y
       spam = line.split("=")
       self.configKey = spam[0]
       self.configVal = spam[1]
    # now check to see whether the config item takes only the values "true" and "false"
    if self.configVal.lower() in ["true","false"]:
       self.isTrueFalse = True
    else:
       self.isTrueFalse = False

class guiConfigItem(configItem):
  def __init__(self,line):
    super(guiConfigItem,self).__init__(line)  # run configItem's __init__ method
    self.frame = Frame()
    self.keyLabel = Label(self.frame, text = self.configKey)
    self.valueEntry = Entry(self.frame, width="60")
    self.valueEntry.insert("0",self.configVal)
    self.keyLabel.pack(side=LEFT )
    self.valueEntry.pack(side=RIGHT)
    self.frame.pack(side=TOP)

# get data from the file

fileName = "server.properties"
fileObject = open(fileName,'rb')
fileData = fileObject.read()
fileObject.close()

root = Tk()
configLines = []

for line in fileData.split('\n'):  # this splits it into individual lines
    if line.strip()=='':
      continue
    configLines.append(guiConfigItem(line=line))

root.mainloop()

When I run this I get:


Wow, is that magic? The way we defined the class meant that each of the instances packed itself for us as we created them.  This is an example of why using classes can be so much fun.

Exercise: how might you do the same thing without using classes?

That said, the alignment is a little wonky.   This is because each of the individual frames (there is one on each line) are different sizes.  The overall window is big enough to fit the biggest, but that means that the smaller lines aren’t big enough.  This can be remedied by adding fill=”x” (that is, fill in the x (horizontal) direction if necessary to the pack command for each of the Frames:

self.frame.pack(side=TOP, fill="x")

Now the window looks much better:

Exercise: confirm that you can edit the values on the right.

Exercise 2: check through our docstring to see what we’ve done so far and what we’ve got left to do.

The complete source code with the final edit is below.

'''Minecraft config editor:
This is an editor for the Minecraft server.properties file.  It:
* opens the file server.properties
* reads, then closes the file
* parses each line by
-- stripping leading and trailing whitespace
-- if the line starts with "#", marks it as a comment
-- splits the line into a key, value pair, with the pair separated by a "=" sign
-- if the value of the pair is either "true" or "false", the entry is marked as a boolean (ie its only values are either true or false)
* displays each key, value entry on the screen allowing you to edit it
* renames the server.properties file to server.properties.bup (overwriting any existing file of that name from earlier edits)
* opens a new file called server.properties
* writes each of the entries to that new file
* closes the server.properties file.
'''

from Tkinter import *

class configItem(object):  # name of the class, it is based on an object called 'object'
  def __init__(self, line):# this is called each time an instance of the class is created
    line = line.strip()  # this removes any white space at the start or end of the line
    # if it starts with a # it's a comment so check for it
    if line[:1] == "#":
       self.configKey = "#"
       self.configVal = line[1:]
    else:  # otherwise assume it's of the form x = y
       spam = line.split("=")
       self.configKey = spam[0]
       self.configVal = spam[1]
    # now check to see whether the config item takes only the values "true" and "false"
    if self.configVal.lower() in ["true","false"]:
       self.isTrueFalse = True
    else:
       self.isTrueFalse = False

class guiConfigItem(configItem):
  def __init__(self,line):
    super(guiConfigItem,self).__init__(line)  # run configItem's __init__ method
    self.frame = Frame()
    self.keyLabel = Label(self.frame, text = self.configKey)
    self.valueEntry = Entry(self.frame, width="60")
    self.valueEntry.insert("0",self.configVal)
    self.keyLabel.pack(side=LEFT )
    self.valueEntry.pack(side=RIGHT)
    self.frame.pack(side=TOP, fill="x")

# get data from the file

fileName = "server.properties"
fileObject = open(fileName,'rb')
fileData = fileObject.read()
fileObject.close()

root = Tk()
configLines = []

for line in fileData.split('\n'):  # this splits it into individual lines
    if line.strip()=='':
      continue
    configLines.append(guiConfigItem(line=line))

root.mainloop()

Minecraft Config Editor: Tkinter Text Widget and Frames

Furtively he looks round, then takes from the desk drawer a comic-book entitled ‘Thrills and Adventure’. We see the frames of the comic strip. A Superman-type character and a girl are shrinking from an explosion. She is saying ‘My God, his nose just exploded with enough force to destroy his kleenex’. In the next frame, the Superman character is saying ‘If only I had a kleenex to lend him – or even a linen handkerchief – but these trousers…!! No back pocket!’ In the frame beneath, he flies from side to side attempting to escape; finally he breaks through, bringing the two frames above down on himself. Cut to a picture of a safety curtain.

Last tutorial we covered ‘parsing’.  We broke a standard config file up into separate lines and then we broke each line into pairs, each pair having a ‘key’ and a ‘value’.   If we’re to edit the file, we need a way to edit the values (and after that we’ll write the edited values back to the config file).  We’re going to see how to use Tkinter to do that in this tutorial.

Let’s start thinking about how a single key/value pair will look.  I am thinking of having the key on the left hand side with a space to input the value on the right hand side.  To do this we will use the Label widget that we’ve met before and the Text widgetLabel is used to display static text – ie text that will not be edited, while the Text widget allows the user to edit the text which is displayed in the widget, and for the program to read what is entered in the widget.  The Text widget is the GUI equivalent of raw_input() that we met so long ago.

from Tkinter import *

root = Tk()
labelWidget = Label(root,text="A key:")
textWidget = Text(root)

textWidget.insert('1.0',"A Value")
labelWidget.pack(side=LEFT)
textWidget.pack(side=RIGHT)

root.mainloop()

Here we first create a Tk() object, then create a label widget and a text widget in that object.   We pack the label first and we pack it on the LEFT side (LEFT is actually the name of a constant in Tk which Tk translates to ‘put this on the left’) and pack the text widget on the right side.  At location “1.0″ we add the text “A Value” to the Text widget.  Here the number “1.0″ means “row 1, at character position 0″, which is to say, at the very start of the text.

If I run this code I get something like this:

Which is sort of what I wanted – a label on the left, and an editable text box on the right (can you see the cursor in the screen shot?) – click the close window widget in the top right corner to close the window.

Exercise:  Type something into the text box.  See if you can do it to the label.

However, this isn’t really what I wanted.  I wanted a little text box, not the enormous one I’ve got here.  Since I didn’t specify a height and width for it, the Text widget used its default size (which is way too big).  The user also doesn’t have any way to tell the program to use (or cancel) the edit.  Let’s change the code to add an ok and cancel button, and to change the size of the text widget.

Here is a revised version which is nearer to what I was looking for:

from Tkinter import *

def okClicked():
    '''Get the edited values and write them to the file then quit'''
    #TODO: get values and write them to the file!
    exit()
def cancelClicked():
    '''Cancel edits and quit'''
    exit()

root = Tk()
labelWidget = Label(root,text="A key:")
textWidget = Text(root, width=60, height = 1)
okWidget = Button(root, text= "Ok", command = okClicked)
cancelWidget = Button(root, text="Cancel", command = cancelClicked)

textWidget.insert('1.0',"A Value")
labelWidget.pack(side=LEFT)
cancelWidget.pack(side=RIGHT)
okWidget.pack(side=RIGHT)

textWidget.pack(side=RIGHT)

root.mainloop()

This gives:

I have added a couple of functions to be run when the ok and cancel buttons are clicked.  The ok button’s function is still a little empty at the moment though…  I have specified the width to be 60 characters and the height to be one row.  Note these are not pixel measurements.  If you change the size of the font the text box will also change.

Notice also the way the geometry is working.  The widgets which are pack(side=RIGHT) are added at the right in the order they are packed.  If the buttons were packed last they would be between the label and the text window.

Exercise: change the program so that the widgets are packed in a different order. What happens if you try side=TOP or side=BOTTOM?

The value can be edited by the user typing directly into the text box.  The text in the text box can also be edited programmatically, which is to say its contents can be changed by the program without the user typing.  See the Tkinter documentation for details – or clamour on this site and I’ll add a tute.

One thing that I don’t like about this layout is the fact the buttons are on the same line as the key label and value text.  When you did the exercise above, you should have noticed that side=TOP and side=BOTTOM don’t really help, since you can’t position the ok and cancel buttons on the same line.  What we need to use is the Frame widgetFrames can be thought of as empty spaces in which you can group widgets together.  By treating the widgets within the Frame as a group, additional layouts can be achieved.   Frames can be packed inside other frames. We will use two frames, one on top of the other.  In the first frame we pack the label and text widgets.  In the second frame we pack the two buttons.

Here is the code:

from Tkinter import *

def okClicked():
    '''Get the edited values and write them to the file then quit'''
    #TODO: get values and write them to the file!
    exit()
def cancelClicked():
    '''Cancel edits and quit'''
    exit()

root = Tk()

topFrame = Frame(root)
bottomFrame = Frame(root)
labelWidget = Label(topFrame,text="A key:")
textWidget = Text(topFrame, width=60, height = 1)
okWidget = Button(bottomFrame, text= "Ok", command = okClicked)
cancelWidget = Button(bottomFrame, text="Cancel", command = cancelClicked)

textWidget.insert('1.0',"A Value")
labelWidget.pack(side=LEFT)
cancelWidget.pack(side=RIGHT)
okWidget.pack(side=LEFT)
textWidget.pack(side=RIGHT)

topFrame.pack(side=TOP)
bottomFrame.pack(side=BOTTOM)

root.mainloop()

This is more like what I wanted (notice you can’t see the individual frames).  One might quibble with the location of the ok and cancel buttons.  Maybe they should be offset a little from the centre?  Maybe they should be off to one side.  In any event they are in the general layout that i was looking for: a key label and an editable value text above the ok and cancel buttons. Notice in the code that the widgets we used before have had their parent changed from root to either topFrame or bottomFrame?  However these Frame widgets have root as their parent.   So the original widgets we were using have effectively been pushed down one level in the hierarchy.

We still don’t know how to get what the user has typed into the text box, but maybe I can leave that as an exercise for the reader (try textWidget.get(“1.0″,END)).

Homework:  change the okClicked() function so that it prints the contents of the text box before exiting.  Use the hint in the previous paragraph.

Follow

Get every new post delivered to your Inbox.

Join 67 other followers