Python for Kids Book: Project 5

In these posts I outline the contents of each project in my book Python For Kids For Dummies.  If you have questions or comments about the project listed in the title post them here. Any improvements will also be listed here.

What’s in Project 5

Project 5 introduces functions by revisiting the Guessing Game from Project 3 and recasting it using a function.  The project covers the def keyword, calling a function, the fact that a function must be defined before it can be called. The project also covers how to communicate with a function (both sending information to it by passing parameters and getting information from it, using the return keyword). In order to define a function, you need to give it a name, so the project sets out naming rules for functions. You should also be documenting your code, so the project introduces docstrings, how to create them, what to put in them and how to use them.

The project illustrates a logical problem in the code an explains what a local variable is. It introduces the concept of constants defined in the body of a program that can be accessed by code within a function.  A function which conducts the game round is put inside a while loop. The user interface is changed to allow the user to exit the loop.  This involves creating a quit function which first checks with the user to confirm that they want to quit, then using the break keyword to break out of the loop to quit, or the continue keyword if the user aborts the quit. The sys module is introduced in order to use sys.exit.

Improvements:

The callout on Figure 5-4 should read “The right place for an argument.”  <sigh>

Python for Kids Book: Project 4

In these posts I outline the contents of each project in my book Python For Kids For Dummies.  If you have questions or comments about the project listed in the title post them here. Any improvements will also be listed here.

What’s in Project 4

Project 4 introduces the integrated development environment, IDLE, that comes prepackaged with Python. In Project 4 you learn your way around the IDLE Shell and Editor Windows. I demonstrate syntax highlighting, tab completion and editor history (making your coding life easier), how to save a file, open a file and run a file from the Editor window . You also learn how to automatically indent and de-indent your code. This makes working with code blocks much easier!

The coding concepts in this project relate to adding comments to your files – how and where to do it and some tips on adding comments so they are useful in the future. I also show you how to use comments in debugging to skip over “comment out” sections of code.

Python for Kids Book: Project 3

In these posts I outline the contents of each project in my book Python For Kids For Dummies.  If you have questions or comments about the project listed in the title post them here. Any improvements will also be listed here.

What’s in Project 3

Project 3 explores a guessing game from the command line. In order to get a guessing game up and running you need to know how to receive input from the user. This project introduces the raw_input() builtin to accommodate this.

In order to tell whether a guess is correct, the computer must compare the guess to an answer – using the == operator. Further, since raw_input returns a string, I explain that strings and numbers are different and introduce the int() builtin in order to be able to compare the input with a number.

To give a player feedback, you need to determine whether the guess was higher or lower than the desired number. The operators > and < are introduced to address this. The if conditional along with the variants elif and else are introduced to be able to structure the feedback given to a player.

To choose a number at random you learn about import, the random module and random.randint.

The while structure introduced in Project 2 is used to allow the player to make repeated guesses of the answer. This is coupled with the break keyword to exit the loop one the correct number is guessed.

In the course of introducing the ==, > and < operators, I also give an overview of more common operators used in Python (at Table 3-1).  After introducing the operators I discuss why division in Python is a special case, how to recognize problems with division and how to get a decimal answer if that’s what you’re after.

Python for Kids Book: Project 2

In these posts I outline the contents of each project in my book Python For Kids For Dummies.  If you have questions or comments about the project listed in the title post them here. Any improvements will also be listed here.

What’s in Project 2

Project 2 is a Hello World project that covers some important basics, including what literals are, how you name a literal in order to store it (ie variables). It shows the different ways to make a string literal.If you’re going to name a value you need to know about Python’s naming rules (and PEP8 naming conventions – yah but I don’t actually mention PEP8). Those naming rules themselves rely on you not using a keyword as a name, so you also get a list of Python keywords in this project.

It also covers Python’s print statement, looping with while (including the notion of a conditional and a code block). looping with for and counting with range. By the end of the Project you can fill the screen with “Hello World!”.  To do so you repeat something 300 times. I introduce the concept of magic numbers and explain why they should be avoided. I offer the use of variables in ALL_CAPS to use as constants.

Python for Kids Book: Intro and Project 1

In these posts I outline the contents of each project in my book Python For Kids For Dummies.  If you have questions or comments about the project listed in the title post them here. Any improvements will also be listed here.

What’s in the Introduction

