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()