Python for Kids: Python 3 – Project 3

Some people want to use my book Python for Kids for Dummies to learn Python 3. I am working through the code in the existing book, highlighting changes from Python 2 to Python 3 and providing code that will work in Python 3.

If you are using Python 2.7 you can ignore this post. This post is only for people who want to take the code in my book Python for Kids for Dummies and run it in Python 3.

Apologies also if you’re seeing

>

rather than

>

in this (or my other) posts. The way WordPress deals with the sourcecode tag is pretty much completely broken for the > character. I will do my best to fix – but WP often then breaks it after the fix.

Project 3 in the book is about getting information (input) from the user of your program. In it, the raw_input builtin from Python 2 is used. In Python 3 that builtin had its name changed to just input(). The bad news is that that means that none of the code using raw_input will work in Python 3. The good news is that you can replace raw_input by input wherever you see it and the code will work again. Alternatively, once you have completed Project 4 (it introduces the idea of putting your code in a file), you can place a single line at the start of your code that reads:

raw_input = input

Then you don’t need to change the rest of the code. This is equivalent to creating a variable called raw_input and assigning to it the built in function called input. Unfortunately, you’re still in Project 3, so that isn’t going to work. So here’s how you tackle Project 3 with Python 3.

Page 61

This code:

#Python 2.7 code:
>>> raw_input()
I am typing a response. Python won't see this until I press Enter.
"I am typing a response. Python won't see this until I press Enter."

will not work in Python 3. Watch what happens:

#Python 3 code:
>>> raw_input()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'raw_input' is not defined
>>> input()
I am typing a response. Python won't see this until I press Enter.
"I am typing a response. Python won't see this until I press Enter."
>>> 

Python 3 simply doesn’t recognise raw_input as a valid name for anything. As I mentioned above to get your Python 2.7 code working in Python 3, where you see raw_input, just write input instead:

#Python 2.7 code:
>>> raw_input("What is your guess?")
What is your guess?17
'17'

#Python 3 code:
>>> input("What is your guess?")
What is your guess?17
'17'

Page 62

All this code has problems because it uses raw_input. Replace with input instead throughout:

#Python 2.7 code:
>>> raw_input("What is your guess? ")
What is your guess? 17
'17'

>>> prompt = 'What is your guess? '
>>> raw_input(prompt)
What is your guess? 17
'17'

>>> prompt = 'What is your guess? '
>>> players_guess = raw_input(prompt)
What is your guess? 17
>>>
>>> players_guess
'17'

#Python 3 code:
>>> input("What is your guess?")
What is your guess?17
'17'

>>> input("What is your guess? ")
What is your guess? 17
'17'

>>> prompt = 'What is your guess? '
>>> input(prompt)
What is your guess? 17
'17'

>>> prompt = 'What is your guess? '
>>> players_guess = input(prompt)
What is your guess? 17
>>> players_guess
'17'

Page 63:

All code on this page is the same, and all outputs from the code is the same in Python 3 as in Python 2.7

Pages 64 and 65

With one exception, all code on these pages is the same, and all outputs from the code is the same in Python 3 as in Python 2.7. That exception relates to how division works.
Python 2.7 tries to be helpful for you when you divide. It does this by dividing integers differently to dividing floating point numbers:

#Python 2.7 code:
>>> 3/2
1
>>> -3/2
-2

Python 3 doesn’t worry. It will do the division as a floating point division whether you’re dividing integers or not.

#Python 3 code:
>>> 3/2
1.5
>>> -3/2
-1.5

If you are really desperate and want to do division Python 2.7 style, you can still do that, using the “floor” operator: // (two slashes together). Here is an example (it gives you the same results as the Python 2.7 code):

#Python 3 code:
>>> 3//2
1
>>> -3//2
-2

Page 67

All code on the first half of this page is the same, and all outputs from the code is the same in Python 3 as in Python 2.7. Page 67 gives you tips on how to work around the way Python 2.7 does division. These tips will not cause a problem in Python 3. Things will still work as you expect them, but the additional work is superfluous.

#Python 3 code:
>>> 3/2.
1.5

#Python 3 code:
>>> a=2
>>> 3/a
1
>>> 3/float(a)
1.5
>>>

In the second half of the page replace raw_input with input:

#Python 2.7 code:
>>> prompt = 'What is your guess? '
>>> raw_input(prompt)
What is your guess? 17
'17'

#Python 3 code:
>>> prompt = 'What is your guess? '
>>> input(prompt)
What is your guess? 17
'17'

Page 68

Replace raw_input with input:

#Python 2.7
>>> players_guess = raw_input(prompt)
What is your guess? 17
>>> players_guess+1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: cannot concatenate 'str' and 'int' objects

>>> players_guess
'17'

#Python 3 code:
>>> players_guess = input(prompt)
What is your guess? 17
>>> players_guess+1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't convert 'int' object to str implicitly

>>> players_guess
'17'

Note that the text of the TypeError changes a little from Python 2.7 to Python 3. The comments about raw_input always returning a string apply to input.

Pages 69-74

All code on these pages is the same, and all outputs from the code is the same in Python 3 as in Python 2.7.

Pages 75-76

Replace raw_input with input:

#Python 2.7 code:
>>> computers_number = 17
>>> prompt = 'What is your guess? '
>>> while True:
...        players_guess = raw_input(prompt)
...        if computers_number == int(players_guess):
...             print('Correct!')
...             break
...        elif computers_number > int(players_guess):
...             print('Too low')
...        else:
...             print('Too high')
...
What is your guess? 3
Too low
What is your guess? 93
Too high
What is your guess? 50
Too high
What is your guess? 30
Too high
What is your guess? 20
Too high
What is your guess? 10
Too low
What is your guess? 19
Too high
What is your guess? 16
Too low
What is your guess? 18
Too high
What is your guess? 17
Correct!