The introduction of the book sets out some information to help you understand how the book is written. It gives some examples of the different fonts used in the book to show code, how special terms are indicated (with italics), about the Python console. In particular, in the book some code samples occur with >>> in front of them. Where you see these you need to go to your own Python console and type into it the text which comes after the >>> . The introduction also explains some stuff about how long lines are treated and how to indent your code (important in Python).

What’s in Project 1

Project 1 of the book is not so much a project as some Python basics. In it I give you some context about what Python is and how it is used. You learn how to download and install Python, and how to start and stop the Python interpreter. You learn where to find Python documentation online and about PEPs! I also try my best to tell you to be an active learner – to do, rather than just read. Remember what Yoda said,  “do, or do not, there is no read” (or something like that). There’s no learning except by doing. Try to freestyle it if you can. The book is quite structured in what it tells you to do and what code to type and when, but please feel free to mix it up yourself. I really can’t emphasize enough how important it is to get in, try stuff out and make mistakes.

You’ve read the blog, now get the book!

Good news! I’ve been working away on a Python book for kids with the folks from Wiley. The book, called Python for Kids for Dummies is due out in a couple of weeks now.  The book is aimed at kids from about 10 and up.

See it on Amazon:

It has 13 Projects, 10 of which are in the book itself, and three will be available online from Wiley. I will be putting up more information about the projects in the coming days.

Hooking up the Sunfish Chess Engine (Advanced)

In the earlier posts on MVC [1, 2, 3] we did a fair bit of work putting together a GUI for a chess game.  It was  all very interesting but for the fact that there was no chess engine behind it … so you couldn’t actually play a game of chess with it (d’oh!).  This tutorial is aimed at remedying that by hooking up the Sunfish chess engine.  The Sunfish engine is written entirely in Python – awesome.  The main objective we are trying to achieve is to reuse existing code, including, in the case of Sunfish, someone else’s code.  This will primarily be achieved through subclassing existing classes.   This is an advanced topic, so you might want some help from a responsible adult.

The Rules

The rules of this  tutorial are that we must not make any changes to the Sunfish code, we must use the interface code from this earlier tutorial but we are allowed to subclass that code as necessary to interface properly with Sunfish.

TL;DR

Get yourself a copy of:

  • sunfish.py from the Sunfish github (it offers a zip download, you only need sunfish.py from that archive)
  • the code from the earlier tutorial (save it to mvc2.py)
  • a copy of the chess_data directory from the earlier tutorial as well (this contains the image data for each of the chess pieces that will be needed by the GUI).  If you run mvc2.py once should check and auto-download the data.

Then, save the code at the end of this tutorial into a file (I have called mine chess.py) in the same directory as sunfish.py and mvc2.py and you should be up and running!  The rest of the tutorial explains the process of developing the chess.py code below.

Discussion

We already have a model, controller and view in the mvc2 code.   What we need to do is integrate  Sunfish into these existing objects.  The final code has kept the view, but modified the Controller, and substituted an entirely new Model.   In order to interface our existing code with the Sunfish code we need to understand how the Sunfish code works.  First and foremost, Sunfish stores its board as a single string with line breaks (“\n”).   This is how Sunfish represents the board after the move e2-e4:

'          \n         \nrnbkqbnr \nppp.pppp \n........ \n...p.... \n........ \n........ \nPPPPPPPP \nRNBKQBNR \n         \n         '

In our earlier model we represented the board as a list of lists, with a separate element for each of the squares of the board, so this change means that we are going to have to process the Sunfish boards before we can pass them to our View. Other things to note are:

  • Sunfish pads the board with blank lines at the top and the bottom
  • Sunfish pads the board with a space at the end of each row (and further investigation shows this padding swaps from the left to right side)
  • Sunfish uses the “.” character to represent empty squares, while we used a blank space.

So the first thing to do is to write some code to convert a Sunfish board to a board that the View can display.  This was done in a convert method:

    def convert(self, board):
        """
        Sunfish adds blank lines, spaces and carriage returns to its board for its processing
        however the code we are using to display this assumes that board
        is an array of string.  So, convert
        Also need to convert "." representing an empty square to " " for the current view
        :param board:
        :return:
        """

        b = board.split('\n')
        converted_board = []
        for line in b[2:-2]:  # sunfish has two blank lines at top and bottom
            if line[0]== " ":
                line = line[1:].replace("."," ")
                # sunfish also has left padding sometimes
            else:
                line = line.replace("."," ")
            converted_board.append([c for c in line])  # split each line
        return converted_board

