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= "https://python4kids.wordpress.com/wp-content/uploads/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)

7 Responses to A Different View on Our Chess Model

  1. Just a few suggestions:

    Top line might work better if it’s:
    #!/usr/bin/python

    In the import section near the top, use the following:
    from Tkinter import PhotoImage

    I also used GIMP instead of inkscape and manually converted the images to 60×60 gif’s. I also created a grey 60×60 “black_tile.gif” and a white 60×60 “white_tile.gif”

    I also put the program and the images in a folder “chess” on my desktop and ran the program from there, so the variable should read:
    DATA_DIR = “../chess”

    The program works just fine.

    • brendanscott says:

      updated. Ta.

      /usr/bin/python – yes, I have upgraded to eric5 recently and it is giving me a lot of python 3 warnings. That was tinkering to see if I could stop it (it didn’t work)

  2. Allan Lowin says:

    Hello, I find very interesting as my hobby is python! I would like to see it working. where you have downloaded completely?

  3. Pingback: Links 5/5/2013: New Debian | Techrights

  4. Pingback: A Different View on Our Chess Model | Ragnarok Connection

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

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.