#Python 3 code:
>>> computers_number = 17
>>> prompt = 'What is your guess? '
>>> while True:
...        players_guess = input(prompt)
...        if computers_number == int(players_guess):
...             print('Correct!')
...             break
...        elif computers_number > int(players_guess):
...             print('Too low')
...        else:
...             print('Too high')
...
What is your guess? 3
Too low
What is your guess? 93
Too high
What is your guess? 50
Too high
What is your guess? 30
Too high
What is your guess? 20
Too high
What is your guess? 10
Too low
What is your guess? 19
Too high
What is your guess? 16
Too low
What is your guess? 18
Too high
What is your guess? 17
Correct!

Page 76 (still)

The break keyword is the same for both Python versions.

Page 77-81

All code on these pages is the same, and all outputs from the code is the same in Python 3 as in Python 2.7. However, behind the scenes the code is working differently in the two Python versions because of the issue with range discussed in my previous post. In short, Python 2.7 works out the members of a list before it uses them, while Python 3 will work them out “on the fly”.

Obviously, at the bottom of page 79, the heading in the readout identifies whatever version of Python 3 you’re using (in this case Python 3.3.5):

#Python 2.7 code:
Python 2.7.3 (default, Apr 14 2012, 08:58:41) [GCC] on
            linux2
Type "help", "copyright", "credits" or "license" for more
            information.
>>> random.randint(1,100)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'random' is not defined

#Python 3 code:
Python 3.3.5 (default, Mar 27 2014, 17:16:46) [GCC] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> random.randint(1,100)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'random' is not defined

Page 82

Replace raw_input with input:

#Python 2.7 code:
>>> import random
>>>
>>> computers_number = random.randint(1,100)
>>> prompt = 'What is your guess? '
>>> while True:
...     players_guess = raw_input(prompt)
...     if computers_number == int(players_guess):
...         print('Correct!')
...         break
...     elif computers_number > int(players_guess):
...         print('Too low')
...     else:
...         print('Too high')
...
What is your guess? 24
Too low
What is your guess? 86
Too low
What is your guess? 94
Too low
What is your guess? 98
Too high
What is your guess? 96
Too high
What is your guess? 95
Correct!

#Python 3 code:
>>> import random
>>>
>>> computers_number = random.randint(1,100)
>>> prompt = 'What is your guess? '
>>> while True:
...     players_guess = input(prompt)
...     if computers_number == int(players_guess):
...         print('Correct!')
...         break
...     elif computers_number > int(players_guess):
...         print('Too low')
...     else:
...         print('Too high')
...
What is your guess? 24
Too low
What is your guess? 86
Too low
What is your guess? 94
Too low
What is your guess? 98
Too low
What is your guess? 99
Correct!

The code in the Zen of Python section works the same in Python 2.7 and Python 3.

Python for Kids: Python 3 – Project 2

[Oct 2016: edits to correct escaped characters inserted by WordPress’s misbehavior]

Some people want to use my book Python for Kids for Dummies to learn Python 3. Choosing Python 2.7 over Python 3 was a difficult decision and I have given reasons why in the book.* Nevertheless, if I write a new edition of the book, it definitely will be in Python 3, so I plan to work through the code in the existing book, highlighting changes from Python 2 to Python 3 and providing code that will work in Python 3.

I am working from the downloadable code samples (they are cross referenced to page numbers in the book), so it might be an idea to get a copy, although working from the hard copy should also be fine. Get a copy from the link in the right hand sidebar.

For Project 2, most of the code works exactly the same in Python 2.7 and Python 3. There are some changes though later in the Project (from page 50). Those changes are set out below (page numbers from the 2015 printing).

Code on Pages 36 to Page 50

All of the code on these pages works in Python 3 and gives the same output.

Code on Page 50

#Python 2.7 code:
>>> my_message = 'Hello World!'
>>> while True:
...       print(my_message),
...

Comment

In Python 2.7 you use the comma -> , to tell print to NOT include a new line at the end of what is printed.
In Python 3 print has become a function. Functions are not discussed till Project 5! Implementing this in Python 3 needs a lot of extra concepts, that I’m not going to explain here. Instead, I’m just going to give you working code. You will need to come back to this after you’ve done Project 5. Hopefully then it will make more sense.

#Python 3
>>> while True:
...       print(my_message, end=" ")
... 

Code on Page 51

#Python 2.7 code:
>>> range(3)
[0, 1, 2]

Comment

In Python 2.7 range() creates a list. In Python 3 it makes something like a generator** – and generators are not even covered in the book! 😦 The main difference is that, with a list, all the items that you need are created ahead of time. However, with a generator, you only create the next item when you need it. Practically, the code will work in the same manner and you won’t be able to notice any difference (see the examples on the next page). For example, the numbers produced by range in Python 3 will start at 0 and run up to the number one less than the number you give to range. The good news though is that you can ignore my warning (on page 52) about using range() if you’re running Python 3. In Python 3 range doesn’t blow out your memory, so feel free to use it for numbers as big as you like.

#Python 3 code:
>>> range(3)
range(0, 3)

Code on Page 52

#Python 2.7 code:
>>> range(3,10)
[3, 4, 5, 6, 7, 8, 9]

>>> range(3,10,2)
[3, 5, 7, 9]

Comment

See comments on generators above. You won’t be able to see the practical difference until you cover for loops (see below).

#Python 3 code:
>>> range(3,10)
range(3, 10)

>>> range(3,10,2)
range(3, 10, 2)

Code on Page 53

#Python 2.7 code:
>>> range(13,10,-1)
[13, 12, 11]

Comment

Same comments on generators. See below.

#Python 3 code:
>>> range(13,10,-1)
range(13, 10, -1)

#Python 2.7 code:
>>> for i in range(3):
...        print(i)
...
0
1
2

#Python 3 code:
>>> for i in range(3):
...        print(i)
...
0
1
2

Comment

In this example both the code and the output from the Python 2.7 and Python 3 code was the same. However, the code created the output in different ways (the Python 2.7 code created a list, while the Python 3 code created a generator). Anywhere you use range in the book, you can use it when using Python 3.

