After wrapping our heads around first widgets and how to make them react to events is it time to use that fresh knowledge for something fun. And what could be more fun than playing the infamous TicTacToe game?
Introduction
TicTacToe is one of the simplest and yet one of the most useful games to kill some time. The principle is simple a grid made of 3 x 3 squares is filled one by one by two players. Once one of the players has a row, coloumn or diagonale filled he wins.
Our knowledge of layouts and buttons in Kivy is more than enough to rebuild this game on our own. A new thing we are going to learn is how to create a pupup window.
The Code
The imports are just the usual. But we will be using a new type of layout. The grid layout. It arranges the widgets as the name tells in a grid
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.uix.button import Button
from kivy.uix.popup import Popup
The moin class is going to be a GridLayout representing the playgroiund.
The playground contains a list which represents the playfield and is initially to all zeros. Our players will fill that playfield up with a 1 representing player 1 and a 2 representing player 2 once they start making their moves.
Upon instanciation are we making sure that player one (self.player = Ture) starts and we add nine buttons ot the grid layout. These buttons are bound to the functin set_image whch will set the background to x or o depending on the player. Each button gets an id. This id starts at 0 and end at 8. This variable are we going to use later on to identify the position in the playfield.
class Playground(GridLayout):
playfield = [0,0,0,
0,0,0,
0,0,0]
def __init__(self, **kwargs):
'''Populat the playground with buttons'''
super(Playground, self).__init__(**kwargs)
self.player = True
self.cols=3
for i in range(9):
button = Button(id = str(i))
button.bind(on_press = self.set_image)
self.add_widget(button)
The set_image function will do two things. If the variable player is true (menaing player one is the active player) it sets the backround_normal of the button to the 0.png image. The background_normal of a button in Kivy is usually a light grey.
You can use any custom image. To make things easier can you take mine.
The second thing we do here is to write a one or two depending on the player into the respective position of the playgfield variable. The right position is derived from the id of the pressed button.
At the end the function calss the check_win function which will determine if the player has won the game with his last move. If not, the player variable is inverted to make sure the next press belongs to the next player.
def set_image(self, instance):
'''set background image of pressed button to x or o depending on player whos turn it is'''
if self.player:
instance.background_normal = 'O.png'
self.playfield[int(instance.id)] = 1
#print(self.playfield)
self.check_win(1)
self.player = False
else:
instance.background_normal = 'X.png'
self.playfield[int(instance.id)] = 2
#print(self.playfield)
self.check_win(1)
self.player = True
The check_win funtion checks the conditions of a win for row, coloumn and the two diagonals one after the other. The rows and coloums are checked like this:
First the playfield is sliced into the individual rows consisting of three integers. Then these three integers are passed to the set() function which will remove any duplicates. If now all the integers are e.g. ones the object returned by the set() funciton will be of size 1, indicating that all elements are from the same player. But we have to make sure that these three elements are not the initial zeros.
If that is also the case wer call the function show_winner which will display the winner in a popup.
def check_win(self, player):
'''Check if one of the players has won by row, coloumn or the diagonals'''
#check rows
for i in range(0,9,3):
if len(set(self.playfield[i:i+3])) == 1 and set(self.playfield[i:i+3]).pop() != 0:
#print('you win player: ', set(self.playfield[i:i+3]))
self.show_winner(set(self.playfield[i:i+3]))
#check columns
ar = []
for i in range(0,3):
for x in range(i,i+7,3):
ar.append(self.playfield[x])
if len(set(ar))==1 and set(ar).pop() != 0:
#print('you win player: ', set(ar))
self.show_winner(str(set(ar)))
ar.clear()
#check diagonals
#top left to bottom right
ar = []
for i in range(0,9,4):
ar.append(self.playfield[i])
if len(set(ar)) == 1 and set(ar).pop() != 0:
#print('you win player: ', set(ar))
self.show_winner(str(set(ar)))
#top right to bottom left
ar = []
for i in range(2,7,2):
ar.append(self.playfield[i])
if len(set(ar)) == 1 and set(ar).pop() != 0:
#print('you win player: ', set(ar))
self.show_winner(str(set(ar)))
The popup shows the winner in the title and contains a button which triggers the reset of the playfield and closes the popup. Here you see that binding an event can also be done to multiple functions. The first call dismisses the popup and the second one restarts the game
def show_winner(self, winner):
'''Announce the winner in a popup'''
content = Button(text='Start again')
popup = Popup(title='And The Winner Is ' + str(winner),
content=content,
size_hint=(None, None), size=(400, 400))
content.bind(on_press = popup.dismiss)
content.bind(on_press = self.restart_game)
popup.open()
Restarting the game is very similar to the __init__ function. The playfield is set to all zeros and the existing buttons are simply deleted from the Playground by calling clear_widget before repopulating the playground with fresh and grey buttons.
def restart_game(self, instance):
'''reset the playground'''
self.playfield = [0, 0, 0,
0, 0, 0,
0, 0, 0]
self.player = True
self.clear_widgets()
for i in range(9):
button = Button(id = str(i))
button.bind(on_press = self.set_image)
self.add_widget(button)
class TicTacToeApp(App):
def build(self):
return Playground()
if __name__ == '__main__':
TicTacToeApp().run()
What's Next?
By now you might have go the feeling that dealing with layouts and bindings in the python file is quite troublesome and becomes a bit hard once your project grows. But wait. KV language is comint to the rescue to provide you with a convenient and easy way to separate the function and layout of your apps.