This splits the lines out, strips out the blank lines at the top and bottom. Then, it breaks each line into individual characters, stripping out a leading blank (if any) and converts the empty space characters from “.” to ” “.
I said that convert is a method – the logical place for it is in the controller in order to prepare data to give to the view (it could also go in the model). We also need to have something converting the board before passing the data to the View. The logical place for this is to intercept the call to the view in the update_display() method of the Controller, pass the data to the convert method, then pass that data to the View in the format it expects.

Finding a Place to Call Convert

However, one of our rules was that we couldn’t change the existing code. So what we have to do is subclass the Controller:

class NewController(mvc2.Controller):

This says we’re making a new class (called NewController) based on the Controller from the file mvc2.py (we would need to import mvc2 to do this). The new class has all of the code from the earlier Controller class. We can augment that code by adding the convert method above. However, nothing in Controller code will ever call the convert method (because when Controller was written, convert didn’t exist!). So we need to actually meddle in at least some of the existing methods of Controller. We do this by defining a function with the same name in NewController. This is called “overriding”. So, in order to hook up the new convert method I rewrote the the update_display method in Controller:

    def update_display(self,  debug_board=True, board=None):
        if board is None:
            self.v.display(self.convert(self.m.pos_list[-1].board),  debug_board = debug_board)
        else:
            self.v.display(self.convert(board))
        self.v.canvas.update_idletasks()
        # sometimes tkinter is stalling, so flush instructions

In essence I have made two changes – first, you can pass board as a named parameter and second, I have made all data going to the view’s display method (self.v.display) to first go through the self.convert method (the code below also has a “headless” option which is included in anticipation of later testing). Compare this to the code from Controller:

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

Preparing Data to Give To Sunfish

Not only is it necessary to convert Sunfish’s output in order to display the board, moves from the board have to be converted in order to send them to Sunfish. Sunfish has its own parse function which we can use, but only if the move is in algebraic notation (like e2e4). So, we need to create a function which converts the BoardLocation objects used in mvc2 into algebraic notation. These can then be fed into Sunfish’s parser:

ALGEBRAIC_DATA=zip([0,1,2,3,4,5,6,7],[c for c in "hgfedcba"])
ALGEBRAIC_DICT = {}
for k,v in ALGEBRAIC_DATA:
    ALGEBRAIC_DICT[k] = v

def location_to_algebraic(board_location):
    return "%s%s"%(ALGEBRAIC_DICT[7-board_location.j],8-board_location.i)

Move Logic

While we could make do with most of the move logic in mvc2 (in the handle_click method), it is a little basic.  For this exercise I have added the concept of “state”.  Which is to say the interface is in a certain state and that state affects how the interface reacts to clicks.  The three states are:

SELECT_PIECE, MOVE_PIECE, WAIT_COMPUTER = range(3)

