Jump to content


Check out our Community Blogs

Register and join over 40,000 other developers!


Recent Status Updates

View All Updates

Photo
- - - - -

Tkinter Tic Tac Toe tutorial

python tkinter

  • Please log in to reply
2 replies to this topic

#1 DanielTan

DanielTan

    CC Regular

  • Member
  • PipPipPip
  • 25 posts

Posted 22 July 2013 - 06:17 AM

Having seen the java tutorial on tic tac toe, i felt that the python tutorials page needed more push.
Since we already have a shell IO game, I'll do one using tkinter (not the best, but I like it)
 
I'm taking that you have a basis in basic tkinter(if not, another tutorial is underway, i guess), and let's build on that. 

I like tkinter in that by manually writing the code, it gives me more control over where, what, how of the program.

http://effbot.org/tkinterbook/

This site is where I learnt tkinter, though. 
 
Note that you can choose whether if you want to do single player mode, or double player mode.
But we'll start with double player as our basis, shall we?
First, we start by importing what we need and starting the __init__ function main class, along with calling the root. 

from tkinter import Frame, Canvas, Label, Button, LEFT,RIGHT, ALL, Tk
from random import randint

class main:
   
    def __init__(self,master):
        self.frame = Frame(master)
        self.frame.pack(fill="both", expand=True)
        

root=Tk()
app=main(root)
root.mainloop()

This is a basic model of a GUI. I  could have done <from tkinter import * >for simplicity but i think this would improve performance.
then, we add the canvas, and a few buttons to control the flow of the game.

from tkinter import Frame, Canvas, Label, Button, LEFT,RIGHT, ALL, Tk
from random import randint

class main:
   
    def __init__(self,master):
        self.frame = Frame(master)
        self.frame.pack(fill="both", expand=True)
        self.canvas = Canvas(self.frame, width=300, height=300)
        self.canvas.pack(fill="both", expand=True)
        self.label=Label(self.frame, text='Tic Tac Toe Game', height=6, bg='black', fg='blue')
        self.label.pack(fill="both", expand=True)
        self.frameb=Frame(self.frame)
        self.frameb.pack(fill="both", expand=True)
        self.Start1=Button(self.frameb, text='Click here to start\ndouble player', height=4, command=self.start1,bg='white', fg='purple')
        self.Start1.pack(fill="both", expand=True, side=RIGHT)
        self.Start2=Button(self.frameb, text='Click here to start\nsingle player', height=4, command=self.start2,bg='purple', fg='white')
        self.Start2.pack(fill="both", expand=True, side=LEFT)  

root=Tk()
app=main(root)
root.mainloop()

self refers to the main class.
the <frame.pack()> means that I'm using the pack geometry manager which basically helps me to add the widgets into the frames.
Frame are used to organize the widgets.
The canvas is where the lights and actions meet.
Labels are used to display text.
Now then notice that each button, Start1 and Start2, unlike java, has their own method to call functions.
We'll write the single player function, start1 first.
 

...
def start1(self):
        self.canvas.delete(ALL)
        self.label['text']=('Tic Tac Toe Game')
        self.canvas.bind("<ButtonPress-1>", self.sgplayer)  
        self._board()
        self.TTT=[[0,0,0],[0,0,0],[0,0,0]]
        self.i=0
        self.j=False

