This program has four main sections:
In order to support the graphical user interface (GUI) we must import the package tkinter. This ubiquitous package is sometimes verbalized as tkinter, kinter, or tk-INTER. It is what allows us to open a window in which the game will be played. Here that window is called win (although you can define it to have any name). In line 6, we define the geometry of the window. It is 800 by 800 pixles in size and will be located 50 pixles from the top of the screen and 50 pixles from the left margin of the screen. There are two players; player=1 starts. The game begins with 9 empty cells, and winner="". One of the important data structures in Python is the list. We define three lists: trackrow, trackcol, and trackdiag. trackrow and trackcol have three elements (rows 1,2,& 3 and columns 1,2,& 3 on the tic-tack-toe board. These are initially set to "". There are only two diagonals that can create a win, so trackdiag has only two elements. We can refer to a specific element in any list, by its index. These always begin with zero. Thus, trackrow[0] represents to top row of the tic-tac-toe board, and trackcol[2] is the far right column. As the game is played, not only will we place X's and O's on the board, but we will keep track of the number of X's and O's in each row, column and diagonals.
We are going to skip over the definition of the function b_click for a moment and describe the definition of the GUI buttons used in the play. The Tic-Tac-Toe board is a 3x3 array of cells. In this program we represent each of the nine cells by a tkinter button. The names of these buttons range from b0 to b8. These buttons are defined in lines 57 thru 65.In defining a button object. We specify the text displayed in the button text=" " (initially a blank, which we will change as two players place X's and O's in the grid. The size of the buttons are defined by height=10 and width=10. Unlike most geometry specifications, the value here is given in terms of character height and width, which is specified by font=... The last arguement of the button is command=lambda: b_click(b1,0,0). First, what is lambda? A lambda function is an anonymous function (i.e., defined without a name) that can take any number of arguments but, unlike normal functions, evaluates and returns only one expression. Here, there is a single arguement: the name of the function b_click(button_name,row,column). In Python arguements can be any object, and functions are objects, so when button b0 is clicked it executes b_click(b0,0,0). Likewise, clicking different buttons will cause b_click with the appropriate button name and position in the grid specified.
The final code in this section places each of these buttons in a grid structure by row and column to create a 3x3 array.
First, we want several variables to have global scope; that is they have the same value in the main program and the function. We do this in the function b_click by specifying the following global objects: global trackrow, trackcol, trackdiag, player, empty, winner. Remember that the agruements b,ir,ic have specific values for each button. These are passed with the lambda function. If the button text is " " and player==1 we replace the empty string of text with an "X" At the same time we concatenate trackrow[ir] with "X". Remember that for string variables "abc"+"def"="abcdef". The incrementation operator += is a shorthand notation for trackrow[ir]=trackrow[ir]+"X". Since we know both the row and column of the button which was pressed, we concatenate both trackrow[ir] and trackcol[ic]. If ir==ic we know that we have a mark on the diagonal element trackdiag[0]. A little arithmetic will lead to the boolean condition for the opposite diagonal being 2-ic==ir. So in like manner we can concatenate an X to the diagonal trackers. If, for example the first row has only one X in it trackrow[0]="X". After a second "X" is concatenated it becomes "XX". Finally, if all three cells in this row are occupied by X's, trackrow[0]="XXX". Now we can declare X to be the winner. As similar set of logic is applied to to the placement of O's
Don't we need to include a loop to continuously read the status of the buttons? The answer is yes, but this is accomplished with the following statement: win.mainloop(). Mainloop() is simply a method in the main window that executes what we wish to execute in an application (lets Tkinter to start running the application). As the name implies it will loop forever until the user exits the window or waits for any events from the user. The mainloop automatically receives events from the window system and deliver them to the application. This gets quit when we click on the close button of the title bar. So, any code after this mainloop() method will not run.
#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# Program initialization
#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import tkinter as tk
win=tk.Tk()
win.geometry('800x800+50+50')
trackrow=["","",""]
trackcol=["","",""]
trackdig=["",""]
player=1
empty=9
winner=""
#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# Definition of functions used in the program
#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
def b_click(b,ir,ic):
global trackrow, trackcol, trackdiag, player, empty, winner
if b["text"]==" " and player==1:
b["text"]="X"
trackrow[ir]+="X"
trackcol[ic]+="X"
if(ic==ir): trackdig[0]+="X"
if(2-ic==ir): trackdig[1]+="X"
elif b["text"]==" " and player==2:
b["text"]="O"
trackrow[ir]+="O"
trackcol[ic]+="O"
if(ic==ir): trackdig[0]+="O"
if(2-ic==ir): trackdig[1]+="O"
if(player==1):
player=2
else: player=1
empty+=-1
# See if there is a winner
# X WINS
for it in range (3):
for jt in range (3):
if(trackrow[jt]=="XXX"): winner="X"
if(trackcol[jt]=="XXX"): winner="X"
for it in range(2):
if(trackdig[it]=="XXX"): winner="X"
#O wins
for it in range (3):
for jt in range (3):
if(trackrow[jt]=="OOO"): winner="O"
if(trackcol[jt]=="OOO"): winner="O"
for it in range(2):
if(trackdig[it]=="OOO"): winner="O"
if(winner!=""): print("winner is", winner)
# test to see if any empty cells
empty+=-1
if(empty==0): print("GAME OVER")
#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# GUI buttons used in the game
#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
b1=tk.Button(win,text=" ",font=("Helvetica",20),height=10, width=20,bg="white",command=lambda: b_click(b1,0,0))
b2=tk.Button(win,text=" ",font=("Helvetica",20),height=10, width=20,bg="white",command=lambda: b_click(b2,0,1))
b3=tk.Button(win,text=" ",font=("Helvetica",20),height=10, width=20,bg="white",command=lambda: b_click(b3,0,2))
b4=tk.Button(win,text=" ",font=("Helvetica",20),height=10, width=20,bg="white",command=lambda: b_click(b4,1,0))
b5=tk.Button(win,text=" ",font=("Helvetica",20),height=10, width=20,bg="white",command=lambda: b_click(b5,1,1))
b6=tk.Button(win,text=" ",font=("Helvetica",20),height=10, width=20,bg="white",command=lambda: b_click(b6,1,2))
b7=tk.Button(win,text=" ",font=("Helvetica",20),height=10, width=20,bg="white",command=lambda: b_click(b7,2,0))
b8=tk.Button(win,text=" ",font=("Helvetica",20),height=10, width=20,bg="white",command=lambda: b_click(b8,2,1))
b9=tk.Button(win,text=" ",font=("Helvetica",20),height=10, width=20,bg="white",command=lambda: b_click(b9,2,2))
#define my canvas to be drawn using a grid structure
b1.grid(row=0, column=0)
b2.grid(row=0, column=1)
b3.grid(row=0, column=2)
b4.grid(row=1, column=0)
b5.grid(row=1, column=1)
b6.grid(row=1, column=2)
b7.grid(row=2, column=0)
b8.grid(row=2, column=1)
b9.grid(row=2, column=2)
#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# The Main Loop
#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
win.mainloop()
winner is X winner is X
Do I really need to have 18 statements to define and place tkinter buttons as I had in the first program? The answer no. However, there are some tricky issues to deal with. In the following program works similar to the first program except that (for the time being I have not added trackrow, etc.) The players have to look at the board and see if there is a winner. I will reindroduce this in the final program in this notebook.
As before, we import tkinter the GUI interface. We will need to import another special function called partial, but for the time being, I'll skip by explaining that. We open the window named win with the specified geometry. Now for the big difference: We are going to define a list but=[] which will be a list of button objects. In a bit will will draw button names from another list called b_name and we will initialize the text content of each button from the list b_text. b_name and b_text are defined here. At this point but is an empty list. Later, we will define elements and append to that list. Rather than defining player as 1 or 2, I'll simply designate the players as "X" and "O". Initially play="X"
Skipping the definition of the function, I'll move to define the buttons. I'll use embedded for loops with indices kr (rows) and kc (columns). The button number kk=3*kr+kc will produce numbers 0,1,2 across the first row, etc. just like the diagram in the first program. Within the embedded loop we define a button bb. The object bb is very similar to that constructed in the first program. However instead of a lambda function we must use a partial function. What's going on here??? We still pass the function object click (oops changed the name from b_click to click) as well as the row and column in which the button is located. However, we also have a new list named but. If we skip down to the last two statements in this code segment we see that each button we generate bb is placed in an appropriate spot in the grid, and the button is appended to the list but. What would happen if we did not construct a list but, and simply used a lambda function as we did earlier. There is not binding of the function in command= to click() until a button click occurs. That means that no matter what button is clicked, the program will assume that it is the last bb that was defined, which would always be the lower right hand cell of the board. The result is that no matter what button the player pushes, the mark will appear in the lower right hand cell. How do we handle this? There is another type of function called a partial function that can work. Here we must use: command=partial(click,but,kr,kc). Again, click is an arguement of the function.but, kr and kc are passed to click. The button number kk is computed. If the text stored in that button is " " but[kk].cget('text')==" " then we will reconfigre the text to be play (here either "X" or "O" depending on the player. The last code in the function changes players alternating from "X" to "O" We can now describe the fifth line in the Program initialization segment. The partial function is contained within the package functools. We only import a single element from functools named partial.
#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# Program initialization
#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import tkinter as tk
from functools import partial
win=tk.Tk()
win.geometry('800x800+50+50')
but=[]
b_name=["b00","b01","b02","b10","b11","b12","b20","b21","b22"]
b_text=[" "," "," "," "," "," "," "," "," "]
play="X"
#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# Definition of functions used in the program
#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
def click(but,ir,ic):
global play
kk=3*ir+ic
if(but[kk].cget('text')==" "):but[kk].config(text=play)
if (play=="X"): play="O"
else: play="X"
#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# GUI buttons used in the game
#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
for kr in range (3):
for kc in range (3):
kk=3*kr+kc
bb=tk.Button(win,text=b_text[kk],font=("Helvetica",20),height=10, width=20,bg="white", command=partial(click,but,kr,kc))
bb.grid(row=kr, column=kc)
but.append(bb)
#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# The Main Loop
#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
win.mainloop()
The final enhancement of the program colors the winner row column or diagonal. This will require that we make an enhancement to the function click so that when a row, column or diagonal is filled with a single token, we turn the appropriate cells red. It turns out that there is a method in tkinter button to do this. We should be able to use .donfig(bg='red'). HOWEVER, this has an error for the currunt version of macOSX. The work around is found in another package called tkmacosx import Button. So in the Program initialization we have added this new button definition. It automatically replaces the tk.Button, since it is imported after tk. The coloring of the cells is done in the code segment lines 30 to 41. If a player wins by filling a row, the following segment will color that whole row red.
for i in range (3):
if(checkr[i]=="XXX" or checkr[i]=="OOO"):
for j in range (3):
but[3*i+j].config(bg='#ff0000')
It should be noted that there are a couple of major differences between tkinter and tkmacosx Buttons. First, in tkmacosx size is always designated in pixels not characters as in tkinter. Secondly, tknacosx uses hex notation for color, while tkinter can use strings eg. "red"
#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# Program initialization
#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import tkinter as tk
from functools import partial
from tkmacosx import Button
win=tk.Tk()
win.geometry('800x800+50+50')
win.title("Tic-Tac-Toe")
but=[]
b_text=[" "," "," "," "," "," "," "," "," "]
checkr=["","",""]
checkc=["","",""]
checkd=["",""]
play="X"
#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# Definition of functions used in the program
#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
def click(but,ir,ic):
global play, checkr,checkc,checkd
kk=3*ir+ic
if(but[kk].cget('text')==" "):
but[kk].config(text=play)
checkr[ir]=checkr[ir]+play
checkc[ic]=checkc[ic]+play
if(ir==ic): checkd[0]=checkd[0]+play
if(ir==2-ic):checkd[1]=checkd[1]+play
for i in range (3):
if(checkr[i]=="XXX" or checkr[i]=="OOO"):
for j in range (3):
but[3*i+j].config(bg='#ff0000')
if(checkc[i]=="XXX" or checkc[i]=="OOO"):
for j in range (3):
but[3*j+i].config(bg='#ff0000')
for i in range (2):
if(checkd[i]=="XXX" or checkd[i]=="OOO"):
for j in range (3):
if(i==0):but[4*j].config(bg='#ff0000')
if(i==1):but[2+2*j].config(bg='#ff0000')
if (play=="X"): play="O"
else: play="X"
#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# GUI buttons used in the game
#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
for kr in range (3):
for kc in range (3):
kk=3*kr+kc
bb=Button(win,text=b_text[kk],font=("Helvetica",20),height=150, width=150,bg="white", command=partial(click,but,kr,kc))
bb.grid(row=kr, column=kc)
but.append(bb)
#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# The Main Loop
#++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
win.mainloop()
Here are screen shots of the window win given the title tic-tack-toe for each of four plays of the game.