3
\$\begingroup\$

Accomplishment: Tic Tac Toe game with Python & Kivy

I have created a simple tic tac toe game with a random bot logic (really random after a user selects a mark). Next step would be to insert the minimax algorithm for example, but I would like to know if the path I've taken to construct my code is correct and could it be improved.

I have two files: main.py and ai.py

main.py

from kivy.app import App
from kivy.uix.button import Button
from kivy.uix.popup import Popup
from kivy.uix.label import Label
from kivy.uix.stacklayout import StackLayout
from kivy.config import Config
from ai import Ai
from random import randint

class TicTacToe(App):

    title = 'Tic Tac Toe'
    board = []
    choices = ["X","O"]

    # On application build handler
    def build(self):
        Config.set('graphics', 'width', '600')
        Config.set('graphics', 'height', '600')
        self.layout = StackLayout()
        for x in range(9): # range() explanation: http://pythoncentral.io/pythons-range-function-explained/
            bt = Button(text='', font_size=200, width=200, height=200, size_hint=(None, None), id=str(x))
            bt.bind(on_release=self.btn_pressed)
            self.board.append(bt)
            self.layout.add_widget(bt)
        return self.layout

    # On application start handler
    def on_start(self):
        self.init_players();
        greeting = "Hello Player! You are playing with \"" + self.player + "\""
        self.popup_message(greeting)


    # On button pressed handler
    def btn_pressed(self, button):
        if len(button.text.strip()) < 1: # Continue only if the button has no mark on it...
            button.text = self.player
            self.bot.make_move(self.board)
            self.check_winner()

    # Initializes players
    def init_players(self):
        rand_choice = randint(0,1);
        self.bot = Ai(self.choices[rand_choice]);
        self.player = self.choices[0] if rand_choice == 1 else self.choices[1]

    # Checks winner after every move...
    def check_winner(self):
        pass # Just void..

    def popup_message(self, msg):
        popup = Popup(title="Welcome!", content=Label(text=msg), size=(300, 100), size_hint=(None, None))
        popup.open()


if __name__ == '__main__':
    TicTacToe().run()

ai.py

from random import randint

class Ai:
    def __init__(self, choice):
        self.choice = choice

    # Main function for bot making a move
    def make_move(self, board):
        self.make_random_move(board) # Do a random move

    # Makes a random move by generating a random integer and checking if the place is free
    def make_random_move(self, board):
        free_buttons = []
        for button in board: # Simple for loop to extract the free buttons
            if len(button.text.strip()) < 1: # If the button has no mark, stripping spaces...
                free_buttons.append(button)

        if len(free_buttons) > 1: # If any free buttons left... let's make the move
            rand = randint(0, len(free_buttons) - 1) # Generate a random integer from 0 to the length of the array
            free_buttons[rand].text = self.choice
\$\endgroup\$
3
\$\begingroup\$

On comments

# On button pressed handler
def btn_pressed(self, button):

I think you could rename the function to on_btn_pressed and just remove the comment, as it doesn't add any information that isn't already apparent from the function name.

There are several other comments that doesn't add anything useful, like

# Initializes players
def init_players(self):

and

# Checks winner after every move...
def check_winner(self):

and

self.make_random_move(board) # Do a random move

The comments describing the build and on_start functions could do with some more explanation, as it's not apparent to me how these are used (I'm assuming they are used by kivy.app.App?).

if len(button.text.strip()) < 1: # If the button has no mark, stripping spaces...

I would extract the above check into a function, like this:

if is_button_unmarked(button):
    free_buttons.append(button)
...

def is_button_unmarked(self, button):
    return len(button.text.strip()) < 1

thus eliminating the need for a comment.

You should strive to make the code self-documenting, and use comments when you fail to do that. I recommend reading rolfl's excellent answer to a codereview question about comments.

Constructor vs method injection

I would inject the game board into the Ai constructor

self.bot = Ai(self.choices[rand_choice], board)

and set it as you do with the choice

class Ai:
    def __init__(self, choice, board):
        self.choice = choice
        self.board = board

This way you can do

self.bot.make_move()

instead of

self.bot.make_move(self.board)

which is easier to understand. As the board is used by all the functions in Ai, there's currently no good reason not to inject it in the constructor.

Reuse

Your popup_message function lets you pass any message, but the title is hardcoded. You should send the title in as an argument as well, to make the popup_message function useable by more than welcome messages (maybe you'd want to use it after deciding a winner in check_winner?).

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.