First, we refresh the canvas by deleting everything, then redrawing them again.
I bound a function self.sgplayer, which will do the gaming to the canvas when the button is clicked
The self._board() is the function we will use to draw the board.
self.TTT is the matrix I use to record what is drawn at where.
Note that in other languages, they might not choose to use to use a matrix to store such data (the java tutorials don't, anyway), but using the matrix will simplify our calculations, as you'll see later on, especially in the AI code (We are never going to go through each winning case manually,never! )
self.i is a counter(for turn taking) and self.j is simply there to be used in end() where we end the game.
 
You might notice that the python code is pretty self explanatory. So I don't do much commenting. There isn't much of a need
 
then we draw the board.

...
def _board(self):
        self.canvas.create_rectangle(0,0,300,300, outline="black")
        self.canvas.create_rectangle(100,300,200,0, outline="black")
        self.canvas.create_rectangle(0,100,300,200, outline="black")

now that's done, on to the gaming!
 

def sgplayer(self,event):
        for k in range(0,300,100):
            for j in range(0,300,100):
                if event.x in range(k,k+100) and event.y in range(j,j+100):
                    if self.canvas.find_enclosed(k,j,k+100,j+100)==():
                        if self.i%2==0:
                            X=(2*k+100)/2
                            Y=(2*j+100)/2
                            X1=int(k/100)
                            Y1=int(j/100)
                            self.canvas.create_oval( X+25, Y+25, X-25, Y-25, width=4, outline="black")
                            self.TTT[Y1][X1]+=1
                            self.i+=1
                        else:                         
                            X=(2*k+100)/2
                            Y=(2*j+100)/2
                            X1=int(k/100)
                            Y1=int(j/100)
                            self.canvas. create_line( X+20, Y+20, X-20, Y-20, width=4, fill="black")
                            self.canvas. create_line( X-20, Y+20, X+20, Y-20, width=4, fill="black")
                            self.TTT[Y1][X1]+=9
                            self.i+=1
        self.check()

Ah. Now this gets a little tricky. When the user input is registered, the computer:
1)check where the user clicked.
2)set a bounding box
3)check if there is nothing in that box (so that it can draw
4)check whose turn is it.
5) draw the circle or cross. and add the corresponding value to self.TTT, the matrix
6) check if anybody wins.(that's what the self.check() is for.
 
Onto to the self.check()

def check(self):
        #horizontal check
        for i in range(0,3):
            if sum(self.TTT[i])==27:
                self.label['text']=('2nd player wins!')
                self.end()
            if sum(self.TTT[i])==3:
                self.label['text']=('1st player wins!')
                self.end()
        #vertical check
        #the matrix below transposes self.TTT so that it could use the sum fucntion again
        self.ttt=[[row[i] for row in self.TTT] for i in range(3)]
        for i in range(0,3):            
            if sum(self.ttt[i])==27:
                self.label['text']=('2nd player wins!')
                self.end()
            if sum(self.ttt[i])==3:
                self.label['text']=('1st player wins!')
                self.end()
        #check for diagonal wins
        if self.TTT[1][1]==9:
            if self.TTT[0][0]==self.TTT[1][1] and self.TTT[2][2]==self.TTT[1][1] :
                self.label['text']=('2nd player wins!')
                self.end()
            if self.TTT[0][2]==self.TTT[1][1] and self.TTT[2][0]==self.TTT[1][1] :
                self.label['text']=('2nd player wins!')
                self.end()
        if self.TTT[1][1]==1:
            if self.TTT[0][0]==self.TTT[1][1] and self.TTT[2][2]==self.TTT[1][1] :
                self.label['text']=('1st player wins!')
                self.end()
            if self.TTT[0][2]==self.TTT[1][1] and self.TTT[2][0]==self.TTT[1][1] :
                self.label['text']=('1st player wins!')
                self.end()
        #check for draws
        if self.j==False:
            a=0
            for i in range(0,3):
                a+= sum(self.TTT[i])
            if a==41:
                self.label['text']=("It's a pass!")
                self.end()

here I used the sum functions for lists just to add the elements together. Also, i checked if there are passes, i.e. 5(1)+4(9)=41. five circles and four crosses are the maximum, but if nobody wins then it's a pass.

So you don't have to set boolean checkers for each row== 

note: it also checks if self.j has been triggered so that it won't check if someone has won.

If it is a win, then we should end the game and stop anybody from clicking.

def end(self):
        self.canvas.unbind("<ButtonPress-1>")
        self.j=True

And we're done! copy all that into the main class and you got yourself a game. (NOTE: the functions for the second button has not been written so you'll have to delete all those single player controls if you were to play double player.)

 

I'll continue onto the juicier single player controls in my next post. Au revoir for now.

Attached Files

  • Attached File  ticTT.py   9.25KB   1262 downloads

Edited by DanielTan, 23 July 2013 - 05:52 AM.

  • 2

#2 DanielTan

DanielTan

    CC Regular

  • Member
  • PipPipPip
  • 25 posts

Posted 23 July 2013 - 05:37 AM

There was some bug with the game AI in ticTT.py so I attached a new version, ticTT2.py, complete with documentation and complete AI(the original only had two aspects of game AI)

 

Attached File  ticTT2.py   13.91KB   1389 downloads

 

This is how the interface looks like:

GUITTT1.gif

Now then, on to the single player programming, working from the main class we had(with double players).

First, we will start by initiating our single player function, self.start2()

def start2(self):
        #Starts single player
        self.canvas.delete(ALL)
        self.label['text']=('Tic Tac Toe Game')
        self.canvas.bind("<ButtonPress-1>", self.dgplayer)  
        self._board()
        self.TTT=[[0,0,0],[0,0,0],[0,0,0]]
        self.i=0
        self.j=False
        #Trigger to check the validity of the move
        self.trigger=False