Also, using this for loop structure you can test to see whether the earlier ranges are practically the same in Python 2.7 and Python 3.
For example (from page 52):

#Python 2.7 code:
>>> range(3,10)
[3, 4, 5, 6, 7, 8, 9]

>>> range(3,10,2)
[3, 5, 7, 9]

#Python 3 code:
>>> for i in range(3,10):
...        print(i)
...
3
4
5
6
7
8
9

# note same numbers as in [3, 4, 5, 6, 7, 8, 9]

>>> for i in range(3,10,2):
...        print(i)
...
3
5
7
9

# note: odd numbers from 3 to 10

Code on Page 54

#Python 2.7 code:
>>> my_message = "Hello World!"
>>> for i in range(300):
...          print(my_message),
...

Comment

See comments on print above.

#Python 3 code:

>>> my_message = "Hello World!"
>>> for i in range(300):
...          print(my_message, end=" ")
... 

Note that, in Python 2.7 when the print run had finished the >>> prompt started on a new line. However, in Python 3 the prompt >>> starts immediately after the last Hello World! on the same line.

Note:
* I had dreams of extending the book for parents to include, for example, loading applications to the cloud. Right now (March 2016, a year after I finished writing the book) Google App Engine, perhaps the easiest way to get an app live on the internet, still does not support Python 3 (although it is available through a separate service – GAE Managed VM hosting).

** It creates an “immutable sequence type” according to the docs. See Terry’s comment.

Reviewers wanted

If you’ve got a relevant blog and are interested in reviewing my book Python for Kids for Dummies, the publishers have some review copies that can be sent out. Please use the contact form on the About page to drop me a line and say you’re interested in reviewing the book. I’ll get your details and have a copy sent out.

Python for Kids Book: Project 10

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 10 (Math Trainer)

Project 10 is the last of the projects in the book. Project 10 is a math trainer – it tests you on your times tables and prints them out to allow you to revise.  This project is largely an application of some of the earlier work in the book but involves a little added complexity and some thought in solving some user interface issues.  For example, I use a formatting template to present the times tables. It’s easy to print out a long list of the times tables, but not very readable, so I show you how to use a nested loop to present 5 tables at a time across the screen. This uses the slice operator [:] as well as string’s join method

In order to ask random questions I introduce random.choice.  The trainer allows you to compete against yourself on score and on time. In order to time each training session I introduce the time method from the time module – time.time().

There are three additional projects that, as at 8 September 2015, I have completed and am now waiting to be loaded online.
Update: The additional projects are on line! Follow the link in the sidebar to download a copy.

Improvements:

@306: The time module gives you the number of seconds since the Epoch. The Epoch started on 1 January 1970 (for Unix operating systems). Why is it 1 January 1970? No reason. It’s just one of those historical accidents that infest everything to do with time calculations.

Python for Kids Book: Project 9

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 9 (Address Book)

While Project 6 introduced the concept of objects and showed how even a lowly string is actually an object, Project 9 is about how you can make your own objects using the class keyword.  Project 9 uses classes to implement a simple address book application.  I discuss the difference between a class and an instance of the class and the importance of having a reference variable (self) so that an instance can refer to itself. You learn about constructor functions and how to override a method (__repr__) to make print statements work properly on your custom method.

Since having an address book is not much use if you can’t save and update it, I also introduce the pickle module. It is used to store general Python objects (if they’re hashable!).  The address book is given save and load functions as well as a rudimentary user interface.

Improvements (1st printing):

On page 278 in the add_entry method, the second through fourth occurrences of the code

if first_name == "q":

Should be


if family_name == "q":

if email_address == "q":

and

if date_of_birth == "q":

respectively. This should be corrected in the second printing. The code samples have the correct code.

Python for Kids Book: Project 8

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 8 (Silly Sentences)

Project 8 is a short project which teaches you about (old style) formatting strings. Formatting strings allow you to create a sentence template into which you can substitute words.  Four words are chosen from four lists (at random) and substituted into a sentence. The random matchings make nonsense sentences. The substitutions can be streamlined by using a data type called the tuple, so that data type is introduced and discussed. You learn how to unpack tuples and how to use a tuple to receive multiple values from a function.  The project also relies on the random.choice method.

Python for Kids Book: Project 7

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 7

Project 7 (Cryptopy) introduces dictionaries as a means of encoding text using a Caesar cipher. Along the way, you are introduced to the string module and the characters in string.printable. There is some discussion about escape sequences and examples of \n and \t are given. Since you don’t want to encrypt escape sequences the slicing operator is introduced in order to take string.printable and slice off the control characters. This becomes the character set that will be encoded.

An encryption dictionary is created and each of the characters in a test message are encrypted then joined using the join method of the empty string.  I explain why this is better than adding one character after another to an existing string.  I create a matching decryption function and decryption dictionary and test the round trip (plaintext-> ciphertext -> plaintext).

The project introduces file operations by reading a message from a file then writing the encrypted (or decrypted) message to another file.  This is first done with the base file operations open and close, then the with keyword is introduced to make the housekeeping a little easier.

I demonstrate how to use your newly written encryption functions from the command line by importing the code from your own file – your own third party module!  In order for this to work seamlessly you are introduced to the __name__ attribute and the if __name__ == “__main__”: construction.

Improvements:

On page 203, the first line of code:

>>> file_object.close()

is not necessary  (because you already closed the file in the code on the previous page. It shouldn’t give you an error message though).  Ignore  it.

On page 209, there is a reference to code being in C:\Python27. This reference is only relevant if you are using Windows. On Linux and Mac things are more complicated – if the file is in the same directory that the shell is running from then you should be ok.

Python for Kids Book: Project 6

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 6

Project 6 introduces the concept of objects and lists to create a program that converts text into hacker speak (ie text with numbers substituted for letters). The project revisits the creation of a simple my_message variable then shows, using the dir builtin, that the variable has a variety of characteristics (that is, attributes) other than its value. It shows that one of the attributes, upper, is like a function and calls the functions of an object methods. It shows how to call a method or access an attribute through the dot notation.

