2
\$\begingroup\$

I know barely anything about Python, I just jumped into this project as I've already coded it in different languages and it's my go-to when figuring things out in a new one, any feedback will be appreciated.

I deliberately tried to avoid making things too object-oriented with private attributes and whatnot.

This runs slower than the Java / C++ versions I made, despite having already improved the performance by doing a lightweight clone instead of a deep copy.

Board.py

from enum import Enum

class Board:

    #the three states each cell can be in
    class State(Enum): 
        empty = 0
        x = 1
        o = -1

    def __init__(self):
        self.reset()

    #resets the board
    def reset(self):
        self.cells = [self.State.empty] * 9
        self.turn = self.State.x
        self.currentTurn = 0
        self.winner = self.State.empty
        self.gameOver = False

    #applies a move based on the current turn    
    def applyMove(self, move):
        if self.gameOver == False and self.cells[move] == self.State.empty:
            self.cells[move] = self.turn;
            self.update()

    #updates the turn
    def endTurn(self):
        if self.turn == self.State.x:
            self.turn = self.State.o
        else: self.turn = self.State.x

        self.currentTurn += 1

    def getTurn(self):
        return self.turn

    def xWon(self):
        self.winner = self.State.x
        self.gameOver = True

    def oWon(self):
        self.winner = self.State.o
        self.gameOver = True

    def draw(self):
        self.gameOver = True

    def getWinner(self):
      return self.winner.value

    # returns a list of the empty cells
    def getMoves(self):
        moves = []
        for i in range(len(self.cells)):
            if self.cells[i] == self.State.empty:
                moves.append(i)
        return moves        

    #checks for a winner/draw         
    def update(self):  

        self.endTurn()

        for i in range(3):

            xWin = True;
            oWin = True;
            for j in range(i*3, i*3+3):
                if self.cells[j] != self.State.x:
                    xWin = False
                if self.cells[j] != self.State.o:
                    oWin = False;

            if xWin:
                self.xWon()
                return
            if  oWin:
                self.oWon()
                return

            xWin = True
            oWin = True
            for j in range(i, i+7, 3): 

                if self.cells[j] != self.State.x:
                    xWin = False
                if self.cells[j] != self.State.o:
                    oWin = False


            if xWin: 
                self.xWon()
                return   
            if oWin:
                self.oWon()
                return

        step = 4
        for i in range(0, 3, 2):

            xWin = True
            oWin = True
            for j in range(3):

                if self.cells[i+(j*step)] != self.State.x:
                    xWin = False
                if self.cells[i+(j*step)] != self.State.o:
                    oWin = False

            if xWin: 
                self.xWon()
                return
            if oWin: 
                self.oWon()
                return
            step = 2;

        if self.currentTurn == 9:
            self.draw()

Brain.py (wrapper for the minimax algorithm)

import copy
from Board import *

class Brain:

def __init__(self, board):
    self.originalBoard = board
    self.count = 0

#returns the best move according to the minimax method
def bestMove(self):
    if self.originalBoard.gameOver:
        return
    self.count = 0
    scores = []
    moves = self.originalBoard.getMoves()

    for m in moves:
        scores.append(self.miniMax(self.originalBoard, m))

    print(self.count)

    if (self.originalBoard.getTurn() == self.originalBoard.State.x):
        return moves[self.max(scores)]
    else:
        return moves[self.min(scores)]

#classic minimax with alphabeta pruning                
def miniMax(self, board, move, alpha = -1, beta = 1):

    self.count += 1
    clone = copy.deepcopy(board) >now replaced as I said in the intro<
    clone.applyMove(move)

    if clone.gameOver:
        return clone.getWinner()

    availableMoves = board.getMoves()        
    if clone.getTurn() == clone.State.x:

        for m in availableMoves:
            score = self.miniMax(clone, m, alpha, beta)  
            if score > alpha:
                alpha = score  
                if alpha >= beta:
                    break
        return alpha           

    else:
        for m in availableMoves:
            score = self.miniMax(clone, m, alpha, beta)
            if score < beta:
                beta = score
                if alpha >= beta:
                    break
        return beta                   

#helper method that returns the index of the highest number in a list
def max(self, list):
    max = -1
    index = 0
    if list.__len__() == 1:
        return index

    for i in range (len(list)):
        if (list[i] > max):
            max = list[i]
            index = i
    return index

 #helper method that returns the index of the lowest number in a list
def min(self, list):
    min = 1
    index = 0
    if list.__len__() == 1:
        return index

    for i in range (len(list)):
        if (list[i] < min):
            min = list[i]
            index = i
    return index

TicTacToe.py (main file)

from tkinter import *
from Board import *
from Brain import *
from math import floor as floor

CELLS_ROW = 3
WIDTH = 400
HEIGHT = 400
CELLWIDTH = WIDTH / CELLS_ROW
CELLHEIGHT = HEIGHT / CELLS_ROW

tk = Tk()
tk.title("Tic Tac Toe")

board = Board()
brain = Brain(board)

# Draws the background
def drawBG(): 
    def getBGColor(i, j):
        return "#222222" if i % 2 == j % 2 else "#444444"

    for i in range(0, CELLS_ROW):
        for j in range(0, CELLS_ROW) :
            canvas.create_rectangle(i*CELLWIDTH, j*CELLHEIGHT, CELLWIDTH+i*CELLWIDTH, CELLHEIGHT+j*CELLHEIGHT, fill=getBGColor(i, j))

#Draws the naughts and crosses
def drawMarks():
    thickness = 20
    def drawMark(x, y, mark):
        if mark == board.State.x:
            canvas.create_line(x*CELLWIDTH+thickness, y*CELLHEIGHT+CELLHEIGHT-thickness, x*CELLWIDTH+CELLWIDTH-thickness, y*CELLHEIGHT+thickness, fill="#d32f2f", width = thickness, capstyle = ROUND)
            canvas.create_line(x*CELLWIDTH+thickness, y*CELLHEIGHT+thickness, x*CELLWIDTH+CELLWIDTH-thickness, y*CELLHEIGHT+CELLHEIGHT-thickness, fill="#d32f2f", width = thickness, capstyle = ROUND)
        else: canvas.create_oval(x*CELLWIDTH+thickness, y*CELLHEIGHT+thickness, x*CELLWIDTH+CELLWIDTH-thickness, y*CELLHEIGHT+CELLHEIGHT-thickness, outline="#5a64c8", width = thickness)

    for i in range(3):
        for j in range(3):
            if board.cells[i*3+j] != board.State.empty:
                drawMark(j, i, board.cells[i*3+j])

#Applies a move based on the mouse position         
def click(event):
    def translateCoords(_x, _y):
        x = floor(_x / CELLWIDTH)
        y = floor(_y / CELLHEIGHT)
        return x + y * CELLS_ROW

    board.applyMove(translateCoords(event.x, event.y))
    updateCanvas()

#Resets the board
def reset(event):
    board.reset()
    updateCanvas()

#Asks the MiniMax algorithm for the best move and applies is
def requestMove(event):
    if board.gameOver == False:
        board.applyMove(brain.bestMove())
        updateCanvas()

#Calls the functions that draw the window
def updateCanvas():
    drawBG()
    drawMarks()
    tk.update()


canvas = Canvas(tk, width=WIDTH, height=HEIGHT)
canvas.bind("<Button-1>", click)
canvas.bind("<Button-2>", reset)
canvas.bind("<Button-3>", requestMove)

updateCanvas()
canvas.pack()

tk.mainloop()
\$\endgroup\$

Your Answer

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

Browse other questions tagged or ask your own question.