HiveBrain v1.2.0
Get Started
← Back to all entries
patternpythonMinor

TicTacToe in Python with MiniMax/AlphaBeta

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
minimaxwithtictactoealphabetapython

Problem

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

Solution

Your program looks pretty good for a beginner. A couple of things could make the code easier to work with though, and thus reduce the risk of bugs and extra maintenance should you ever pick this up again.
State

You set a winner as follows:

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


winner is a State and the gameOver is a separate variable. gameOver can't be part of State, because State is actually being abused for something it wasn't supposed to do:

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


You're not using it just for the cell state. You're using it for the cell state, the board state (who has won) and even the turn state. Apparently it's acceptable to have a board without winner and without next-player (board state and turn state can be set to empty). If you set either of those to empty, I'd assume the game to be over as well and thus there's no need for a gameOver state. But there's an even better way.

You've tried to reduce duplication by using the same class twice. However, by doing so, you've introduced duplication elsewhere in a way that makes less sense than when you'd have allowed the duplication (making either 2 separate constructs to hold state or make 1 that's configurable).

Holding game-state in an Enum is a great idea. Holding cell-state in an Enum is too. But their possible states are not the same.

A board/game state for Tic Tac Toe can be the following:

  • Incomplete (ongoing)



  • Complete (finished, game over)



To keep things simple, we can replace the latter by 3 more specific states:

  • X won



  • Y won



  • Draw



A cell state can be the following:

  • X



  • Y



  • Empty



Do we have to check for those 3 specific states now to check whether a game is finished? No. Just check whether the board is unfinished. If it's not unfinished, it must be finished. Because those are the only states defined. This would simplify some of the checks you have later on.

For example:

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


Note: this construct is used 3 times in the same function (update(self)), which indicates the function is either poorly structured or doing too much. Or both.

If the winner is part of the game state, you can return it straight away and not bother with a double if checking temporary variables. The only question at this point is whether there should be returned at all. This could look like the following:

if gameState.xWon or gameState.yWon:
    return


This:

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


Completely defies the whole point of working with states in the first place. Why have states at all when at one point you set the game to assume two winners? One false step in those next lines of code and you've broken an otherwise perfectly safe program.
Comments near definitions

You like to state the purpose of what happens near a definition:

#the three states each cell can be in
class State(Enum):

#applies a move based on the current turn    
def applyMove(self, move):

#Resets the board
def reset(event):

#Asks the MiniMax algorithm for the best move and applies is
def requestMove(event):

#Calls the functions that draw the window
def updateCanvas():


This is commendable. Did you know Python has an in-built feature that makes this even better? It's called docstrings.

Consider PEP 257 – Docstring Conventions.

Docstrings provide a convenient way of putting human-readable documentation in the code itself that's easily extracted by other scripts/programs. They can be used on modules, functions, classes and methods.

A straightforward conversion would result in:
class State(Enum):
"""The three states each cell can be in."""

def applyMove(self, move):
"""Applies a move based on the current turn."""

def reset(event):
"""Resets the board."""

def requestMove(event):
"""Asks the MiniMax algorithm for the best move and applies it."""

def updateCanvas():
"""Calls the functions that draw the window."""


Minor note: if requestMove fails to perform a move, it will not provide feedback in any way and there was a typo in the comment.

Implementing docstrings allows other programs, the Python interpreter and most IDEs to provide guidance on the functions you're using (with IntelliSense). You can then extent this even further using typing (assuming Python 3.5+) and verify whether everything that needs a docstring has one with doctest. This can be very useful when using code defined in a different file/module/library.
Readability

Most of your code is quite easy to read. Whenever it's hard to follow, that's usually due to how you play around with states. Se

Code Snippets

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
#the three states each cell can be in
class State(Enum): 
    empty = 0
    x = 1
    o = -1
if xWin:
    self.xWon()
    return
if oWin:
    self.oWon()
    return
if gameState.xWon or gameState.yWon:
    return
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

Context

StackExchange Code Review Q#161888, answer score: 3

Revisions (0)

No revisions yet.