Note that it's almost the same as the double player function, except that this binds self.dgplayer and an extra self.trigger boolean variable, which we will be using later in the code.

 

then, it's on to the single player code. This time, we'll just copy paste the double player function, and simply change the second player into our game AI player.

def dgplayer(self,event):
        for k in range(0,300,100):
            for j in range(0,300,100):
                if self.i%2==0:
                    if event.x in range(k,k+100) and event.y in range(j,j+100):
                        if self.canvas.find_enclosed(k,j,k+100,j+100)==():
                            X=(2*k+100)/2
                            Y=(2*j+100)/2
                            X1=int(k/100)
                            Y1=int(j/100)
                            self.canvas.create_oval( X+25, Y+25, X-25, Y-25, width=4, outline="black")
                            self.TTT[Y1][X1]+=1
                            self.i+=1
                            self.check()
                            self.trigger=False                           
                else:
                    #check for wins/losts/draws first 
                    #before allowing the computer to make its turn
                    self.check()
                    #Game AI code
                    self.AIcheck()
                    #refresh validity of move
                    self.trigger=False

Note that we should check if the game has ended before we let the AI player play. 

 

Now then, comes the tricky part of designing an AI player.

 

Keep a few things in mind when coding an AI player:

1) Keep your code concise and precise as the AI is going to play every game loop。

2) Reuse your code if possible. Identify parts that keep repeating and write a function for it.

3) If possible, list out all the conditions that the player will meet.

4) There are three aspects of the game AI:Offense, Defense and Neutral.

 

Right, before I started coding, I played tic tac toe for some time and noted down what I did and what I thought at which conditions, which was basically doing (3) from above.

In this game:

 

1) the player starts first. If he chooses the center, the second player is going to have a tough time. If not, then the second player might just get the center.

2) I might not know what to do on the second move.

3) By the second and third move, I will start to attack, or block the other guy's attack. 

4) I need to check the whole board for solutions. 

5) Repeated draws circles or crosses.

 

From this, I gather that:

1)The AI needs to try to get the center.

2) There is a need for a random move function.

3) I need a function to loop through every row, column and diagonal to attack or defend.

4) I might need a function to draw circles and crosses.

 

And with that in mind, let's start writing code with (4) from the latter.

The circle drawing has been taken care of (in the self.dgplayer function)so we'll have to write code for the cross.

def cross(self, k, j):
        # k is the x coords
        # j is the y coords
        X=(200*k+100)/2
        Y=(200*j+100)/2
        X1=int(k)
        Y1=int(j)
        self.canvas. create_line( X+20, Y+20, X-20, Y-20, width=4, fill="black")
        self.canvas. create_line( X-20, Y+20, X+20, Y-20, width=4, fill="black")
        self.TTT[Y1][X1]+=9
        self.check()
        self.i+=1
        self.trigger=True

Now then, we'll move on to the randmove function, which is (2)

def randmove(self):
        # In case there's nothing for the computer to do
        while True:
            k=(randint(0,2))
            j=(randint(0,2))
            if self.TTT[j][k]==0:
                self.cross(k,j)
                break
            else:
                k=(randint(0,2))*100
                j=(randint(0,2))*100

Our matrix, self.TTT has only, 0,1,2, elements.

 

Then it's time for the juiciest part, the AIcheck function, which loops through every part of the matrix in search of a loophole.

Since the AI should have a desire to win, we should put OFFENSE code at the top (first to loop through), then DEFENSE, then finally NEUTRAL (when everything is exhausted)

Using self.trigger to signal that a move has been made eliminates the need for the error-prone elif statement ( since the code might jump here and there )

Again, we should recycle code that has already been written to save time and energy. 

We'll recycle the check function to check for two circles or crosses in a row, then write the corresponding code to check for an empty space and draw a cross

Here's the code in it's full glory.

