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

 

 

Follow

Get every new post delivered to your Inbox.

Join 67 other followers