In order to implement the hacker speak project, the program must perform a number of substitutions. To do that, it uses a list. The project discusses how to make a list,  how to add elements to a list, how to iterate through a list and how to test whether something is in a list.  It highlights a ‘gotcha’ with list methods – some of them modify the list in place without returning a value.

The code includes a logical error in the manner in which substitutions are made. A print statement is used to debug and identify the location of the error before it is fixed.

The project also gives a short introduction to IDLE’s debugger. There is a problem with IDLE’s debugger on Macs (there is no right click to set a breakpoint. Command-click works for some people, but not all), so if you’re running a Mac and command-click doesn’t work for you, you might have to skip this bit.

Improvements:
Page 150 does not use the print syntax from Python 3 like I said I would. It should be:

>>> for i in dir(my_message):
          print(i)
__add__
__class__
__contains__
[...]

Page 164 does not use the print syntax from Python 3 like I said I would. Should be:

>>> substitutions = [['a','4'], ['e','3'], ['l','1'], ['o','0'],
                     ['t','7']]
>>> for s in substitutions:
          print(s)

['a',  '4']
['e',  '3']
['l',  '1']
['o',  '0']
['t',  '7']

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 name of the project is actually “A More Functional Guessing Game” – named as such since it will be using a function to make the guessing game work better, but some editor somewhere had a humor transplant and changed that title.

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

The code at the bottom of page 133 should read:

QUIT = -1
QUIT_TEXT = 'q'
QUIT_MESSAGE = 'Thank you for playing'

That is, an additional constant QUIT_MESSAGE = ‘Thank you for playing’ should be at the bottom of the page.

The line avg = str(total_guesses/float(total_rounds)) in step 4 on page 135 should be 4 lines down – being the first line in the else: block. Otherwise the logic does not work properly when you quit in the first round. The corrected code reads:

    # new if condition (and code block) to test against quit
    if this_round == QUIT:
        total_rounds = total_rounds - 1
        # removed line from here
        if total_rounds == 0:
            stats_message = 'You completed no rounds. '+\
                              'Please try again later.'
        else:
            avg = str(total_guesses/float(total_rounds)) # to here
            stats_message = 'You played ' + str(total_rounds) +\
                              ' rounds, with an average of '+\
                              str(avg)
        break

This same correction needs to be made to the “Complete Code” on page 138.

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.

Update March 2016: The book is designed to teach you Python 2.7. However, Python 3 is on the way, and if you’d like to see what’d be different in this project if you use Python 3 see this post.

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)

Math (aka Maths) Trainer

In this Tutorial I would like to write a short (command line) program to help you to practise your times tables.  The program will ask you a series of questions like, for example, “5×6 =”  which you need to answer.  The time you take will be recorded as well as your score, which will be printed out at the end.

First, I will define a model to store the data that we’ll be using. The model makes an allowance for an upper and lower limit on what is being multiplied together (so you don’t have to answer 1×1=). By default this produces the four through 12 times tables, but that can be changed by nominating different values.

# trainer.py
# Brendan Scott
# 19 November 2014

# train user on times tables
# from lowest to highest

from random import shuffle
from time import time

range = xrange
class Model(object):
    def __init__(self, lowest = 4, highest = 12):
        self.lowest = lowest
        self.highest = highest

    def make_guess_list(self,random_order=True, allow_reversals= True, max_questions = None):
        if allow_reversals:
            question_list = [(x, y) for x in range(self.lowest, self.highest + 1)
                                    for y in range(self.lowest, self.highest + 1)]
        else:
            question_list = [(x, y) for x in range(self.lowest, self.highest + 1)
                                    for y in range(x, self.highest + 1)]

        if random_order:
            shuffle(question_list)

        if max_questions is not None:
            question_list=question_list[:max_questions]
        return question_list

The model also contains a method for constructing a list of “questions”. Each question is a tuple with two elements. These two elements will be the numbers to be multiplied together. The method has a few parameters:
* random_order determines whether the trainer will ask questions in a random order or in the normal order for a times table (eg all of the four times followed by the five times)
* allow_reversals – this determines whether the trainer will allow both 4×5 and 5×4 or will treat them as the same. This reduces the total number of questions and avoids repetition.
* max_questions: limits the number of questions being asked by chopping questions off the end of the list. This parameter makes more sense when random_order is True.
time.time() is used later…

To this model we add the following interface code:

if __name__ == "__main__":
    m = Model()
    question_list = m.make_guess_list(random_order=True, allow_reversals=False, max_questions=10)
    qn_format = "%sx%s= "  # a x b
    summary_fmt = "You scored %s (%s%%) out of a possible %s.  " \
                  "You took %0.1f seconds in total or %0.2f seconds for each correct answer"
                  # score, percent, possible, time_taken, time per correct

    start = raw_input("Press the enter key to start")
    score = 0

    t1 = time()
    for qn in question_list:
        answer = raw_input(qn_format % (qn))
        if answer.lower() == "q":
            break
        correct_answer = qn[0] * qn[1]
        try:
            if int(answer) == correct_answer:
                print("correct!")
                score += 1
            else:
                print ("wrong! (should be %s)" % correct_answer)
        except(ValueError):
            print ("wrong! (should be %s)" % correct_answer)
            # something other than a number has been typed, just ignore it

    t2 = time()

    total_questions = len(question_list)
    percent = int(100.0 * score / total_questions)
    time_taken = t2 - t1
    try:
        time_per_correct = float(time_taken) / score
    except(ZeroDivisionError): # if score == 0 (!)
        time_per_correct= time_taken

    fmt_args = (score, percent, total_questions, time_taken, time_per_correct)
    print(summary_fmt % fmt_args)

When this script is run from the command line, a Model is created with default values (4 and 12). A question list is produced which is randomly ordered, does not allow reversals and has at most 10 questions. These values can be changed to suit your needs.
Two template strings are define, the first for asking questions, the second for providing a summary at the end.
The user is asked to press the enter key to begin the test. Before the questions start a time marker (t1) is initialised. After the questions have ended a second time marker is taken (t2). For each of the questions in (the) question_list the question is put to the user. If the user responds with a “q”, the rest of the test is aborted (break) and the summary is printed. Otherwise the answer is either correct (in which case the score is increased by one) or it isn’t (in which case the user is told what the correct answer is. There is a little bit of error checking.

This script could be improved by, for example, keeping track of which questions were wrong (for later practise) and by keeping a leaderboard of results.

 

A Letter from Guido (creator of Python) to you

Guido van Rossum has posted a letter to all you young programmers out there.

Read it here.

 

Comprehending Lists and Tuples

SUPERIMPOSED CAPTION: ‘SUBURBAN LOUNGE NEAR ESHER’
Elderly couple, Mr A and Mrs B are staring through french windows at a cat that is sitting in the middle of their lawn motionless and facing away from them. A car is heard drawing up.

The last couple of tutorials have been a bit heavy, so this week’s tutorials are going to balance that out by being a little light. We’re having a look at a couple of different aspects of the Python language.

List Comprehensions
The first is comprehensions:

>>> a_list = [1,2,3,4,5]
>>> b_list= [element*2 for element in a_list]
>>> b_list
[2, 4, 6, 8, 10]

In this example, we have automatically generated a new list b_list from an existing list* a_list by using a list comprehension. The structure of the comprehension is, I hope comprehensible. If not, it is a little hard to explain in steps. The comprehension above is equivalent to:

>>> b_list = []
>>> for element in a_list:
...    b_list.append(element*2)
... 
>>> b_list
[2, 4, 6, 8, 10]

We could call the variable element anything we liked – it is just another variable:

 
>>> b_list = [baloney*2 for baloney in a_list]
>>> b_list
[2, 4, 6, 8, 10]
>>> 

You can also add conditions. Let’s say you only wanted the even elements in a_list:

 
>>> b_list = [element for element in a_list if element%2 ==0] #ie remainder is 0 when dividing by two
>>> b_list
[2, 4]
>>> 

The condition here is if element%2 ==0, but you can substitute other conditions (as long as they resolve to either True or False). You can have a comprehension which relies on multiple variables (this example is from the Python documentation):

 
>>> [(x, y) for x in [1,2,3] for y in [3,1,4] if x != y]
[(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)]

Unwinding this is a little tricky – for each element in [1,2,3] it runs through each element in [3,1,4] (ie 9 comparisons in all) and appends the tuple if the elements are not equal.

List comprehensions provide an easy shorthand for constructing lists. Moreover, they are often more readable than writing out the for loops explicitly.

Tuples
The second topic is tuples.
The other thing we’re going to look at is tuples (pronounced, variously, “tupp-lls”, “two-pls” and, to some people, “tyou-pls” – my preference is tupp-lls, rhymes with couples). Tuples are a little like lists, except that they are immutable. That is, once they are made they cannot be changed. Tuples are made by putting the elements in round braces [actually, adding commas between them and, sometimes, also adding braces]:

 
>>> my_tuple = (1,2,3)
>>> my_tuple
(1, 2, 3)
>>> my_list = [1,2,3]
>>> my_list
[1, 2, 3]

Elements of the tuple are referenced in the same way as you’d reference a list but, unlike lists, these elements cannot be changed (tuples are “immutable”):

 
>>>my_tuple[0]
1               
>>>my_tuple[0]=2
Traceback (most recent call last):                                         
  File "<stdin>", line 1, in <module>                                      
TypeError: 'tuple' object does not support item assignment    

The TypeError is telling us that you can’t change the elements of a tuple. To make a tuple with a single element is a little difficult because the interpreter could just interpret the parentheses as determining order of operation (think (1+2)*3). Instead, we put a comma after the single element to indicate that we are creating a tuple:

 
>>> my_tuple = (1)  # no comma, so Python just thinks it's a number
>>> my_tuple  
1             
>>> my_tuple[0]  # just a number, so has no elements
Traceback (most recent call last):                                                                           
  File "<stdin>", line 1, in <module> 
TypeError: 'int' object has no attribute '__getitem__'
>>> my_tuple= (1,)
>>> my_tuple
(1,)                                                                                                                 
>>>my_tuple[0]                                                                                                       
1              

So, if a tuple does what a list can do, only less, why bother with a tuple? Why not use a list? Well, tuples are easier for the Python interpreter to deal with and therefore might end up being faster. Tuples might also indicate that you have an ordered list where the ordering has some meaning (for example, a date tuple might store year, month and day (in that order) in tuple). The fact that you’re using a tuple then flags that each of the entries has a distinct meaning or use and that their order is significant. Another pragmatic reason to use a tuple is when you have data which you know shouldn’t change (like a constant). So, if you accidentally try to change one of the tuple’s entries, you will have an error thrown.

Finally, since tuples are immutable they can be used as keys in dictionaries, unlike lists:

 
>>> my_dict={}
>>> my_tuple=("A Name",23,"A location")
>>> my_list= list(my_tuple)
>>> my_dict[my_tuple]="Client 1"
>>> my_dict[my_list]="Client 2"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'

Notes:
* as per Don’s comment, you can use any iterator, but we haven’t talked about them yet, so they don’t yet exist.

Yet Another View of Chess

One of the women goes over to the set and switches it over. As she goes back to her seat from the radio we hear the theme music again, fading out as the sounds of [comic] violence and screaming start again and continue unabated in vigour.
Man’s Voice: I think she’s dead.
Woman’s Voice: No I’m not!

In our last tutorial we substituted a graphical representation of a chess board for a, rather more boring, textual one. This tutorial, we’re going to move our representation into three dimensions with an isometric view of the same Model. You are probably familiar with isometric views as they are often used in video games. Isometric representations attempt to give a three dimensional view of a subject, but without a perspective distortion (when there is a perspective parallel lines meet somewhere).

In the 2d representation of a chess board, you can think of the board on a table, with yourself leaning over it, viewing it directly from above. Here, both the columns and rows look the same size. However, if you think of yourself now sitting down your view of the board will change. The columns will appear shorter than the rows – and if you keep lowering your eyes until they are level with the table, each column disappears entirely behind the first row. With an isometric view we are simulating someone looking from, perhaps, a normal sitting height, but with the board turned. In order to do this we will shorten the length of the tiles and skew them. This is what our new tiles look like (err.. you can’t see the white one):

chess_isometric_white_tilechess_isometric_black_tilechess_isometric_black_tile_demo

 

 

These tiles measure 120×60 pixels (and are twice as wide as they are tall).  Also shown is the actual size of the black tile (they are rectangles with transparent triangles in the corners).  We have an additional problem with an isometric board in that before we used symbolic representations of the pieces, but for a 3d board, we will use a visual representation.  Compare an old pawn with a new pawn:

Chess_P45chess_isometric_white_pawn

 

This, in turn, will mean that our pieces are of different sizes.  In fact, we have lots of problems.  Simply substituting these new images into the previous view code doesn’t look pretty:

chess_mvc3_B

Some things to note include:

The board is totally wacky.  For each row, each successive tile needs to be lowered by 30 pixels below the previous one.  For each column, the tiles need to be moved to the right by 60 pixels, and shifted down a bit.  The chess pieces are aligned incorrectly – their tops match, but their bases do not (this is because we used anchor = NW in the previous example).  The pieces also don’t line up with the centre[sic] of their tiles.  The pieces themselves also take up more than one tile on the board.  So, we need to allow more space in the canvas for the board to fit into and we also need to change the way tiles are laid out on it, as well as sorting out how the pieces match the tiles.

The newly laid out board looks like this:

chess_mvc3_C

The numbers printed on the board (eg 1:420,0) show: The order in which the tile is drawn, then the x location, then the y location.  You will notice that all of the arrows point to a position outside the tile itself.  That is because in all of the tiles there is a triangle of transparency in every corner.  The tiles really are positioned with their top left corner at the arrow’s location.  Moreover, the tiles at the bottom (the row apparently closer to us, used for the white pieces) were drawn last.   Those at the top were drawn first.

The next problem that we have to deal with is the fact that the pieces are of differing sizes.  This, of itself, means that we can’t place them on the canvas by reference to their top left corners (this would mean that their tops would be aligned, but their bases would be out of alignment).  Instead we have to place them by reference to their base.  Each of the images for the pieces has been specially designed so that there is a transparent area (of 21 pixels – you get this number by trial and error or mathematically*) from the bottom of the gif.  The images are also 120 pixels wide (ie the width of the isometric tiles), with the piece centered in that space.  The tallest piece is the king, weighing in at 150 pixels (including the transparent area).  However, the base of the king aligns with the base of the tiles, so there is an overlap of 60 pixels.  This means that there needs to be another 90 pixels of headroom above the board to accommodate the height of the king.  This is calculated automatically in the view.  In the code, we now calculate the location of the bottom lower left hand corner and use it to to draw both the tiles and the pieces.

We now need to connect the view to the model – that is, to convert mouse clicks to board coordinates and to instructions to the Model.  However, unlike in our earlier view, there are places on the screen which are not part of the board at all.  Also, finding the row and column requires us to count diagonally in both axes.  This turns out to be a little complex so I’m glossing over it here.  If you’re interested the equations are in the controller’s source code.**

Our new board looks like this:

chess_mvc3_E

I have commented out the  Model entirely, importing it from the previous tutorial’s code.  You will need either need to change last tute’s file to be mvc2.py or the import line to refer to the file you saved it as (or uncomment the Model code).

Code:

#/usr/bin/python
'''
Representing a chess set in Python
Part 3 (Isometric tiles)
Brendan Scott
4 May 2013

Dark square on a1
Requires there to be a directory called
chess_data in the current directory, and for that
data directory to have a copy of all the images

'''

import Tkinter as tk
from Tkinter import PhotoImage
import os.path
import os

from mvc2 import Model,  BoardLocation,  View
# Use the Model class from the previous tutorial.
# rename mvc2 to whatever name you gave the script from that tutorial
# if you can't find the previous tutorial just uncomment the definition below.

column_reference = "a b c d e f g h".split(" ")
EMPTY_SQUARE = " "

TILE_WIDTH = 60
'''We have used a tile width of 60 because the images we are used are 60x60 pixels
The original svg files were obtained from
http://commons.wikimedia.org/wiki/Category:SVG_chess_pieces/Standard_transparent
after downloading they were batch converted to png, then gif files.  Bash one liners
(Unix) to do this:
for i in $(ls *.svg); do inkscape -e ${i%.svg}.png -w 60 -h 60 $i ; done
for i in $(ls *.png); do convert $i  ${i%.png}.gif ; done
white and black tiles were created in inkscape

Isometric tiles were created in inkscape
Isometric pieces were created with povray using ChessSets Version 1.2
by James Garner ( jkgarner@charter.net )
then post processed in GIMP.

'''

ISOMETRIC_TILE_WIDTH = 120
ISOMETRIC_TILE_HEIGHT = 60
# to display these tiles many locations in the code rely on integer division by 2,
# so this width and height should both be even numbers (otherwise rounding errors will accumulate)

BOARD_WIDTH = 8*TILE_WIDTH
BOARD_HEIGHT = BOARD_WIDTH

ISOMETRIC_BOARD_WIDTH = 8 * ISOMETRIC_TILE_WIDTH
ISOMETRIC_BOARD_HEIGHT= 8*ISOMETRIC_TILE_HEIGHT

DATA_DIR = "chess_data"
ISOMETRIC_DATA_DIR = "chess_data"

ISOMETRIC_TILES = {"black_tile":"chess_isometric_black_tile.gif",
    "B":"chess_isometric_white_bishop1.gif",
    "b":"chess_isometric_black_bishop1.gif",
    "k":"chess_isometric_black_king1.gif",
    "K":"chess_isometric_white_king1.gif",
    "n":"chess_isometric_black_knight1.gif",
    "N":"chess_isometric_white_knight1.gif",
    "p":"chess_isometric_black_pawn1.gif",
    "P":"chess_isometric_white_pawn1.gif",
    "q":"chess_isometric_black_queen1.gif",
    "Q":"chess_isometric_white_queen1.gif",
    "r":"chess_isometric_black_rook1.gif",
    "R":"chess_isometric_white_rook1.gif",
    "white_tile":"chess_isometric_white_tile.gif"
    }

#class Model(object):
#    def __init__(self):
#        '''create a chess board with pieces positioned for a new game
#        row ordering is reversed from normal chess representations
#        but corresponds to a top left screen coordinate
#        '''
#
#        self.board = []
#        pawn_base = "P "*8
#        white_pieces =  "R N B Q K B N R"
#        white_pawns = pawn_base.strip()
#        black_pieces = white_pieces.lower()
#        black_pawns = white_pawns.lower()
#        self.board.append(black_pieces.split(" "))
#        self.board.append(black_pawns.split(" "))
#        for i in range(4):
#            self.board.append([EMPTY_SQUARE]*8)
#        self.board.append(white_pawns.split(" "))
#        self.board.append(white_pieces.split(" "))
#
#
#    def move(self, start,  destination):
#        ''' move a piece located at the start location to destination
#        (each an instance of BoardLocation)
#        Does not check whether the move is valid for the piece
#        '''
#        # error checking
#        for c in [start, destination]:  # check coordinates are valid
#            if c.i > 7 or c.j > 7 or c.i <0 or c.j <0:
#                return
#        if start.i == destination.i and start.j == destination.j: # don't move to same location
#            return
#
#        if self.board[start.i][start.j] == EMPTY_SQUARE:  #nothing to move
#            return
#
#        f = self.board[start.i][start.j]
#        self.board[destination.i][destination.j] = f
#        self.board[start.i][start.j] = EMPTY_SQUARE
#
#
#class BoardLocation(object):
#    def __init__(self, i, j):
#        self.i = i
#        self.j = j

class Isometric_View(tk.Frame):
    def __init__(self,  parent = None):
        tk.Frame.__init__(self, parent)
        self.parent = parent
        self.preload_images()
        self.canvas_height = 7*ISOMETRIC_TILE_HEIGHT+self.board_y_offset
        self.canvas = tk.Canvas(self, width=ISOMETRIC_BOARD_WIDTH, height=self.canvas_height)
        self.canvas.pack()
        self.parent.title("Python4Kids")
        self.pack()

    def preload_images(self):
        self.images = {}
        for image_file_name in ISOMETRIC_TILES:
            f = os.path.join(ISOMETRIC_DATA_DIR, ISOMETRIC_TILES[image_file_name])
            if not os.path.exists(f):
                print("Error: Cannot find image file: %s at %s - aborting"%(ISOMETRIC_TILES[image_file_name], f))
                exit(-1)
            self.images[image_file_name]= PhotoImage(file=f)
        tallest = 0
        for k, im in self.images.items():
            h = im.height()
            if h > tallest:
                tallest = h

        self.board_y_offset = tallest

    def clear_canvas(self):
        ''' delete everything from the canvas'''
        items = self.canvas.find_all()
        for i in items:
            self.canvas.delete(i)

    def draw_empty_board(self,  debug_board = False):
        ''' draw an empty board on the canvas
        if debug_board is set  show the coordinates of each of the tile corners'''

        for j in range(8): # rows, or y coordinates
            for i in range(8): # columns, or x coordinates
                x, y = self.get_tile_sw(i, j)
                drawing_order = j*8 + i
                tile_white = (j+i)%2
                if tile_white == 0:
                    tile = self.images['white_tile']
                else:
                    tile = self.images['black_tile']
                self.canvas.create_image(x, y, anchor = tk.SW,  image=tile)

                if debug_board:  # implicitly this means if debug_board == True.
                    ''' If we are drawing a debug board, draw an arrow showing top left
                    and its coordinates. '''
                    current_tile = drawing_order +1 # (start from 1)

                    text_pos =  (x+ISOMETRIC_TILE_WIDTH/2, y-ISOMETRIC_TILE_HEIGHT/2)
                    line_end = (x+ISOMETRIC_TILE_WIDTH/4,  y -ISOMETRIC_TILE_HEIGHT/4)
                    self.canvas.create_line((x, y), line_end,  arrow = tk.FIRST)
                    text_content = "(%s: %s,%s)"%(current_tile, x, y)
                    self.canvas.create_text(text_pos, text=text_content)

    def get_tile_sw(self, i,  j):
        ''' given a row and column location for a piece return the x,y coordinates of the bottom left hand corner of
        the tile '''

        y_start = (j*ISOMETRIC_TILE_HEIGHT/2)+self.board_y_offset
        x_start = (7-j)*ISOMETRIC_TILE_WIDTH/2
        x = x_start+(i*ISOMETRIC_TILE_WIDTH/2)
        y = y_start +(i*ISOMETRIC_TILE_HEIGHT/2)

        return (x, y)

    def draw_pieces(self, board):
        for j, row in enumerate(board):  # this is the rows = y axis
            # using enumerate we get an integer index
            # for each row which we can use to calculate y
            # because rows run down the screen, they correspond to the y axis
            # and the columns correspond to the x axis
            # isometric pieces need to be drawn by reference to a bottom corner of the tile,  We are using
            # SW  (ie bottom left).

            for i,  piece in enumerate(row): # columns = x axis
                if piece == EMPTY_SQUARE:
                    continue  # skip empty tiles
                tile = self.images[piece]
                x, y = self.get_tile_sw(i, j)
                self.canvas.create_image(x, y, anchor=tk.SW,  image = tile)

    def display(self, board,  debug_board= False):
        ''' draw an empty board then draw each of the
        pieces in the board over the top'''

        self.clear_canvas()
        self.draw_empty_board(debug_board=debug_board)
        if not debug_board:
            self.draw_pieces(board)

        # first draw the empty board
        # then draw the pieces
        # if the order was reversed, the board would be drawn over the pieces
        # so we couldn't see them

    def display_debug_board(self):
        self.clear_canvas()
        self.draw_empty_board()

class Controller(object):
    def __init__(self,  parent = None,  model = None):
        if model is None:
            self.m = Model()
        else:
            self.m = model
        self.v = Isometric_View(parent)
        ''' we have created both a model and a view within the controller
        the controller doesn't inherit from either model or view
        '''
        self.v.canvas.bind("<Button-1>",  self.handle_click_isometric)
        # this binds the handle_click method to the view's canvas for left button down

        self.clickList = []
        # I have kept clickList here, and not in the model, because it is a record of what is happening
        # in the view (ie click events) rather than something that the model deals with (eg moves).

    def run(self,  debug_mode = False):
        self.update_display(debug_board=debug_mode)
        tk.mainloop()

    def handle_click_isometric(self,  event):
        ''' Handle a click received.  The x,y location of the click on the canvas is at
        (event.x, event.y)
        First, we need to translate the event coordinates (ie the x,y of where the click occurred)
        into a position on the chess board
        add this to a list of clicked positions
        every first click is treated as a "from" and every second click as a"to"
        so, whenever there are an even number of clicks, use the most recent to two to perform a move
        then update the display
        '''
        i, j = self.xy_to_ij(event.x,  event.y)
        self.clickList.append(BoardLocation(7-i, j))  # 7-i because the Model stores the board in reverse row order.
        # just maintain a list of all of the moves
        # this list shouldn't be used to replay a series of moves because that is something
        # which should be stored in the model - but it wouldn't be much trouble to
        # keep a record of moves in the model.
        if len(self.clickList)%2 ==0:
            # move complete, execute the move
            self.m.move(self.clickList[-2], self.clickList[-1])
            # use the second last entry in the clickList and the last entry in the clickList
            self.update_display()

    def xy_to_ij(self, x, y):
        ''' given x,y coordinates on the screen, convert to a location on
        the virtual board.
        Involves non trivial mathematics
        The tiles have, in effect, two edges. One leading down to the right,
        and one leading up to the right. These define a (non-orthogonal) basis
        (look it up) for describing the screen.
        The first vector V1 is (60,-30), the second V2= (60,30), and the
        coordinates were given XY=(x,y)
        so we want to find two numbers a and b such that:
        aV1+bV2 = XY
        Where a represents the column and b represents the row
        or, in other words:
        a(60,-30)+b(60,30) = (x,y)
        60a +60b = x
        -30a +30b = y
        so
        b = (y+30a)/30.0
        and
        60a+60*(y+30a)/30 = x
        => 60a +2y+60a = x
        => 120a = x-2y
        a = (x-2y)/120

        HOWEVER, this is calculated by reference to the a1 corner of the board
        AND assumes that y increases going upwards not downwards.

        This corner is located at 8* ISOMETRIC_TILE_HEIGHT/2  from the bottom of the canvas
        (count them)
        so first translate the x,y coordinates we have received
        x stays the same
        '''

        y = self.v.canvas_height-y # invert it
        y = y - 4*ISOMETRIC_TILE_HEIGHT # Get y relative to the height of the corner

        a = (x-2*y)/120.0
        b = (y+30*a)/30.0
        # if either of these is <0 this means that the click is off the board (to the left or below)
        # if the number is greater than -1, but less than 0, int() will round it up to 0
        # so we need to explicitly return -1 rather than just int(a) etc.

        return (int(b) if b>= 0 else -1, int(a) if a >= 0 else -1)

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

    def parse_move(self, move):
        ''' Very basic move parsing
        given a move in the form ab-cd where a and c are in [a,b,c,d,e,f,g,h]
        and b and d are numbers from 1 to 8 convert into BoardLocation instances
        for start (ab) and destination (cd)
        Does not deal with castling (ie 0-0 or 0-0-0) or bare pawn moves (e4)
        or capture d4xe5 etc
        No error checking! very fragile
        '''

        s, d = move.split("-")

        i = 8- int(s[-1]) # board is "upside down" with reference to the representation
        j = column_reference.index(s[0])
        start = BoardLocation(i, j)

        i =  8- int(d[-1])
        j= column_reference.index(d[0])
        destination = BoardLocation(i, j)

        return start,  destination

if __name__=="__main__":
    missing_files = False
    if not os.path.exists(ISOMETRIC_DATA_DIR):
        missing_files = True
        print ("Cannot find data directory")

    if not missing_files:
        missing_list = []
        for k, v in ISOMETRIC_TILES.items():
            fn = os.path.join(ISOMETRIC_DATA_DIR,  v)
            if not os. path.exists(fn):
                missing_files = True
                print ("Cannot find file: %s"%fn)
                missing_list.append(v)
    else: # whole directory missing
        missing_list= ISOMETRIC_TILES.values()

    if missing_files:
        ''' basic check - if there are files missing from the data directory, the
        program will still fail '''
        dl = raw_input("Cannot find chess images directory.  Download from website? (Y/n)")
        if dl.lower() == "n":
            print("Some image files not found, quitting.")
            exit(0)
        if not os.path.exists(ISOMETRIC_DATA_DIR):
            print("Creating directory: %s"%os.path.join(os.getcwd(), ISOMETRIC_DATA_DIR))
            os.mkdir(ISOMETRIC_DATA_DIR)

        import urllib
        url_format="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.

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.files.wordpress.com/2013/04/%s"       
        for k, v in TILES.items():
            url = url_format%v
            target_filename = os.path.join(DATA_DIR, v)
            print("Downloading file: %s"%v)
            urllib.urlretrieve(url, target_filename)
            
    parent = tk.Tk()
    c = Controller(parent)
    c.run(debug_mode= False)