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="https://python4kids.files.wordpress.com/2013/05/chess_isometric_black_tile.gif"
        url_format= "https://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="https://python4kids.files.wordpress.com/2013/05/chess_isometric_black_tile.gif"
        url_format= "https://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= "https://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.