So, the computer first waits for you to select a piece, then, when you have selected a valid piece, waits for you to move it to a valid location.   After that it waits for the computer to make its move.   This “stateful” approach means that you can unselect a piece during a move if you want to by clicking it again.  At the moment though, I haven’t added any visual cue to tell you which piece is currently selected.  The overridden method is “handle_click”.   So the bindings are still the same, but now when Tkinter generates an event the click event goes to the new handle_click method, not the old one.

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
    '''

    logging.debug("in handle click valid moves are: \n%s"%self.m.legal_moves)
    j = event.x/mvc2.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/mvc2.TILE_WIDTH

    b_loc = mvc2.BoardLocation(i, j)

    # Select/deselect logic

    sunfish_pos =  sunfish.parse(location_to_algebraic(b_loc))
    logging.debug("received a click at %s"%sunfish_pos)

    if self.click_state == WAIT_COMPUTER:
        logging.debug("wait computer: Currently waiting for computer ignoring clicks")
        return

    elif self.click_state == SELECT_PIECE:
        logging.debug("valid clicks are: %s"%self.m.valid_clicks)
        if sunfish_pos in self.m.valid_clicks:
            self.clickList.append(sunfish_pos)
            logging.debug("select piece: piece selected at: %s"%(sunfish_pos))
            self.click_state = MOVE_PIECE
            self.m.valid_clicks = [m[1] for m in self.m.legal_moves if m[0] == sunfish_pos]
            logging.debug("valid clicks now: %s"%self.m.valid_clicks)
            return
        else:
            return

    else: #State is MOVE_PIECE
        logging.debug("move piece: valid clicks are: %s"%self.m.valid_clicks)
        if sunfish_pos == self.clickList[-1]:
            logging.debug("piece moved to: %s"%(sunfish_pos))
            # that is, the currently selected piece
            # unselect that piece
            self.clickList.pop()
            self.click_state = SELECT_PIECE
            self.m.valid_clicks = set([m[0] for m in self.m.legal_moves])
            return

        elif sunfish_pos not in self.m.valid_clicks:
            # wait for a valid move
            return
        else:
            self.clickList.append(sunfish_pos)
            self.click_state = WAIT_COMPUTER
            # execute the move:
            pos = self.m.move(self.clickList[-2], self.clickList[-1])
            logging.debug(repr(pos.board))
            logging.debug("about to update display")
            self.update_display(board=pos.rotate().board)
            logging.debug("After updating display")
            self.query_sunfish(pos)

 Getting Sunfish to Move

This looks like a lot of code, but its structure is repetitive.  The first few lines parsing the location of the click are the same as in the original method.  I have added additional logic to test that the click is a legal move, and to identify legal moves for the click that will come next.  When a valid move has been made, that is fed, indirectly via the model, to the Sunfish engine which implements the move.  The system then asks Sunfish for a response (in the query_sunfish method) and, in the meantime, doesn’t respond to clicks on the board.

def query_sunfish(self, pos):
    logging.debug("in query sunfish")
    move, score = sunfish.search(pos)

    if score <= -sunfish.MATE_VALUE:
        logging.debug("You won")

    if score >= sunfish.MATE_VALUE:
        logging.debug("You lost")

    logging.debug("move = %s->%s"%move)
    pos= self.m.move(*move)
    self.update_display(board=pos.board)
    logging.debug(pos.board)
    self.click_state=SELECT_PIECE
    self.update_model()

Unfortunately, so far you can only tell if you’ve won or lost by watching the logs :(  But this is something that you can improve in your spare time – it wouldn’t be too hard to pop up a dialog box (look up tkMessageBox) and maybe also some code to initialise a new game.

The Model

The model has been completely replaced:

class Model(object):
    def __init__(self, *args):
        self.pos_list= [sunfish.Position(*args)]
        self.legal_moves = []

    def move(self,i,j):
        pos=self.pos_list[-1]
        self.pos_list.append(pos.move((i,j)))
        return self.pos_list[-1]
        # Sunfish returns a sunfish.Position object to us

It only had two methods anyway. Now they’re shorter. The move method assumes that the “from” and “to” board coordinates are given in Sunfish’s board location format. The model initialises a list, with the first entry being a Sunfish Position object with the board’s initial position. In fact, the model gives responsibility for updating the board and checking validity of moves to the Sunfish Position object. The model maintains a list of the board positions for each of the moves in the game. This could form the basis of a game review function later.

The full code is here.  It has some foibles – like allowing illegal king moves, not knowing draws and, at the moment, you can only play white.  These are all things that you can take on as challenges.  It also maintains a log which  you might want to turn off:

"""
Brendan Scott
Python4Kids.wordpress.com
November 2014

Chess -
attach a graphical interface to the sunfish chess engine

"""

import sunfish
import mvc2
import Tkinter as tk
import logging

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

SELECT_PIECE, MOVE_PIECE, WAIT_COMPUTER = range(3)

ALGEBRAIC_DATA=zip([0,1,2,3,4,5,6,7],[c for c in "hgfedcba"])
ALGEBRAIC_DICT = {}
for k,v in ALGEBRAIC_DATA:
    ALGEBRAIC_DICT[k] = v

logging.debug(ALGEBRAIC_DATA)
logging.debug(ALGEBRAIC_DICT)

def location_to_algebraic(board_location):
    return "%s%s"%(ALGEBRAIC_DICT[7-board_location.j],8-board_location.i)

# def move_to_algebraic(start, end):
#     return "%s%s"%(location_to_algebraic(start),location_to_algebraic(end))

class Model(object):
    def __init__(self, *args):
        self.pos_list= [sunfish.Position(*args)]
        self.legal_moves = []

    def move(self,i,j):
        pos=self.pos_list[-1]
        self.pos_list.append(pos.move((i,j)))
        return self.pos_list[-1]
        # Sunfish returns a sunfish.Position object to us

class NewController(mvc2.Controller):
    def __init__(self,  parent = None,  model = None, headless = False):
        self.headless = headless
        if model is None:
            self.m = Model(sunfish.initial, 0, (True,True), (True,True), 0, 0)
            #sunfish.Position(sunfish.initial, 0, (True,True), (True,True), 0, 0)
        else:
            self.m = model

        # logging.debug self.m.board
        self.v = mvc2.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 = []  #TODO: update to be sunfish pos not board location
        # 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).
        self.click_state = SELECT_PIECE
        self.update_model()

    def update_model(self):
        self.m.legal_moves = [m for m in self.m.pos_list[-1].genMoves()]
        self.m.valid_clicks = set([m[0] for m in self.m.pos_list[-1].genMoves()])

    def update_display(self,  debug_board=True, board=None):
        if self.headless:
            return
        if board is None:
            self.v.display(self.convert(self.m.pos_list[-1].board),  debug_board = debug_board)
        else:
            self.v.display(self.convert(board))
        self.v.canvas.update_idletasks()
        # sometimes tkinter is stalling, so flush instructions

    def convert(self, board):
        """
        Sunfish adds blank lines, spaces and carriage returns to its board for its processing
        however the code we are using to display this assumes that board
        is an array of string.  So, convert
        Also need to convert "." representing an empty square to " " for the current view
        :param board:
        :return:
        """

        b = board.split('\n')
        converted_board = []
        for line in b[2:-2]:  # sunfish has two blank lines at top and bottom
            if line[0]== " ":
                line = line[1:].replace("."," ")
                # sunfish also has left padding sometimes
            else:
                line = line.replace("."," ")
            converted_board.append([c for c in line])  # split each line
        return converted_board

    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
        '''

        logging.debug("in handle click valid moves are: \n%s"%self.m.legal_moves)
        j = event.x/mvc2.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/mvc2.TILE_WIDTH

        b_loc = mvc2.BoardLocation(i, j)

        # Select/deselect logic

        sunfish_pos =  sunfish.parse(location_to_algebraic(b_loc))
        logging.debug("received a click at %s"%sunfish_pos)

        if self.click_state == WAIT_COMPUTER:
            logging.debug("wait computer: Currently waiting for computer ignoring clicks")
            return

        elif self.click_state == SELECT_PIECE:
            logging.debug("valid clicks are: %s"%self.m.valid_clicks)
            if sunfish_pos in self.m.valid_clicks:
                self.clickList.append(sunfish_pos)
                logging.debug("select piece: piece selected at: %s"%(sunfish_pos))
                self.click_state = MOVE_PIECE
                self.m.valid_clicks = [m[1] for m in self.m.legal_moves if m[0] == sunfish_pos]
                logging.debug("valid clicks now: %s"%self.m.valid_clicks)
                return
            else:
                return

        else: #State is MOVE_PIECE
            logging.debug("move piece: valid clicks are: %s"%self.m.valid_clicks)
            if sunfish_pos == self.clickList[-1]:
                logging.debug("piece moved to: %s"%(sunfish_pos))
                # that is, the currently selected piece
                # unselect that piece
                self.clickList.pop()
                self.click_state = SELECT_PIECE
                self.m.valid_clicks = set([m[0] for m in self.m.legal_moves])
                return

            elif sunfish_pos not in self.m.valid_clicks:
                # wait for a valid move
                return
            else:
                self.clickList.append(sunfish_pos)
                self.click_state = WAIT_COMPUTER
                # execute the move:
                pos = self.m.move(self.clickList[-2], self.clickList[-1])
                logging.debug(repr(pos.board))
                logging.debug("about to update display")
                self.update_display(board=pos.rotate().board)
                logging.debug("After updating display")
                self.query_sunfish(pos)

    def query_sunfish(self, pos):
        logging.debug("in query sunfish")
        move, score = sunfish.search(pos)

        if score <= -sunfish.MATE_VALUE:
            logging.debug("You won")

        if score >= sunfish.MATE_VALUE:
            logging.debug("You lost")

        logging.debug("move = %s->%s"%move)
        pos= self.m.move(*move)
        self.update_display(board=pos.board)
        logging.debug(pos.board)
        self.click_state=SELECT_PIECE
        self.update_model()

if __name__ == "__main__":
    parent = tk.Tk()
    c = NewController(parent)
    c.run(debug_mode= False)

Follow

Get every new post delivered to your Inbox.

Join 82 other followers