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.

5 Responses to Yet Another View of Chess

  1. Pingback: Python 4 Kids: Yet Another View of Chess | Pyth...

  2. This might not be a reasonable fear — but I worry that there might be children who will be disturbed at the opening story of violence and screaming and a woman who as a result of the screaming might be dead, instead of entertained.

  3. Pingback: Python News Roundup – 5/7 | Docstrings

  4. Pingback: Hooking up the Sunfish Chess Engine (Advanced) | Python Tutorials for Kids 8+

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 75 other followers

%d bloggers like this: