\chapter{Case study: Catch continued}

\section{Keyboard input}
To be able to respond to input from the user, Tkinter and most GUI
toolkits use the notion of an event. To get a program to respond to
events, we need to \myidx{bind}\ an event to a callback function. Here
is how we can implement a mitt for the catch game that moves the mitt
up or down depending on the key pressed:
\begin{python}
from Tkinter import *

def move_mitt(event):
    global mitt
    global gameCanvas
    global mittx, mitty
    global MITT_SIZE
    
    if event.char=="j":
        mitty = mitty+10
    elif event.char=="k":
        mitty = mitty-10
    gameCanvas.coords(mitt, mittx, mitty, mittx+MITT_SIZE, mitty+MITT_SIZE)


MITT_SIZE = 20

root = Tk()
gameCanvas = Canvas(root, width=400, height=400)
gameCanvas.grid(column=0,row=0)
gameCanvas.wait_visibility()

mittx = gameCanvas.winfo_width()-30
mitty = gameCanvas.winfo_height()/2
mitt = gameCanvas.create_oval(mittx, mitty,
                              mittx+MITT_SIZE, mitty+MITT_SIZE, 
                              fill="blue")

root.bind("k", move_mitt)
root.bind("j", move_mitt)

root.mainloop()
\end{python}

The callback function, \pythoninline{move\_mitt} takes the variable
\pythoninline{event} as a parameter. This variable is of type
\pythoninline{Event}. Each event we want the program to respond to
must be bound with a \pythoninline{bind} call. You can bind as many
events as you like, and they can also be bound to different callbacks
if desired. In \pythoninline{move\_mitt}, the particular event is
checked by testing the \pythoninline{char} attribute of the
\pythoninline{Event} type. Type in \pythoninline{help(Event)} at the python
prompt to find out what other attributes an \pythoninline{Event}
variable has.

\section{Checking for collisions}
We can merge the code from \pythoninline{move\_mitt.py} and
\pythoninline{bounce.py}, from Lecture \ref{lecture-catch1}, into a
new program called \pythoninline{bounce\_and\_mitt.py} to
produce an almost functioning game:
\begin{python}
from Tkinter import *
import random

def move():
    global ball
    global gameCanvas
    global x, y, dx, dy
    global BALL_SIZE
    global WAIT_TIME
	
    x = x+dx
    y = y+dy
    if y < 0:
        y = 0
        dy = -dy
    elif y > gameCanvas.winfo_height()-BALL_SIZE:
        y = gameCanvas.winfo_height()-BALL_SIZE
        dy = -dy
    gameCanvas.coords(ball, x, y, x+BALL_SIZE, y+BALL_SIZE)
    gameCanvas.after(WAIT_TIME, move)


def move_mitt(event):
    global mitt
    global gameCanvas
    global mittx, mitty
    global MITT_SIZE
    
    if event.char == "j":
        mitty = mitty+10
    elif event.char == "k":
        mitty = mitty-10
    gameCanvas.coords(mitt, mittx, mitty, mittx+MITT_SIZE, mitty+MITT_SIZE)


MITT_SIZE = 20
BALL_SIZE = 10
WAIT_TIME = 100

root = Tk()
gameCanvas = Canvas(root, width=400, height=400)
gameCanvas.grid(column=0,row=0)
gameCanvas.wait_visibility()

x = 0
y = random.randint(0,gameCanvas.winfo_height()-BALL_SIZE)
dx = 2
dy = random.randint(-5,5)
ball = gameCanvas.create_oval(x, y, x+BALL_SIZE, y+BALL_SIZE, fill="blue")

gameCanvas.after(WAIT_TIME, move)
mittx = gameCanvas.winfo_width()-30
mitty = gameCanvas.winfo_height()/2
mitt = gameCanvas.create_oval(mittx, mitty,
                              mittx+MITT_SIZE, mitty+MITT_SIZE, 
                              fill="red")

root.bind("k", move_mitt)
root.bind("j", move_mitt)

root.mainloop()
\end{python}

Which is all very nice, but since the ball and mitt don't interact, it
doesn't make for much of a game. What we need to do is check for
collisions between the ball and the mitt. Take a bit of time to think
about how that might be achieved before looking at the answer below.

What we need to do is check if the ball is located near the mitt. With
circles, this is fairly easy --- we just need to check if the distance
between the circle centres is smaller than the sum of the two radii
(draw a few pictures to convince yourself of the correctness of that
statement). I'm sure you will remember from basic geometry that
$d(p_1, p_2) = \sqrt{(x_1-x_2)^2 + (y_1-y_2)^2}$, where
$p_1=(x_1,y_1)$ and $p_2 = (x_2, y_2)$ are two points, and
$d(p_1,~p_2)$ is the distance between them. That function would be
easy enough to implement, but we have a slight hiccup since
\pythoninline{create\_oval} uses the top-left corner of a bounding
box, rather than the circle centre to represent a circle, so we need
to calculate the centre coordinates of the ball and mitt first. We can
add catch functionality to the game by replacing the line
\pythoninline{gameCanvas.after(WAIT\_TIME,~move)} in the
\pythoninline{move} function with the following code:
\newpage
\begin{minipage}{\textwidth}
\begin{python}
    # check if we've caught the ball
    ball_centre_x = x + BALL_SIZE/2.0
    ball_centre_y = y + BALL_SIZE/2.0
    mitt_centre_x = mittx + MITT_SIZE/2.0
    mitt_centre_y = mitty + MITT_SIZE/2.0
    distance = sqrt((ball_centre_x-mitt_centre_x)**2+
                    (ball_centre_y-mitt_centre_y)**2)
    # did we catch the ball?
    if distance <= (BALL_SIZE+MITT_SIZE)/2.0:
        print "Good Catch"
        x = 0
        y = random.randint(0,gameCanvas.winfo_height()-BALL_SIZE)
        dy = random.randint(-5,5)
        gameCanvas.after(WAIT_TIME*10, move)
    # or have we totally missed it
    elif x > gameCanvas.winfo_width():
        print "You missed"
        x = 0
        y = random.randint(0,gameCanvas.winfo_height()-BALL_SIZE)
        dy = random.randint(-5,5)
        gameCanvas.after(WAIT_TIME*10, move)
    # or hasn't it got to us yet
    else:
        # make sure we're called again
        gameCanvas.after(WAIT_TIME, move)
\end{python} 
\end{minipage}

Note the structure of the new code. There are three mutually exclusive
options: we caught the ball; we missed the ball (and it's gone off
screen); or the ball hasn't reached us yet. The perfect structure for
an \pythoninline{if-elif-else} statement. But even this code is
getting a bit complex. Can you see an easy way to simplify it?

Well, we have essentially the same code as part of the statements for
the \pythoninline{if} part as well as the \pythoninline{elif} part. In
fact, we have very similar code at the start of the program as well.
It makes sense to abstract that code away into a function. We might
define the restart function as follows:
\begin{python}
def restart():
    global gameCanvas
    global BALL_SIZE
    global WAIT_TIME
	
    x = 0
    dx = 4
    y=random.randint(0,gameCanvas.winfo_height()-BALL_SIZE)
    dy=random.randint(-5,5)
    gameCanvas.after(WAIT_TIME*10, move);
    return x, dx, y, dy
\end{python}
There are several things to notice about this definition. First, the
variables \pythoninline{x, dx, y, dy} are local to the function in
this case and are different to the global variables of the same name.
Secondly, the function returns a \myidx{tuple} of variables. Tuples
are a very convenient way of returning more than one value as the
result of a function. To call \pythoninline{restart}, we simply use
the following code:
\begin{python}
x, dx, y, dy = restart()
\end{python}
That's pretty cool, but why would I bother going to all that trouble? 
There's no particular good reason for the current task at hand, but at
sometime in the future I might want to add a second ball to the game
(or a third), and I'd rather not have to create two distinct
\pythoninline{restart} methods if I can help it. With this scheme, I
can call \pythoninline{restart} multiple times and assign the results
to different variables if I wanted \footnote{Almost, we'd probably
have to move the call to \pythoninline{gameCanvas.after} somewhere
else too}.

\section{Keeping score}
The last thing to do for our game is keep track of who is winning. We
can do that fairly easily by adding a \pythoninline{Label} widget and
updating the scores after each round. This results in the following
completed game, \pythoninline{catch\_final.py}:

\begin{python}
from Tkinter import *
import random
from math import sqrt

def restart():
    global gameCanvas
    global BALL_SIZE
    global WAIT_TIME
    
    x = 0
    dx = 4
    y=random.randint(0,gameCanvas.winfo_height()-BALL_SIZE)
    dy=random.randint(-5,5)
    gameCanvas.after(WAIT_TIME*10, move);
    return x, dx, y, dy
	
def move():
    global ball
    global gameCanvas
    global x, y, dx, dy
    global BALL_SIZE
    global WAIT_TIME
    global mittx, mitty
    global MITT_SIZE
    global player_score
    global computer_score
    global scores
	
    x = x+dx
    y = y+dy
    
    # check if we've gone off the screen
    if y<0:
        y = 0
        dy = -dy
    elif y>gameCanvas.winfo_height()-BALL_SIZE:
        y = gameCanvas.winfo_height()-BALL_SIZE
        dy = -dy
    # draw the ball
    gameCanvas.coords(ball, x, y, x+BALL_SIZE, y+BALL_SIZE)
    
    # check if we've caught the ball
    ball_centre_x = x + BALL_SIZE/2.0
    ball_centre_y = y + BALL_SIZE/2.0
    mitt_centre_x = mittx + MITT_SIZE/2.0
    mitt_centre_y = mitty + MITT_SIZE/2.0
    distance = sqrt((ball_centre_x-mitt_centre_x)**2+
                    (ball_centre_y-mitt_centre_y)**2)
    # did we catch the ball?
    if distance<=(BALL_SIZE+MITT_SIZE)/2.0:
        player_score += 1
        scores.configure(text="Computer " + str(computer_score) + 
                         ": Player " + str(player_score))
        x, dx, y, dy = restart()
    # or have we totally missed it
    elif x>gameCanvas.winfo_width():
        computer_score += 1
        scores.configure(text="Computer " + str(computer_score) + 
                         ": Player " + str(player_score))
        x, dx, y, dy = restart()
    # or hasn't it got to us yet
    else:
        # make sure we're called again
        gameCanvas.after(WAIT_TIME, move)

def move_mitt(event):
    global mitt
    global gameCanvas
    global mittx, mitty
    global MITT_SIZE

    if event.char=="j":
        mitty = mitty+10
    elif event.char=="k":
        mitty = mitty-10
    gameCanvas.coords(mitt, mittx, mitty, mittx+MITT_SIZE, mitty+MITT_SIZE)


MITT_SIZE = 20
BALL_SIZE = 10
WAIT_TIME = 100

root = Tk()
gameCanvas = Canvas(root, width=400, height=400)
gameCanvas.grid(column=0,row=0)
scores = Label(root, text="Computer 0: Player 0")
scores.grid(column=0,row=1)
gameCanvas.wait_visibility()

player_score=0
computer_score=0
x, dx, y, dy = restart()
ball = gameCanvas.create_oval(x, y, x+BALL_SIZE, y+BALL_SIZE, fill="blue")

# gameCanvas.after(WAIT_TIME, move)
mittx = gameCanvas.winfo_width()-30
mitty = gameCanvas.winfo_height()/2
mitt = gameCanvas.create_oval(mittx, mitty,
                              mittx+MITT_SIZE, mitty+MITT_SIZE, 
                              fill="red")

root.bind("k", move_mitt)
root.bind("j", move_mitt)

root.mainloop()
\end{python}

This is a reasonable sized program, and minimising the complexity of
programs of this size and bigger can be quite difficult. There are
several techniques you can use to make this easier:
\begin{itemize}
\item Use symbolic constants,
\item Wrap code segments into functions,
\item Don't use global variables unless you have to, and if you do,
explicitly flag them using the \pythoninline{global} keyword,
\item Whenever you see how to simplify your code, do it. Simplify as
often as possible. This is called \myidx{refactoring}.
\item If the problem seems too big to know where to start, break it up
into smaller milestones and build the program bit-by-bit.
\end{itemize}


\section{Glossary}
\begin{description}
\item[abstraction:]\emph{Generalization} by reducing the information
   content of a concept. Functions in Python can be used to group a
   number of program statements with a single name, abstracting out
   the details and making the program easier to understand.
\item[bind:] The process of associating a widget event with a
function.
\item[constant:] A numerical value that does not change during the
   execution of a program. It is conventional to use names with all
   uppercase letters to represent constants, though Python programs
   rely on the discipline of the programmers to enforce this, since
   there is no language mechanism to to support true constants in
   Python.
\item[refactoring:] The process of reorganising code to make it easier
to understand, read and maintain.
\item[tuple:] A python data structure useful for returning multiple
values from a function.
 \end{description}

\newpage
\section{Laboratory exercises}
\begin{enumerate}
\item Type in \pythoninline{catch\_final.py} and run it to make sure
it works.
\item Move the code that calculates the distance from the ball to the
mitt into a function called \pythoninline{distance\_circles}. Do not
use any global variables in the function. Include several doctest
tests to ensure the function is correct.
\item At the moment, the program will keep going forever, or until you
close the window. Add code that will stop the program when either the
player or computer score reaches 10. Announce the winner using a
\pythoninline{tkMessageBox}. 
\item Modify \pythoninline{catch\_final.py} so that after a player
catches the ball, the computer pitches faster, and after a player
misses, the computer pitches slower.
\end{enumerate}

\section{Optional extension project: pong.py}
Pong was one of the first commercial video games. With a capital P it
is a registered trademark, but pong is used to refer any of the table
tennis like paddle and ball video games. \pythoninline{catch.py}
already contains all the programming tools we need to develop our own
version of pong. Incrementally changing \pythoninline{catch.py} into
\pythoninline{pong.py} is the goal of this project, which you will
accomplish by completing the following series of exercises: 
\begin{enumerate}
\item Copy \pythoninline{catch.py} to \pythoninline{pong1.py} and
change the ball into a paddle by using \pythoninline{create\_rectangle}
instead of 
the \pythoninline{create\_oval}. Make the adjustments needed to keep
the paddle on the screen. 
\item Copy \pythoninline{pong1.py} to \pythoninline{pong2.py}. Replace
the distance function with a boolean function \\
\pythoninline{hit(bx,~by,~r,~px,~py,~h)} that returns \pythoninline{True} when 
the vertical coordinate of the ball (\pythoninline{by}) is between the
bottom and top of the paddle, and the horizontal location of the ball
(\pythoninline{bx}) is less than or equal to the radius
(\pythoninline{r}) away from the front of the paddle. Use
\pythoninline{hit} to determine when the ball hits the paddle, and
make the ball bounce back in the opposite horizontal direction when
\pythoninline{hit} returns \pythoninline{True}. Your completed
function should pass these doctests: 
\begin{python}
def hit(bx, by, r, px, py, h):
    """
      >>> hit(760, 100, 10, 780, 100, 100)
      False
      >>> hit(770, 100, 10, 780, 100, 100)
      True
      >>> hit(770, 200, 10, 780, 100, 100)
      True
      >>> hit(770, 210, 10, 780, 100, 100)
      False
    """
\end{python}
Finally, change the scoring logic to give the player a point when
the ball goes off the screen on the left.
\item Copy \pythoninline{pong2.py} to \pythoninline{pong3.py}. Add a
new paddle on the left side of the screen which moves up when
\pythoninline{'a'} is pressed and down when \pythoninline{'s'} is
pressed. Change the starting point for the ball to the center of the
screen, \pythoninline{(400, 300)}, and make it randomly move to the
left or right at the start of each round. 
 \end{enumerate}