def AIcheck(self):
        #Offense should come before defense so that the AI will try to win if possible
        #This is built on the self.check function
        self.ttt=[[row[i] for row in self.TTT] for i in range(3)]
        #OFFENSE
        #this is the vertical checklist    
        for h in range(0,3):
            k=0
            j=0
            if sum(self.TTT[h])==18:
                while k <3:
                    if k==h:
                        while j <3:
                            if self.trigger==False:
                                if self.TTT[k][j]==0:
                                    self.cross(j,k)
                                    break
                            j+=1
                    k+=1
        #this is the horizontal checklist    
        for h in range(0,3):
            k=0
            j=0
            if sum(self.ttt[h])==18:                        
                while k <3:
                    if k==h:
                        while j <3:
                            if self.trigger==False:
                                if self.ttt[k][j]==0:
                                    self.cross(k,j)
                                    break
                            j+=1
                    k+=1
        #this is the diagonal checklist    
        if self.TTT[1][1]==9:
            if self.TTT[0][0]==9:
                if self.trigger==False:
                    if self.TTT[2][2]==0:
                        self.cross(2,2)
            if self.TTT[0][2]==9:
                if self.trigger==False:
                    if self.TTT[2][0]==0:
                        self.cross(0,2)
            if self.TTT[2][0]==9:
                if self.trigger==False:
                    if self.TTT[0][2]==0:
                        self.cross(2,0)
            if self.TTT[2][2]==9:
                if self.trigger==False:
                    if self.TTT[0][0]==0:
                        self.cross(0,0)
        #DEFENSE
        #this is the horizontal checklist    
        for h in range(0,3): 
            k=0
            j=0            
            if sum(self.TTT[h])==2:
                while k <3:
                    if k==h:
                        while j <3:
                            if self.trigger==False:
                                if self.TTT[k][j]==0:
                                    self.cross(j,k)
                                    break
                            j+=1
                    k+=1
        #this is the vertical checklist
        for h in range(0,3):
            k=0
            j=0
            if sum(self.ttt[h])==2:                        
                while k <3:
                    if k==h:
                        while j <3:
                            if self.trigger==False:
                                if self.ttt[k][j]==0:
                                    self.cross(k,j)
                                    break
                            j+=1
                    k+=1                    
        #this is the diagonal checklist
        if self.TTT[1][1]==1:
            if self.TTT[0][0]==1:
                if self.trigger==False:
                    if self.TTT[2][2]==0:
                        self.cross(2,2)
            if self.TTT[0][2]==1:
                if self.trigger==False:
                    if self.TTT[2][0]==0:
                        self.cross(0,2)
            if self.TTT[2][0]==1:
                if self.trigger==False:
                    if self.TTT[0][2]==0:
                        self.cross(2,0)
            if self.TTT[2][2]==1:
                if self.trigger==False:
                    if self.TTT[0][0]==0:
                        self.cross(0,0)
        #NEUTRAL
        if self.TTT[1][1]==0:
            if self.trigger==False:
                self.cross(1,1)
                self.trigger=True
        else:
            if self.trigger==False:
                self.randmove()

I used while loops since the break statement is damn useful when I want to stop the loop after the move has been made.

Since we used a function to draw the cross, the code remains clean and easily understood. 

I did not use a for loop for the diagonal checking since there are just 4 cases and it isn't worth the effort to design a for loop for this when we could have just copy pasted, and also since it's easy to maintain.

 

And we're done! Just put everything in, and you've a functional AI player! 

 

Also, you can change the "personality" of your AI by changing the order of the OFFENSE, DEFENSE, NEUTRAL code. Try it out! LOL

 

I'm welcome to comments and bug explorers. Happy coding(and playing)!

 

 

This is a screenshot of an endgame:

GUITTT2.gif


Edited by DanielTan, 23 July 2013 - 05:57 AM.

  • 0

#3 Rodrich266

Rodrich266

    CC Lurker

  • Just Joined
  • Pip
  • 2 posts

Posted 22 March 2014 - 11:47 AM

Hello, I've found a couple of errors on the code, but I suppose they could be caused by the Python Version I'm using. Anyways, there are a couple of errors with the Single Player AI:

 

- It seems there is a bug when clicking the bottom right corner as a first move since the AI doesn't respond after that move. In every other place the AI works fine, but when doing this first move the cross is placed in the center (this only happens if the player clicks the center after doing the first move described before), the other problem is that after doing this first move and clicking anywhere else in the board the AI makes two moves at the same time!

 

- There is a lot of bugs in that bottom corner, it seems like the AI in that corner is flawed (too many bugs for me to write them all!!) but then again, it might be because of my version!

 

 

- The last problem is that when there is a draw in single player, the program always fails to respond and I have to force it to close. Why is that? EDIT: I have found that the program does not respond in draw, only when a move is made on that bottom right corner if the last move is made there, the program runs fine

 

My Python version is 2.7.6


Edited by Rodrich266, 22 March 2014 - 02:43 PM.

  • 0





Also tagged with one or more of these keywords: python, tkinter